From 69d9961ac97b152571329ae9757ada4d792c4123 Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Sun, 19 Mar 2023 19:23:43 +0100 Subject: [PATCH 001/117] new version of the ha diagram, from sqpit1518 jira ticket --- .../install/img/architecture-server-ha.drawio | 2 +- .../install/img/architecture-server-ha.png | Bin 363523 -> 158543 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/how-to/install/img/architecture-server-ha.drawio b/docs/src/how-to/install/img/architecture-server-ha.drawio index ecc96bb9df9..6ed345dfbf7 100644 --- a/docs/src/how-to/install/img/architecture-server-ha.drawio +++ b/docs/src/how-to/install/img/architecture-server-ha.drawio @@ -1 +1 @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/docs/src/how-to/install/img/architecture-server-ha.png b/docs/src/how-to/install/img/architecture-server-ha.png index 1702d79a213c34c3b6a404338957a2f3639d6ba4..2007448d8e5fcba1102cf3e28869b1a8469fcf20 100644 GIT binary patch literal 158543 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

uh_ygb_z|faZz%&_(J##C1|BJs7wOzSvLXQU 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�zcJ&8M9GrL&2~ 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

c{fTIc1|f6d{aNFFi1w)ipm7MK+S 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

NVmhe0Cu?g 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

    )t_0xEDqAs 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≧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 VOBhNHGB Date: Sun, 19 Mar 2023 19:28:10 +0100 Subject: [PATCH 002/117] forgot the title --- .../install/img/architecture-server-ha.drawio | 2 +- .../install/img/architecture-server-ha.png | Bin 158543 -> 170040 bytes 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/how-to/install/img/architecture-server-ha.drawio b/docs/src/how-to/install/img/architecture-server-ha.drawio index 6ed345dfbf7..c7caf837bf1 100644 --- a/docs/src/how-to/install/img/architecture-server-ha.drawio +++ b/docs/src/how-to/install/img/architecture-server-ha.drawio @@ -1 +1 @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/docs/src/how-to/install/img/architecture-server-ha.png b/docs/src/how-to/install/img/architecture-server-ha.png index 2007448d8e5fcba1102cf3e28869b1a8469fcf20..115674626de7ac0362ca974785c4f5180fc4abb4 100644 GIT binary patch literal 170040 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ßgSFpf9ve)E7ww|h#NH^(lI`JR$->vzo z#vb4w9KMB>(5)wB)E)BL4T~K2a@0L@u^+A~Ii6{;&eW}TGF4Kgfu_taB)5wUY7d;> zSq{+bVm{zwFd>qY*ku-=bf8qB=$wQBTsT``%}&|6zg zy3vn@K_LUir85Q4O1&X!W*4R_{IKWd@J@vduCKTAz#P`$bs1(|tHFvHZbZ3SfyzLT ztZWwy=v`kmt=GwU?EVM*(&D@ISpIo*U#e?oxDc*?U8z~;*Hll6#1g!GAo;r!Yc%`i z;jb>vo{xgDOcyI3_Q*~aF)O|75erkyEpb<(%-oNEL6rY{x=IT3Cd+2# zmb?l4e6W8%9@9(n(S0VJ4E7i7q;oum;`rioz6oqu?mG0YUM>W5@ ze#eK~y`Z;cefKtR@N|gg2C7Ss2>|8iN9$|%gc}QgHA>9fU;6a)e1RXOeWct~(?b{t z)Qo|$9w@rWvjP*!pjUW-F~%91v95Kck_Q4499N>;ZYR&%#2Y0U`fl}W!(6|b`TT;u zu8yaef;&^JI0+78sADY2<6!Y)K{1@9RimnnIr9F3TetjGyLb#mJtklhgO2KCuVR92 zUDu!^3{=L1xmc>HHY9%iiXM?IFc~D@#|NfxT|Dkgmo!S8Xl$OLEIKGnUg!!9QquK+ ze%k4MFs5#b)DAx#PviA%!KBOgu}#g)^38X%MA9{z-ch>$^sV%|@rC1ng|MA1z2>sX z=g2!eK^rb_n{e1pQ%W{wkU9i2lhW$yR9M|~O_E6#vbAV_Oj2$=z}?SNE$P&RdF;Qv zJTfnOq_*xJff>K^5a;NO!x) zJVVrn#!Dj9k95n-A}*!}2_WNo+f6zWF3-yvZ{B1d-}-fm=xsZ@ol4qOJzpcyHTNj> zCWVJM{6(5sPsA=iRL7iP5IfUZwe?MZ4*IlfxxES!fYFbl^CPvDc#(SL7E#PU_b$aK z-VYVHg0@s3t*D%t5j_RXNbMe_JxmLyBY%@uWIkM>6qt4}Gk0GYLLydrk)nbuW9$BBZF9TR#r+STTF z=#-s#qBEKky(S`S(;F~rBmd~jeJUKK)otOc8B%--K|%7<9f_n?1%eW*JR(vBmc7~Y zWzb_y6Kt|QVH~ct=UQ&9G?lL~ncoZdzg~I$yb+q>TXl?=1gjsEs11aM;G{o#H*m*s z38fNQ*U= z*I6&V(T0gF=kA!%vvQG-Dt+#b%%I=kDh&sYyFU&wa($m8G7RUbje$O=3jRRI^-l)ZZ0&ibLBA;uxI5Xd|Zmgch@)0|g?!jl-!?MMc5G1@aNxpneXM(c- zTiDDj1L1;8Kp1z8fqZ6oZ_Vu~SS#Cc+>6~_>uTu`Im`1hDfhM#XaknNi z_|M>$;VZpCeGwsE`b6)q&BhWWVCc6y)y+X?Gh`e2h+OL9zy(K#htKcX=1al=7YLj( z2(qn-bmshSZV$pN5NW#T6F$R7L@%KN?LM=?mY5P#3<@&X5uj7^VZ}=qBHJ|S(PjEV zBTdtDeQ9ae^GrZmM}6KI@x!IAf`f&Pn)i#(FG&Ms?zdbEpdR&^-{So5Ej^U=NVhm^hSQa>D0bZyeYJWutZA0{VCnXvkOFg*+cbIR-Bi4H z??`<5Bpa!wnr+89uZuXI!Zw;Jz5nUV7Js_LyK{R7@msWSns{!Ct{F9LTw_g}DZKji zPGLqUk>r8KSL(-!*PkW&`-~k1g-N= z@iw?UET$hfq0B6ScJSST9OF6n!IL3+!GLaf8={^!R@A3P?wEVudzN4l+)iejz7g1`9 z@xZ-GX`0Y-{$d6~zo^!$M|%rAuC@fuzl;ebTpt=yI0spgYJo;}W_9^K>S9h+H`Cabp;=@t< z3Yzm?al-{b%v@D*ho)9evvY5N zE%*}L>a#q}5<6p?rtHeIg)?+<)wMV(mI6h#i|X9CWsA@h9k_LLsV?+D7$ z*1f-f7RTxG{pl_afZk7Y6qjymfW6W$sP{i8&}!j+aBwPMf+gkezvv{T-sTWKfQQIPo$tqmnGE)N)^G+w``!07 zU8jvnVyiGo_0WglY`B2M+AI9ihxCRh z{q2=J5%~dGYSrEfB7knQRY$ZTEM4e znETu+I%KTO7Qxz+K%B{_+Bce(K67febvSt*__OIe+uHnz*e0L7v%~hxY%A@9?if4o z=-LCYhjtQU&jF9TG5^<-N?L-P)IC`^UQmtUfAVq|SNOJ4x>giLoNnY#@((odqFL&-b^s=b+cQdVQY+#GCr(&L5Py-V8O}h7d06(KAfb z9afS3$A^wn&u__*eIaQ+cp0ZYIM&O*GbMzRy=sY*@;*r546R6pn&6lXX3P-i|5Z|OlS%1)V2iT&&|!v$DZSq3QC`e zu@lkw6{9Y}*uec*J$78ncL$9p!SM+535c7B>&U0aB+S=t1v`EanUFFH}iqha%XJvYhTQqAav%&D02)cxWunc>%cLsqD1^+ z@WueFj}A-P;Q)WQe1o$)raLC|H}%}i3-+u~xgRqB&Gz+gfn3qMtYSFl9JI@x@9<)pX`7!&_1>zLk=uOi_lLEoa4cMeMY77UU>c9@|RV2LRI`OE}B--I!c8yxs zel3?o>dbq59_^e?o551qi1uu>3+Sh=NWRBzym$bPZq$*Y}1|a>JG+m{lUDMsdxTeX36y=0 zbH5uJ+iPI}m?gOC0;X+b^W86kNTg{bj}OXd^p)bc6qAMRvt!i6T7O28=GH6TS48zc zUASSB|0%e2OXkmVW!(cZfO9=Z1$I5Okr(fWE=W^ilVQv%mWe$;9CqqiMa!4Je`}ha z!Oib{kfvf6iT#`XcrNfi8#989=}A0-jnV&oW9|(09qVD6gy)31VT5+>wP3uyOhwXs zqsE5qU~4YM+@(8jZKw%hUR!ee7mRLKLd}$EbGT*lQcM#U+Agr(c2y6R^u$uf4 zR&0co_xv@j%02Ab_~t$DQ_D|hu%ExHJgCwgV^&QEQ3rp9zbq!$do|__Ut$FmM$AKe z3TegJQy0#nAL1qm#Ni5Zm|!EXFUczkxuArB;fq#D0yshN3J8!uv3&vmc<H->lmC+dXIU+?$A>Cv9#FWTOlifBJ3-yk{xQF(XICZ#0uudl1iRnlr1@Ee;3=x$g zGLJ=}42gLy&ot>y1c%%uMQ_3{Tv2uP@`J12wr<;M5z=5)t{{eea-29(#L|VXbB_qY1=M$KqSz~6sffF;HL;98kd35>e??5Z`tXDb zn5w#H#Y&Di@ma;x@*5Pm=%4^a#Qx>oBfA}ybh0AxQ{b!gjnYk@5>L5-hwC8?aCGW^ zC?(w^RpM^QUY1<1`N%e_2tg=s^4}=gQ_1szPV6+8sN1*+EO*}E_DndDp-^dmsE~0w zjA2gbV=0b!UW`y1D$p3AR6rVmT>3Rm%@|}w<}9D5ZkR23{@m-6dfe$=3ay!S324r^hhnkIMdn(Sz7F zM=P%^0wr-1gyIymg3@BaJ0WropzFMVh0Z*kAOvKI`pvr-2s}rg^ZM^RmmbunzdFWU zlWnTi_W3hztbaEZF>gduU!Iobc=U4t;Wp+JJ)AQY=I$y*elO$M3;B1lYIOTmz>Csln%1F01ty?X&M9Y( znEvw^G##T=k})$8^Zf9q{8yC!$&0eF{~?+R$a3L1OLxCxVu+tSN8k2A+V#js<+twg zXrLdI=5?y+l^Y9PGT;q^zyrkv`WKfOjPU|DHwl!iN78zVY&dVT#yk+}RY!61@Ks97$Am7lxrtzKr@_ar(=aX;NNat|&I#f6%Ck z)p(CKprrYANEAK7F8D^a@!x6mHm|dD*Qs4D>GvMhkKKM9F|SQr-}1#Esa=8y3=9J_ zjeylPhDKi0l|~G z;)W+bl2n_BKo?5?6UzuOBs+j?!yZ(g(o#eilHsg9EPn%PDPrM%%qa$tAWb;w(li_a zbgEF%LgK(5rx5fna-2fBp@%y#4DIqi=rf!`9WjlWfoO94LwLtff>MaJ*)kOC4$w-fU?)vxE^EXt5X`uUWU~Q}=uCaXm zzEo2@dKecJpU<3=HVx=mgfm5v{{kQ4H_T&Zpd7BJqcDiyoUn%D+CTKH9|L>=^ehh1 ztEb2zLx}X~e^NyaEMsz@)+d$YU-dAIoko`K68x`I9T0FHLl{&a%N`kSK!7a58dVZ6 zO(eG7f#D_cii~&_6Zt~RSxFBnS2W-YLL@sZ(e!gf*5=x?>QF>h78D57c0^&?8^qjoW$mA(t6<_79O>{0&+E z%Z%VTT}1+>>u%3ft^lsbwG*jg{FioPFqUun0%l971Qv?H_~gK7J^Y7CIgo-c05TBK z$(={e9zrC-zd_b1Avk)7k>sP;6yflW>2o}{e3o<*>4{X~CBDIpw$tMB?v4?&e8oi! zgL#pYHLJME0Y8T!tVv?}-#dB_s`464f6WLSZe1>S{-h)3wn+8A;1C|Iym_it1fSi! zT7Uk)dZvZ&`;YCi#4TslhY4SCC?E(^uKzDa?}P==BP;XY;^-3+JiT)lwCI1ljsK{$ zu}8=^!ddOEhSwo-MGw3=`!~5_ut=WApYF`4%zm0UV#V=dTPQW`Gi4*^~?e%{P-sO3n z$;O-%jjFxbYNunLAxb}3%FN8$YIxnt%F*@8*Ncf1nSmwO`|5eIQPsE!B5?|}t^Y)^ zfX34aA~+r;2u`o~J^7KW-qifRDt*PKW&~I=L+;`uTEr2Mwfb+Oi75Rz)%4!SjJ}qw z6S99Q<4kKu+aMgCeVP9Kl~!S(>NQ^eq z5*Lq%^RE)nXpja#)x77{e0FD`@kn;a?L0|A znxZrfm2&eio(w}I4|rM8KAg#8n4kjB7M2!#dckfmE@MWl0A|*IY-NtYtgo{yB#5&m z_y22;*NenCm#6MbMqL>zID@BK~0|I2{6ggyzUSUQU0VSoxm{5Ik4e+se`FXG!t zsbjxVe?gw7h^*)b=08;+CF1<@#(9k2{f=l{#6i}#`L}t-)|UWfXuS}4)J}uQh-QEO zP13gXfe*I~T*>SAmut0Q`s?0iulh#CV1uuBl7z3=>)RDixS_(#Cq3S8td%`I6Oyr< zWsL{x_~aa#50SdYy{!QwODG`HetG@$9Fwno*NNx6Jpv^vGMn=S#XZfTq?S-BGhch- z#Wk$_n*|gjkQpcWITUeDSo2Gg;Ku7d$S3DQfkU~=9K6T&`y0kf`fs_WovGy4KUz#; zBHRx6yJq@w5#NjN6Fr0`x0QjT5Ys-4w}5c=LFirD*7T?mQuvR~+mY^&Yy8Xkr!62m zNj%nfpLpb2JT2)&UVQ#K-7&lT57Jy(KC5!0EvsIH)CIBwUz%z~y0FbmO>b;Wf9an& zcv%hwQB1sNPxp*DB@0^1UAkSKu|4Xoo_TOR;(A#8#K*ASowZ+c-}LRXYL{!mp!THt zx+?aOC)Wei%7g~yGg{G?4e82pT&-naA!(BhHGKxYX^`{?XFghbhU|{4B##@>T|)0S z9iBodg2|clu&Mo*5MOAChRrF!=5{RZ(uO|M+SK~{<{*^$n9cAcn=(<)#vXzMBr;)h z@vu1wweFZTM`UxYmasVy)({^K=~LhcjGGO~CB4DT*UCBb+MqVBiaXxO&r>tyF1z6w zNbZtz>ST`8y7xj!Zk}eAl44$RUhq8b8oS%%VKrx z-6o{qde=VDt6yJCHem(HQu>wAcbC-*aR6+ZPhzk@7-U4uJ_)~4i6{j zACsr|jm0LUA^Ym#_d?8dUVH0`-;KR)8KS%eB@zD$tNy;cv#DaPnyBwekjT|`t4e8m z&RJkBgXGn#imvE2_=pNx^*!#C*teP3yY2QteS+$WTVZLn-H4cL@O!AHy9Nm^=j-*y zkf=mbRu^Lyd|Rl^yJaa2G7TASwtBeXZXfRSu)%(cyVuU$-VFHvW)wHV}tS zqifBT{oq=HR-@nHERyqs;-_S&l%(!>I`QVFE?N{l^T7uG{pPLI)$IqE7vDD?L%DNi z+$H^doiC|Z;)|-MlO8h=9xY3(HIq74?v=K4zj`Kll%N#Pt?K=IF_e?OC8~;-dOs4F zBEVvkuZ+a)B6#gZWuW%SYw*J#yqU9PnpG@Ru3x;1NHPSfW7+V;^Fl04pa`Qo#_;zQ z^ZY$92e^QKF=N3eZ}wk-}I|tcM=2y?vK@Z^1ANY*ao)(7TUKA^rHNLf%Bz} z*bQRqjcMjzo9Kppc?Ayqxf{NJSu zYY!_<;?nRuke|8CA@6;}o~&9rh6-D;0o*UIR(ooFZ@E^Z+G%4ZkgMi{)+c{-Qnms8 zZJi3FpsoqUmffH6PnnZMp{nFBK6#okqX_D=LPz^+Igh*3a*3avq&#V0WI4!uoPs4z zJY!TgY+k91V^GwIR*uV1=^a}Z%mQqp8!Tvl?ap2c`iYOBeHx2A)}(yG3*Pn1NM&96 z&tt}^-7D$S_$kp@efKL!?r*)L)0D=Ed|sW)IR@D%!hc81V+=-!bz64QdGYZ0h#PUD zL&|s`86P31j>>JTu+tc-5OjCUZa7@t8CV4fJtt-NZOW zAJYw4^IaGeEejm+M)dvbKp_O2W=m7~7z6AebN zhTYA@JTDP}AJiH3XoENTGkGenQAB0+l9Vn1I8cj?BBb{;d~b1gsOY|*&}DDcx-)#H zG3+5&q7^o?*8nP1ch7Gw+hHXF#zJJNKaV!AN_$^izoYr2-Y*!wX&z91qD*$O*)Y`x$N>QEN{8+;7-SiySsge)kD|B zJ)^+8X`wGT=%b1^{^53wY_sBiajdtxfXu)%Ly0TsnoS=pwNR_yx#(XtI&LfVcyqfb z;zJ>u8MOjTVY>x+uIlyh-50YEpb1P=FIRIS^{0ybKiBL?G>&L&;~A%(;hINC?teDF z@T<@|>z7P}4IX6c^ZlKdsH5vZ48+L#Yx%t2N%<`SgtgPD{)BvJ@E<|s?>zacVYZ=V zGv#Hv<^img99la`TKd8Ifg}rExUGKI#IdqTlH5fXCkq-*3=-4so;#f1?G)R)_$agm zD#QJ3G%nBN*Od%mn7oy101OG$C*B`p$r6;9s>fqutx-&c0xSCptI%_=`3DEK;>j=T z>#4Ng;3X6Hq)PdPeupF7FUfWD)`qoBD6Tla*${69cmVygbn=3~y|>xmBAd~nR)fuk>kD0_qR{u&(5*pW$0FyXmIuZlw^laaU z5$A9C(SJIBJz!iJghVF+uF#P@)tv+MQeUB=_%JFMuz?#+P8ID9SxSAcZ}n1<;$5|{ zz(R|OdKZcmh1(|FeI3F3TH|s6H5a2(o5_z(4*AIsVJuMHKkCf#TwnSw+*fn7KP`@Q zEIga5vn|GyL&{*b8-%rIel|%`Loqegh{=y9EN^s1oqZe0>pscv|Jc3ssbPf#j^UC+q-n`r| znu5O!;wi)Hp8Yec;%rC-KGc1niuU4;U7~k2zDb{Uym8^2nl(ZO{nEb@I{XO;#!@|d zD@+@9JUGzvmWVC=+r<7?R%fWfmLn0CL+*`z%n}&5RNSS}TEj#kOXzFLyMtE=w+Ee@ z$D!6s`E@-(AWX=9iGC1#>WF27+^gZr(=`kCa6>nku7-LXbV3blv3CZFJ+6s`(B z_A2BZPa#ag&wqepOmF|qaxU&AoO!t++e_u=mE*GAJ7X?O;Y3*68g<*dgp?O?vTyp& zKS+F8WHYO-MTO^MQGwKA*Jrv@jRa#JwuPUYL!eGhZ!8^h*RS-i4WcT44|LU{84%bh z;OJ#9)4DV5tjDh)Ji2bB-KDE=S-JkD({ACiX`}JS1MD#G%Ax4I$( zgJ9eiO@3RyU0Nre>CCE5lY9}7_{`C)H3ABXu)X`a`d8rf;0xz7sJ%G zWdrvEz%OzrR->4q#fj0V3f=oDD}j;wDgl)TZ&^o4b9PO4J+SF-J!b%a9qy7901Tr;NO(-`HSw2Jk9NiBBGx3kSWf|&wsN3V!2llPl2A8cb^RL6g2Jo^~x^gvcI0~JKSI0fJxSNo3 z=9-;*OC*{T{cZrLX5?IJ9jt*Zv5BtUI5xM@qQ~Yo@%+NStDrZraS)G;-lZG-D@uua zitE6fXsLm?)YF>S^Vefi4i6ya>lK_%NQdJ|*Jl2Uac=H~vvBfLet532|1iTw@3LFAbr}#dOaRT(!+01mMKA%^6 zFG^3^^VURXV;_kLp&zJS#Rl)o~q%(vHL9+ZlAwhu~W5c#indgr%SAY`0@LZVTB{*TTjM#CLN3vzogRi5yxq zPCb=}zYnd%tCbzAH`jx?8`1@1cpk* z;PF#_sLQ&)a+S%&RbjiISygo7bnuzWUfXfuOW!MduXKu}o332N+>c&OcH3oJy~;+f zR$os83>aZl{f$+ZYj{UjMWd03z_u92FrvYeu?RXTSYR;D5$}%OA*o3oP}B~^ot`#P zK=6Nvw{*}x7L4<8l=F_nX=jkw!Wz25Y=(@px|^vDuLBPdgDj6jSbueI~Q>s#rV z+&ovV^!w9)QNRF9@QVVA%e@aBiNfArZ_t;uABXf(=<}h$we$2-O&IDfKiZ(vC>yXA z_78zrwdN3(P@Za0u!nqD7&g7}SUO^49sUg3H==OhaGHJi^tUV9(<@kZ>{;bTAA45+ zsvnG4Azc~kh{J`He|mbmA{;u$&5n2wG$7}X#OZNPe>SgWCF&fYfT>6n4w$Y4z{%Ph zBqTlH*grT|!}APFMIiKc)D*I*5h*bc9hOxGr&C|MifS zEg4x^y>PF(CO3BSulhD06j9A#1~6=MySOz}zJ(T&yM7+WW?>z9xW(g?$LT*;=nZXN zj89mH3lzuJ_HaQe{B4T>vw}w9C1icRl=^qEr&p<7Kdg_s23n2*n+*PQ#PcZkM_c73SbuOp1#5H zJwCCw?)5wWO@D1>P)nXsqA$mYm8c^_hFOm2dDwpxr2aRgi2?i=xs;Amz47xDJDAm! zeFPLT#G@{W9+8y zpWKHr0`CC+AbQFfnRECYo4h~fTtbbnACKgHV1e=1NFE*@%)e+DZb-xFzJG9z=*+L6 z{}&5mHMYIDPP;`lrL6M@5<7Un9rWV!n)d`To)HRQ*U913zWN)6DW~+6K%K|vg(JiM z&yWNPy({7vy+2RPBJC1KTQ^GUn2xcV90b1mHxSo@z&qTqLC$PSK#c-EXIA@<8ii+y zX>N%I)uk9^CF)OUv%Oh3?Q#Gm#9Tv(ihB3xzRN1*7!J^G%RmT|w1Q`sN#C5iGXXU^ z_J5poIGBg$?@atvqgxgW7~1njup3_J!}vR44z!#^IKVrwDHTW>sgME&e9k|P57G=} zTf0vUj!(PR2V^x0lSD6Ex)j_V$wqO(9ay21_!FZaUq(gAa0S!IF;tAOF{acnUcQ8I zR7(cFztlZFW?|uw{O7_6@tC#tpgtzL=CiVkcCfEADP<7+?SK7-2F(d##K1SOoWDDT zhm<{YdxubmrQ8_pZ4WT%%j%X7a`qQ}j+FC$M^VBl*7Plr^p#bf?y$X2Kk=EmT-@|;(gR`!Tur4j0h zr2nvGAX7qu3>!bAtN{Y9Am|RkAEz#96Ob!{_`w*Sr%`vFgvy>wk)x1AsU0&CsQ(y_ z-=u{{bo|^36`&Ti$9%-?lmDo6|NrF5Z;0~$Cs%-0`TrWZ@_#lo=7}=Tefo7K6HB2Z{Z;37{$^Qm% zSocXVQDa_pQ1)Wu;0QFJz4D?*cq`?nj{bB*oU1mVS(3C9OCFM>E0>NTr}V$tN_3Dq zBjZtOWZ&5nqOn3XvPzP5%h)OCh-pb^AUS>%aZ)H)ct@+r0n~IJ81Szoq#2&eIQUH{ z(hM2Wr`d%!_xY6Lg>c01Fi2NC{v>a1%$xfJ>Tu0S)8XF-d`|Z_6b3;FayC`PAn^J& zab^Z5#!Oq^hNvzvrW*<#LWJwCJE(Nr;KAPxhKCy!@!j>3ctYlp6tvFo#jBool>U~0 z9KW6U@yA!4#!aC$r4LrqunER2Nl$o6!%03Av@0$Yo||x+CCv14vMq~Vm4hVu3-=iS z>bew4CX))mkQNFOf22Vd`ie?6Wr_$QaHED4D#XHx5=Q%kT# zn42WE&a+igu=-)gM+4DcJ!HTcN9$YFA?v9wy7eyHq@9LW>xLY^@vz13CUD<_pIlP2X9Z2Ur-S@;(xc zEy~6XpI%EBH@SQ{erNQT3&%sQay;a>{x#$_(4H_U01X3QVMx1)fCIb^!yGG8c<1j) zhf&Bd6!mlRmUsj6A-hX0Nn5|P{5Vkw8Fj5ow?kp`{S3z~_CA5%BsFGuv`cL^Y|EM? z_y2UX@4&Fm;K6vECx<@eBpqm)64|^!gS`*$-&2?*RV|4cJf7}ARPdcG`_hhc7#F?t zFs1EC5oSH?kyw$VYk%#YTKJOnylJ(k zr1*R=pim>N^h>u*1Bn6hW`g^uP;x|UfPgs{IxF6!MfDxOdUayn!?xO_u$)+$jwVb(dS zpPuv)_H!%aId2a`7|`8*>ui2}?096Y?GwVI{tw=rEQ4%;@x>Q^@;YYu)94^i2}RO6 z<_qV(1<87OEF+&T5)H}<$JXJW@7Ff9Lk*95ym|jQkIeEcgQE2L6z4o-LN9bk&^rHW zL+7|39p;)HBPd)32~cfR0>QB?3Kap*!~HeYBD-49LqeoZAgh?ANmJ1a$VoQTI&ug9 z$~h{1udq!gpg7+Jm_jyaxK-pmc;I*wn`V|u^CY%<5(vSx&BFk--CvFlM`{Dq=Cx*>Ks7jk$hA2w)xXnl4CNpbZK}PuHp?wVAW)7@}J08eGacgFs|6%=-r8cC9H>kPw@g3y`wrAh(Rs6@pLMrdye?&9uo{$kXG>TU$ z#ssStCq2++aYze?6x1-2_)G^SfTc(SDyV0`&^@1a8yYsdJut8y!|;^aFB{mhOD~U9 zv$3$SVAetIac0DMITJ*{G>Yyu&wwD8rcRv5N*&f*`F?@0Bjlp{wa!7)Jr0A2LntDx zH#b83lD`-xQisOG;fXV%O`t`hu zK(7}LVYXJD_Hhq0x^tz7**TLd#yvjN#w%F%a#Q2Wi*uhVa~%A`%T4?9Z@hFH+0$rR zF6>;m)+qa8D?4f=rkVKzUBT+jXGuYR;X9oYs)v&-fD!C*tY=t54YK2FrlQJ>!gP{v zl1)d48T|Gg_U8{IkjBj^L0?l9$|4e-P0-Js@=3B;GKR%(hu>o{5*>O42bS#VQ|Q}) zDONw)Z@_Mw{meg#KtVl7f)+HA5bc2kkbvXAx7t2n3k~VgSNA~yEfaG7x$bKv30qKT z@b_rIAls+Og0c*Kp9K^g^5zxODDX5_rX4duTk)BB`46fmf1zUQHyuJ{p?_X;Ou=Dd zx8P>cwZxhFoZIgb_GlJ+?uUrVBFc`t&*uk=A1XOP6=%)V1SFFH#?rd;0f^GaKiZ>! zdZ@l*L+_Vg$Nb^!`vue0i(WOmR)i1B%;opo4`e8txcaj`-BQ%b?YA(nuv5V{o*Emj zvJ@2+zG(Dqay@z45L%Lz-rwVhjcuLYecO>@=r)q|X{0NaL-g>Iz%X+X<1Q+gW+P+w zwS}G9P%E8%cN?0zh^S}G$RLR)gw}9`r{N2~u)Uh@_uTRYP(n!58NIniFZ#_XNSH;e z9mbi(s5zS*u{Iq^0E8s)!3^1{3nJzzb65SMFtFqoD1wuU{g!Hq!fxKx`bbs!otlE9 z&O!>=pF))v;O`G1Gpg_dku!>%Q+hQI_F-m)w;fzN)7-r;wq%s=KyyJ!48yx{I$JH( z>tx;IzHG8fhBXPCl?yy^!uFh$mq6rgeMSGFJ~9REtC48L{` zD&>=X8md|wf@`0y^b^p69HdU!Hf=bx_;)uvBh(thK@(HafEdKVUrJV#A8vqpL;n{r ztKC5~S1w$+q9N)$Z1yfELQG(L0`v=CNgcUcNjil3K8#TPeun44b9V74NP~1#Cyz2e zURJGStJcEW-Er#*CqEF?KLYt z85is*(mITdzU!guHPAY*D!OQ+TO-Ef(7~Q(wx;xWjXF7p>9(FLgQ&RMtKE0s6=?VO zDwpW84+&M*M&eYqzKvYg*&xdp???y1gjTMRZ-bg4(V8xuNLj9BK;uCYpn4r-PCbIc zISI(5sk^ehWSCK4h(vsDq}H!qp^CHv{>S8p&m!HDLL$LDE=oZ6)!6DwH#bsCf+!t< zzD>8x+!}N)`xX#`pc3(0<6hZ=q(X7v`T_!$rcI~81{$h5w38lTbjz`?V&P{YGBwMfj5 z5D<`WJgMkkK$zTAFUjfxx5YTFd39UTmth8?{Kkq)nsu=mJ{=y~nhzv=DZHI|atxJw z<15$qc*EGWYTJ0yj4-J#lE9SAKq|E@l>r|;F{5tXT!RujY3jieXX>j}W1qSVjc)-6 zC*_D^%xmg|Q_W*0>66%sx0$Nb`r=c6HEG895}lv2t^NMQ=U`CYQfS_yhwb*jMO5Y| z8`wa4(6A|qXeU4bi~i_)Upq!z^OQO7O3m&=!;$n&f=$s#jPD{TveJuZ*NPvq;)>Ls+u*rd6awoyYSfMii4#? zvPVX$$?^wI_8PT>wR1l1FNk{t(p_+N4~LI#bl+<>Ui^~W*RP}9Zu(vRvf$eVno|4D z5cZT{3NPMoj%4w3OWV!y(E7g`9hc*&0AR^*~oy=U%28PbPqn=PB zOC{5O#L=(J{5vgsfH$|;;r4wPd+mZ>JI_#tMGdpS8)Oc>J=!r@MfMoON1zMXAZaRa z<_A!)H+~p`G+^%jO4Gn=3s*>aKi*xRMW||9T0A^`k#CmTf3pB9lw8{F?d@rxeUZlm z($*>5@ymZflT3P;WNx66kd%O0+|`bV_XO2iiz4sJW`X|uaNxs~g9C}Y!WX5TGiJ2&0l zxV5TlyiG`vbGMg@cfSblg30)B-%;36mn}_V&f11?>62!>xuT#K@461Kk~55wU3{pj zh5g({wkVt_N0B$PctE4IKqUWF#n zJd;`Gh-Pc<%LOIzGV!A_T)B|C%c-Zs=fg>#I&w zf~1kQNPL0v^7Z^e6DHdEiCM|5QQIrkSFOXH)+Z{6jOP+E$-Zs}kk{n;Xl!et_W}Kq z%`2nfb?-Q}v9M1fMzXU7E}+egsPOfHdhfpCjg82SCB2C<>uaD0Y0$n$1BcC$Zs<*O zV7VP)cX^Lb?;5Cq!|9F4-&Er{@ffHdiEMp~eAu_v!sBnWjskT-JTy3@QfL@yH&R-{c#x%G=fjTv5T!URjZ0EsHjccXQTQAkwFeEt}9p8l-&W5t*- z-5@CyQ>3BS=W=JyN-EZj#U>kx-E5igZjUR8D5IsK6u5Yy<6PZS1nI^~b}4vh4vBN% zW3-zLb(dYDGj5H(ZAEGMsM-4QNY92%!;$;D9nYtSTUWpLrn9&n&OR9yS|uQ#%<9Ne z)Us-+cktpRf7@o~c(FR&q;jDyyxeg&U)GNMW5(COlGavcOWTy!TO+TM^b0gPfA#a{ z@g1BwslEf`Y>t6lT^rX|Nxp=JLZR_w*sF|A3p7p(sm^^RQgT6=>vYyCV`Co<=5K$| zI$DZmpkG^`B!Qrpb9w5Bl&^oyh;D&lN;Nm#KfQOFplfIw+L<3xBs45 zpg3*>ia_=cSwNbq&=k>NM|#v!z^}pmm&JQ%6lj3GN`SXqy(T*o;Qx_UU#0N+?G%Id z>8K1Z-iv!KJ<{V1=&q4uJHLRb9#o$9r>Yt~)oz8vOuO}jj4`5Kj*>k&Yd zoKJ92bHt*0<6Y90uNyH*PqyBM$WV=$#>|yUkV^^CC}9|Z7ct9 zwOi19(tlq6Al~fNyC*52(bi9J#3>|yzoqKO95!_l#dc|UYf?w~Pk--6jILvx8_PM? z;%ZrOW30i?Ij6VrpcOstAfgCj^+5&iJDn0!HAdq4#se@& z?`>VYWb?H1iwjR|D;+!&#^YFNqe(#A!Z=Vd0~`{o2aqs538dYC-lv}Ji!EB4;Ubpa z%Z+=*pxDBWpLU&Q@4nRX`$=LaFdVa^M~D2P5F4Zl`TQl)buwxO=zox?Plfn=NGM8m zeXLhRVDVdd#317p0@ZP|VB(%J6RnsZ**DVr{AqI%z`QIS%*d#fW`<`f_9ko%sDHiE zK7H%?gHYV$q^^xk=WGd2%=M+=r)r~>71?=Q0=LjkD}e_lar$RYzL~M@^j=?f6GTs0 zOynsTxID_<<(Iq5b*oYe=eh-`6^$i#=38RCEw90fVs~Xvbnnib^9fu5wlj8QizID$ zsmd_%SBdQ!Zbgwn0^pi}sIH)194vCKOmJ=J4>^uU0I~%c$sxAHAP< zu2>2X0_&|Jy0oO~iaVt@e40bPG3WEJP}wVz;t{RQLgi&?)nF&BgZSQ3|NF*c2i1MG zv4qLb>qk=^$lry`H?fJ--S1neZ#uFJr~zk-njz!zviF9~D1&~VEjPrnsKZ!|+L7;% zH`o2yvzH>w_WJrkA?SJQl^t=(3U#*5a6M3Bhc&UpS1-FN27(9Dyi9N)lvE7>J{ zcWb%u7q~{j$I7_p+96C;0asX4T&{M(1yWr8N0TPkx0_j#6D*gjw|b&<3}|i~BL1?J z@2MMErYo~FAhnzcFk-is&W{8tr1RjDitr-ugo(a@VWwi#pK;2Mclh@{x+vlp2G(O} z*(tEgrz;Fa@tXe$=gN0GbV1*7tEdU*w4;4Gd*6rLK-lM+a@a}u1} zSjOlpb>tAL{uXP)?febD_Qj7hCAz8E;m7V=3HZ{5rJrcF zL{&_?HUwEoM${RXA70zEy;Ar>duPD>z<*meo`bGmQ|Dt}t}8+H>QbwX!43s!2H7IE zpE%?*&c4SkIV;a1d1pt2SagR3HKXvg=AjYdj{~KR{5L(Cyl-_+Lsb0Qi0g|3h2=rG zr2RP2R4aL$XhH(7+5No;LlsCgUBmDBP}n-Q7I_}jKou)TV_Md1FCY*glFb9$KL;Y8 zT=Q|by%&SIEt@x)uK|PJg_!^^GQ+8X`GX_Lo7>o{l<2FSwJ|N#E2^RO`PED5dw9ep zm*m;y?|)c{zEIMePxjD)yPim=pX7kkq@Qo7bo@%i{Fk8jbgl!gSg>Fq;O!O{ex7#Q zj$6hCrsjRBl#bJWtR-29$Q4smu-m5PMx}MJ>WG8=SL(UZ{P-^&HRk80M|aLGmBiPG zW*3C{Q)Z0!{rr%H+fB$~r80-F>as|%w^i&LtugtrFoQx>h^;nP)};f3aih3UWM?uY z!6XJQQ&s*^T$nOGy87POzMnuGW})6~!P$uVp;5!S{p>#BeAl^t#XO1-p;0TesK`3h zPgtiC2T4HqnDo&p4g8+2#;;tNkj!t^5Zpkrc3emdCG7-mC3ER>F!g7++Omqo*%T`97Pon$P6!Wn%rPQdy)Sr zv&@*f_vxOymbkS<{lYVoVpWyrRqU|Z8<$vvgOdx>=XLdHpNvgh9X4*vGRd*u+MKg* z4v0n(FFlV^fjpN`$%Nos7<5mKb6wBiTeR{bY>4PZ1#1U)W-Sg|706A@tLT@G{LijtS|y7$Hu)^%$#jI=8P8vi|(rn%E{A1XVG#Ag|9-&6^L^3}bn@Eu<8 zV0QnQnE&)(GE4V@P4*#UjkLD*!DS}4X)VwTDoxK?k5r#Dp?z)9mXY^yXf9sz64KVZ z_#^%%3dt>!gIldVU41oisr(@Mu{rU@7#<@waGx(|&O{NJbiG)--QSs5@m&&&iHYV$ zD(CE_$3Kdj=j{I#ZvTv4+YcX6bWxPEsgv2xp+qM+kh_*m>I_vlV!bWjXO7AT7p2sE zuhXVS0WB9}(8DTs1FmXNugM*$?C|9;c50N0fOFQrQiy+nMziI`+afXIf zJ!n18l0XeqXbX6$b=FOEqPzRAkN&9OKP^5gIMPm>mIU(!Sf45U`1Z(A=I6z)i9gA^q12d$VnozQ)- zGEF~L@5z|(k~6MQ#dIv?gvuOIsw=)yywyTNAI~ z3YvH#=ADhGR<5r8!NSAPmY}xD*;Fx6O+j$wWg|v)uNmpImv>CoJ>8paDN{BE=Age5 z(hRCBva0Gwkn{SW`}UH}FF%TSL-)7Jt3^GKd{*~9|5*yM7smWo%ePI1)&~sd>?hCH zrmGaovp%bpc=PLrZ9jo~l1^yLN3K&@S-$H!=T9=Yi!gN5yvp2(GMj1E8P%+n0vU1x zutj|RKM-34+V;PN=%7X|sP#V^`ww`SZD=SEf5Stz#bEO9u+fW`nE;OMT1M|qjX040 zQy)5bEP^k<4Aoc`Q+5rJK$KYQcm>B?m%F_S4g1V9ok!i2NA8KO)<=$!N$yVaGgrj) z$4h%Zc6U0vr$GZ3OPbXVjCkWYRW_McYB5i@{qagQ12MMy$hBt?`8g4iUF>?O>Dcg5}!JT4C*4ogc-t(#aoIZ zGx$qpdUll~X~0ieh!eSTM??R`!;4CTnghqGPSk)hBx*cMiX7Io&pkkzZ4=$SCp$qj zT%3a+0@Vu8Ye0qSL9M=H2kA)%f!CcabZr7eFn`3OfWH`TrLUsKo_i<+rd;mbRO?*F z{pyKuC$Jm+3Wm1-qHDtEF33)jwc^jaX*ryB!aD+H(ai)%VPQf_sGsI%2ij|G1t!E(wx=2zKue)Z22z-hKza~tFiizT*O&d znYc8v9y}P`xJKELd)@fbZH?A~>g>lw6XkPncLF;f52kPBX-I58i7DaGO20*@?UQ7E zFGBV6Se_wOhr1F^*TUBWti%{s<~Eui>}rXoeDBuhq_Uz!8!e+N4_=uVuQu3gx`}B$ zQ`KqX_yHNU&a8J&B<2V5)o%%={EXn*B1=waXzP@ul?ypkPB(Is79EPV{B)Ct1+q}a zKg|f^=Y9)(c>!)?o!b>rjG(%ku28qyvDairuV|ByKV; zsJ5KLjI-K=p%b^(wBl+klii z8IF8&rnB;m`)+}>=ffn_s3i4!dmr!kDMiSW%Xmx7kv<-L&!!v`d+y5}emRfNF-IJ4 zjjb>!MR%}^cIy!S>Ii>9d<9)yvS6)P%f8jd*7lgli+5cuniwuD=$qsRDCs&V%N?8m zR*DzlMmF#_(Gee+GQB;R5OAj3B>T#5aK`)y!C71x^Q{$77XnkvX)ev=^BbwV9%nnB*Lx(LmLdT0qGH(a5%W%f5JrnD;$hU1)gAr~@&WDkY4MmP8$D*p z0OXs0MYO(t-_=;*%O0@;iVlWF1Y0rkqQb4bGL52%a}$mtaIKGN?$g0dG=recNqyat zdwLJY&jN9Ghy3QojtaZWgjnv4AJYH*aKZ7s>3F^`(%Q9h1OL|^?s{PBA>XdaSzWGw_#Is6CCB-L3`C z(W_>?WPc#t3?;-?q8>?Fkb2qoRacLfaB{-Ud~2?1tW!pb1JnAO-8!Du#eYpJGfbHCq!=qpX^OsNP#-lt@keQqPAE;@1Nv{aN5DH+_Oj?5MO<$$`Qzxf&*3}b4 zoF6~G5}IZbUPRA@86=-%)H%_C(0h`Ye#A_ft0g);`81iq#-gw%-ZuoIh-MT-Yoyq6 zWbQ!ocH}?!e+!f!UxAC4ZVq-e{)MX>Vo=9&TQQdyi2l_k|(e; z@Lei?&??g=A|Chih&-l)xn%p}t9+-bjEB>df1oaD*{c`GDMKEWFR>6~ayU z1?ve0W)9eMo~Y`OGH(xI_)4If2E0RsA0pRN6c@-fO4gmhBo9QHf12BHSIe0&>MA#i zy86m28y_CT9-D7@+tW?#P2gODdz$?-fl|w@Ts(&WrA-wjgccm zI)KNukWqyxjJm_;S$iZV%XQsQ<(>V7`wwK0MRe)R+VArh51!s_g&g!+9{~u>`;lod z8Ggwof=iv@DKyuGhEBn~@e-;(J4qm1yV5|xb{*#Ow4GQSF{UqRs2;Z2h+F99fff1H z>Wtj8`BGP5I;v1_95%GsHFhCNOQ5LYULHE{#jD2>^bx{?i3T4(1SEyS*C)PPo|F`j zQ?`KKx0e@GD`fGp0G{Q9cdo#GzJLn#Le`MI5cs@CK4|qActN%W7hVSJQ)f}-_!H+p z8}8C28vdXcs?=kX(tZ8}wy!+-_S|st<4K*P%)GeMVZ0nq2qkIL1lD+c|9eISkr`cm zrbPj(cbcLPA6p%u@CUh$^ouyht9NqJ4;8#EFrIT7$WpZcA;!uCtn>& zzk6F5bdpk}BjsfpJ3O^H!M^P_el?FX<@dO|cBGL1`sO55vk!h|2t^X6rzXOmpEQuA zla%mr1=TADrKlO(^+V#jYNqC-pFc>to%K*uWbBM%xVSByDA1Yq8jynYL z;PU$%kh%;K)7_Lj=|rWBi?Q`vJoulYomNl#TD+(OQw%Ga76$)j0XCUZUq|8PxPDX; z$+;2)&Je3#U8((m`kF6VZIu}(Lc>!)NhVY_Aw^H@=fhCk^$ewHa5VPC!CcwkfEDV6 z*^2uamau!rz2|yuiZ<#FCj1dam4^@vxeo>K^R?m-kGN?&@ZccExb)7_DJPRThq?ZF zJ4Gd>bOa>-H3zf~qTt593|C8Chu_#fz>AeYeZ@XJ+=81R4Lw#%o)z4)@O?&wiJ31Xifgek<3H~?4Y&VZVAgV4W|&1)ZyCrq8Q!_OWA&)kXTjWVYh>yX&(_bx z>D9{*=a#QG2uIi(-D^I15V!{S-@d3h*u4GD`OEv&R?QkG(Om74nqk}b>@$}laNljL z({$%5tKRA3=DnAF3VVto=Is-v{@16IF(*Gf9ZsaW+hgGucgQosZ47M)Ay|qx!NG-6 z%!ZMm>n2?AOKk zLXP{~2fW#6vwi34v)z@hdwg)?k$j)l>zkJqBQJ!V-AE%R4OOoDAVa!U?eZ+3&$*5( zlEq%E(RV^Q0@prVwQqZRj4=0h4k=Dkfbq@R8)RUhwg^@jLJ0ZkGpuAjTYC8>;$Np7 zNv8Ye)qNgZ13^nEj-PPC6Nnnu+zMp$uA_rz`WZt6)}8?kd-eWdT68xoI!MU5y+q;R zXDh)0cQ9C&F)k(?xsHXYeGA)lppv3L|JF0OKy~c-5CV4tY&ZeerF_f*ICsLbC5UDg zY5O)wm#rU=E z=+kWKYUNfxknAM0Y=<#i>Wth9eEG$D%gKO4DBmZ-u>AbMXSZ;(-oF=8@!4`AjLa_u zt)@utKN$s*-5Dxt%!e`or&B>~!hghdOaI0rMAZbGzE=9}-T6Y}$(Gp=**g>N4V1KQXxnncqA)|4=H8_o$sn?!yU+jhZ){hSN z3LO?f`H}Zd-HC~tqZPJF7KOTR*;fa9vs9PaO_8f@bj#g1T(3;&L@!sarM*+8{g$;y zV%SrqwKeNq?8s)a`(Vk2;vt+ktP}$78^rlvEyHPDLu1I#of0McaFO6Vkzg3l5JWpj zu9xOU`Zd*>9WhY;U;F+-8);hUijeJDH`gCbZaRcSVv#sxT9(~9)9k?HZGp+FX#H$( z@;p)Jc4}!e!Oe$ceZ`WLx@9(J&nd_Ckre@Nrw-S-%)k|26L_1F-njluS-qJ`=wATi zd4paDTBYp{RwF{4ntXX}bGYwlWt)()Mly-iq5*~tbQ8I5jFSEDcUh)ai z!}@ASI8PLP&!`tKi@tEm4@jc6KO!D;<2NXccO z7o(d%(HeZ8O5AOq`+P9yFA1I!+1&qR0otASMrM!&ptDgtJ_VK92}SZ3IR+y)`W@%` z*zW#BZu*|FhHKHDt@=6^-|`D)->1P(-nOn=3lbL}r-&Dv<2SuVG5y+-hQi(;@@~(Y zEt4OwS|ho+$+XzRdJ_xMPJ!9L#yoqr(71Qx%DTV|nd^#y3&ADYQIIihxnir8er!7s zGtQsPTPX{;`t~B0DBH9!V?J^jND5jsbvnNI}DIZD%lsUCno434GYHa^ox_V#9^ z$3rSfB97j&OYzLoIRK|mJRm$gf~v4FK>vsalAM@ZA+VJbtf#~nyj5=_x1*+AHGXs1 zYkr)9v}XU8(y`d-htRVVa*I$CCOfha`+XZ(;^RQo?8hJ&6j|~-nA*ik3Z&Ya6bN1J zVJ9!uWhq-sXpn5n*@>CmLxfUuHQ81tof~`znia@QamUqMrn#PloA>Mzj$Z1$(>KpR^m??j&F%j5Qr;?N1GGQE=n*XR_u z36_la-getxmAkt>CNOxU=%Gvt${SXJ_BOV%b_prpvnhn_OebnXr-B{$(5^u1+`q2SS59efa2vmWrfh$cH+jkNs@T95&DrCi!;z4n zg>%n1o5&1*50cq`nE>ON0d{sI#`XX&4R57^@|i-Tz{~75FXSj}k3IkLBgKUU@v4%r zagb=7@B@77C<)B9)bP8f)Z(8?!n{6Hx_C9im5e?+%oP#5gLQSXxXu$1c$@}R7Yg{s zId}yuT}>y(1{TkUWxpb&6Mn%YuF@8P2H|rj|9Ne&C)WVWqi!B_s7GH~F-aJ-_ytW5 z{;e(t1cvkVX21N;nqIWvGhP^e7jlxClm3Ij_rU*LF=FRjVF@%>SsYBiY)8t zV}|Gwva=#|I`@C_ea*frUfz`CM|8?GVZ7+g>a)VY9k|K2N$Y}3u^ik^C#~(4aOpN* z(Opxf!V-o1jlM(^rDRa%80+QP9twwE4F6uK?d>W6Q3JOYTL|H1bJx2cmc<=bBx0_{ zh#J~AW~m)-eZKL*LMBVT+`;KY;i~k!E5*tiP-O3h0|%QV>7A+9c+8s1q{f}?NV27n zyrzes>~>&snVeVOys_i>9avUg91_)th z@N1zfFD4VePG%R3vJ4LKkowJNR)td5e#c$K|zMSiA7k2fD=w@8cwA z*T3=sJdJSmW0o4+`KPSMZ@IT=rkDUn{COg=YEMbSVuF_0x4H7HTna-vefTbW6uU}e^i$&FO! zE~`Sgbd|Wvw#90~UVBS8HJ8C-MwrF=Smg%?F3_$tJ67+FXpCACDLu#mEy9T*q8i#~ zUj@;7vuK1q0jUQ+K?OLK>0WT?EtswP)92|FQfa*re5TBvpcl*Qu>WZ|2?%j6m2Y>f zNs{lSWf^vu!i7{EpUx!=3N-4(5AM~yqR(HGH%Sz!Wf|6-j*uS|Szq{irBz_3iIVT# zipW&246~Bx+ySl9hW+yiGG*LPz$__et%X_yQFXBYVmTJ_6o+`c$yHvx+|BTbG6oGLfr(eRB zX3XfBwyr{+83n1Do*M-CGte`ZLl@hF^9DJi7gS|=jqkXVBimMUH<{C>0~1VmHIsA< zZ&l}+OEEDX{nU~!#nN!@&4P(SBwGV1*vVPtCHC(NCKuFa;k1M*PYUdbJ z@8Y7+JDuVO3YS%2B7+%q3iSND&|x8$^nhd(5Eo;FaeG zFb|_J<$kbj}BFjk+g8yWhFoE|Z=y z4$Ey?x;_63PP>`?YW1>bRUQ5K-J~D(+#wd>deo7?R`7Xz=Z7ipy-8BK=X&QjVtEr< z?U#O8?`}Q9PZZel2|q_%8q24ywqG|G$$LXYk(7(Cxs0a#9$r(aKF5P#rZDAwRq6_J za?C=5XQW(qUWPRpeyT~m)qaxe>t6{gEEdZg6809iBl)yWwZCku8iB0HQ3h|vOihI_W=o%`Y=5kR{mu)pYdvZ99PM$gy^d+PI$HF6TuA1#@ zF*mn0LqQ%{ut2n1h_m`UQH`$m|Ha;02UWShZ=)MT1O)_9=}?eGh)9E!v>+X#C?b-J zl$KTrX~c!n-5{le0)mo)bccW--LdF;?+5qy%=!K1yfg0~?>RGP=AAu*&fe^`p63&H zT=#WfcbKXPhCpAoCMBjDID`Hx43qu4^BCyY(7Ji&9s9eRS*xFg^uD@kKdN8r+Nw?T zNFBI?!KX0?rYpvl0Ewff5%IGop`gye**NmGZUU3~e zUe`~q#ZLq|SkgrLnxbd#K9m#K(Jy#8znbfdk93&v^j~V6)G5UnOU)|0SvvlHMSwTx z?)B%LukkWS%%xhZqo-==lh8VNmdDt^VC7P;5Uu_?Lm4J;)7Bq*JgcG=xGy*Fap9<(SD1r!v1&S9xo;K9~X{q zw@E~7giV9S9k~Y)0RbuS4CvfzaA+t&G*jvH*^5c!C=~+lhF@Ro>zf!wNd$DCO)w1Y z7KhBLbp5Wh^w|Mzc~+G;p_(Lv>i6}ds(F99EV`n2j00yGP~!DzP++K`t(kt^7{(x? zvhn1-dmuilD!~L~2J0Fkx0Z&3x+Qi3V-EMX&#}nG@O44Xf?-j<;Ke@#{Z6lo{K6be zK7}QC&T*rkH;dfD_#K+lO*ds$t@iS{JD$@QuEi%4kM;>$Ga*-krd6Nt2JQ7T7(e5x z$wMQ?-L3(ZnJ>0Is&YXFF)#l?d>L~$Bq~&2z@c?*Zn_|Gi(M6AXP`O~on8%a4F+5p zeX%KP;bj8RPsrQ{5{vbT+uT|^m+Th?Hyo-$srKQ280!dhhqPurI<-EFFe?dV(Dm*5 z>6f4q;jQooVpu7yq3%+v;ku?vUPrqqWMpz>93NW4_AN|&|E^G=@CQQ42_q8gAo4^L zn8SH)ElS_?lPp9fDRDtgn!aM>$E0l;#445v$TU)+%@?X2a0UEsYCoeFU|&Vk#pxEC zNkaB~Ijkg#?}lDEv&wQ|CRg==2BqMl!op{44BzudmOQqO8qzS{*mtd_E!SmZnh%6> zBNrhk5I69_gtLS^`eNt%#hOL*p=W-HzU$@d3F}VAUB9b%x_?G;ol}80=c1v#K=bN? z==M^7e4(WO43$34B>VR?vMG5go4pqR5{kbFWl3dD0xeP#|G%f1iC&fXp|rcR5qkt%+QjF?QllR}?LaI%m$*<-RA z-{Kt@XvUvvL~(-PvR0hR_x^jUB~-7^Gh{$dk9=JTw2Dt?oppm#_S?=~7HLLCLqb2n zj63?lx+^U}X#7>dt$-UvolhaMuX*#WKjoRk;3Ex?Y_yFzb=+aC`pelfq?O2hU1qe> zrGD;$`vu>_c~VXm;`}gGzu%fNLc;ovPi^l0M3(ZiH9c2i@OP9~oT$rNjZ76~Xd=4Q zU$JU9Q|E_!)J`uQY6{hr`mevYjM5am3!aQyNC>@mufb!mZEIh$T(W|6ogU-Y=aN>| z1|S16CjCY*bxUYi`(z#J6{M|l&y2nAtWP%iW@mv=rxqx0-^skLMBV8TvsPsZoP#Eo z(5)0ESY_67aq3-hgp7kVlEHwiC^jv8>{HgyeZ(n)Kb{3FCp#>%;)^jnbl5;* zCa9~-%SQd#G25=6KB7T>~A8baa2j|W#W zX^}S`558ASu4X`%_O&cw2JpnVE9QkrO=rm#8YRf_GXso1JN_L?*r03G zhrZd>8+isOg#Y~@xY$CemzMCfIxsiUW?>X|dsmR|T7wR5qezeg=o06KA19v%8VM(_ z@ZxL`knQ7E0y5UG3f12$nE2E$#Q?)_kU4WCWhW+sb(cuOBdn+P#}D6Co^4f2}6 zi}kX{)ix+~h6t3Rv^H?8PhQIH9vaHXU zK4x|E;1y1Miy*I}C`2rz!Iwx{tbK_#V&MoT^OFOcy1<0#|FQr6Oh%f`RI0w?CD&xKPs zIB59zw|>;bXl7tP$*KKd2|>0F?Ono;C&q>m7{bM|fFn{YN1MlY7ABs{c2gU|>yXPs zIQ2}9gYh@=5Ti+fx5N;#0-;#MCO*z3EJR16{?~!)45#EFO9UkW!5t`Tz2WOIyTb(4 zJib>_0%$xf7^+C~A@9n`O}I>OA31XT6K3h^w^eo7V)`i0fl(8xNxSlzR~HAz1|9;7 zuiZ082%D)ooBToWuZdfgGg8kqCV-Q{lU#QjNswVgUb37tHEd2A*~O0v$aTLn^u&r7 zGOV|;fX%DJpxK&@P**RJ ztyH^i2HMTEDW9hoO#+Be^DVOi-#clRC1g+k=l3N~_Y^iwn`vS9Ghjfb{?x%xV?ke6 z?U@#amv5&s&&FXP=8SNUJ>%L{cAyPH>KaqCJEqVbb;!HS9;SQpz|FjP9vi~QEd~03 z({DicB6v+zTo3p70nH15%!2W2;xbsbR0v1X1gwVC0Ari&*Y|oi>YOVG6@y+T%c?a4 zGNY=f2VWbJ+r;CS5TP(EY9*-}gSuBW3u6?yvQk^KV4a?NX2E&aP?157_~6LBB-F^Y z8HOo%9HurC>TB4v=#(i24{b@+nzV5Sd*W0F3`~+6!82WZ>TcHn;qNTyA|!)I!k$ra zLKNRCZ*MmH=@y5LX=RvAauI}3TTzCE=BfAb9`JvhrlYtlIUN$O?Us*N(TB*{Y3;rY zbD*D21tmzS?!CG-gu*Re=Xm97trWA0Q-xc z)lHr!4r1JG>3rG)=ck59{p6}fDi_A9Q-j{ZotO7yJt3h364t3IfOCP96$iU znovbIkhev84sv1Comc%ZQL1kOpLE1QZrF(nJFsXoD82<2Ru6Sk;)gi8iy6A(`QLFq zd=Fi}^OQxO6qxj@+#v}ly2%GM6iFDHkVMJ-)W84XU+A?968&tOv8oBqDvfm4&G};J zE4le#@L&zlrojG1mJpcHq85Ht7W%W@$Z`N|=U5u4h=eK|%c!B@9Z*?dGpO-#m}#Rs zOF*#dJ=;We^|It&pYxxaf>=M(wB$T^p){+|QYeIyq4c+aZySm2Ead1FWnP+9aL(G*fl=GXfn`*3=fu!{th+i4+K3C=>5r>5bG+~k=9xe7eDf8W#{s!t zB+!W$isggpK(gd4N~D?hR|%??B>Gnj`&4vEV5{v$`e@1OhW{l zX5C-y0|5(BSFAt8%2!m{cVITsEHo)yU=*fYEp$URH zjQL{YZr8*Azkhog_MM({;%g~@7l&4f#PSW)g$vu8G`azlLXoKrtKkQkh#YJixlR~N zYmts~7>`3rYo={+$)GFI1$7ZN7>HF<*_p2=LM`E0$+Q3`JPQ!Ti7#*fJaV8RSj=s! zC7eq=MFzg)8q|>GeY$xT^Y_72bi?ooZY3X+(yMTKwedWrP zP#*WwccBtb+rGO7#kqQZcBJE0Cu-L){XQN#Y;S`JH8+{b zwMD#AhHvC7X_UvuF5Qor>|VWaCanAzuZW#kGaU8`V;wN@uW(#v9uc<>ocJ2=i~Exz zQ)t)TrR>z@Y&~DuH%bv57#?))77jjNGJBqYH5B#t+H8lZb!5xmf4;tywPKgP0@S`JEqfRBC8 zq-EFdTqOjH8U16SvQthjpbKU=9F$G&LX|UH=o%B$*Cys8zCnZgw<_1ooX???aA2`8 z!lAQl%=0dkL&^HzO}55H0=CPZ&od6a@VD2*Q2+oINV`~xcrXHjld=re0P@7oow+(( z<@PfqYXS7oK<$M60@)#-*OB}DNQK}b%!{H|lTE9;n84XgX7AxMjU|K+-PHlYAgta?uVzL!D8n$ZFTYWgK9Dz3%OIF4W%dVp&%T{h#x;$!T=HQ&(#oG0feA#!3iUW$N|AM zow)T51o=-wfNWZ%6opS(MQ;q!C!yjSn;gF~xeXiCGF9UFg6SU=BVuvpieWldDgCB5 zG~Z8LV^zoy?8ikL1c@>x-FmrSx{!TiaYUBe&LkPH_h;^?UgD-IoUWGd(*MoTA+B!Y zyw(1D2vG%&;0Ou1#6W6tt#wTpD@@-KwVYQ?OnDV-VW=*qe8zoCX>C!8Hx2eIrHq z8zO>G?w%j3u71AqcIk{n{t5&)2J0Dj!lmLOzBgGT@YLB7%fYU2F1cXJ&;=D-T-;gk z`x($_6u9keCBKDIjPOqw3|%laJj1mNZcXiPwalCQ-JtodO)GUEn($Qzm_VeUJoRCownntYu5;N97# zA7Ie->`YRu5Y}622C*2MsQ|TA7f@|&ZM_rtLesS^Ad^fep8wVGxfrMzTKeGql0C$k zG8rxu!WhzAxn8rinlBNeE_M>ZDMiwci=Li_Ig&vDCrG=0^%oP4$*MpDVA@CJF)9K%J5mdpv~w-ZZD{lAcI8!uRzOxwH(L-F9BClfl?Ay>RStqz>&{U z+}DPvvsNYd>(ka8`s@@FgoM*Mu=xg-YGediO23DWJRDH7SL$sj_w+)reh^(xwcw`; z_9ml3f@rX5w%7CSlVgo0)qN9+CxRQJQUI5u5(W__;Y8#|+z1*1WIFRBO?;$%$;(7U zjvA^^3MB~0-P!u2a-|>d!_zLxfy{Fr_rK!uoBLGHZaPT4_VG~*r4^n9th(53+uF8- z00kifAdhUe6Lkp!Q@!h37*W6T3@YI9u;vesw02=qgxOLiNS`+(L%4iqJdnKB1dpAE21i_XRiW7@qEOf6ljDfR zVSPqUF%krRO zb2*W#Y~Okxdk;Zt_>ryPF_A#rZ7$Ri{#nrBOi|9A?v4}wq)c;O`_|F=)Ll4bq-!NC z?w1H@=v$1sX8Q{mh^csdkl3q*EYr^#G$*5(ZX4zSYCB9qgZ7X^eWiMa)K*rARs!8YCHSifCfS=v$C6cOg z!2p|Sn+P_Hvq8s8+iow|Z9m1Y8W7Aq<>Xz>< zEFljil#_m?|I~b|6y173151uqDfN4_KFGm!+exvrq(o<4@L~FZRwOyMjU^0Krlt{@ zRWNjkDG``$%3?YrhVF|F@uUISQH`(4j;V&U+8)4zR2Y7Qq{1AJN9Q>a{IG2L@_fl< z*sDzh{pi~ZHsHf2FC4n+zZNI#5^QqJpKv2y1ksmCTY4x+qeR^M@`V>EtLCy848yu? z+?o(c?J*SkafWX>klgf@2|pT-PE3L8cvRN+S)iYnJLAn}GeX)Zks#chcgf!>g;QHC zHi@7V5>Qx%lp^gWUf(+a=)$PS)-M4Y){Y~W4Ittq4Ge-Enh8OHpq^Or>yM0#fV{z@ z#zp(x`+z{0oXFMC7JoqbT8H3%XWIBBwXBN(URmI^n0I~VUFK@mE@r;g_NLRqQjMD+ zD&X7U&&au!WU+`vFXh%As{-t$*!jobsgpHqtaE-+;Q&fQdNUB5lRC5dd4e6pP3gwY z^O&C6`iocngPV_l1&?36K+wRPbV$An8)nX?u3iAlrV{URFR(n#Se`V}a z^89pOQcE_E`Z}%f0gnkKAAyYA@2zZ2hp9Gx#hxTTed$*U(keR!|AL29uyQe16la+T z%n*pWEu*~Dp9*tsAJgzmh7)+X%i!4@66q9Qy)Z}aTPNJNtgX4*UJ*!dqsI7c)7#&; z2%~Jt94saoA~G#cE5>nS^c#r0@n8NR_*Ly5pt3w5#a2kXsvq>PhLFy&ao2TGq zcx4DZ@Nle?@sfI}fY~!{xoJk~AAGxGq@vhs)KZZ*sp9{Ym$f6qwi2-S zmWtJb^dXm_pQQI~BRr|cRcZ0$o~eA^u6z8<02uf%{EGnoZ`)X%TidB;J@$5uE0OmM z{g$zEMzew2JN2i@+%{|{`Qv{P)F=3`cqaZA_6Tr?fZK5aTv{Sm0;rE-0f&F}q6Bta zmm`6a7`ZdaNOjDV3@?T89}IrR8Q_-{0M0qNK#=4?dhw1&5gTfOS3rJO9CZ z1dvEw`-L5{ zdpDS8NeQ0I{1e2;4)`Y)7(;N$<67|PBsdS`j zz}|(VNa{Q?kR03>ViH|VIM?^+kZ4`+2lo?FwNQB6W?wuyT<9bLthV9mk5tC_>qKJN zE>dX|<&isuee)F2YohX-|l%jn`$xbsN(CLuR_1;%&4B>+Jv1?lz=vX85o z0LG|3CyY;o+ld=dqUcFF-ze%Y! z$Gm_8e>KGE_HE=W&o>f?;lT_JSRw>2nvE{`u>k7P*~`vX8ed4>7Dnzc#RUffJW=Fc zQ3o-Gy~_G@6Pu>;H_sZQ#6*)O4okZ2IPKZu}G zgNCvL0j@Z}!cfb$zy**zh;Il7XA|Bfhk*%8iiZWbTuT~>a83KJ7L1Vr(lzEwdkP5&Zxk$odC+4F$X zRReX?2pU0df_Q)0_#O2t1UnHYp6CN1lq;@xnVrGwLF!MQ!5z{tTLhK}N}m|Nga<*D zp7M|rds5gDQQ}1)B?P-GlCN)nLmG&)iwM>Mfjb8{2A*Bt?{o`osr8JOaisCOoV=P(7n|MN$e^|wL^1J|Z;q$q z*VJ&`@rZJn&7pkyxisgCi99x(?{4Tcjm)dH-Nh@l(nUSBQB6ILyW4BgYd=Wcj!|9E zcBZQN7v?*8uAc!FZg4ngc0dY?1d9gX@WWd9@^shCIG~q=9P#;4z zZ3RWD!vRUv@^+GIlDB35V>z@A9FkOvB9y6{pUk+a}TN&7%Y>g}2~iUgDLqo=q`qhWX2^DuI{i-ckYSbFy;aA4f#N4y z7uAh(deG?MWR4rT1$ybM@j|X@l{$GQdepqO3C|bjnTb8W{){}V_i97ePYCzqd2|zO z)}|R4$uJe13u1)OHBcC-j5b`oGrb5EaXA2nX;3!P2sDD7KInQVh)sL^^v8UMg$58mLuO1=l0YaTvOBlXa&K1(-L zeOK!;uZa@1)qdyK=6wcqu4oLhmAE{$SIxTjw0rUgUx)hNcu*a=KFM{FrKZe!H0k4t zQCfe2eo+4sh~@nGp+s%Wp_Q)iNyj&PX8%acteK%&CQ1x;Y`rL`^uqO5DY8C9p3vHd zL?%3$jQ=Zp)r9Po@FxW5&}dWY?t_w;fXa`l#Q%8uotc^QseO0A=M3GJEzNF|D0W77 z`}gJFpUg7r2&-2AqdIJ!dtdrS6rX=6{gC{bt>rXH&=~bUFb&4^5+@+@hi5el$In`=rY!1?K9L1xO4;h<3KiZW?Y$ij>jlvL5EATMkWBg zl0A&=duSs(Z~iDZ?#Wwr_5@R@&2Mg-7N_E z^k_c(5a@kPvJ=R1xP?i5+wSIhH{Q+t&&RHubi3~KbEfrf`S}I2gI|O>qa~y(^~>Fj zV|biHo-a11t^FR=dpJ7h@NDeHTCGPw*GhoXr(s`YexpRi@%GPJe66u2D963NZ&{L(g~DuPMi_axDESReyB2 zcCgFa9C1yg_ru5=YJLl8!V4^R5p3aH#3L2 zkeHp={p7Gwy-=E~*rndk@*rEKRyF(HNEq9_4}zj8vE2HOXvv@%Ws1ST2-drw-dmp4 z^eJE+&s9VyPBrbdv3%w|)-oKCEtgz$ti>1oZJs^5d8sWtB0<7U4vcUrac<;=uy`PW zcgp^V+AbpC@nbSCV)@si#_54IHACb+%wySPIxxBlUz@fUTJn0vF%f&`_6qT^Y3IjN z(Nq80*EC=>pU{cNC3vAiQ!NYH6ui=eq-xO3{5Pt6Cvj~yww-XtT+?ID52aOYN0u0- zZ*k_4RnD2yV(BZc&Q4luYo<$PzgKO;@1Y|UyuBjyKe-$DIPDbsc6zm4!aqO5=W@VG z&7F4s^stJ{vv)&I^9uS~`{FHZ#DEn`GI?fFVZ2f?0;y**4XTw=%0nd18FoECuHAFk zo8-RbHEra>!einYd(ZNg3>Z1O6}>O+FUt=L*yzLyhm8|=@B#up-P0(g{ZhZL3(3L4 zr0ZZ(XST6NY>^Q<&Gx4$?5PH-5F8jtU>!<}Xn& z{J!UbRe!e=vy=0dFm^si2KP`c{BIy^)CQe=R>cUFG#ORmaqm|X2%VAeN8sQ%{=4e& z1Br`VM{in$(T7Yti>g)0(+BcK(0shY5Dp2~-n6X93B>Yu1B5r|@}eSM6bSSl$GOGo z_H)V>oq8Jc>d}s}qxkrf*7eh&94w0Oa%C_k1s5cm<=+SMpa&La0Pm^`xC<`Afscxm z^tJ^woe)ud^6v+SE}tiU^leQ%F(#b>G-bl)904arQ#7@2z2ip*_~C1?>?ekx+(dLriDd*+1+8Iq1wUh_=ds6@3a|* zK#KOnbDB+<#xR*db+48bY6kv`VPD$7v-vlUgL&((iRGKlACOZPcU5iS70)wT{jrvE zJ=O%@eMD9?p~T_S&6cQ5<`9bST&{J|ygP+?n{dW+J)YyHGyavK*(pjCj`m-fL% z4^qbU|G29+f-{|O2~V!Yd`(5qq<6|iiJPTtS-xXh=f~#KnlwA!I=ZnbmlK7DHUbG6 z^iw~22nLcd;paUrec|eazL8(`+eo6*+iT#saH4|5Tei{KBio_?C-i8om-&w;{ndmG z{Y|T1ZjUKhgrH=wLu6k;)z`^!F3-BHf}2wmqS)sC9e^sZ7w{mxb1q`JGJ z9v|q-s!$`S^W+XgWhj0sG~rwoQD#R~-wg{^&_ zhZI~Ae!h$YA>}Lcs_oF{A+59(s=pi_ReUcHD~-phi8?DbhV8fM*Z|pMr##Gs!&!zh z@{@vhqqy7P`Q>DqJwI?6Zm79m6A;3p@3n0hWygfSoh;En zL{acHUMx~b*;`YKh1hvvl0$lDo>V2fJ0mkxZ(q%qouAceFBp5Bm9@q3x^GZq0AK0& zsVb-XtMk8ZVX?sx*enin;k9~-?M%UZqNV>Mx6 zHOdvK|APN>Uc13`M~0u8RgwMuOgdcwG`+=y zG6>(%8{+E{eN^S$<#|5+lR|1)t4wFJNd`e;oH8|WOmW(ZR{|-^&vxnkZkyC9SYs;@ zpIVjj|8N0Z3^)5yCv5Z1`daqq`G|+!DB>R9={238j?Pk|Uhdodxw>Mus0uib5UOg+ z=g-3&%WLfVa~@}dDY(k>{ag}>>91MoB#3(K$y8REEm(Wa&V-Kut zp>>a>IF|MY%BROT@_8e~166owTh|sUJ*Rv))R>);R^f?yEz+SaoDl zkLLT8w*iBx_)I1lW5ldPN2;CsjU#ugl?8HEDL2!&hCG#W-?vi8mT%JEiT-GkX??se zBkBy=e$~s(X=H-djF4|>M(kJ4lugvVzZ{YiG{e+*D9bREpUl0XK#k%WO*_|rL-QRE zA@=vC%VRh}rx$uNyD7MY8+Z%tQF}Sscgq^LA8))bUid+#UM2lU4r^af@GJlB-S5|X zbGk!@on`uxN6Iz&Riz7f*6%fIFEEum9J3Dn)%Z&(}68EaIp zpQ!)Z%URI#Fjsv(&U?l?T)%Tn#{5+UM+5ch^ zA#rR0I$D1*SjU#D!Y&kxyYH&9FF7U3s{E{i8mtD5^Z(QDgYwWueWc4@;+h?PlGPNr zFS6p9Rr0{KduQ1nFn(GO{_H(YA^9&_z_RDL*({r*_k}>rb?3IIsr2`h5;sc}%7-pi zorPFOWz+EWw&sYrjpvI4xAq6O?4EFWbS@iqX37L$NMl7(6tgul(}SbOERi`gccZMf`-nmbwPVU61Ck{{ANru+9sfP}> z&2PCZszr$x?V5m!C7VtHZp_R=bNWV>MFq=M2^x-%mGRz}+oIVMDnN-)-`Tk9M>q$= zyBlv3tjG4kc32higH+kZiw(A^nxoiq;pT~u=mFtyMI-!RT1dN594GiDniiNtihP6> z-&a-g9~E2l5w&hycbALelM2W(>JoC_Xo-Whj<(neOz_Dx)V)4+@~6M zjN(fne)p3I*s#AAzPP1>g#~KC|INZeQrRBI{U_rVHZ6hRA&Lu1ZTEp}I)FiP=+T5s zk5WS~jkcLlZRk=0GL;DfA_ltILZLtXmmK?(>IGCr5#Ps4k=U7TQ8eexUVBNlLPnEpU&?a=VyBYn6T>UH7WN&mE7!j(Cs4E z;{k@6mC-8MD+SUr2|H`#xAHevGZa!abMcFf&bVMZlv15&+oJrU0z(?^s{Su!GCWrwJoZ zGkW*<=r8W%adex78?0yPZW%!M_2|XSYF%_6<57!fg6XyJn*Lo4< zw0P6eSE9H6hy}7Wzh+~6AowqD{`#r`Ng7fBH|=^Rzi%6XJ*{5{RaPF@q_I zHXWEw-j7d7fL5Vij|4d`88E+fafTS_bn*%|JBl`6q|K-Pxv|aEmV+F@oI0z z-pKy<;_V+6IS=xazB8?>ICfg)*ynz&Hd_*6dzn~fwtae&N~o(d`5uGUZHV{C+jq}w z4gM@uGa0@_iPXdAFb!SXhi)A}wX5HU4cYM!cwX4)-JP+?Q6E5TdG>eDD&`*FWTphG zkWf_4u+mK(E`8F1jqFSM!}&{vDDmEx`z-;=v>XC-DVD+;{xhR|=$3fy@V(8QL^G6Z za}S^4np|@XC-zoYV;FrGM64Z6f0E@7`g8WV53$1zYeL|h{PZ&EP+p%kXMF85Pcyj1 zh??@hb14Y?vl~AwIQRAHgSmRpo+u&YeIJCmj%A)gvUSYWactQ>{jFndKEhjMMkGx~Ge`2e@BiYtOE9Rwj-f z#6F$*HMP%RR&&s?pnL+zKmKxO&?5t-C{SE^n;t{-9BkTsTE03$(y%vzML*<;b>9_MOs`?#P z8o=$}4kH5!Go<3)wK%}DWa1wRI#ed2ysk7`?5|*(QtRYN{d{~9 zQaZC9&`^s`ILnLMKzBgvEdiA+4%rLLN;2uq+85-)M?}g9=;J0DRd1i`TTChYG;^U{ zbs(Q{+`@P<`*%fs_MHHpn&%VN@ohRC&qU8sa7m*rxyN%Yww8xjl$hcMv_35-YDO?h zVs$PGov|CNdC-&kC)405&7d|aj_o7p@%XlKv}MbWUBOo0_*!=tco3q5u9BbbEu~Ly zzV)SwrAl~SPB7#$OR8h+^d?>^(C#aS$o_AYXfLLe)7q`EaZ|kG4op~ zK%o~UJYO9NyGNCu_cDC*@L+)}LR8@?`+EtmVlz3FctsB{D$ez5{uE*PHotDWU+}ZH zGnsIQP|#KYZKR0tyD|3pN+8L?Xh@aW)-*PAQ{*^VH~v=8cHG9AJ>m^8|J@t#wWW>c zgF@~HQj?Fc$@`1096)I-`yr=y(xqo(l5lhn;s9RzGCZWgpRMl~#NqBf;2)OD0uJnu zK1ll@#5+_)yX-wYaSO>3)`+f=`tUwm1nzTFEpZwy6S@MuE~N73mr@%_ngcbj z3$0zE2BDzn|{vE8d&YINAb+C~J1%BRtY8!{9K~mG z?GlIf<84u6jC|7pnl97!ncgvN#T6pfd<~JDSx{bh}D*B&a21a_SY8l?c zG&BVX&efFs)JhMwpf7)U_xkU-tNvca41#Wor+s5H`3@L}NMw>CNe`|eMazj3kX3)G~2Iz*&(+HN|{Mx>O&!%-!iPW7wtn0QW~ z0Je@+W#(Q%nm0j#_W-0B?fMM@OOl*_)=eCGu3aDad@pM2X7*^WH7?b(@ZuTGqY z)u9Q(hWjMJk}jW&Q=RQ+GOR&-iR~?m?O?$|nRS5O@Oj##4V{9s-Vdf7(Aj2H?mbK0 zs&h-hRJ2o9MVhYWTODjWTZcLQqOjbY+06=N8v8elrE6E!ue@`5f&ZDPGt98icd)MR zbDKif=IdR=E(-S+==z!v(af`M*CvvqzkMF7+wd!OxoNYc+vs)^EknZ+NdH%K6#5xr+AF5|Au zw}qPW;6hswg0BO?gK#h1Bvim(D(az}ym&S9@1M$>e@wk+x8k2`eUX_}AY@17#mQIkgP|y)3gPJF@2I!)f1%KNcR_{~{RLXuoHx=(iN-fk4RG`HzEQN*C`C zl3xq$C?JFqN0 zmX^BCi;~&hY4&cerN%F6@hW zpiI4RB&?9FiLahu`O}ht#@YfF~fcFS}o5)u}^++XP zuiSGe*w1NU-psY7!;HL{!hpyV}-K`?d=-AdrZ?8C-->NY8O-*pscSwEr zuuP+7XHAyI+mm4hDS#+z@=CG%w>S)p{rs|-#9!Fy{>lu*Va7yaR8IMls($NGTcnPi zA3bJq@5cRG8-nP~M6vGCKw@wlEn-{1Phe%b20SQO~h-Z4dTJBSeqd)#h|7SRx0}e z2g|i$DvxC`qOW}@cg6QY+#_DtAGVwqs`ZoabL)zUW9v?<=nD2O?P{_pE2_SFHJja< zW~{0}el5j5_nt=Ox6crvLU*W@NhVPnO06&;T?Ugefd^_h4nH@X?rjBhHl3rO{WE1D zyVa5~CDb|TyX2Eb%WvN4uJ`9@1yc`I;Us|qUE zh)DWsX0Z3pPLEV*Wy{#I>lYJ1BBmT{(AB})JvylkQ&S$dk8l9EP7f;6u%^HYF=Ca5 zO>D{%SMI|p1-)tkMTyt zH?|E#T0J-059c^ztcd9eOMQ{-{o1ae3&&W$}RW7A){MdB?T0$Oyydab`>tGX@4$15{mwkfo=)jOwB99{7jwKn3SmD+!yI- z+o|!@XM~?l^>m6OS#KY&?fG{3)IK3iTSkPKe3-_Uc3q)zvtw=_{pSbR?3fZz(?S^pFNcZoqvO6f1f1>huQ2kU{S5Fnm^Zv9t1yY>i0kmzZsE5fOq@$nG-G(0K5#8 z@FPy^=V>uE!K*-p{YjJe@&)@z#SzJesNsjL2500+T-$4MdN1`Nj=oJ!V)<%t{vl0l zPGno|MGD|Q-u_mBch5>fCAzyhiS|K?gxL2AG1~$sXJzW|-?`W{d(A_;Iq;ya?UGZ9~^V;H@FPQ7upHZJcr{Jy4VFRZkn8#@_Ki6Ahvn9(q z7=8=O8g~r{I+xy58qic3CM1ir`9-Q~q4VL6z1-34bDORj*&f?f(#8#xn^ORXvk;me z{ee>OJ$t0Py?Iyf@lom`gU@5a8gyS+ZI<;^bLdRC*lJJO_sl0o`MM+Oho3z?vbDdK z8^q8^I488q@U*N^$7&43eYE>|FQ>mG_Rl5@zj)Gn#|5>fSEmD1M}BJx?eLvdahH-U z4W_C$R8^*aB;VP{;xYvyfkju6yxLOoWPNFM`>HCCvEvTlUhhWU~;qNZHWlx-wVo{ z{iPdI6nP#>A-NQqZ_nR44a7E4oTFdudKHoL+JZA|)Jk=F7`YvWy?9l_->&fmqJ&;0 zvacn*(d#MoR~Z?!Kisyr%G47=O~-uvw*Yn$P#`3r@aS3{_~i$2ZNbuU8@ zH0FHxYb{|TZ08_Fge8QRBT}=04^8X1j;1CIjC2q^UUMq-pChH9@a@!F4oCCa+${9NI!s{Zy#Ix=Dz3V5qAfU2KN#6+z1X+?=x~H>XG<`cc0;nX3lHjcYu) zh;37Q%AQ1?b76HeDDxM7VJ5=JhyS(LiJK+zB^~hBnz_OoXU;V zcn28WWsm*NMeSVvZQJ8Y;RwNp0^!w)3#G+N-*DDGd!)+oN6pgKxLxq2E)k{XM6dT4Y=3Hh^n!55H=}-7ru^fQ6m<(YOy+EHpia-lq3hf4kio zB2;A9?d|0x^W;a6gTR|-48qFPd~d|FA4rQ%bEvu=VtJQD9ioI1bFq($Qc81;{M6Cn zIy>vnp=av?AO{crP(=Y*_}oH~>6c`b*q_&3u*!QUB@HRn`C+Zs2|dSRe-@yLVjYlWFOKQjV}nc?VBg)zVOx;Lg|iOvGYXM3kj$QQCn(Pl&PzxolGW>Q;W%jo~@r9@v_4- zUk)h9dy(?x7dN^^zalhd^GLVf-_E2-={3a-Qe)Qxp;rxUF`V=$*Ck9or zVXmb{C}08`-T5X;56@(6XQ?}^PG)y(Tdn8~7OIFsiI97>Y;10y&@1rPZbazu@-MP5 zCIoSUVh>=%#X`Q`4k&kM|sH%=-&*l&B3%y!Ta+ylBkr@EVe?1GNGKR!`2^ z`r=wIAiT(`IUMibRfV+?D-Qk{bg1y>ehSM80|Pm0lhloD-(1CW2NHXve{TPa-h0v~ zojFUcW~?bT;1tvFVQM?uair^Pw^Tpq!HQm6ovESgDR#OLc!}39Q+v0#60Bi^m}}SD z5E|L)jhP=R6(()Zg#BCuc+cnuJyqSxy}lE3ai(7)=l)BU^YqO5)t?Vmh|N3lDh6(r zprZJ$#_&Zc`(DnQO{{%=nngh5`kwK{OKq|8qGO=?-McarVm(&Pl@NIY8@f~+5HMl8 zA?|^OXlBCpOO0>+94*Y9v|C;Nj~RS?-AjDvmQ~Jk+R-$!qtWM)m~Zt#@+ygBEOD!B z{~k#EyDBAiBVzm#n1Sq0^&H-uos z7~wQ_U_{@!L*=a$;aw2#-3s~$|D7s=sT68sW6$Js85v{$Uf@Y2y!KuWqu)X~GHsy$ z>WqlTZ8OHj|5N-6vSvY(mME=nCNYGBF3R%l369T*3aM@m ze13zNUz@&WZ6rb@X_(VY{}|1^J{(L^G}}{u*I`y`mW?{@KOv5&)6rdN&o=zfF5k1A z;O`w&=MNm`I|qM8(oLm6NWlAr2fez$P`tw0!n9Qh#1L9-cag*9YBRfb{j zC2u3WX2?H_s?9t3>dC|>N-pdzcv#5?xa?LrylKK5%iU*;&#J96Pgk9l9DI>#$oQKH zw`^G~!%-wmy_Pz_qvw=iY_CPvj>Vvz`Tmk~sETC_Pk`d(>y+qB_A+Z}Kshe1n6^g* zgx+|}O3!t_EMe$8EwhRwLE=Z_7Bi7#=8V>x&(teVWBe>I_@AQ7OgKHb9VkaUR=*h& zX)r5DrS5wwJ96lNee6%Jo!kCmpBksc5W#Wxoaa%%(>ek%PQMW0-G!cX4#6Y)h#(pi z$;j9jz3sF`36tx@iF||VH*&yuY8tT+&!iFx>+hz>nk_N>k^KrBf0Ce|xZuEpsRBv? zuqd&Wx&c?d9e5*Bsvi~l65P=+OM@9C;}-Xo?xJTEI3p?}LDOv1Fepay8`9i{oL(sH z{#;=kv^w$MZ|EG0P1$jCta8SxD3tcY{M1_QR-yu#Zk>aPGyE^79+Iec;&QXPbp3NT z3DmcCvNmRlof=8Fodo`Q^tifjjj4)*@jzPin9&BVI_r+`n75e8^mPRMq!YGpaB7yk z#co{jJ{3@~o~-@Ile#DoOUMsp>Xw;m5$D;=?C#m5CA{E2rtO^%ZN6SpX=RN=op-c3 zqDsFLQ#bW!Dp%tuuKJskR0GxCb|0rJ7R#1?H~Q^pThz;;o&xz2j(Zm!g;C^u=n0!O>Wz|xI|D9u_01aKoL+8P^xq+pnwpRCS65@fQWSIpi6Km zh;(TIq=Oix_h3VMNhnHJnxQJah5W{s=-TV-z0bYpKKDGocKyo-44L1YbJTadV|@C* zxB$v5yR>Yz<69qa2|lZGnN%H8l#AYSvUDrT8-117ryYI-UIYKMeyua^)BViOlrM{6 z8#b&x+VE2Lb-a7tsQyZt-Z#?W^`|hV=3pH=G?^g{Yj(@Y&M+FD+HkTE=hzOWM)x6V zWdo`jdD8?;;6Txd{c5ea=8tAt`6h-sb~{TfP&bzQQspg!qelLF6wL4YgDaF1xU*0v zBxC>OP=%R7(rCm{osAbcw=!%rxrB7SSi?je&27IllNp7B8`T_Ud2W5Bn5lNS zWyj9%@M^~t%QY=d`i2X?r~w>`^562rQCYC8FQ5~bfK?l-50lg~GVn9iNxmXM+grZEqe+_EA=L_wEK-5u7 z*ffF8+QU4@KtBiF6m8Ah_>`)U_>{`cx$*_xN`E)Vw+_I%Z~Zs>o;NEV9WD5gd>s^J zdgm6fedeRLk{2{CyQkA}cWw>y58miI5&rLG!A|7%rf@F4hH4+^=bbeFbYuVb)Z z!*ea%J6n)JLi+@UP;`RO!$Iv}x%L6gR(*q=u3f4xpef=sAVd|iFv^alQ*hmKd@ zV9iLehJ)mr4)8iFjf|68JD)e#W$hUkask3u0rq6k@?q3mO8wND~BGsRk*XB zA@1!>c44l|@ZUj(e#b56eGiPW?43y5J0EsAkiA3P>ibh@(Q4c5SM!Tzy&vB6rn=A8 z&lh3d7%En?AKY>dXdb)WMN6f>K3)Wq!8B&YLf-=tPXj~*LDXnFPerp+O<13nA*Yew7 zcszCa0%UpFO7;U=tf+lMna(e3nqVyE=l88#TfQOfHwheSzP_yRF>0ULtFP%y=AwbV z29oWQ8%+zpoU8~F6tk#*N;t6zfdi@>QI@!#=kL-*2mui+?xxX<*1oFm=ue+v^ZG@f z7XaD4*k#2;{{ng4LFwxcHA+e%R1Z_(sN&)cP6p=jDIq+ZM_wCuJNW38`)x3=W&zO_TmaHh=ET z8U0k2pJG>qjBh~ZjTyqas|JFJ(80%kjv7%XW)96;L&eAWz10T#=hVyKm^^%X6aMhe zZnk$Kir(z`dZ?SdI-@^&!FlX+kxSZ^8FuNx^HaKKZ5l#$&<9cjn^Ny_E?waR384av zC!VWmx6_6BXTE*Vr1neat_A$Od}r`u3($@T)8*nhfb#NE`sH1uf>?*o;b?&h7x>rfWP+F_uh0Ddt zVo0NXve3>G0?;%35SbA%YOI1o<{<)0ON@jff zz8SI7{InApzf&!aM`74amf1oxwr%?BBj};rMK5fESN|x|lXxDDy@ z*9G$->@RQhrS`arK|OSwr6~{Xi}(-8k1@eOorirEao9F`sT8%#lLS7@r?i&n;pBU;<6iBmkH;79L0RS8-DzfTsLXM$s;aG$Ga)!UF+Q1n@`_hpqt%!ch_2| zDLwxAKvWW?*Ngq!B9~mM$BqQ8nsI%n!3A@*1R2RxG)q`+O0b5X{SSkO$4jr!M_cwK zr_cO6;G088jAqd!l|O6*qFDVSfa=n7xu^#`-e`Sj0&RFQi#-e(%g~!NQVNsO?2lgx zoBu@~e${8m#4GyLSMgR>z(?olH|nLU0}+z}q$nrK7n|5?yqn0bqYMn213q~dZ%@*x z-AL;4K{KZWW*y>v<pq$?vJmcBTp9&CSFjn4h#8`J@o~oot?U=8xTV^eRUVGX z@jMmL@Ro5I%-AfE2vc|c!GuKFZWUQbVUBOnVj6z3=9o%{HDOYS*#vct|R^vf2XPFf*@ zErH^la?;rU2528W?=gGCTE`A(HH^ZSdvT$hl&(<&gso__QzPH9u3|8QXPeK&t|pGaa##r!6`@WK{2c#mL!RBiJQK zUEzoWLtR)%*9xZ)589BWU~5fSfnHNFFa40Jo9Oj;(*3GiuqVU6?}>V&53fwFWaw2T zxfi6`ysxso$9bt*PxCE*MudeFNdYdl?i2mI!OPy%?#0hDYqrSgQ|-7 z(d{{o91VjA`m7Dy$mgo(g*Vh@Zm4gyjbYjSRG_{lOc-;~YS~@eh<<-oQ7XZriO8SP zP|Du^2_<>je4m@L2W3`htS^NR79IZK;m zNpAT?Y{T1(5JtF7mOo|-RoOsvD(E+9y+V#=Z@CZWuM{C1`j^<-DFZ?tG zCzFQ9HEeg?$>@tzi;_lz{)}JOF=zTjA7nekI>}01VNVHaxb}jrSJ2+40y%#g!krol zTK4P$(;~<5x@qq8)k4^l(BJn&z0q?oT)cDyK^VIT64wVLPX`?_7ZkY0(pHxn>y!W& zYjNw|%5nY3sM;_Cb_vnbprQBf^huIHbW)kbzdflj$&AJZkhyI-vSMN9aR3jg0{jT{X{vA%p6=RJgvl~ zzZt@C0p5;ig)I%po*w*dPqZ6-*#0uunqhyIzEP>lwjTbBT^6^758t|HXy6hyrCr6J z5vZY+FkCky>L$vRtilhbM000TH@^vp&fIQxr8mgY7<``*&))<$!hGCXJ?${MFr_6; z`XI|}dERR}!LrlpBwpN-q0P3T+K%wjnm_Bplp{YRu7EhLD$RuI!K%R# zCfHlUGg2f~&@cf;)1r-FlQyzgCnkX;NF!3~zy35@j|*hWZGVC2NjSDYOWZn#n0lNB z-eB&>yK0~ftut%619$B_a$#UycIpBfm!p&67xPtC@vNn!Q#!#?o#q}O3kw~U3(@sq z1^O*Z0eoQp9WxYOa5m#iM*r2y1b}`895;d)uKo&~YD+ z#a0Yc)YUH<6%=gZhiR!OS5pt$(ywu1MB;*nO}3vg!OD4ALjgR{woRQTl5}pO`%?Wm z`KiltmE^}EDpt?OY?F8tcBy>~!&L%L=zy5@68)kDPQLFsc?w~YM&l7dF=4fByda!d zLk?0o%JsnPyrm;?`4Tx%NDrvQ6Hbs z;l9p~sl^ZtC$G+ct86G==yICpV@pY0>?xQ(B8fj_P zf{Ahl%hx5>vS9tJI*V7y;I!25Rh7e48-PGFD<;vqZz)c>=5{d+-1UvIaStuORgtno zjWY9bb8DRwqE^~{=)_$pw)O8B;uhUGgf>fD`@`jJqKpA!_rJM``2B!;8=RdP%Y_AU z$F7NoiGW}^XK~!&u&&+aJ8H9^!S`vyEeec0mks@o0A3Lk+D63$bq? zK=MPeB!k%75s}8W62kO`LMLWpYzPsf31Qg6X|5cXXYy`Q;>S0l?BVg_3b6jXJm~Eo zb+hN;8D={!Vqe%X@3XIf|EnkvoB0Sk74)kzhacUjJB|ORoyrDOZR|8n;5xNFH@6n= z($pF!xkX3t&X<$t(=%l&4r>@#q*{+YbsX5)WnkJ}%!-R5KMj4#xOOZdVYPj^vbiZE%GKrm)lGxdhI^SRF7Gs>60(qY$~6p@KFP zuj*T-eYjmFn?t6gNnKaqKYg_vr61?5R_gO0mGHIumULf{9OvliYw?bky!Ajp~9X?sC0N1UMRQ}>M{2Yzgs_ONc=quYnfE|ul zn?DD`!4ffBNuZxP_4Qf)+g!m^OmqLM{3MPYeo0#!iU-P_W*dwMm+g8m8RWK(; zq-Kn-I%k*)Zg3Y}EKhx+D$14H)xq%Q*jHg|@yv`7g7($w<!08%3FnT zZs#4o$MN>;wliA|LITpC*B@^au^_ z0}-D=7G)q;v8%bEgfZdzVUj^L{23bUx0{Wpm)q@#@1iIYIh8-sFq!F!74j@nM7=Ey zvb`?!(XI<=QZ^ zv>U76ez?o+%C_N%vDVu)2-R-%Lb|~jM6B?JZo29WVB0ZYn4$|cv<^Nv5jcU`VeERg zbX7ngU75?|M9yo{UQDg7eq&$2CdPAHd)#`RNp_=g5jTax314Mz$ELl=y?kS_eO~u1 z|B>Aq{|L^lvLN1Ut1EOhof%rP7C}F0uFq!Nb_jIs`Dg_nhy;K^^HNb+$ntmB+L}MGYwcSL6ll8XDfo)hw9+IGmFfQIaN?^9F z0(o2&tf6&k5`;ke3_q!*S+MA+CT(-P>UVy9Ua6GwwVrriY(0dlbise_yNPT1Tn#U# zqd|7_ZKI9!4qK1szSq{~ERoA^3593J6*?F?b*Yh5o4xc5@6?6I^@>;KKyDIJ^S30; z2!sYg+;z9QFgNQX=n?z2t$yC{JMZxFG)A~ymaI=tq-(<5#*C3`5VRn^U>3T? zN6cl+O40IHO}slEH5g9PJZV}F}MHQEP-D`5njHh%&vi9ff3mhPnhQ^-oOWJZba@zylX;WH0YfzCw*JguI9*q7!Pk++Lrt)Re32NK8QGt&D=PG1Ef(=4FsB0_3OE)GvXqgPsm0 zO)sYoMu|7wS1wu`5*T-;rERayY}r|JaKIq+?4^Cu3~ESvGxQ>(kW&ES4ST*E>ofP% zb%#Z-pgwH98@b0QrepgHNWKqO{tQ$VHojm)(L2C?hP2vLG$F=Ghg^DY?{|)?4|eiA zJzJG`IN-UY!`ASp5J}Y-T(XtWggA0g&9MCs`Y6ON(m!JTe*9>mc+P<e36qv6 zP);oJZQdaG#|E$`SU%}J1tHprR7GfdWSwn~D1tsAi~n%3j;`>1KCKt9r`M^N$tpF_ zvadh24E7tt^SAvz75M2B6X^7|{Ov-O7!l7C$J(wp2x+O*%M@LW3wJWJj5nz#UV%Mb zS0{G)Awv9~D#Q|ouwRMOf7>q)dZevwuU{VuE_CPLF4S)~5|2s?X600a5Zf86X2(1o zxU5^UEmG+fTERxaYb<;I6#1DjHgB-G_qY8*M3f0>PXz;9=)u2VC?A54IOypZ z1tIpUI8;yK;G5Je2ae-@*BRv09|>vLY}P}}quFl({w=`21^9o_0!z5=e(;M6@Y@}vTn37iS?D;_1^4SxQB?lFtJBJDzcf=`MvP0V5| zh<&`2*#IR*1o6~jFz0DGdsS2z(z{oEFD-9*4+QgRUW6`_4yA~V1x7bD`) z)u+ZEAYZm+=R-CCN>=!TmP*H89|Aywm9<`rk+$+NdIWDcQ;VQ_hiHp;@c|1oVftNXrk9NHF9;@9Km z&(I$8JrflHdWb9I65I`hh)b`xMbJK+aR%AI;qZUmfC=HDeFqcu`FVDV&=8r*Dfbr_ z06|jzAgc3ow_E)XfJci8@ccYYNhoxk)OAtitu_#$ov>0l4I%k`0KX;Rw+j5W0l$sl zZ!hrM4gQV;eg{IoBf;OH@b6sUcQW+Y0FJC zZSTAClrNs5Y5wk*N+8v&amF5`L%rKYr3;*l>#{C5h|-YN!LO8VCJyV%$8{LlM2Ooi z^AX#QJ{C{8oOb%E1O1fGRlOK{VmyhEjYE&Wthi%n9+Qw+n+cO|c4nwjv+gKb(u&aF z=Gz+{GVXlli7dY9T`6^OZ+A>EERHpdJxYgKrl)Z$veP3ukF)>_scF-~rkR;BdU8J@__zs(<3 zd7PFSHJ6RPK0L4DtE7QK=nNG@2uX5Rh8eK!@cuV}=<*%tQD64Cy4QHAzFWR=v3Cyo zGV(V;o_(T_mQlYRA7T~Si>$;k+83s0bgdx2Bdfl!I3`W=>Jm!d)I+^=J(2^H9h4xq z)-;D;9cl~+;y|Dw4qABN*!eIw!tcKT2J%j7YVPSObzh~V2SC+khO54Mi$+5*@2nLx zJl+z1c770DA!}RIzJeJGS2i@(&=xr1>9;!Nb0Xez;R7jxm>~M!v{c;Lc@Lsy_pMJ~ z)na7)PWcULmhsps_DW&i|2h8W2EQ&Ub`nF|NedA2aygvEnNalpz^B{Z>@pQOYeu-E zz@aT&vGal|Hh(J|TZ<9(g8X&W5(AX9x3}+59P5~CGAX)jgQ7>#S30%X5lzch+RxCn zm4b|4rpmYzO~%E~0=Hb{KqK%gzJSEN%R4Z_$2=N#(wWHQ_XLjW0HPH|)LUWB4u{tZ z28=~+rb7(^F`g6mtdoVOMUwh;Bmc?Q#g`5D^rw6YRY7h!5olb7n*lueCPYPE-W}s> zIa`%fI(*u<>MnFRR11#G({IVwpz1*hO%ED@w--7`U4%(^YxMR{5Q!c5r_$SW zZFlpu9H!l+hT)}=P!m9MTrM;3wkn6dgF*FM_vp85JEHZL->T|p;P@nPE{n^bZSdO# z1W-}H#SkW_=*m|*v_MX|B)oB$4;;U?GPCa>ql z6278Mu4CD}=G7Ba&6e5%$|zHx!kRR*6$hg+?+T)#M+bWTzqkNG?8*oMk4Y%-T563(%Ta-K z@yWc8*=1v9Yb&iJi*Ch*#m_#bMPw!-!|1wjPVU$k35Ln5xl(3degr1GSI7;VRK>CQ zg?g5e{4b&Y${L`y3kZqrQClRnkirFgI61@DI3OaA=SZRG$5@^Um|Ks8ssuVVR@8(I z>!Gp7izHRR;Be3o#kwm@Dwi^VyLE=i{7O=kIM-4qo^W$KCw!`8HG$M`mS?om6BYbS z+fUZAFGSL#=@IEm0JkQkWO_5fn|tZKrwFFpVtO%Uk?VW3^mO-xv|wttU71CbVp+IK4K%;4$=b6z089-aCm4^MmxJl5VY=OFMo;CH4=WFH*ak;*dG5f!P{r%9Ytfh+co%hw<4ucnY7xka6Eo~+$_~&YT2^u zJA+Zm4)3)AaoYlkkvVyrju&$unz^27N|VxinRdq7$;($*k4mLc?oJl=b~da>J}T*! zACAMPB;yx@=8~s<9EbFu#)-`cUg*%2>c4w-rS4(vXG?74?Mjk={`=kPsg^JEOuJ?e zOsmJx9tS;b_gMyZYlu8oB}Ex+Hi%$ef-7Dsu8U7na9 zDM|S;C()tiL+RGV+z*mMpHH}RbJ4kzOvYhiH4;9PM}lJ4Px8vowVbWx3!3rn2z&^5 z@VngrCX2r)X~da;&nv@MBJE802vIkjLxzIef;+!b;&#NtY6@fIUgtr-Q1#j#&2lz3 zhp#SuptiC!waquri5{khju`aL+ZXK01`Cad*`_P^oCcZ2xH`{{hQ4tPmQVF?8+cSN zqKb1{9wC`6_EU;GorO0!39C2>J2kME480yyH+?!)>Nw(Qwm4|Eeo&~Sjo^iU+3!7C zdGU3JSf_@*iT!6eGLHOWG$jG+?rnJY%L8I^utLnjr(p|{t7V)_vrVLt_Xnr?MkyCc z@0u{C{7a)9x_`BU0<)27EloNq?&J~+W7F^^^HCogokNt_G(KYQy0|YmcKE!lBagJP z5V2*p{p9KR`n~b$Z1IZIpGSj8OM_3;;@#H11}ZvJdVBCwH_Ci`Y!@9=N1Gq$_l4|@ z7v&>QeBvdp+xcjBeQC#`@k^u~ptywQ$2TFw)yksA6Z_z7JtiulOF=AD;G?NX*!a|W`rw_lg=vM%r%x}QRIv4{ka1#*y#TK%@jVNpD*aaUc)1{v$+Qd z`MRAK?D{?z64j!16PF%$ctPtHAM|klw zDUM0Qw!Ho2Kg_sgj>yl{@b;QHi-O`4%laul@;l2EK6$P7M2Wk2PR0@nc>Ck*-*7j@ zIF^{Z9FVO{SZb++o|i9{%37QLo_N+)5BlbsHXI)q3k z=j~O^*PD=rwpo@_S43upu6p-r_$tlP$h+b`@zGy4fRW!^wePKwl!@zyDcoVw8uz;5 z><7C>QqpYBws92iKK_FJ|9N3%{*N z580egopkvm-Pori^r{MlSuCm$tQZ=mV6jDS_({!C#-A#RU-fJi3^I1OQZd~*-#rZ* zy=lfy3LH-v4AxkkDp7Y0G+S=6C-tic8#q-e55DM+3+=cv8%=sICK5esqoVL5$+dAg zEpXWcUXga5B0KpbVH#^bSCoyr5KnOywk!YEU#Pz0iShSSs}0&jQ#*GUbonKqEb5X> z6u&!pxy9Qf&HTxV%a|we{Al+DVY%sv{CdHnsmj#Zaa}zlug0nV^B^NGC9H(b%C*mP zeZijm6&4ALDf&U49<>&1X3w4|lHX2AMK3uEoL#!sdKl_WTlXR*ty*3vX$AL2ft7hu zp~g>P-QF&M-=qR@2sA3+xaeg}c!sB$Ao}eckL9GFCAdO}t3$M+s1LyWl=()YqC9V79*Yk@QEx8js1^$60~~rPv$W*;JX*T5{uiT4(uT`R_QN z#^q_W!CEGUC}h@FG4QID-^hjHg~yGT7bbuOO0gfC#$Cu(AanG33zLZqn2%An3FOrY zI|jGb>gEel4aJfb3(R&J2}{%OMgwQt!h+Er)0O6n38Sv6$&P-G{oh_NtGX*&DexWj!uQ30byOc0{Dwgeuh` zAl1EPB1mJfrNjc)pJ(R{k6FagZ@CEVCLFxeq1A&lo0D$$9kDsg1#r}PKZV*OD7x1d z73P+qFwipUw%U#VK8{4qb)RqCHFE9oHtKTfWi2Ihea{qmvep&sg}t*a;DCdc%v|2) zvD-}p#IilC;RxK+cmC%WzV!b#8 zFVtL&B|>Xk$H=!32u^;*R3MlKWPn(`Zm&S{BrM-(O33K5tqrs9aECxrlO~(aFApSd z-~FuN?0o(0Qt|HNND4A0f^j+_>D`e_dMRs}rjMUeB&2U&uHK)}g!}SLdUj#Xu6yb! zY);BWWOt9lY!GHO-bEvUQb%zybDgT;d(f*QyxD2J0VH#t)kib<$r?U2%D`nvKl!-S zOt#K#^>p&g(%drIJ8^aVvSi<_ai<}$|Kv*Wrym_v%@=T+48V#LM4Fbg$JVSPn_$`7 z%LE^v7psfj!kCGo&fBC>MQNFwSA=7vK&F?Z!r6h1J5rbvN-i$Oo?BVw57IQ__M&tX zFP-<4khkd_O~o58e7f6I{(Ty=%&L#o4sM0(#>d<&V^a_@asFU|NjGspe>~uIt_z%w z#_Gi4wJ7mia<~l=gUr+7*-(gQKf*bOgNNh`qaYsAY$Gp3l+X_)nx;_sU$?>iMbliF zj&H8D9@Qp{R(hUW+;`C1yefQ9rZpBnBp3(0M)cexPYHCo&@ zJ$}=UP(9`<7bULXT;-F>Te<#Ws@M#_YKke6nkJG9j%==Uop$Q?S_FHd7$v^&d9a-n z&f*iZ*TAWDZZa9ImaS+$IXB$wBS-%35p3c@S`4jr8;y%LrDU-+XHGFm`dnLC4@SqP zcg>ci{0Jd0jxDs4d^+y$NSo&JQ0uxi)LFO0A9Qdn(JB)!4PO|+iHYr3s!CefaQE!p@kS{!Z^c-@q-IgC`|O~h54h%# z?UX5WY5&A4N_T&_x64GxZ28TS5GM9`yUuTGZWO-+b&rVxvy~q^@ZA;VCs&*AF$td< z?TkO3TXU%QG5tQH<=#(Muw^<8cY)ctHb^&=6v`{%wsk8d`nw zyqBzVrTvX5#m7PmL-|!Tanlu}Q&Yo{O?*+W*PXl%G<58flkXM1{J1G0*lR(smx;@f z5)Gk60@o~guFrL%&+GFUCZTZQ;;|=fB>4U7_heU~Fo*+U0|bt&pDurh!ciM$j*F^w ze{gobHndZO4po4#86q71s{%(bnl7H~aQdFE5H5UbdctXNzB3h*ofA^%Gyf9&_f$98 zKL}B@%#m!>+62Y<97IB@{oB@;=4n)oghg{7mw!pp1-3u0%2^uZ8@-3P4{j6eyx6@D&!E3QE42y5QN%G>2(%sI!tn#D@ zcaO9tjbtkAdS;Fdb&cyT!C=Qv#m}dYpZ7LtAda{-LrC3FJQX8OX0UNKTZ|jEedfJZ zujoXX3ax(DV~PMx)r^Sror?fXUL8j?dDZA>*y1tJWJ{bS~!=Q}@ z^q2P@Ap!!oN8@qv_>pY+}LLG6kXV>F^7_C0z z{v`ktp-Df@nnREd6?PENk0TC!|0zQAlT`CRhAJ*3{(!F2Ad%OMyLnC|A|XFr`iYlC z^pese4)=p60AoX%UJollYb%wJ6qEnFfv*e#t%(%Q!!38A7qvR-6FvOB z7N}K=uQq#U_i(Mfz1vAT6faCVut4OZ58}6mt@bo}kUva9*?v|x7Bmq+U8GGhOfS6T z2HTwsY`o5WdQOq6ENx4>V|nOM8@D3edUv)Cx|hjG3k!?zD_uPYVCXg~ss}KP!9iin zd!StXY7PwB;K$EM1!1fRk3^#;;J)+1te+|kf)sI@U1vG0bX5tFqK%r@jP0TXK7JVf z001tWStLaI(3=NZFZiBD!ur1vKNBC;A}{vBtfRU0I0;=2SvpW~}L!QwjeGG;Ry^jZx#Jt6lahn}lAbWwNkWR_O;X zn{?R*q!PlF+4g%|x+CkRQr9K@weF`7v-qNR6>S1RUD%j%qXE&{v&g#ivRR)Y(~3h^ zw`qvb$Ds{3LY&P`!N~yWslm|v zrY@!wU45rS1vv{L$Xn>CXZ_^oSqt6yij*P})-M=(vEtKmr_?y*VaK3~0YYn6R{z>@ zm(YM`ZtuL52a0|Lp%|8?wE9$(=RQDb$mmRmPvdymB!nxj>lxZL>JsuXydeJLl|-6xtbrJo619N_|5;12-ySw<`lZP`S!|QS5f5n2G!R zJf^}@=<7WdFFtXwWBkzh9(bC&nhJs!5Y@}z9164hZmRn#anT}OH|xv@|E=Paa1d$T z)1F66T-rC1UizfXeM>&td#B!uIR}f`#s0FYu3jYaW~2MzoZ_A{n0KQW=LI$W>vNn@ z^qhwgdmO4w6ZKMh88-*Nkb@!x?@^feg1s8*B474>9?ME>1v_g1MkR3Vl`vHn5=^@e z!pR(PmQ+X4&(Ls_8Z&(%$qR{pLXYvW;7pKRA04}%L0`A)id)Ht%Z5Cud~9e;Z$Vm$MWkVI+#3$Y{|If2KUOjUnKhXg zHAKN5ftR&D*`UtZ1pj74jez%Q#J%PK!QM~PrFWS*dixHw zB}STwYQ*PWID5>3lb-+l!20H!sT@+h(ZUrgEBRAhOz9G7%h54$YTqI@_{C5ihx^aM zUjh{V%o+osz*7rj1k9~Zyr3>VmL9bLE@d*aAbK~?Q(8GwrPAFx(~oLYZu!29e|@LR z<*_s8m805+PAsDY;++i)%?r*Ow>?|CjDTc+OFHJ26K_NCHMPP4<_j5K5;%W?rlIks zU4lI5!lH$nn!Z&_G(QoDyJ^}h>X)`Zt6$PlPiFP40VbWaFaO28G<6=mfnsfui$=He ztqk2&@hgY*Ozr5mv{B6iL7iqERv?^Ov9W@>(l)4{?ys~@5BP_PhJjg#dYjYKn7vHX zwDmf&QiA=IJf?tEPu(V`^w#5S%ox4D$erPWqLgvF=#av$rP_DX7k7Yd&(J~Agv0T) zO4~eK&wOVKgdXfr-j?_*ovpuHxp|u&Eia@$w&X2mTHvN!Ani%H~I^`Ovz+<$Z~2bpNX(Q1Rx}? z7z;b0)eNI)JNY*?Lum@tqDMekGki!j8#%TL8VqzL6-LP*0m}E}+Zxk7&VmK}N)V8! zpC?OB7G#?V?NQ2s>gzEoP@*b8Wuz;K!K8+pZLP=QsIYKUa^2TVsWn-tik;t(qoT!H z)G+Op+F!=?1TsAD$0Iio8(S6xcfkn%Ctp`$)I`?cgvbwZ#4-?KI@3Z-YIOSIGD;qh zaw9GX?DS#ATKRQTw69V(t!zwf@jvwM%EmP2`~Uaj=X`tSsr<<_Qt0C*v=M$Ol@xoP ze7$FO-yAtvcI}NTK61Nfze2AMIf&N>YkJD7+s$<5<7~4k&g+9-uTK%>yh40mY4bWJ zz*bT%%FMkruG5ROPw`@Rr9-+j#j}+GkCwG78jkb9iaJ}D4oxoxdc3rCyRV^Tbj%hD zup*){E$2Qege=yEv0Sq0^w`vR|9+(z_PAX|ZP;-8`-gx`!Z@M8%m5_0kap(~-?4qg z+fDR5>uljc^vHZ0CmpJhg%;J6J6ou5Ac@QSTUOflZ(rZ!MX~CTyTW-{=f4M7(MBH3y ziIgf^Jx;=4%ejEwe3(takAxiEH&(1JU?(lr4O2ienc0pA4vRgI$?VEY(g3j8ktn~|6BcP`O`4k+ zeW5sIuzh8H*}3?mu!y=Pc_ciaa4b8mKaNu5W-7XBBqsUk?yS=sqooU_`t1`6?kX(nWbBJ8WvLPjtJ}vREG`o%O7U*-aFe-mzt;74|3QlOrfrcLXv|-kV7p zwz*SVF(Z>jsht%hTg+ISKq`y(+Wio_q`OCKm+lFvR6h9XLy)JG3wq7)PEW0?g?&gU zJ-4#hS$?pBj8_QVYiN};6*%HbT>G#O7yUBf$724wY%|3fd@tb|Z@+Qtvh$&O+^fr7 zw!szW*B3wGc-@{ax~`KEpy`0jOb#2H$x;9nLrDi%-JC@Ul^L>gUOU7 z`dGPWBOMR8W(`DoGZ5*GH$`S*E?S2PI6k%x-r$$^Gx6-o5>0r4B%Tg?KYyCg$O#$# z>m30t&$ZK1Hgn&-iaqHf%-2vpSm@LlFE+Zo7;nZvSdQR{SFC^vTA$Cm`OY+h9SWzK zS9dITUBZ+daPfY-Xy*Mn0YTD7?8z>DuMk-MwLpAWq{;0E0&^3AuCt>rdnR4+J3J4O zM=QC%RW6)A?=9%HgzxL9xau&%jf*E%PpETdU-WQa>`YkuG9gA-^zk%we^ZnCv}ri7 zi7$v%&yX)i!Qs+STt5~79dp<4~lS<{HEtz4=&;9}!;o^vZ z8eK3qcCd$}44982PSQ=ZC}RPnD~2XrX*tJcYmb3+30j5(HTKa{wbW#K^@{s?4UUfF zdH=(M(fHm2(_$UoE-UbAZ0jD;8KFJXKAo{;Y}}IFV;@LmAJ?*0`#nqu-_{l@`sUw= zlT&dQEQ5`2FTG7y=<#Zn?7p3_UH0q8SuYAw5=ICWWn|aNlY@ycH4xM)lUePJ_{ zR6f2i$RpsV*Zk*7M7+||N4xu8on4tfioY;!SBzO{6=@o)beX1nIvVdzXeNvpq0#5p zzC}6qDK7E_O`fkL4+75sDv5)?YHjYzHL0P%l)yGA%!)^eS6``>JgvN*v2PC@ih&A- z4LyZSLm*wm&7tqCvzx+C(bz9w)!oxISMv$`1UGxaLJe&6E6x>vA? zX=Kw%<5A3{rMv)nO^VH#VuC{|N6)``@_v|56zN|nA&m)A;;;o%vxFb%CMWtAn~N9h zFf05vu9WU|?qw(UafQ`pH=j|X6Egk&uWOz^PJ4_i@o2&sM{`d!Kam%mq%26K0tMj& zdFm?wgSz#hm@rF`sHuuYtWyhHCDt@=oXfFK5>6{Ps9~`Y=i%o5WiBe!Zok`^mAUha z=>FxggQRx}a);yBvlGA$e>_&mIJU7?9fYL-!Odz=HDJrah*5IV(;#NjOETdCo2A-N z;202CYHS=Kj-<0eNvolvYpAy8c^9uzBIYMDRhZQvkPXPxi#TOT$;D~UwNI9K!hyj| z0q%r_KCh2c{j&XMT9+jsH6?uRKi4|V7j!_SGU6+qGFs+1BEQ-#O&&<`aE-*47KB~S4^cm$p5iEi=bN0V%OP?%D+-X8qI@nS zD1N&yjG24=$oAff(#&oYi~~emQtpOdD%a$ZA&lN$xqbMrA|0sQ%Ku#DPDBTcNej#@ zh^)y}ICkbZNPd1+SlvZ`!13!eVOH`}ifli?eDP+N2qg`z%Z@#@c=UJwZ06BiJB3V=vx{8~ zttO3{#1|%1UR$ZR@KIA-ZZ9Df;ChY;mAAYltqIzp?LIrpMv1RnSq3bZ#|;vs9BS#; zU<1OCa99Zy7?szF64-1OFf%mZZwQbSt@I0jgH+g--^U=DGHaPbjQql+oL7CzbFrFj(xDm>DYDU0Kn5aqq9 zY~;y=UJAZ5fhAaxzrv=uGgUy*;z$0RXnk4}?(*{6Fa2oT-FD&Ga@#o&ZLfiN0OnRReHiLLDt><=js7+hZ#4bq0x!5R0dy1PAE*1I8?|u68knJz1E^ z_DEY>A&oetKdM+ed*asZSLZSHL7rhMe-tSIrUQcwp@<##zkyESZAZ?M;+VKE%4+r} z{zRuv0fQv|RzYic{#+>24!oE=px*zH6W<2A=`cGh29+S%@LE~I4K|Q2cr>PrDrftj_PRhBn`5WWThX}9v#a~yI5AT1tDK~-~RBDh;gN@3gEWrtVCYm|f3anB+;j2xlOHpB?Z5x>AP|E zvp>5`8n{4Lw`uMQ8hsWSyS zy>8phl;YfBC@xg-hnc;ov3(AdgZ5C9tjM3`pnqwu`K!Mr0Qy^=$i(tNe+zBjL%8g{ z`wq9fKRdrE;-d~yFNA{YR;uUA{nrYG+cPloHxt=FF_2GIt^LILkde`+bc*C?e@};U zL%v`*{?c>@tu#g}p4cJ<{HzID{hGGYU7FYV;oNDi3~8_6vB;#|v=8f2X+3`+RFwZ% zY00IPmf<-;YrfJR;C-h_Fe?na#ARIZSEH3>bf~5I-D*^<7tv(s<=BMaFm<%5FY?zv z2W>zkZ00JjcNiw9KEKX!jP_xz2UJ&j1YD_+8Ev~t`v9AKcC3un&_Mkik0~vF^z5C# z5(2ryqem7QKR?V$t&1i7pL769EIZom5|*_}=dv~vOemf&c1ra)m?^WK)Ikkzc~RbT z{ix7W7&x7tKV_&*5LFj4ZW;*u11^GrraRQv%_t`dgYLX^=MbWUKZ1C+fSuMhBhjWl z3n9ly=cjGpL!Z!e=S!PjHuEm4uB4UrPWflT_^~hfeTLa7n0k?D1T{YxAg8qVI{AReNnE?Ap~*L=_RpPE(4X-FcT)v85<_4-PD-$MP{HkaqPhoM z{z6;VMaG>q0<_^KX7UAXN`CwYvRoyVI-9PXvMu`{!~7!r=&om;eql|e$8%nH@d{gC z^gbF0I`x*SQ@ei_Q;MLN;z|w#`*R9pvfBM0=7Bna-$P75>8dBqxL50o;ciDG)o{C5 zX~B4}FE8?mbIgW%r-^(!1@H7^vZV=0A;uuZ%xkwG4thAO5mNph5^ciEzw;zhw8Bjq z8}i-?MBiW2v?$O-0B7}@T6aISh|TZXnw?spXHxj9^f1$9M3^sXxL{6~-l8r0wM2ktBW*79J?6&7|le$JR%?AZpPd z#v#Ld=~7Xe>r`okA$0uld{0gGj$P<#$L-yk*ug-DilF8@DYVLQA_w?)u`#Q?5Lko6 z?Z-|+`S>R(!3pR-n`?BQbN$_MN@e`UDslR z2*XjP;-k)HE7d!Qg}5F}lcJf^n`vTGx2^POLi$B`|MK#xeAsERn+fm^qTOy#(-LAs zl-2+R?q>DRRnEiRbFv%V985D$Hw@SIr&YL^6!xc%I)iJmiboP-hbZwGi&$?#$ce?G z;9WM}K2zmSHu$;F_-a<8U1H-3kQy&CN(@CYJ5+T~w`?a*_#tghW0h-lv0O2;g%2xN z510>)Z;D<#P}Cb!c(?BuwN(q(?}c+3u&k78ZrQQiKu2Z$?Y(l<&uOJZKRqHnXQ5b~ zvuwJ=Ze`%(4(x2@wC7~Y9BE^}*k}S|!p6+d0OzY@ibNxBmThGc|6x0#2=q(Rs_0LU z#R2XXkL1t2FlIc;b|vSTTy(QBLir&Z0Td{e4KaKc%&~IldEu31oPB+7yr)$B#o#Vp)j?M;o$%LH!&qXgF4; z#RWpyA5ICtt9VwvfA(>$&LqVKcqx$GhV)I27gyaQE$_39S#a;%Ol`qQpIv5{@#*#P zS(%c;hijgi+D!82@IgO5UTPIn*d<12DyxvLa49NkaGO56Lq7b;dS55;^?5G=6OYDr zOkH>;Ca3@#(f8`30`vvqOIJ&!CQiwd0jeK~p7^NfdZlYP$RkalYunmDtrlz<@J-6h?rD2zK}YCFaU^Nj4^&q1_V&yOEJ8jKV7V-pfw zU_Y14vat*5t#&9NrX)-(Pe}dVu8tCqb?~Hh>FVho4b#Bk(&pKNrd!HvG<%Y&L>kd< z&ZQ(lq5{r;D;(Z~9e-+$Inbu?QM2@Hgy}Hrydq|UW<#b|0Cr4v{!O?zI`{+`0P-6$ zM+ElhCTwORN}6NQSdU=G7EZarr#}W~1!E}3d667GlPBEjXVrXf2S1^%@gDxnvc@@*jat))6?T29q)0sa?a%V3Ybu?soNtOa zJlBMU#DIronP+|Ic^I2X*&L5+$z#$`*7}PCG*V}>C1i7-)cWz z*|i_MjFR;6lWE_h7U=J=c&}FEX5E_njVg?14@=r4Uftnvfh3+|#N8sp@m4G`UL@8& z>SzOU%L7qcLof3)!HI`;;6Plc>g@Yxnn-ZqLssByMf0{v25^FVx8fU7+Ev)0t$BUa z7*KE;@jW<4lnqg()~xhh;!MZK)LsgXM^t|*4?8^f1oAen>Z{eORV=myof#TkFA@_+ z=;X$k(x|V?JT5+c#bGm&wvh~qV@_3>L)~j%Fdwr~Oo2Yi&|pqeyU5|(=r7z}3UR;o zxk)m@_#Vqf=YDbi!pLKT+rj5Mbg z`j!PJXf{doq6ZG9o+KsGLwJ~TY+(=hruxZtHXVmXWB*8I%2!D z#U6~q8@yrJw|_QLAGKN&nN4)85*VXhv|Y7E~*nbPB%X8d?-f7MtPiV z_%oU_?uWqZ;E{@*0b`e^(TveS*0VL_WuZS_SB?HeC=(4Xbgd6?>WrB&GYl#qZtLvJ z+Hy?%K0ckufhZ%@$5E&gI};p}{#uS}FwlF}Z0>L`vfD&pXR?%l3Qag~SpsI^1Hy2E zGY~Tw6qtAiqVO0W3Yxt(1jgPtKEQ_Tbq{$X3>@mo#r_5{3K5(~{_L2#@=QRK`Bw1U z@X!Hp9`}aT3w?n)a+qo77+!wA=CEqD73RlhHQ;(tIQqSdLMm1GA_DJ6GL3Qj1jUn0 zyUE=R%&#)OL^iV}Y&&yJ-9@BameCHy@(R-q*|v+VA>+}B98Ol#(pH2oFvl%t>;s0f z%JI3}=w#2G{CtPv3#r0XSMvw5Ch1qxdD)ySWNnL0LJ4;fHfA;r=hzHRac(}K|IYqJ zYPY!^$J{!A#c%CquAYhGkBS={uI()UDN{U#xqMdLw1`)=>top`aXiFS&DDp4lWJ0C zJIBi$Ca*|A0C3gHFuoO!A1nZHpd8+$fJ3;YrZJza0y2VZWek=AlHjI$YC84(rekU* zLFvNnc2wv4HCQmCMO6gGnuNmTD{~(@RK~r9jFe3XPA;EnP;%c!fU>*f2izFA#go<%+SH7PmGU7USYFBlXd zTN(=qiX3`Gdt@Jo@f0VCqsmHC>`CG{j)LXf`*79l?gjcsl?K zR=^R1S2bcnol)AmhM43rd+nEBo;u!EcL*xZl}guJF%|Z~iEd(VV*SPYPP+ z+V82tRv((G-jG=Ni7w5gyvLDcv#>oo+J5XF&BkJjg3U#I<`8fiEK$Dkd$&*`B}J-O zWhslceg5bxPUf=I@eS|p%Id@J=sop@ST;xK;BkjNwb^dP-b$@mrL)IkqE0lmr$Gl* zn1u$DvVX>8du1k7-xt}@?V#pK38BxXT6^!46y0fbecFT1o8DgpkUz4>Hq?)9|8aF; zGNjCRjG9n>5#Q)H?KF~A>bOj|HIl*QAA#{HLn1;3{r7yj-7ub7ZBON|*xGT0Bhg0w zfk+oM2`XG6cxoGO@(TSU#ZN)^snGZxoNr+OlavTLx2WcG-&FMoE)XVrpMyk?f0zaR z%~^p83x!Z{GIU-wfeIJNFQqr0ks1$tuF9>;fG)_HnaEXw1s34fFEj;&j~iKXSLcBc&L87lKn(3g-I1r zNc8f7TKA`ro1wk73!I}qDJae20&T(*AY8-azN~bIUv8*DS8D$GH~qw7%$A%w^S2Uh!&In`O-bGv*tbQH3j1}FgS^Y(@C!BnWqql0-&cc$k|8uInS<#E5n1ESDhem?4$D)NDq-8U7h|nm1wXVKR0Mub z5<=gbQ|Ugy<>LTJn7oYV)aWHoi79`j^{eh8Pm82R#Lx_?!ukOKop4SHVS$vFBFKgc zjU+R&^rC%+Ff9{gyt@9vt)bW>dU0%H`Z0`7yyA{+g@LFp^lnC1)!M0KW34>>PmwHU z&J-W0ZwGjUys=0nEoXiUjxsXchPY%aaSyYu_FTX_MWzmJ<$%7A(y}Y);G1U%z zwZoY*zB0ftk0oPlmGjd}g$WLSt88?CPP-opbL06ti4B|(e%tVgzsEH?n&t(d-B2rU z)MlMSsPx8wL14tx+qo*D)$eM`yED|8?L}5ZW(i7$mi>RL1~2UC9nNVl&;S{zY%m17p9~@CY4yy%|qn09(~=v@kgO&Y@l9EZK~e> zAp$x{GNLRM+WT(h1ue;-uW|iBzlb!lZakcC6p6nKiJKRz*V66R#2oiMrdj;VVbUy5 zI1?9H%1#0O!U6nm2oJ468Nxz8%!;iT ze2k3m3$i-v8m0=!@VQbjY*l4mRbUzwkZ6}_g@OD49P%wH`o!Jm9&!y9oZyB`JcqVf zhkijKHZbC~?}JZ~YXsnCAhnk`#Gt^;U=?F0@k8oJB-c4SoFN7{l)O`b`~mpi z+r!$sUiQTr3@Jx3O(DX$f~&v6gh7Y}?z@YZ$cnb0gQ}xz9|*-Z`ku89F=fCx_=E`s z4F54Eh$)%N5V?DMu>!D7Xh?#I04xd$J3rm@W2KI!Q*X} zz9=^aI3O#eO0*^Q-r=*C5EvLbRS-dkVo{+LsABx_1=d+A08~Uw>6Jt#6_hRno@w&V zA=U<>G>0Vh$n;UcwL0Ua#9=Cc9cq6xkw3{MPiQ_QF-@kgbPtL}5AJvma!K_WNf0Fp z@g*(tsVKmxuSmey!?9WvK4!KnlY&TM2|2XI&Z4y6kYO~-WX51Y1lKbBL6HHop$MQ# zsO6|7Ky{p(8CDo2)b|8~aKRn#!F$|cjQd}4AeUsoFc{Fm*riYwsJ^Sb$wh}J)91Q1 zi%8!8lUbkwt8f_PLY~0(N^K7Ycf1GJd}ndqFCM^qV6^1$#Ug>RXF_bTS7=yAKvl#- zbc-sxiJ_`;QyUKu2Gi?EW@?qYG)f*GM^&s#q)5*NHh00y^k=dW-6`l~H0rWA3T;F-h!@?QFBdxc%qto4Ekl!ou#++G9o{FA#UpSb~t@{=1Qg#+|S+{tEk6j^I(* zVL7OcfkG1Y*mqyb5r5lUeaxC%_dsGG(#OJt|L;a#MR(Dg?5~;r_oybYC=>8nG8FdM zcV8fw-}>H2l3s=7J&?J$Tb_|a{@qCDF1?6+zDqB6BDx3pAp*i6YI!I=hz5V<2C~g< z5uMB3{C4slK-|_|GUCYpZd6Bp8;QI@iwTzh?nDIQpf?B%1SSNmAVL2pH|QO7Qnv9J zEQ15VbM{_TAjaRj+=Dns+!hD$-hUJH-$!M+58Q(N2iu!}`i14|yN=b|Wrp#65Z&wG zs4$=Yy$k3E1mA{W2>jm={P$7y5rKu9#2dFG|0g0?M80diG@eQHBEr(oeR=Zd|8y~c zduvE2kpF8)Ac+58L;Aml^nW7~2oC?Bjz}%ZVidGCJ^P4(kf&Z=%Md<}UFmwk_ZLuu zESgU=X8T#uv|5tpqqVpaL3k2Ht|wzUbQ%jkT}@YyaifLopHe{wRJL-B#z|bNiRsUSQAF+!ybs>gLo?vu#nh6U*KPmj`4+`Gq~Naoi#3 zB-%$!18~SS@ZOo++-k${bHc^i2EX74C=QK8au*3RK^j)~q`IG93^1RZQhsyfzD33vauXS>pT?9vlJd1J)w>%L7U`eI>Rk5N zpqHqAuc(ufY+(v=S&vc{7LG<3wWg3|Sz8|T1r(TEC;bg5J<*KVjMH@^T3wCPCplqX zxm;XHsL+h=C70LTLW@S4D(iJY`RV zbR>)cRxq$9N{<2%;pt!lhs%$nN~aFXqkeBaDPa*;lue{zGmtcH(;ji8nx!ZjrpcN0 zW0L)Aa|sa2M!c6UtZL=ceTJ_&fQ?_DNrbDY<=YzYfb`J!8?uOqA$<4@WN#ocTVB@U zq#*m_Q!y~HWLuQ%f{nwbF3o3mz~leG%7$(^l*%%3Pg8$LnMcMu*XgF;-CVm8MbLd; zy#hW;l1zU|wGHF+a_9a|b5&wpG`>}Q{N+OkbV&HQ^XzvR7*EsLkXg3MS0WMgHwjG> zZT7g@w}XqJ(eUZ)$M?=^{o>9Q#oQ&jnF2)i0MsL zOkG4A7Yxreh}mzAe91fyc_`@RrHHa20hSj=8Ud|oI;dWaW# zeZI|nln&kemcT7Ka~RndLMs=}@Q(TSTvMi-+5RwS9yWHYnNy)PeJknwX5V3-)3v|X zCUt%?*jyp^?kjU@x!+s^lmZ=_3Qe#dzcY)|`y@?oIEX)Wq$&5rFusB$E&k^*32(&l?li?+?Kt;;i*A(h$QjXm7=m3y=80)78r7*8qt zdB?-N<`$SqeEe$?+Qj&g9?0_Z!hvj54B{$nT|+-VvtVHi1bXc#D35HYY!0mY&Oz5W z8Y+86V$*D<3(+b+UDvkSQ4<;*mJ^xIKsppK;!*{mPmK1^GeIg4-U0)nF7oVDMj?JSk2IHqwE3l6u zk01fSx5fN$LuM7ssr{5(aDA&=&iiE2Q=OxIGk3ipRe|+Q=VuULR3o?q-;R&XE>&VY z6&vW*qHZuAe0YgG0FS%@D{rsYktrZ>rm*fHl|tII4@^1?QPm8BH5VfCPL*{p%myYz zmwsz7>#7{7a&9zt-2`>LFxSX=y38oWk$Ky3ffR!tka++HE(i+T*as<9NKr!>Z@khaC092)D2v(L*I}L)8sC5O<}iU zNU9DEC*h&$-*r7%dqS!3DY|T}hpw})6SvskbGGX3@f*H|6Ba>_(|De*&kJ};X;g;9 zht38^bKh=eI1!K~+hMC`sbiXjN^SP8Gppyj(Gqf76bKY-swS?;kVLgFv*F4`tp`}` zTj(kfo>4UUNuz2)ZW=8y>{7MGl!r%V7&r4bg7K1~gM)-sG*=>F`g7#PpuELZP= z+8qsXmO2@>2a9g9XUA=gXBY66pW|06DM5AGbhlfw((x(j2;*DI-$!ih9vqDD=%L@2 zG9x8C%reU%^gajG6c%_5u?m{GMzERZBh_ly#m&EFX)#>*Lq*pxAmF61c9#U+8#H{jsjeETATES z)&XS}v8Q|VtW2ZZ8ehCKktXbHUVp)TGG#9(l%;h7c2)oa9aZyMHOGiDH&meZW37q?ILK<8^Ue%@L+*qU9C(H3G zEq#BupT#ViK`&BQ$WtbnFu_LCDQ%LndcF#5Oj&I&45pG&3FWA8gyyF-Efom)vom?? zIDRi}*N(gOm3RiIWGCY8JlEu~U=1VX(oJPA-rIMXTs8@h38U3rHs`;-e`|8c>9vT`xqHJB zb@xary9*P+Wd0L+TAdt+eZ%6UUFF{4*MC``_1baStjHzl8HFyiCgAX7wvL8qbl~$GAjEC3W~y~E5Nu3W+q|Pf z@DMUsf>a!ByqsiNh?B{*i|KS&`Ytm_LL|e3Ey+PE1~J_(NtAYWvv^Nb6FE#+RtkZBI2G zta9!(GsQ0fQw5_sn*zpuG64@wX=I|FhT$I+ZdV)KSIiwq-2VmpdY&yMuULCsqXi{}cZb7j zl8kS;tb_TK49R^{U42v5$3j?s<;D%3C%OXHt}FBEUsV*KdzT2c#taO4?C+0=dvwUE zoqw2o7^>|>E{8ns?-?l2QQNC7mnU4!E+F3_dhHP-Zr!zFmx0Bm*R z3mR6qjr0V>5alU-5PHWih2BLBj%pqH_&ecPl4VjH@ zuPvGF>EMWjm6PGQ9oG&uOY(V!*^?4pVg$$!Nzie#WDj@l1Dp(yEH0wFr_)wW20mR$ ztrWDe8g^xQt|@*72t4U(8DTH!F94^~^+TTZgh#Tr&NFLt^?uxCa`xg(p8dPEM?$-Phn8o4o#Px>t-1dGlZM*q(?r_$lqbGAx?b?Hd zk#B`>31(YQ4X#V^`WYgb6+9N1*56#L25Wbx34_=m`{3w@_(FgALmW;^^`5k)pX*tJ z%O6rX&QU3Znn6$aId~x|D(zo{{Qd-$Yla%xb3SfbPpERHTeZK)Y~GWBz?+bR#E>&7 zSwJSzn)O|HOI5Q(IOs}dTl3p>mcMx45NCGix46}6jNdvM1u81%dj`S}u(?8qs`0kG zz0#3yixY!fV}Jc*g@utucZY*B+@9p``>WIskmG~AD7ZzFzrD$?P>E+SX7m33Ri-V` z`&-ZNZ?j11R5q^()-%FFXp*8|c+1@|U73L#vh*8usFaevit({$9J9Lju53*dgI6-Nt<;HF{!lS&D-O5`Y5h91we z^FbHlzo*)*uRUS?aG7jXn#j>Wg8lA%msTQUZY=8?H=J9F)$2whG_(yq5dmCsab&)m z?`-JWAk9n46Ds27Ym`{Tp;4GBCFA}awS47!?t1v;Nz!}Ob(5a;DursUk(cM|(%v#{ zn3}m3c<}4HzcWWl*;S9*{eNJwi7F+U*Yd(!eVmMlO34}IR!oJ7{|@R3d7!KQQ6uch8?WK4?h z>8_k+1v5=PL#B$%&Ha;9i8Dq?{yuIrTh3P)Dy2%_XzyDdsnNtXkYA9; zO)h=Y>6ZYMcIq6t*G4tqv-xnP08=0?Zzvm}zOvz8GWJGML8Dov=7V&E6Yd`a+1>p9 zI4q6XQ3hW25x1@pb8Xt96MwWVPxVj7-?sq|Wbkob45cP4IF68)%^ zX52aIc43W>hM!E^Q|(+!EO?~i;>@8<3+1Ja*vv0KlLTi`c7?=8stkSQ7!e%#b#sP2I^Gyh9BYKhB5za|B$-ThjeCrdrci| z!bn9#i5Nk|AP;}P9t~$!v)Smx4Asq{>)Y&yjfGTShqZPBgWVEF_iRJaEsm<>pq(U{ zOv${&_D6k`5>knkqa16?(39Qi>R3RA(>({aah5UQ8BC zs2M0%-Ony1Wc4(>1C)q&0at_Dv(WOaz_P#R(>P->&wQd9y{TNJj?*hG1;Pub-7i%~ z)4@#>(rsOlCA{s;OY?h7nu#1$HZzJknhv;PYlezB42etYe+)arMNzP+51Ga5#|?&T zkss3~`L&E4*yqJ)c?M(fmC2A(fhxC8okE%PZ&}Mhs;!iBkq;02LrJS@Peypv#kk%NT zMVef^5^W$8fer$8rar5WmoP-qxr*ZCM<0#&pO5+o@Hyld>fEBJlVa}$wRw&- zOY}Vl`x)!om(MKLRr=j{JLZ!Sd?Ij5V?mYI+Zm`tDjdX4FLf!X+HbA$x7^I#1(KIa zO06*7!Be5GkCu6}c#hz7Af(VUQR+VYXixj;CO)pvBjf()S`O>)JbT)|YD&#c5_Fy; z+XKcGf2}bSLxd>MNoDVNo_T40h7y;F<*Qni%d3;`&3P{~6Ppq_G)Ju%3A7#s*X_QT zF&j}w=hfx!7onws5 z@_Ed*dUmeAMv4BnjnhIy942j^`r0)fkE<%;7uZ5>&MsDyFZP$=Zam7(&+*L1Gu(U5;|&p|bhOCz%%AhNp1-JB{-oB&t4~2h2{yB7 zUo0VB?A3C3BO*szZ!ypwlWPAyTP%nx^ zT%fim-p)Z0WeK&Gh_H5byFk`BhGwaLwsTk?LA~M*j^{RQROiX>`8@6G{X@_{W91SW z3y7#L#S_XbwdwL$dRkn4bTKB^HU2D0Ph%emD2XOLJOR1k{4|uZ3Z75E+(2vZ3#~lPNhGej}f;8@1$Kt7nR$x#fTf%uCR30tDtg6Bg3jT=Pqb zZa&SZ&1N^+CD60sd$xY5568P9O_ht&p}vOPA*qB02X!nwZ&a5)fjMC z=4vf64p#x~COoSh*;G|2i|*d|VDzlU`I!9bwE2Pr#N`zpBF(ucE=rz0Hj#Ed=<9~f$lmQak$DKifOU=NKPiYKeQ4iIzns00Dl*qQEik)QKh*A*1uwe2Ajc{!yt zftWs`OMx%OMY9ucYWA3Q83C}}vvrD9RN(I}El;4d3a9|JAX?qK#e5!<4|1HFaUs5) zsE1a3ZtkEP|KZajyf3!%pCv-~%k$uT6ASA<(v2UG?h5-GP}rNXKCt&rt+4e;e;##;lHcc&(7iq0 z?2t66N}&N3LP#@3B6DpAir_1`&_fCoyJU*~vMAPK@ zyc+4Dh#jqdWR@bytz6D6Wnar99F0-y%hvm z3vj?T!OZrLs&kCuHVD&=Yp~+nyi#psjMJ&6_C5F_d9qv^AD$3IQ&2Suaru@Dm$+*0lJtuOK0$?PkojpvNOaB}1S)@(Sl8ILAp;M`lX9Z2qM`0u!0eJnd zXvWhxJjrnxbw)O+!o*x3sr2#{u6QI?+DN8cXtGJ1nhY(B@6OjMEq@66V*jOoxHzqu zM(tQ$J=4RHPQ3x+1-(j2k!}NBwac$g6|iU~<1$`Pi^H{4zkMwhzx0iA4+m|YXvlfA z8e7l=o?s$}vzw_>YRnHBdOzCr^a_tRU117&S)mL^Nu!y+T*{D6tx%&Ry#^Pb`(p8U zOh@fMW0W;pHerA3*YpA5e|!Sg>;i3$vb>~-m+G(y9#Axeii67bB1%&8XP^YZ9fp=! zg$^C-IO>nXY1Y1t;b}@@C+$+g*Xt2Vi8kjdpxv=qe{qZ`&WHhO+FKQ&*0f+m+M@`# z-o#hwy;hiv!g+7q_7)AWdi3C51iwt**02tQDok@@S~EHf*Mdz7MbS!C!7-6s+6&6&Heo zfxkKF-D6}e|J3(`sxr|F@SDk^UhD?Z{W<|8g3iwpwJHrMvwNfa2^2EX?x-taC~g;f z{!ZH3;)1eD12v{s4oEOgQ;YNuFrPRiU>_z8NH(E>Vi}1H9Xj16#@vGOB(K*X6!3rz z?Vm1cnelEH`jiEHSY9wRd9; zk5Op(yy4lvz9sLppg>5P>m-=xg)=m3r})x2$1UNZL!9eqnNA7-Fck6H;B&&rIj0i0 z&w%wC6JsD8Jc3ifAU5p%w-O>Yu+(2@LUL8Z;f?+NWMJpA62Jk8F!^VAvKt#CD^q5ub@3eqqK zi78S}gq4Bm&zEbS7T~}n4oHLi%#1nET7K0-WIJt6KamI`DBA$?;5}p-Dlkckk4pz5 zBYE|Ldp+|>bx2qZ_9pZK=PYe*D{p*U#L@LO2RDX$ zYHNEUT4%RIB@s*<#Ac+ZWX7I@Ce2=hcq?uZYKV~Mh4e*HQC1OU*M^a}C^stLjT=U! z_5lG8X&;2W{j2c(pTuHHgr1EQ&)vbZjC2X;!|0)83k>kk5paJV3#Y;GMU(;o&ae!> z2sxCDYz{ZT8vc)v1uRbjbSSqEuMh0=Qya6(6CEar{k zS2U%8$2dCGM@$hVL@Y9<+XaQNyRcBgZFCC{KN2+SslMlPyVd~qHu5t#!7UI|;d%3F zW&|L*?rxGH;bC8}2(}Wa--_S5)g6t-`&r5I(sT3ZW+Vv|BEgT0wb!e_B!xc&{txjC z#=`cD2gK^;>w$kDnjnt+Z9l^o?im&u~CWfFv}p0AO8M73ZA!@?h%;erjzGN ztarb2-zAhV|(m!_CVp3R&C3EqX~jR0=^Y)=@R2Xdd++F&2mdeBHX! z`!T@fEEGP3{L{iYP^h}7f=?>!l$Cm@lTPnDp2M4(> z=}kDa^6KNLBK|63O8NJ2754rAaekdc^ww`5c&70bnf ztt>7#b;m~!+~C_Khkrk~!0x^Nn`AJ>bUoMZI#p!FA0*eB!xPZodm|0}Yo5iMEKsf7$*0Wm}0^b*x>2xB+YQe`td(0+9ZhX@$*Ylgxsk2l3Qz z{XacdqJ#Lu9O>>W2RP;C#i5u|;(4L>!oT0d!5S!sJ^SLb3_|2rD3_;4DUvqZ^0?*I zh_`P3-*f087w~f?K;u&6zg<6gB&iFEnm00c&kg(Tbpw>^ZVD0Hyg3*B8~(LuQ`})uIVHN3Vej% z5fjM0#(E5R7S9pND77E`XJPl?JD>j9>3UrEM_-up`*a77XdVsw8tftEGEA_MmYTc=noLhF>21{Uq5&v@GPcZnf)!IzL#z~ z_5Tlbnydx5@%>Q6CJZRsAJoDBVM1?X*>b|{XuF6rR9gztB-@q3z$+BApT+PcQ^kPh zuN47%_>AUl2v}cN4JkB6e+H%;-nQs|rbgq*Lq_>0Cm18DBiF4ES&+av;O#%!c8eoQ zO(M)09LEVIi`e0%tZlwX+3czXK+YVw*ie!F`+eAo5!=OYqO!z-ysHW?pEoMX53wC) z$xsm*{0r0*VDivm&If!Wk*oNhOuz@xRsrSRBH#8y3P-So+3{hvc6YCGJBx~reg%F; z0y3LuKBb0c04Zyhu7SQ-T$|8!K{|!m%?qq4o-i$n!#uZwUzDJzbuZfzpOgm zkt(az$3OKxYIU~mtJ%No%Mn00pfLxFNbA)>^P_gRXNR7~eeE#T&WyuD;=Tl>C(7mZ zRg%ZmIpr2uVm4EBotOC~7chtp!5Y6o7;(woi5Tp2?%akQ(+dHc2HFAoFc}$!K$!n8ifFk3X^ej zn?26lk&lX`{EYHEy@GKnpxcJpbq}tV%iCQ;L*?wp5v{?BoOtD~>u(a&74WMjB+IW6X+K~ZNuP6on|o1K3ZFx8Zw zU>W9_{NS4f0B`v^F<|VEQrzWNJIra#1BuxdZ@2lBK;H?@{pvghi&{=Tq0Bh?(1<;l zNEqylVJNM_ABb`*3yQ$UkLtKdyg5xxRB8rPXzfBPIv+m5tbOL)^yFgj9Cw@6A%37@ zdBrIhlRZGLGkx@P9~4`?`OSSk`^(T4?Q#ROt>1%s^eZ_#rt|$*BTWXbpJdv&qdL`nMcOEzBMC9v)q|<|kL?T_dh9Q2=zj{Yw?v)DWO$xi!#;f5#Xp)sQkDsw%zYJIs35l$!@z-*yBLTYeWFBT^{PuHg)f z+ww$8?>r&BmZHhSwB7Ghr)BU&Lg* z*f@_N!l74WFsOjBGR}P^(8)e~wM<1t0!)(J-4jypWZHeqv5bS z$#2LQzfz$`MyV+SenNo^Y=eu*JL=?ksBnFp1{7p4WLgXHHgFFZ9;%h=cok=*Lzm6a zyMnbmwoP_qH1sFz6X;2#6&aiyHlyEUd(R?1ptu*eWE`S0?4&>o_*Sj>UQ5LASA16r z+p)z5rL2JG^U#DLOXa>~(6*$)5!ngdZa)ErCY-m(U1=Ad@~MnEUEXsgJIZk5(nxq+ATh-+ zbp?d%ml!znXQZ!1Cj({L@}3_(0E^Bf>!=pCU=^zlGc#&`3J`aR}LPbrIAR) zQLi?8Vpml8nvE?9rs09iVk1M@|(Mp$qx6P*Xb@#Zi!UG%pwAd*%by2^_I@A-yqgC(0Pi&(mn;Du zQ*skonf6Mw$JJPlg@tS?U+pY@?bSO32KdC@ziDB~MyjjXVUCRuP%9!1Mr-v<>C{R& zVtQU^_A?EYyb#-BEveVJ0(@bW(WTLqbv!F=@yU0_M~-L{bIBK#%9@VWGCQtS)nCi( zdzq$6T|T_}8bKnEAs#u+FOe7x_<%BWV9K<5EhjudRxQg@+u6#Bw;=&fHs=HA{0D7k zc7-Z#W>pT9zL%1XWlM-?TNJ9cnUU1U=1CKBkww9x{49U~_|AAov>vUe9{&0#iJX;R zlpdcN4=vbhg_zJ%&HN%LGy04>9ZSE9jVmgG245^JlzG9-q?)3AoYrA}Xnf3-PTki8 z?<3>^o6>EioO#=mLA(Fva8nM%Fss9?aaXU#_2UVGd_bU-@r9z|+2aa5cjogrh)?E4 zhq7%5E3v}E3e>4W(4pOVl}Vij0zQlZfVFAtxxcXMJCbj0v1hU>sL(%Qp+$$2FAWBO zr?iKDS|{z=HU=*kq~y|w$pQo0Vm4nNEVH6|qwCENgi7#kqW01!uoAQ>DOchSJZL|(Ogd| z@KxhQB#orZL%)052_N;G+v2e3U=ik3idF1V9zirvL#^IGUs+9wD7RC8ERtrfN#>sGc z^$q2U8T$mw-No?)0Y6Jq8L2`}b%q~T%({PF&&$}ZQiSTo&$6`u|N7QuZ)s&#xnT3LO zPBI; z4Q&T`VZu-|R8s{%WD!~0%^|s8h)k8^oMxw0 zExOhBw>s$QQoe+KANc04fORRsQ|-BZ&Un5z`>vD3lh^atxylmrOf68TopcZ4P??jQ zi<_IS-q4E7hA{2659caN^V=k+3BXH%+W7@y(l~4kl##8iPcpQ1B3q0hngU|T)mZU_ zS3j{Z1w%S!ig|;1K$rFr8 zAF$C29V2g!ZI<>(thEeq+wohx87K%UZ(qiE`dryN1L4_y>1TZIze2yae)BJHEIAA4 zBMpyb_3e^=t-BV|d&zDVO2ihvCZ9mR1(hd|SWWqi??-5kP42U@yP{_qsq$8swOO&*rb*wS8sY3+;B7SU^GR~++-P~k* z1>ciOUGMx0Y!BC9$x6rr{rqY;xjWl9<>e4x^Rulo(SYIS_KOtGr_uym^m_`!fQ-Fr zNi{?L1#!jBt~-+AA>(AjE17sFM`Bj936jm_QSoKlu9|xgcBT>C`Eed0@YR@YIT?uK6Yxxl50$-+P`*v|JPAgL74&Z+nc5InEE; zPlKHXu>foW=VjOS!uu~XIdA?-rk`BmzO<Uz zy=7uIY9hA-pg0UJvIa7b);`%V;b!2wo%ih`u!B>gb*CSI()w7%!s9p=-f&z?&Tb#e zQ$cbH0HJvVSF#a8AOxQ~A*}tr)$1~iGeq&|Gv`~c?|LVWi;t64fA+=Zb3mmWJLfy# zfuDI|=!BHZj9Lh%@)hDE;_{U_uCLDznGn$O!Nj(q^d=W6a=fM#o(H6lpN}QIs4^39 z-_uDEc7uAx1mJiaoM4(R4gbvfc2&g^qz>MHh9tjPlh;+Qo31pEAB4eYw_m9qfYvy| z8np(7%f|Pef3cDI9)^Ia}j8zmas9g*@ia$k_GAJpSqjQg1)OCo*qv_OQ0O;LceHh+25T|6QbkU&3O1f zaY84Ni+IEs%{}VGeVl7%RY%Nj_=#~Kic!~&`JS#~E zc9(1m1EGR4>%m_iHCVG(lh&E(Gx>syTm4@qzSRwtZ{rgVtTA@)T6> zw?YKCdLzK>?XaNn@yH~lVV6^x25d=Ud!bIM>@L6{7Phi7qVBpjZR2FnYgm}^E>1qA z+mRh~_JYS%WXn%5E<^a(_L4R(7Xhk!_}cJ($05L)NGZ+gZR+$@-9Sf1?sZF_V2e?P z;NSw_A?8>4_MfDwQUZ+t8b2j1K9@1!X;K1Dl{Z`Z9OS-^`~5TuU2lcxm#C13unr3>zO=GP zS(7V=*d_p;k$4@RU;z)a2Q{r8sD_Qxb$N*+hSY+;?%PbMhj2&~6Gdyj`hiAIlN-9I z&BBmNvox+K#Su3$_~v2)1GyfACA;Yp5Z5X!0NEL6O{m0q0;;=gL?5KCmrQ*y(rEP( zZHxupS(RVJ))S*_L>m35w(IFbkQ~1GjbZ5P8&L^iu^h%ANQ7eSRS+i=nG-yVAchdq zG`n{=dLmo_wfbxGa-9YXOp8J2j6Q}^5iewr@LR5JIxCOooXLJ|iG2HsS1vX{1uGc; z!OI1hxd=spc1f`4PO`)N*o-`3HA}RSSS4_3Djp}6)Y+GRhItkV_y@=STpG7Ga8H}# z;9`IC3!5JPPAL;@jO>RcG2ho5g`^1Q=W?d~P#!tHx2OPS+HHBXGah@gGtKm9>FWa< zuADE^PG)19oyDu`egN_S>V@osXGxWLwFO)V&I{|+P9BIUFfw!5?7iec<0j4mVV&^( zsASHRUJbTKlLTc<9H8#;3jokOHbj3IZ4|$$wBv{*t3sz_@i?KvF55GP@MvX(Xy0JH zCA~oRuee->&SPaN{&dFJ%Fm&MjPlQCW4t0-9h4{m`r_eGw*Jn~6YozE6FDO=N#Vxx zqbCKJE{v*}9*TJsxt#>@=&8BWJe?2_#H)60T=k?O#vcxD+h}ZLfE&~Hk}o>*kSGiT zM;cq`#B!SgSRHXw&D@ACVEzGd0tme896(>%%rsdzXQw3(nH@Zjpq{XL1_6B=#Cwj3 zxQ}xD*HFCG#9d6oKK>JkfPm2p-434(npD@Ud1zy&I-)i(B#B)-s+tsZY4CO4;+K7d zNdFx*Ey?nBM3?uVwUA&p^HO-dg356h~K#;r=45!$(Qivi(wh#ECss!0ar+^$^5 zdT-Z1Q@EVOhB9&h!=E6wSD zEk9?B2Hu3~aUAc8T?yl-%%?Vc^bIxJvQR3v?&MEpT2MU?8ySnk`pE7cHO5?}`}{p- zng?=sm|F3vy^CeHwHh8o2vV`sk;5%@GFPnqnp)*miEu%;iC+D1oK#9I?3eLFXMmJ7 zD=>ek#}`naE80Uo*0P=fck+#3*Nws#GIn*&_TY_K-7pK#_36O`(aazV4B|{L;?bW7|CV1|^i?=sfxn9_R397cIDKF+ zy|+-IecyJx;p4n$#uPM@iNt^JOUf7`a!F&8N%aT4Q zc+ok;PL(gC!V``Ef?^jv)U&=rfRT(`i!~CL0xk0@w|XZ7zZuuXripz2&57xH zqXo#>9cKDOv2nbjX5U|=2smdM)V$~%?a<}1ACR6TbHE|@HSfuLmdb4YyH#Z* zEw_+T+9_Uz>3l25uhIS2RElWm#akehuLPp`Z=@e1{26izzFN*9W0__mc zoGF99m(-Rc>c^s8$wkb$O-Gj1O7;Z>L*RL)U8G~RLpEgVTd><9i`)XWJNparYrKIJ zYeBLnw|xn}s}|{G2YdEYmHE!KgF4v`Gd@py5K79D$JOTm{M{ovyhIZvBlSwnbteM; zML1kOEQX2CNe3Xu(Z!90R4BYXcfS;TPsTbcAfUa0RyDqlC9g>ICfOXDA+qXhXXuF^ zzG&X+DC-yVKYI>(H-CtA`ZaaWY}6z~;v`4VoLg~KF9DKBTG3~0%TqL}BbF7KaR~5j zWuUI)-6l`rqtf|SJMn#Ly;$c8Q7u`A_^{WENPHv2y(_Z23r$33Q6x->)U$aZRJrz) z3qvi5OdV^nG^y?w+I&NTWPFnSNnFO|XQzdp1G3*!ul>S~3Gy&##}FpvPNmyHZk6^R zq1>tF|ZNP;WeNlWa3Z*%fQe;XZYLnfg63)rI&9+#lnMY2;2Ixrp#0 zS3%DNY_(@9?zTVQyF1Q6#2lqUaUoG13b8<4@GR$7x0nS($4@W!KrZ!ITuzq!zgxUs zj{gah1Gnkkx?-O)dx6{3gG}5#^kZSa~|&2G2ALPKWiGHuE3w12 zB0)9Ot06o2R2G_=tpzO+2OssJ*mHvp^TN3I=jPclHJ^(0Vnp(ExJ(e`79nLc3m&NK z$yL(T?XvVnRu%BBEbh4TZd>BzsXFmLQzOga@*&)x_nxTKf1R(A-M4JYHUnTJ6BOeB z-#o*lMAcI@II{FQxJKN>B++r{&pCMYJa;;IYGM4o14VJPr|o zxn(NMup3Kc1qN1OmY-*7l#V{smxKm;Sq~P?EHu_t5tRwO;n{q3Alzk|3Y&p(^J!~<=sv;( z5f?Sl2q+rK4C9YS&u8^Nzs#+*Sz=?%&X3MQk-_>=R%i>MV5dj(Q2H0 z8m@%k24D&r&!qS*k3*l@gmr5c^t4@t_a}SDiN^@2%SP(4Z*)yiiTMldpCZ%vxaBZ9 z?3@OGb`001w5Y%C_=t*yWf&Zo&^C+k3%?L(2&q4kKt|TYs4lV-JpE9Gv@KE-vfL2~ zFQFsViL_+T8|zjc8L(5+3Z8n+>{Yb#2~AWg-FO4E7=l||Y`p*40Glg%=6&`}%%9Gwt zqC^uP`l&-B$g*r9MTku0s{j9*uzUdRzy&LBFqD9??r7A^(iI1hv$P$lK$8JM#t5q? zlD=#Yl6WXaG2ri~UQlu;4$$)%Z|~13O_dsuxg>HMD{s!_6G4;{ryUo@pW0kr97pCU z!WYY(ABhH>q)qdT8gZ>_6gUc^bnDd_y{Cq}59qOChjxo}e&kh9lz2fo2@@ph8)O&m z)bqPs;)Fa;llVF@s9M<{5-C-UwIR}gemM`O0FoT%_)2*=4#a{S{ z5FhCDepeco-nnZ3*>*lRL2bO?J_;_;w6{M;4cmCe!?1HRcjr0+gG>Y{Mae0(k{gj0 zVTt^d{vUw&orjn6+6btt3_?ZZsPPJ`fjbUT{A$lctw1{86T&DON{5w1pa2!~}A0`U^; z82!ZxA0L&rxkZs`a3K5P2Fi(|y$LFED+cv~hqQAS3}2Iaz#XX)_`E^DS{h!SNi>t- zdM-Edbs9!puE8*T*HxhPxpwo`k@AN1=ijU+VR-{DOokFHG)-N8$*f6~%ttw}F*MOa_Sf8}=%O19RUm`e;IjVU(L1PXsIGG}r<97VuoX!4 z9Pqikwj2dE%V)3LNAC0Zo;Szr>>^yGvN-eo;T7m=*&0d>zSmrcTrfWQ4ao#>>=jVgt5N3bcX9QX zABUFzbJ1kstF$Hj_m-$21k7+DR0opB_EV8(pQ**-EV_Gki23~u2D(gPF&7>iDt3vb zxy&P3!;|g$%+4CrlBO`;OqnL=`ufXx5gKM?2J_+J;Mx>F(3M_(YR&<`{Cuv{Nkl>9 z>ABD(E`SC!sS4l4q?BT{B~><^w4yi}5j|tgROLV%f`=P~ICWHdam74RS@n zW6fe~J+}p%2)ko;*xcvFb7rt~>4W&#l?n?!d`Bx#W2jqx@Spd66ep5`4z$q6(wLE` z+OFA}>f^(#Sj2>tPhzTOV^a4CeeAp4yJF5QQ(!r&BHhsB!}Q%tLmBrJ?)9eKT~RgY zkFue+oGO($1hB?5{}R&h*rSc}tHrT+yOzT5!4He=a_dYmeam$XTC0r$#LkKi4i7^n zkR~oT_8N_^jeJ^K*IR^O;xNP3vz;NovVjSd2BF5I&LNR6`yF-uXLESfTgir{Fa={V z%&6$t-!<^o(OSBBGic^Fln*LqKh7+A;whL`-=3oWh@X4FO+v3Ss4@I^t0#V8z%!H| z`2ZxRGpUuJ*u6Qy+Du+tp4?R>ufUtVOj6*XZLjH4@->m3IN9kx?iV#iOMh10LT68D z90E*|vLFeQQ;I>~@X70VGqZPTl}H+$4rOKfvcEB>v6c{=orwph=%*ZCE@TwKN+(k& z4Ik%JHLI6c=_Mp3}m?rQ&yYzhIY{M$Jskb=q|P}d)ju}4k*-cKwGOQ5gzi)VT! z>`f_mH%m5{CCV9n(|n~zB0BOF!QIld0!7Y8taV1qPxx}5qKXj;jO_C_yZur5*|2Pr zKr$4Bj#wdLfam%qZ;ldiVJ$h`dhRxwJNaY&H1``I5>Qt=sA~ukVyO}wXg}iX$6=>> zEAl>8&n#|Nk1@?50n9nfiX79whKha|b41~(ugkYZkBE4+$|mB9NDc1B2OD5PD|OA7 zOqgcbSvdIG8^M|<0=+j!N{?W7!?U|hP=QU@7oeY}t@JDn#`NB8VowolqIFewbZ;wW zr2*rG`ceYCbtF)CTBp^un`fx4@81mc*eG(PO@+OqtfI%m+@{t^w4#7}q9#tvcZB{A z%oXN&re3WKU0L`aRb7EJW_<04lNIv7@1{3NZrh-`8K=s>DPWIQDSUO}+gFrL{f|U@ z>X|q0wYas-S?R?srT587POFGZ9V(E#Mwfr7Qr^uWhV_BTk5)p0Lj{WcBa@lvj=XeW z67}q=9L^d7IeL@awmlt#CmGXrwtZ76VLMur&tKk=lKz8!5PWJu3(9!B%G;=^r;|7v z=~B}MWb+g#)^*3gS+LbpsdKN z7*JnwA|Ei+qTX!`P_lXc9-EEl;j%UASK`lUO~quf&6%ko>3UOpXfldxdb&(9prh)o zpje_UKWY|o-zn46Y0)v({Ttm6O{)r30FR>;IgPR14M_Lk15-T(7m%?2PYmmlcbn4z zo7)(OghX%D@3Vqw>J~g+KB2p*A^kLEr#Y|n*e-|pe;bKv>)E zVujfZ-kLwEMMAnI=YNEN-PY5B_6Z`%&+IuuZ;UuYmfMM8MT1W6*;} zTeDhAX~M3gB>ww9X=-b13q8Hgg?hfzv*oiMJD^!mX?u+S{8^w#GCJWq#KFa&$5%x@ zFqgPbRt}zHMW!NOeR%uVB}qU{YN}q`!0XF#H(}nF&*^xA=k9)j6L;$dh06&__sn36a~bNTej!DCA|bR4Vm`I zoib~)0y}m`Z;O+4HxPV`qV<5<+DGaBPa|#k_X)k1&61&Lm#C!)Az8O<3L3_m<5cNT z;IJ}?NguV+-X5RqO8>%-+=dH;m&PUY!5uI20Hc&49T|UtEF399xBgL42~csMZ)*g| z90}YLl^S2b*CvCbYmnI#TKST{RyT5;PjHxo{U0ZMgH9H(Hh%DyqT;`i07JInKN|%v`7wm#DIum0LL4s_ZmvWgXL!FRez{ua&YC=p zop=r`HGf>SM$PRog;!k9&Z30)p8yr3%ar~L>4ciEdA zqeItdlEU2R%4NaH2?fbJolr;rRKQtr%4aA)k(*U<-A?Jsvc#xKzUtt>&^ne@idh1R z{cG5U0x@HoHXv)+rGW?*n3*lpS>cYn*Reoq7H2~sI00AnLMW4%*ilCt(tk}2cN{bq z-@G{NwXM!D;D6IZSZCRO{fucp~r}tqtH4vdDj5n*`(Ok(A2Hb4Y!|6X&7LA zQi?5wH6AYU6U*y5Kf01X<; zkt$ba9F;zD+`=bwfTd}%fD5UqCMZ!imHeoAtcHE39$*-A6m+~;6f_Z#B(5+aUa^m= zS97xL#=}__hjimdUcF~z?AFxMD74-%kQq=xk&9d_Xk&BP3%XNg z5D!kTuD17My1N%n_z2g4f zDl?>@8z6kn;`B?mk{p?Rdz;OX(W{`lbg|i>x;Sb?2a=#ve52Z=*;G4lVRLMuZ7I}W zV^@2)hDkNDyfP{q%BoPzV^gHW;V5k>qn06lCbW1dcJ7LsDD;(_N#<7a$%FRa3oCp! zCh{Nx-+LDGuNRxk4*NOpt;wrDGrJd&T8p75_FwksrD2l6_kswH(eKvLUOzRWy@cas zJRb&1Py!zEd=Nmr6s^(AzOo!I0=T{mC2zKMExdsI+@Z zPK*w>+?QeJmRhy4gpU-&^lW%IZClQH6EB3$mC9Gv(bm{C96x6WtlhhnCuDIn+kT>~ zS0U+ZJ53Slif?xVIMkeS1diS0bEIARrDWyJVY=T7l-Vsmk!YUja!tv$=>o=8W#@7t yMB4`ZJO6VI literal 158543 zcmeFZdpOhm|2SS!>FChi=}r-%QZlDmj$>@Y%xN=IWOJI+j@ag~=&mSHR4T$9rMpB* z$g%E}j)fJHkDZU6qX!$3cn%4F>@ zMC>py@T1Z6{3(7x48L%uUPJ&3TmtvQss5BeN`U|0&lu<%81B_K+zWwuKumWS!XYs5 zN8eb_*w~2p_w#;10TKUN&{$6&EU=RRf$aMGUI5E4=FWU~B!I9uw1B^_`UgZ&$N`e8hIRmzf58kMiSP^m*CQ?f zjJ=S*U!?pCc9xr~AKeAVb>YI$ffU;?D8%t!i$L)ZVwf@00O_I+AsbV77}z*q$V_+? z$1B|3&DntN=T9~vI){gniOvWwXE(ZuE73GOiUEa(g~M>3L>e;M*dPoZP2mPc;r(qR zI3|9MU=3C{_!H>r6yz6WPh@j~OfYn^jU9{tVK5o`TsJ1i0FcaHpXLxjh(wzJ!a78t zU2U9TOoAaA2kzL}g7?X$&JGZyBRtffgRsXsIt4q1fm>7~ZZOh?X@GadIYoPc3ntM4 zY;G6>O1C9LTX>1%B#|;iN)u%yeBub$AHv^X4Gp5)4QPhpaD5`6s6B=RBY~R~wk^ii-zdtR78v3W zF2ms*G|mN&4~BaNQV=dtC?d|yCIB5u4e%tPD0ovF6a|H~Bf~?$yG|4r7S|;>m=n#R z5{UkUpde#*l#4$-&_CGAhG}XSWI*u>bG2i8MR*bY!1D-Gv^|TeAI5z@oXG8OXvpvC+q#(2-iAHku01vSOJv}%ck>Hc96U8|o&<%#v zcQwEoU<^W|pg7wgCoY^AiF0rW41#i4ZbrsTHV0+z=YgT%oVhS2H3AdBBvHL+Y;u%; zh^M=;Kas+4Bm3dQ{XDVQa4(jBATb*3=;RlGc0-47SX2mE@->Vc4GAT9p%I}3AcRIJ z3B7Dg{0v=9oe*^J8DdJO88V1YbR>ZoW`beU2rwHW%-@y>rxA@%Ornd4R|Ex%vv;5X zy|-c5yBc%oAy{Y-$<{E^#^2A=z}el7K;fdJYzzp7NJEDJM=V{!Y_coV*2W|pj-YtL zJ?zmQ5w1vx2bP5Mr+5YdxPpV+VIILY!6DEvV}A%e($n5F02gf!$AnYu!dzVZ0|UZc zXo0K{vb`gUFfD2?`OgX{P z9t1l8dbDj2){ssl+L63SG<_GO2RR&taWaH(-9t%m8Y0L!5akx+ig5-D&^%~VwxJ!x z502K4f(J(!deUq|B>R}S>thYv3_TIQVaI)xGaB0a(p?$L(fekcJkboNGfeo{v8^NI5P`oKE zk{F62q2ORQePnH%1NMaA;p#;ShS}mwjM)x$Y-4AprzzFNk!fsTWbff@$|kvT zLR}#U{U{X1!^kf*h=W3qQNdy123}YXLvk<@!gk`~gN;cfSA0m6i8DRYJ<5)N4-2rR z17l+EMWeX@NJ0$^OyR%<*fYcZF`fp7Tu2xqBFZod>5q_5lkV=}=Maj8qv_!x5v~YN zNB~mbD}ZFeGy*prFkvB)c2tK@V?3MT43BVv`a404g2S;4e1|MAx5}FW5PqICi)?4Iu}I>@uxscP2KgmA<+h5VH78a zP&*pOFCg5RRCoE;p(4tMYm4nc;pTp|fb zBq|EW2yvsC;th}(vcw3wBb_mxY$V;;l;lO_l92A`C|4xT&yG&jXR|`tbT1ee8IJWs zxVRW#FeD<>)(`CwK=KRW(n2C3{-IcuS6CRDKr>{AMu!2zZ|_72ag0FW$WB32M;zBJ zj27e;fpw3v4-O$Z8F?`D8Dx8h5Q!Z$MMty3SVmMxU}T7^y}wfsB!nJFKzd>WnIZlW zY`lSg6aqnUWrRd9{76Aa6Kn`Q#MO>tA7sdiLZF=CCSVPJ=V(6+5o+KCW7&JU1T#@? z3^yRMtY~1;>;sWO4#+5gUVwz=Q83pqFMTL23`O9wsc<&KEsX2Rv;}PRgX_CT`|0Z= z^}|U1&aR=H5UvB&5so$r_jltGX&4iuC=YwAgvuzVXf{JaWfUs}mRWvOEHjp|~(sh%=Q$Hbn;kPZSk`#p0N8A@4NUBU zjU1yG25f3j5Qrtf&M0IAg65>pjt=I8(GhkyCX^OpYDz-enzET5G&F%^6pbNsJUl}@ z8MYC&7^XfZielqPvUkE5hC1800$XTfN&$|}CY0nBg@ofm2#y>m&eaQnw;@L(gQ+OT zC=NEt5W?mJ1{v5BT>RKL3K!47*>RC14;mK%Vig-IiOHe5xIqnp%-X|)!rcRc0>gsA z4JcJ#5(G#HV+?*dFcJ%dRyGJ{0wdIkK{IwVim;R1pgGd<5EzNbq&t`}h>i$qSa1N| z5$_yH$2dU!NM5FN5{K*x;{^K|_#3g%92+A{sHZcFZI5#2z>S%~kqn}tr!g9CL=1N) z;%RQ7VGMEz74M9|ay(!LSR~Ds=FEi?!s+_9@7 z*a%7_nUKQScxoig(7{RK&)I$qx(ANy4@a6J>|NMk4L_#6QMgSI$Clt8h{ZDrLAD-j zKd3*~%Qi5K1+(P@k=UkwUZLR0KnDcN$tDo%5gbl&cQ=7UjL0kp13F+U$%$!T?{GEh)_HxI+7HM(~mZcfS5#c$%ZI~M<4?jB&SdsfWw{S?r9TlO0{*y2N}A% zU~n8%Ktu$HTO$o#pH zHXzQh(RU9)hI#r0aDwe=Fm{kLTH-W<$dY`)Py)=qbA-PV2Jrn~>5rk^n}Vseix#Ob zLL=ZFY@eZz%P(xjyd^Zcag) z$jK`xsaqdjwD`Y%u->rAHsSwp|6jM1C$Q%u`MYMa*Ec1DMb2D@9s4&2{v~4H%D{E!lQ+|3 zuK&6V0C?t6ahnV})_H>fd)CL(6n&Y+S@~d%2vy_opU`}@?lhvM2GT*65r>^{YZds4 zZ#RD8ocyu9>dA-R`LC7c4gl)W#?uJ>SVN=?Kf(3O;Cc&(X)H^WC4huRw3f(=GUzC< zeg_chsTyHqAWZln0Wgky?eR}&CIix5GW|a1B%_C0B|Fj?>t%KX`)Dcw2<429%R-p& zX(bS#;tN6<9v@zmU;t=%_V;3E8KMMd0|1|_xxaRu4EhPhzkof@J-YT<2Evwb0AN*k zLs_OQnnvz`w1$lbOJw%Udjy`|zq)w8%#P~ThY;YpeM6tR>~(!}i2zO9fy(e$N!=wC zfXMjp)>@2!B@nWDjIv}PtOC{s9(^jaU6zIk*8r`8zds$6-IL`3 zo<`os*~{!GuaM$mt@2Relj={`k7)t{G9^y$kX1JM=PIWWF6V2wvP5wO0RC$HVZWp7 z;#GLCXUE#h$7C2`4F%TV+qKuN_@B@`0BH4UYm9}C^qvW80XpB*%WwQ_x0F`_k$_xY zA_K|c2Q5HoSATq-Bg13s!`2)C;`6(nhBAyWLID7eEc5o1UBr4LSo_=Ohixnw2)}Iu z0Dd&Y6J@z3e@S8|pXHZ~$n4ny0Z(tu$1BV1sHCpH5?s%Jz}zW&ebWsf5x+aUm$e-V zN;@TDcyaE#tg<~(06c!_GWgRX-*zYTe{Ih>*LxXOcP1a2+!Hz5zm8dB(0KIVTg_8H z@$#TL0DILk^tzvrw_V!T7vKD{bZ27~ETS1=+4vlGKJN;fv}1MjfPUvF*=t9I(qYE;+pkUX^*&Fabc||27mnEKiJCf zT$Aunep`(&(=noc==%GUPn!6w!6R*3&&pVtyi0PTC08%?Uct%irSFh%v&q^0NplWk zUb{NSD&WI!>yciU9<5N?X>;iH!t&Nr8|#DHclQRC)Ha2-6=^*8SSAD2FS>x!1ngC1 zY4>EkRGzdn5%L0iKk%-_wHMp2KVS#TB_zmo2#4O^)zQ0DJNAT%if-L5ExQRJz>;2X zX9us5U0E_^n+}YQhBtS9A7$As zzu~@A2BFm;%6X`CddNmbls;~!J=%OTx1!rUl{fa~ZTWEOvd|p~$5y?p5>N+HUs%R= zUt8W?%X_qol`9R#Ib|S$7k_V2l9j+yf9shuQPj70dg0Z|{9TQu@(L_`&s^@XdC;fN z_XnxMIhJowD1M79Y#{u6z=6v^c2B9YPj_IAZizS%TT=b8RJY@b?S6RPcDV}S@DW3` zI^o8V594bF2Gs$R9m?9%t4Bb@N1m-1Ig3ul(14^&xM%is@Zhm5B_-1{hPQv*d-aceFFSz@WQS|GE|#VsvPLr@}Xv| zxIf$@t^3>3guoRaV#neQO*pS^i9{x$@21ZG!^nga!2ewT_8@qJEIY70m)cs>_X_G* z?B(yX79}_+9GK52yn3tq^nw*`hrO+)6av9jhk4n?!qqK2X;j}J0IHZ}ZqWY}xO+&# zD7)qMccqpA(H|Qy?pS{SRSqYKkFLIM*&wyzB|V_;13`yH(jGKV8@v)?aJuR>3RtdF zs!^AvJbd02i6HFPzhoyvp+_J-)HmN3v+jC2;Oy>G$I7MDu0`UjFjh+@Wu)C$BJCLo zZ%)}u-1V2+gGN&1TE8CLe4X%mj})*9N}F=P;-5J0v#ifa)R;MWBW{N zyX*ku!Tq`>T*`vx4S;nn#V`A2B}EkZ5Dfs2_L4ugoRZ`VPyIS5Qsmja8{E9IB;QL4 zSP;|wJIqhmEwRY0!NIGfR>Vkhok>G$u@q6%UDg43j5nh`1{VPG*sra2mXZz^S8#Lv z)=jTuLY0J1k~BIevv!3C8j$Cly{)&D6arxenQiXkIqW|~S(I=I1X>`)4xFRTx%lSC zbK0L#1VI*L_Nk%=-B!7xRQ*|LLsZ`6~W+#(Bpr z5x0Fh8EtFY=K14QWslq|yaF%UzW-FUC8%I;uCTT4;6CB|&6If^s_|LdE%%!Hoy`nu zC%!db+okK|T(K~_P}X@vSFu{GXgNva{%JUkR(u(|bF1K!X6yX8X;erCqGcClO3GYV zA9@eCdp_J>QA)ZMl)lLU$+O*35Vb>jb&>E9)k-UFNJ!mA%-=@dhn9b#T{D<7-`Zz3 zk{f6_L*w$Ze6v#LpLU&(YjK-4{ody5+?d1W>Tdqj>C3aSi1>KV{y;lz?OBAr>fU+j zU^uT9?A932{=<=l;7d{MvlKU< zHKZrG@n_q8m{S32WoIpZ)Q%*yhsfUzY(|%Sh)$cl>~0D9eCHT557@Eb_cD3?Ffe&7 zKE79_lno?@$0fFCP1SX+ME2@Gck7nCyTN*vzT+14q3_I~)zyyr`FBP!O&{6^W2~b4 zCwr;!5y5M`pZlaMY|9#X!t1_N&Te;X{rvc3o8R)GTxSarFYUvoT?<{7;{%gNOD@0N zzwrEIvx7Ia^cm{puj6#!DVzd8^!$(RmPxE#S29l8M}ne^q!eQG^$OUgGKx#&T-}v+ zV{)(Be!a`)!!fryI*8NIP7L=Qq$!vXGUu|@1v!S+--E}$(JWq4p6MHdf}k4DT4E< z{)OY(kUomyV!|3@0NQpZZW$K5ykyomAs?fsm=wv`!~Snu$1*Zq62t@ZvR zA6qNwz1v(niuYNVW5hyV?~+>4+kMWymOqZ%GKv4{-GB9CbUb(Ndr;ck$oi7Ag?rJRnu!dWes zuUd(^#rGjwX$@y?q2E6&U$O2S^s6th$G~oB`xN?Wiy6vCeDo8xA7c8X5~4PR%)k@i3qx$2f+9;2A=}y*K=qIZ;oouU%^BEL& zL#zaeCtDqT#C7eVBQX=9ZFDtGm*$By`9$Gb4tDNS^FFWdvlDN0!Y}H<1iF;xD>Kwe zn_uf>UrL=FqTK3eGN0|e<$ok90$83uZiY#V{c7yLJf4i$uU~&XMCD;NjM-*VU90gi z_C?7(_O>=8 zuOmKF7#p7G9=&DHZv!ttu&t70dYJYjQ z8FHUr{kVWvq)u-%73nsl>iO>Zwm`GtOo_D~6kwp8>ca~u!>yO2`CSfG zgtB1Q!KsawBVGdw)a-YjQDUt`!shLGt8X8<84BH()B;AY=75TY;NH5{lezUTG;6GK zJ#Vw;&*)6nmUDb=spIQrnlDEeS_y}r|AJmty5@Y+c-zVN%<26-!?y(F9ly0X&pf?z zEY@ShQDf8k+#@pKk0dCxfvuM0)eAZ?z=FDr8Ili-lH0b`}wn^&R$q^SZ#+@a$d#DM~WZnr#2<(6xB!Q zQv9p?YpY5uKq6U4sf}!pZFu)1@U+;x~p(Xlkrj$Gg6y z{QL7=O-DpG4}HkWaBY9rcmI}PdCZ>2n!9)RY(&d1Ro|WXPe`~}ZYhW{cPh9+q}&rI zR~K93G8D?W(~fQTpiRM_YPj|Ng;d@EIqh)Oc;}Y3jA$N?j~Ktt@tPO<+z!=+_k89b z?CgYUNBfGd^iOS#ZcE`62Gxz`wcl7$THB9VehsTO%cq_yDVt@^;kTRaw%Yb7e`J;q z?e7&9_9c(j`*IHYdJTh2;~x3(`qs&j+|Emf)4~gcUPP6Ls25u(Ay0hPKX` zi-fsmZOgD-)gywD+^7TGXWi9>n=3>*;bo7+_~dU^pVZ76Pb@JTjtS^jjv2h#c&i#e_q0n96Ejlh-l?9sPS<~7_Rh_@DE`1e*R`=; zan^^HY7Bc}Ev%H}s_XhWA+*rybL|!08H7vO_3?iK0FXg2K-{HS!PJs5;U1{G6KMH~ z{(UIjuqtM#hDmw%ih+bMZ}Y(&19Mpsl-duxT?;c7+Z<`n)I{gwCkpEeZ&GK1s`N$x zXHRb2_e1v@C<2#vjW`Ta>pl5{nZnO6%ZA&ttgcjHa~EDs)}_VOjodqK8YSrGm`q<#Qc||9UnGWCh50^YfK9Y~*Jcp{0@skhKyl*a>jJQjk3n$j~LzX+vhqj2j7Hns4j%RPJz_vBR z?%fLowgzRMB>$VpJ4M`}GxUzDYNHkQvgSuR5=VbiSf8D0F*b>;-Iiwj&Y~nlc`Wf` z)c2SJj~;BD*Rg(?HFmr`->CB*S|d2Kh#_rO^kV@TA3>S&vSf6TkWswQ5l-n15(!=} zEfs!RC@Bnz5Emc^hDGDht0&{TUl5A)Y*X{c}YapJ~$hka&4!nl2>jFQ#sE^gzy zs8t?3?+Hr^5XV&HX4Bm|b$Rn~TEb+q#8XyUx^_R%O5};B65A^#V=6A3RjYp}#%q5A z3nNt<@>h_&>CaaVJRDTBpjfqhrg|R8D(6oeMsD6ad{ig=9%Zhvq^^rw4MbJUydrof zdbW0K=Hz^F-8a|A_9`!sRx!jmMm!5X0yCl&qO50Lz8-NZ;Z2=Xi(KGhIq`#Mwv<}1 z9+kKkCAAb|RN&SA=cIc#37;g*mAZ@4#S(SvyhJ&ilg6e>m4~oGwcKfbXa#a0{Gz8u z()wkjthpbLT3^G8>qLF+h9evI5Z-ZxzvXwI>7K00oQ?}!5c{xDbC0fygKImo5TpI| zz89lTC|4Ezdlul`d|ur+*=E1?wRx(q_(xjn_L2S9My_7TP`J|kBD|99JF62vd2nDW zl|7XNL~+kvNvPr4jml7nUpUDxi1QIN;p;z-sW2l%2Z<5)lhbCQ+qcngw`Yh1n@$+k z(VRMik4&BCzWh>i2R_oPHm9T9ATOrq#C~yYymjZR9;NSyVg;T{7%j0X?=*h5Va4+L z=KkjrqdGV6xE`D78!6O`I@zly{u;e4ve>DcU1${aqsEBR%D5#eXM)_y`Cx!_M)6NQ zNZO2Eziz!gLm}}7FY&?ReeZQ2BN*bn+K+O}x^(BaVMl9AR5N|%{`}ZB<~wK41=7^g zXZe^a&GA$iZ zR<=cJBuyi#N!8*@C)LK-$;X3Ufh2T(y6#4KHz}hommHZ;65oG(K;p_{h8=U+JnjU1 z-{5>)Q}%tzxVBkEJpWMby|()C*^VY4erA#9q)Iu9{wZ`wC#;|-hQnygwCL2IwVXX# zQt<|)5?OWepI;B$+t*b+HNQ3bV}=i_rMk^mulmN+JV@g%6pM%NQ(nFG!~iG%WpXT| zuAOU9M~VMPZ9->-RmO*=&nc>d_z=+8PWPSZ^comO z=8PsO@7bMv2n2isCqk)CR8bE-PMCZmV)jLvcgBF`aC0l@{Sqb8_!+X?+j9Qu0LThE z$Gb$&yszfw&K@n}#veeutD8NN>);t3s z>l)<{e?GoD9TUeL{oU)90Ba$#9k{(jlyzPItX86D&DfEFAvNKZd*U7a@mF#2BT;ca z?@1?X{b&X)?ucECxtWmG88rVq zq)`#X!N1>dl8%E@oIo7>^nG05r~j<)!`rLXjnw_-eJ z?$1VkN2lQ5A&T1TnyCrsv!GWwZGuxXB5JBp@*yu_n!vY~KhaiEKUvydEKTAoib6Gt zQhiC;=amDyy236-RQDFweMw~3iYw-*wZp0Sad`If(#I8Z(X;i#?nTQ+tLqPRLq(-- z5BU2QqE66L!*`s+6_)662St1RBzoFcSA{)nBpOn4tga`of}?=!In8*(h$un|0pxxvZg>)~>a2UtNyHHq8I;O=*`( z5bFMkUXPN=E&>!lsH?t5uX?{?o|V zoArX88T;8Iip>tZxvq1!GEF`PA8D6o6`3pX_wXm)A`33;0jZM?@8-h7D&9u{zB*kY zr|j*ufeI^A3v3yd^47RjBr%x|M^ zW=emYB1X_{#r%V}LkB}F#zl*9t4aN}im@?OJ)ZTUTccl<)aRCeU_B}q-8+wH!TTJQ zF_i?6yB>QS5F{;#O4n~#pUY7AGfcFwbfBQEJ|q5e{o?qq_n`N*qjD=}?I~FcYbIyT z3o>#m-{%Lbz2tAQ8lTx$xJqyC;EAvOxpM`3+d#tNxc~EM&jNw!#C_Ih&!!=>zU97R zr_Si+{z+l{kEI*0-W_)n@a_+NpL79Z)pJo;r{#65k+P6@|36pxOqACNPdI2?Fs0iJ zdjBXr%Pabm*7Cyl9e1^@I9y8-hr-XArH|WZA(W<`tW?p7o2mx3A;PKSYvZz_yY1g^ z(PR7lN}QYD)92yh6Add>a-XK%oAI`szA+f#tXpEVBfM^`x9wrcJDpkv-=cM9OnfjQ z?=LzJRd+0=I$G6!6Wl@ud>HSaa4*{bnwK@(^N7;jq42ly<7L?K%U=N)_=3RQ*NeVIX~T!-S9B~$;`{l`nL{l$-~Gt%Pi8=i6|;C zBYTTtrMb5nl(QbT+!{j52>kjDpxN;0;p?*UmFB~<%dgdodM}m1eUA2Avs%!9+zYX< zdQ8?Z;d8{SiMP_$7dk(LLqUDD-;4P&ix?`ZDa_Ad=T4wAiVI@H6`e;uu z(R-`|Oe( z%D?QrVJ?kB$^nTf99I1a2T6D5_lGTKQ&*`jG^Jgm1YORRC?jhai1?FJTcATG2$erg zs_Qfk#tB~y;P!PJUgnH99zXa5r3i|7Zm>q_9!MS=1gnp;cAt^XzvLZ2+0R9;YPYT? zjW0B8ql#j^Zdz0iRmMpwfR7@&Bo!rJ;q<;+y`m8c);YAe?)&W*n+8BN&wh8msWcA1 zNG1x{?mMI_uZOKy0DW_?_S}(>2#TJ+$p5iP<`v0I$RdQDwBJ2!JquoWuC`>ay1ttv zOF73>m1f2XNlU-x4;5KSk_S)j$h(bTi+ScbELEB?j{$dgc6%gCM^x%AiQq|<792WH9lX30zFTG_Zz*Vg>|ErkEc;3w zcxAlNdX+*7=;b{`eN~n2wkCWA{f%F!HxSZPQTm@TI~PfbI#GSSt(0gd$Vmu!__~7Z zE54wVI!n%5DsK;3Mu!R*shU#dUF$JGl@m~$hBQ?a&Pu2+&%&wfl*}c;PoI$LRUNio zEFq+|hiqzdR>I>2NQ<1DCa7%xf0M8kzeqZ7>Zr%k+|FAhA>>a*0r}?=9*Z^wFHSfE z`iPtU$d>5?yGR<%pE2iTJ=_in^%oyJC%;mnHA)2}X>Pk52Cv8|t(R?_f0gi4fy8#iE39u=rW4Wkl;k z1%RgiPi_8xuQs%{3g}G#lj@ubzZJNf>0k(LrDSqI{NamnMTGftlB(>8(|@rG4(Vbrf<8SOjkAw=uL4^A)L@UqrS$ea@?hS8s9f)b=0`j2Zyu6S|8E5k zrrPjT^dnoHyZu|Adq>?NMk=d=$sw5HfpZb_6I~JShVm|ErQO+iTsj*1uUbxfRM9>+ zy&t=O?yYnIDyagE|H}`CdE%az7b}FHw|Z|qf9+JU7E%^EWr^8x`Dvc% ztRC&-ACyx@^jThbT|f2wzFNqye^IV@e!!WTxy6k7@zh#p#nrRwX0ooR3#e|{Df~lO zkSPgUnhCu%k>&GmPI?mJs+~P=S%M)wb^XC^$F^af)p+(RpBRB7l2&OeZBeYjj1yqQ z;TtF!M(jlPRFzx{Z^RJA>6j?t!{=YitZzxi%6hJyoaWHFhq}bn!AGsQN3f=!Cdw=U z>4xv(41O}vVCX0`j(1C^E>wK3J+7EHalWU-LC$tPd$c_FYIU*2z|gC;HS_CjEuO^7 z;uXJG{CjIW*;v}zg4W~GVqqskky_P-#czVr&#mn!0<*=(G;Y(nYX|yH3gUuK+V&M2 zofQsC`y9|6l*|glLXD*Z3P~fgc#ca9ioF!u+ZQ9^9$kz3<5#fScG}10s7n@s4{LP7 z=D#llHA(1`BwCB~Ne2+R3I>2Sl&R{w%P2!M_ita!T~OPsm?ZTV$-V36U4H zG!dq&ccJt{#4~9#5nyikIm<#X;ol zHAZ{Ft$Z2t>l@9pJ=Nd6TPd%p%#sNi_G|W7x$N&Pk{+x9)3L5S$Ed~h>38%O!==+1 zX+E5zZ*z$xM-NOMNAB*ATGrUsOl;iFt9DjhlZUH)-vPDK;HGP{=VM``u!Hndk0&C4%BT6M?Q8Zs6rUS`uZzK^$56?g1UuNaJ1Ovesgiyuof8oxHGrsS3dUON5A{_ulu zf7znF*)g8PvO|H~QNGp1sg|v6ji5XiJ(z8Xm z&y>SIY;RO#IK3MC+GxEQw6}`yr}*|0j1WD)JL`)zG6kionX5}bd>xOUI956G;Nvmx zjt9LueH;4}*^qQCjpUxws+s+4Mk@8o3@J6kF{B@LV@=0M--Vpsg6$jhJ0aU%5%g{) zt2I$mn#W#mpMDr2r2?zrJvZ9gLVC{^#OXN4f5=(2v58A0np3Cf>pCiVl1bKI|NIoS z3*F<`UpRR_&f&&^d$uZsi08}12TTPyxVCp+e{402IO@3ZW!-R4hIWBk7>mX|gj)J8 zSw@t1Z*N(Hmald3ePPv5AlSm(^AxfRNma2f+M~Y%skP}tE;&;*>&px-^%0}N3O`?h zIGx$zIQZp4mZrAQ7yOwZYs{`XlHw#@(MQGYJR-{{K-%j#vq;xl_^VLq9fb;CTz)c(H8`-Md(V>*=s8o!9rRZD+;w44#^ zZ+*}eFCM-+o0-;6*o)M1RL$wlQT3z2u^jh_+#rG3&goNCxfgpo<4Z;^@=^;DX99Uz zda=8al~1PIRCOj7w)LuHbn8lp{T9}JIlund+gX7-V<<=S$*xW%U1-?l-V{~ASGEII zIMxk;ZOhe6-$b%I@-8PsQ)NkelX6^)do#V%;c*qL{24?OnbJS0`kKG*D;wQB<|=lz zuo8&##hdDP)GM#}<@I@;FG;_hXYYMd!{g~q! z#xEo_^I^f5Lp;ky@($&io%L!Ncf?I}eClQxNsfJ6D{;yApz^+#rSYj&Uy^VuMf(qB zFV|KM%jEMR$UVktE0&f#Q(iYH^jtm@C$iXfd2~Q^>|pW*g|rnv)-W3icusna4WoUq z;+JEos;?_hO7Aoq?{Y;c=#g*DFIBC6?3%c{e@yw6X1&V#2$JV&|q_l;@;?I+bsi-U8sN+*PiJG4d-Bk4} zcTc6m{ybE#YBj=%JCajwt|#8yZ&S3lDmHZs^S7WBT(+Y3mn2A%Lf?^(23q!LcpO@ zz_d10jFRtgrc@RC*(59n`$V`Jkw$spU`(o)u6XlJ zjRyHxr(=!z8KKdt7OvG zKAnb?6#AbkCI3<=c(ZlI<|P#bQ2bV10yMCLaBF*AfLhwJZ(`FU1iJe7lhn5(xtiPV zDGjH-l_&jap(4-i;!s4ebOn-|xu{pC95tO3^|;b5m+WRyPG&Y-c%r$PbP~5V>KWZ7 zwW2SHIxXO9{L%9jM#3E@L|zeZ=x1xiBGMn;fYqJrwSawoy-8RBMErFcikQ52+j3>y zEkMp%>YE6k)bZI#xOJq+W0#o-4I3(`v%T+{?uvPyBHD8c`#KOUzhv3-5-CwOk{mK3 zANOgLtv_VQeFQ#_?~&%82yB2B)2aN4|70zUWMEc)K}ifxy^y4~Cs|#)+UmTf_9ODi z-x4o;Nn0`UmtbEQO?|tZv<0xCX@MJCZK-`BrlRnR8tMHUq1#gX4%r4)4vq&|C3IUQ ztE;TPl8xG*v=^_N^ma}0^D%<1GQ!5DXirPbOWwF*DJYxjB0i4wKiI2XM_zhtrf<|0UU#!u2y>f5ZK{yheBQ}MjRuZNhQKvC#j}W|uYX1& zyjp8&{wU0AbkO+b8>F==)}Rr}Xy8TIsXX(6feDW*FHND6Mkf0`dGcBB>Se31`AX3+ zSaO)rUa`JON~po8T%mjws9UQnC6S=oIcYD(>Qq7FWd2KT@0K12eOtyTX9AF z4gQm>m<{RUrA8t(hWS3E(+L6pSO$V-W2vv*w69s2?~q0nH$Gns;i#?a%$i&<2Mvyy zsJPgZmfFp|S+lQm3F%h&%}M*O6r+CoRv&(zmyA;-4TyD_v6e_JwHgiNCQ#8~8h3<=*bCVgW&95MaNG-F5P>A2Qw4@}hw4H@KGcvqsOy6;>|J@Gc0ieLANoV{dkP{2^&8te^(btPYKzhM#?KvQE*5%fnBFc7uSKud z{mLhQ{ms-W6F8d{5XV9HG8q1M1+jw6S&RR6&Mx7GHb@q%?WBx?B>uJb1O=#XC;VDv z6T)v%z%YNbOzf1NlLk57C2#;HZ*y-^rt~tw6%UTxPb8ePz?p2Nd<@wsy^(RJR%d>D!c(jY51$<1i@eHK2Vq2 z($`VNNoQq#5Jknim#zCck~L4`q*9KgG{a|&GYYi5Fa}xvx%;0^ZTGp!P_a2sukpFF zyll|>96xQ4*D#s0$y7c+&R6B3y3W1K|AF=I58&tzsHS&c2nOdDZ8yHV9n`V;h^orS zMXhi?qh>7kTbgllj7xldmJU2$2tlvovo7y{I=j9E$5BDfr=(3Vzn~Sy1Xd&>c6E(B zYkrISyh$fxea*jT0rJrKx15&L&UW!%WfZtG=P?GSV$+jzCknH~qJ}Bm zzjjA>3Wb$CL_=)=MUZ);zW!juydvY}rZ1vRg61T4$rip^fuoOP&OQM!t;bv@0|uzC zOA@OO_n})4GUg>)OX96>?LjBNo+3g=Q{vy%fC*~U%O5va<6^sc>zJHz{6s1+&>Tr7?475JQ!cz-x1^ddC0M>$WuH`;phXWTz;HTF@9)Bf%Qfo-onR<7{YScljCn6Y z#PIt(Z8RqFqI^-GR?nLSK^qjEDH0eK`zp6_Nj(& zKZu_%>=Ru?7(5G2ugJ-$`M3Q17A29Nw4|6vtv?iT@m4=ZB!ujl@em5(5zl-yTrOB^ zDbMgy{xn@LTXB$IwsAC#8f5=omYz-MmreYa(Y_@>h<^cq@{b=cp(+DN;itZoi_BiI& z%>D}|PuXf`eGSne>94iOhB})I`|8CXv;CDiN|Gwe_YTkDzY2Qi)F8Dw%Y3ffl$H|- zdKdo^O$n#YN=}}5#GjLO$L{iAJAGqv^n_N~vI1<}v8p7^)c zL0_&(l?X(a@01{mv9|;JPlkX2g5UC+YrD)$y3)>rc`MQK)&5751}q>-SCa%4FOG`T z8&lv7(3d544KW{Q;sUZ1XzBKNnv{nG_G9iYGPZEpLUpCD;bki_bn0x!#U z7Vb#e1Qv4Nf0AZUoC6J=oetCgXzX7&<%8ow`VQ8z^;}@Uw@TzhIq;kerT+6d8jzN4 z1vIH5XP3#I&a9GH{!GQZJ{j&WN;n3{9sJaKP+JD0vyua*2YB~nAC?$10HKjijFd5u z*gaRtv7(>mb0yc;g6rRu%0gs#nI}0ma>+u({mFga*gabF;r3ldjUjMqliJEPdg548 zs%Ky2?Hi{$^!^bxiFa+eB|$RsOPTCHG*Ecsdnn?^=a;G{8AVyW)2hA9qHOa0xoBTS zsjyVy*!!2>xPTa~eR-Rg`WSX4PS7_KpIXsRFjaV+z0e7Ybf*rFWXxWW6z5XN*k|)~ z>W%A|FMmXT;7>GDUyW5a?hGAV=niGp@HS1YiR_r!@FYc5XU)_iaH#Sh;RN`fSQV>Y zF>*n59Rpzi*HYZ`l+H?>f!3aqb;?RR33)fr^1PSA1*gv&FKEWU_c0yfc zO-@E{2kf)=ibo&g&nV6iQXP-c4{0f@rh}T%oX(QfJCY9-=FCNHwU4uXA4%#Ss8ls% z0B)SP5GPA@Z~4zJmSoHsM|B_OS6@}@l+K8a^l_dQn@4~)J5)hsqP6(v`BvtLl(XVu%au&;@Z zOl|HnC7*Up0coQ%1pf;NkwAAy-&H`Xs$SPs`(0@h1o zG~_SY_B|P;VY(CPRM%J#6UKY0?6{@)Qtwt+PNpWv(6kb<{WB4%B3}PCJcfouzKxGD0;O6F=Rcq4>EBA{f z<+I=Y?AOXfwBWo|R6_S#g(91lVa-2;(vhdS3r7;yHiRq!X%uv_*Gi!d;IKU@SyF#M z&%EHWG|#Am;2IRZzJ-E(d)W6J`J?KKsk7^vonviMsr_Io)A`kFtK=TvSpjM#0P@{u z;Z8IBo^SP2VA)!AEd{|=9Ah~&pc4N zxdk!LOV?CcZ1TJmm)apXyYA*Dgjr@YJtCF8+O|v+B*RJZ$j^_IVRf%3s;p$>_ONxu zs)qW@z5KSn{#8On+O=sJqhx(`>5WT>(Z-?;{6E#gPA1R1Vt)8I5lB^eShO&o%Z&Yt zlVrKirOP2>+tvw7ufU7cZ`-F5HcRRtMf;V|t2+7!A4*!)KmuyD6U={sl0x0ii6B^c z)7XS+>Ha@wRIOgo+Ya|yX7lQy5|$E#hoZ1C&Chv&J0QROa`+qBeA)K7Xs7x+8W2Ar zC=$^EQ&G2*_NFUbsXwDNHkp&OUL{Pva~F-J7aL-x>tZ4?wQrnzK7YtZDct+ZWF-8) zvNC!#?)hI0pBCMo?oS6KmIVCQnuY_7$m21mg)83R^1Q)#VAeb+q4zoHr##IPsuU{EzX{Nq%GXb*0?B@>xYPg)(97T6*(hK( zhn^cRQ?yBd4%v6cLCElw2S5@GED!4G;Woybfu19{K*k!d76AzwRE?w z))vJ7;_khJqH4Q+L19g-$; zWhihj4;z1oWL+qkFZtxiblYDb9^|14Ts50lmENa4roe{8lQ^z_8jF4o2eMRPpGuZA z6~()S=+USzZ(7C(ZsB?r9>3e#ekv*kQp7B8eJ`IJ@+9>vZ2mZ5UpVkXp&UH$7Ldf8 zKa-~ds}^<$^M!WXHmHT=w$f>MV(4SHH-BxH(p+^_J!&T zxl)+|1bki*{CWvzyE*o5c3p(?7x5e1lIt>m$Xc5Y@@X!AIXOlu@rPtEQ8JvsM?0Kp zMxECjb~Azj*eim;XlZV%x;Cp2meimlP_@u8ol)j|0|=B(wZ6Rd7|c|lPheHKgO=?3 zuTV+g1kYCTF`;%I94l_M*cIYJ?8xz{Lup0P1T)diTHt)05tX?Pu`jSgYJt9TAdLNY zaYP64AmYkfy{z=_jvbfq)ziql2X6L+5`X=lC_IKE>QJNraUlhFo6TG4&jbD3@X+p` zP%5semXV`JSXk(XnCXm$qKCj{zRK4xwpV<)7l)dK8mLkU&544QP-sc)Lo6f?HbfpM z6zTiCa6OQ8SQfmwv}9q|AMSPm6o7U%!i_yN|BQQPz@82I5o zmNho9Z+QWhx?p!*KW40HA9D*yvrkCqFCf^vkBr6vXIQoC zC9YD+yf8rBQ-z08*FiG*fZ{^{jQ?SMMETWzGnLyq(fVJ!HF%|$BYLqg5t||&s1Al& zY={JS_Wf(%Lw?Mys?CZQp`n=jKYXNHj)6?=WnWuZoG6ZFdW&xhZi46;he&F;9v)dqpg#kRef;dsR}4s#cH8mh(zj|V{6CsN z4TacqCv82er+t1~wK{1+$CcdaVk`W_!qi@{KlpegKIeapbTXi(bV{ zXq-UDa8e1OFj59^P5>WSgT;QCT8*FhprV|>psv=XMPFN(*WppTLvqz=%dEZToxZlQ z-R}u`g0bmeO3(4YjYPSo=&ts; zAwWgtNQHrDB*85P{&zeMm}1L*3`Z+m2kMJiNPt>~I6D()Z78#_UJZ@_c4pt&@_2!v z7L}<8eDbINl}`p*%o7eatMG#}^({S7%j^Y;O(k(As^O37f_kSH4EwL#K)(g%trDQk z0T^=+F-7+PE!_i!2*3fmej-+6{`KxGIv41kpk32`0!je7FlSMa{BI<4u(**3Xx{^R zpf>+IK^ZuN*P8c2P)ygxbf__lGBRJHhpVsUAKoE}o1h@N%1$)#kHkeV8j9#)n$nwj zjPdEtAigJy9eCc)R08gUQ6T;a0k~B;g+dzQ0mgM%?cHPPc@92`pz}>bqB1QoLbpY^ z1cTL&Ma;}U^D!G*$l#7Q^Xd^`eGwHVOE8AJyw?H$rP=dd1gamzsuaPQ6V*&AAga$i zH}o*kiV?_)T}G1<3jVz?9>&ZtK7AqbdpZg0Lts!!>d*el0OjKd(nP}ISf*O^sJ@5d z^XnOFZJa0n8@bS_X&Rfw5Oc_Xqp&&kek{Pn>n*;DL--ZeQ=bQ9MRSD~ECG+>bPr?u z)ga1f?eL*0&{O=SuL6^pi<}2ofa(UR1xb9U83^<7! z|NBm&{(JRd%KX2P+IYRN1!Fenm(j>4yD3B}R z2IdhogO@Z|2Oe2LlR>UD@|Aed2=W(GV?{XiTnD6p`eN6dIP@0+{QD-6{|Cvn+uVdh z%D<5@*mrNo(Zr}3V_vO6Z;%7#)V6A@;y;!Le7MK-o)&Ot=^NnAV%J}#%t#D6K=Z_h zDjsAN7(t+BG1;--bu7e#Q1H>ip(kLmFS-Iau3>Vm*SEkLX&dF)AcJ5`y@s5kn=%KP z4h)kPlg4YZVbWr%`)Le4PN4RPjLnb6LW4EH^;Q^4PB6YNqvQ_>jy=3Blv$X)NwL>qJIsb4%YC8(om|qKuTAO<~|UdAuEkf|KGzDjbw?QKUW9oE8RQa zg(ZX!o2u!RuDl>&ap1FGs}gufRXdW%Z!zB-cAK;Ad3)9yup=Tl@zTKlH9lBHG<|&2 zR(Q;F<;SJwsW&sN^M#4aX6^;pG|7Y3sN2wAjzDf?Xc^eOXx1CJSWru<`W3EAKxg1E zmguw#Ud0xYx)kwS8`^e$m&45WNY8sROP*jez0PbD&^C{HM-cIO(|kpMaR=Q{#0ZxLok3Ym*9(lVccY_d7F9L>H~VQ881 z@-MA+7U=My6tB6C+gH9E=>Cy1pl4YaIiMf^i<~n#gPdJ2nw-Nh`Z6_R=J56L;_Kt` z*I|p5$sUshwH8vsrxUNs79_b%e!lgXMMkE*92|0*b!cmM@=Gf-J}_!wPj=haVD||KVQz*KLlfQ zg-jpE0|5uN-;b)%)%At7M9|i5rXH77=-Yf~mEyJ^d-X&+`Rm9lGGt-0mVu<=h4a|Z zHk1Gk0IjRH;|TRLm%-YMYhiN%!HW|abjhcv$Wz7zE}gGi=vw--!F#bR1&+gSn^k?e zEq}^NiLS_hd@3+jx~>6i3ZNmbkA2eD3Mxlnf1Uu%{?4=t&f*hqCSFt6BZ2?>xrq@H;?Pb;oD#*S1;f z`4M{fFx_KL^5etjd!y}11q2e*4;?_FR18U%R4^l z?5#d|sTE*r{PiAaM+@^?+U>W@$9hL++`>oGf)PX``0s&`F!MhGMOm=oche;DXF0aQmZUJ z498nMSj(&M2<6))p)huU`I_s>qUv@|gvL`!r$2;Ht z$Xv;(_jN0bfTjCn<iOR` z07=nQKNX95dGqlW6t7-0cT2DyXo(IKOi9;mvAy@+vul;dxap^L7>Z~)~9Fo{9z;MwO zny4j>BGu$E!@7no|A1INng%x!eZ0Uw*=(}#yOZee-@oGyn`KexqDL+hj}HHI7xn)= zFq0!j`z-7wLeOGJ13`8np~Iu(C#(Ecf2$sKS?rJI^n~i$k{FwmtEXNdu&|kXybdLI z)`l$2TXV%Acd#G?30?8Wsul%LE1mX>U#AGwd0S1272z9Gyu?Re$InEv$;~wpEh!@# z>JbRICuHue<7>DCWs!Tc$JeEHN4X3E1dWqv9NaMttGa0)W+x`Omy_nMDQ6ta_dN$? z+%}h9{dcEkEVa&Rmdws=>4(!`u9tRqS7EY@sy%)v7J!rNR;B5t41Hd{rztt5?(peWg^cbsO6y-JI$Wgp+R8noNM{FUqZKS zf7N~Q>6Zu##XI<<`RU<2soo3K9;l8^U?kSLEFvB1x!J3j?s0Bq)fe79TE&ec=rD1I z@3wEvMXkKiKHDiP2I9CCLypL-9qSVHef@#%PsQ z;~UzPjYpqkVmw<~- zaH%~d^US%LP%ekyyhe}4c=o2ju1 z0q4X>v^oD-MTmYd%PCmFu3|F646o04v9FN5fKxuO z1ci``_u`A{p!15|s#)oLK03u$zl<$d6YEA%DRQ28? zg2=WSxhwz74H!MN9Y=nYZ@kf2%|BG_%SMpDN^I&44`D&e9*U}^d(Y!r)jaBdE=r+U z?0aISHGNy^$M@=oAI4OR#b(gc#4aS2-i-d~d-w%77QL}IplAjZ0s?JgCWe__=gdSt z@^9jJb_=DU;P&DRuAESXJ1%w#xGHFcH+b8VSK`2SDYrGOwjbfRo9LV}Kd0qUqI)iw zcJTaf?R_fsYJoezf@E*82H~Sah%-YFW7uh;n}zdr#b-_y6ASS;!8F@VS&m+fOXolZ z_EH?T4tisMw)zvJ5ZA#&G(${RBmoJQ&1sBF?MvN|0so~|ikl3^fxx%mK65)kY2=a< zgu))qHa)evRrRb>;hW)g}zCHKQqVB8xZm#;` zQ@6Rs?x0B~@vT5C&%T?VH-_ajYHpJ#K+)V7cfn&l96>3OL|a$&RY9YLsjmQgJA}_r zrrrTEcdHJ+){}?zOcJIrH@Dtc8|Lb2)GxQ<#yAP*ORB22?mw+ zH@F3LsXPI)A&bgX2uvpo_5N~RiVN+?m|%dD0iTs8$LlQB6Z>IUQ!e`0J8S+D={T5@ zAT?@S%Yt(-QO)%ihbM)My?}>6ctfV17~*p%X+Y;?mBa7QI~GICL-+_PnIz`B|I4NB za<;V1`DW9Xk{fl#|uKXF06t;>=Kci3j^D z1AU0)Z?q$jIJP_k>^(HN;k{QGaoSoruD?I>!6UHZ2@X9W&X@cCum6g^5PgLO4TU|B zgRfg{b(C%j-akG5zF8!C_JgzcfOPW~@X6ch1WWDit@N7=#-TvjN!yu-KID3zDj(SZd^}Z5S7xw zT_OOU$>@tIDFhVn_0y*`oKca!k6o>+NeUQMjN4aUU!(VvI*774nVh_n#W^!@iS$o5 zzn6(j!U1s;9~_Q^H2WEj1ZAQ7yG0^TUda;9jzUPx)z zo>HrLv!JBNqH#XhRq})0`pQ20zKRg%En^cGo_pc=kX|H;SbEQ_fUfAbK4+|jWI{sS zMwF*4?)NGlI>Xjh)je}>)2A^Xm)b*N+lz32)IFf)pB{uxN`UCmN+UtJev~^)Yl9N~ zT6G%&XAQ%uepMY4jvCv)gCyRnRSK(DFU}aQd3b*#m8mxEDYH;87e&rBt^lF3FACgw zRZuh>4jd6wnwP)nEP#mDVkG-o{B^-KZ;bYHo0IclgV2migVkE2s3A%Y55hr5WBdTW zBY99n`d2Iwf6ks*Y?ND->Ht=7U}Fyg&IptbeywBY!E6EYF5?&R;fAsCaT2cOn7Zn% z2<-9S1!NXd6tyA{_JRc+Q+xz2xCcG=K6+T=KfgF00iJX{7>NKZkI#4!T2|;({Vf(QD^(D zY!he_h({JO94B@?vAt0|bTak0*e~7?{hHncPrabXLN?ONN{Jg2Z3W&FWPnS7LmxGZ zlSIwtfb}aw(x0QBazL*r=#C||l-O->&%y2;ifdlU<5A}UO6x{V@ zQvADXFx=+GcFpA~)31gP-uVojA!=uCneC3O+4txk5!?lRF2x(lB$`%t_z# z`ItqzcjSfo=4L?^49wgI`HEqZb`L)F3!HD9^W<3xBQ`dP(39K7gG?iO{4Sa1-dobey%Vb>Z1t!7 z-~^#25NxYjjQ0F*3ZCOn5{&-_kf=p#7rPzI*79!p{%VMM-KYTs(Q zmlp#Zt$eyK1Dpeh{dseLkkAzYLy5`^YqJ$jw>g%}SEm%p^9Kp0mncg)fT~w0_1}ad z9;5=Hcj7UB5p}5kCg!y_lYcs1ha)$3$3OigA7@d-b%|v?If|Rm24&#he^*_Tu--H8 zA!6(QAPfwSP1rbd=#Lj{g|1d)Z;AQTr0~zqTGw1a zrLZ$I@3VQ9l5uV<<^ox2|D)@Kbcp1^0~*fA`EO=8(_t~Yk0xoMub1hfPwW3w>Blpyc`qucn6OIdk<^RULwGVT_J*>1Zh*$?-VaB9+Y`(@l- z5)%V?6XV~pMf0iF1{0Rag*(*vxHHLo9&KckYDem**`55zL6j zsvh~QRuLo|EWdmasP}gT1%=!NqF1LR69VGI1*T>Uw|J#is9Pt);&<9WuYFio=k#jq zdOX`w`-9gVNk6lYY53Q0(5O6cV;NdWOI%PsN|G+MPnp46h`->1j4zxLvVkerZpU&* zvRVVC2XOzEX*8n~oX#kAwXeAJ*NSlK60oImCPo4i-^QAygVHdAdNmvEWURA$yPNgs zU{J%Tc4y1$zN4{l2^GYXS8G~uh267{^?tZCq5>SZgy_3dYj12zOs*|lB8m5&J-f8O zfd5_We`z~0wF*K7^W?&pjhBnQc2Rk^ok^=Qut6?InAr=3z}u4RWAJG2 z9-dG7lsb>5l~ue(FA*OtotCiq8lCUH9^Cia8eZ~P%vD@c7Mi{FPV+so@z&iIeC$ea z`msojA%)=duk<{7IX(-TD=`8HmENY?FsLFhN19XC0~#_3WnyVkOUqEnaq)#Q3z6z4 z7W9A5Q%CL!P7A_pW&K~r;{%dGcE%Wh$P3Qz9<1|rUtc)euRU8`im3~>nVY8O@Z|O8 zakgt(QXXV%a7V!J1Cw%W>6+KfMy|`_Nmxa>JoYjr3b8idCV;pCqSnABcLz<3e`_+y zeK$Qz>tMLkjBOV+@!h^lfhX60br^1J(iKE!$AE=a1JaJZTYdCZzr^*8xA5V7BOIe17B984%99u&;55LoI<94p@Okzbn)k*Y=qQ#W2mJFC$mQ%nVYSs zti}nw^f!JPN**0Z8y`aavr%3Q~)D=j8EgT)%&75+pU9h-A%b1|FAi(KFZIV&Dr;KW?l6rt?P3& z;-dvns0e*6;R{a>f7<3m0r`P>L;zW~JTUGHbr9A3Y-cT!1Q7PbtwyjTSx{Eo)Gi3p z$7GIWl-?aj1fGNW6OOjsk%6CEJ6*>1(O3leHy`leW5XUR0VTUm>?F$riLLW7Isd%R zcB8K1@_ZA!g%BK$GE!$)21}wH%y&Z7Fg`6aB%&1L6_oFSANrsTpE2QN*p^Q8B1`JVvT&6N$W%=iL^e*Ofv9hO9s)`FJw}i)$-g?O1keEW9vxp8w8o7Vz!bH}D%VPe$6#$08zCuZ->{a7x%_4 zJkLgUl{EE{Lkh4vp)gcE6nJv{&>_|+6vh4>c_>d(zpUZ8{&!rL6rWMA!Pw-cre|Yc zF&YjruK^rX0U^|1IR+nHX(3)|+&ChiRwR0Isb$%hF31Rui>3$S^nK9G0-eFQin>I+ zX&z5rua%|tHA>U>{MZ9n7T5lhBah_}wivArWu%Ics)}8$a!v=~ z0L7+-L1g_=g!nZ5yE@gg)1x2kf-89T=o0vtx6ZSQ1-o?j<{xR(`gT($jf8lK?U|2R z`b%A8BhRNppTL?TKniS6e6z)9&-`GlH+Gn>NyZ9u@azA1@VpSz&>lEAnLIzlPtttn zxIDjr&A_#j?)Kw1{$f>Aa8Mlu2mE}1fHMKyripbZ=&8^CF5dO)*7J5kIfEptL@ANq zME1<*WN8uum)h%l`gx#?^5E>B97Wd&J$ZMxGLd|iiXA)nUvRl)|3?yQ%0vb+=>x`< zbE`Lc()lzcLA=l6)ZWiw(){Lq2ABu?Q))?qd>djDad-$GFlvz6N_|T`U;U%L`_7wh zCiI6}#%Az2>!_TDf=JEJlc03McT(SF5txEV^G(hvK&<$ph&_3oUBBjp_=|vC=?KL4 zWhpbyAsK`khUX#OBMh-#3kwBSb<3 zX!#M*f@tmB8(iNm+K893_}kti`~8~>tw6ZWjR2Ea{-|4+wD`3;NM81=vXT9k3Soc1 zd2@b;iUFN11RuKP{JSH=D)`Ip7e!;)|X0zDxlyjndSTa=xj&+&m*4{w151XZj zj@f)I5{Ir`9**hLQiTjv5FvRr|Q}Es1L>Pk_uZi61L=*Gm zMQl%AAB(R_X;?O>m4Ye~Py&J)=+&@EEJ_C>VnIuJET3`Vg;e#{br0!zm3a4QPOP(- z&9|=wb{JZuVZzD)F$GaDS=w&I08#97?D%sZhpq_x9Z&qho_RfXi2LDEdk7xIA;S0# zchefbD#H1I9cqoJ+CKwRoW{KC|{V0c4EXp!QEAbF)zJ7eMgYgP9j-1v84%L}7>p zjtkrhdR^C~q^_35kSAZ`W31@h!sGHGwx`W+0H~gt6;TX#3ST27Z>Q2E+oWW@)1)ziR&{8usdWxVjZ`1=h?XVbe=$&tHWE>S)G+#5__Pdy z5gNJa95D7`v#amDb8v|JD{5)tBkzmRV}QOchi3J|HVCW(1vUWMLW9pRv9oDmF}lQZ&491+w~*pXu`-J$ zgGO3BplG5Lq(M+L1zdQ#2mPXdTu7yrlh$mCgY!h)cuhO1sDrI-Rd}wASn70KTW2jB zcz{pO!$~A4BZOzJK?4B5jxP$?-k_Hl4)vM-*DQdp=mkLGE?I27x~Ys)gwLfB$v_Of z;Wj$oUOd4hE$=AfW`rF)W&`v`*N4u3Jxy6pi8Up$4@9*99`GqB%EG|7I^cmhc2w>M zSf|cbZ!Y~iwYvF{i<@V7;`-qDN%(@m;_do!xnOc25xrAWLKJAZ)ZV8lqY-4XpkNtZ zfZh9sIUR*i0*NzXxxX_1%CC2IiA{W4OQEBU+%(A7OAGM`68paZY_|Q40;}3>r_P3H zuJV($HKS{T^FMlGITewfX0jRzT)FO0Gy(uB0h#wR@PWAdoUtX^Hyc?%{7(70OaSQx z&gG50ZR1qZ)ib#}(@9(1`xBimER7cuY8&2j_eC-nFK$F6z6aU6CI~#zq}fyj z8X4UgRoku1lj)Zk*$R^6Yf1r|v=Wrwgjtf1J@Y?I;ePNhAVW39G%oLBNgN~D_RLpy zgT(ll>ZzhBh4etccIQRXj%GY)6vTjoBmWD5LzdkRI zCFNptKbla?k#5B0T`I*k8ehYxQ9|p!*w$1v{<}%|0MhE`ymc8Udd?S z^5D!4B0YJ1d0Ne2M*!ZR!FytBp@LyENV9UwLR^>O#zacNpeMM`N;LixF`R}H3(7&7$Nl&|It-&cP&^XE-cVh$0sf`R*D6}dBDt5bd&&7{~F z$wswjZootzKDpFxT-DD8%~AlYEe{DArQgeedGd}q3)^N}62bJFD7Y~B45t$xrxR_w z&6R<6Hl<7oZ}tsDh!U+1lv>)$88i?DZX`mSRhZ6aYUy!--X^# z41X_;pn@LQRb~_zmV4&&3E2FNGr2j&5=`^^E`W0!zZiP&&&~zVC;a}gR>MR`K?n48 z73eF?sW52Bgq!`@ri3hj9ITVC*>nNq=Zs73or^a)`jJ3eCHeG#=6}FsXk?tkj_N@4 zQ+;b~rtFn<9_@JJ`g@|8R_AGDUt|Cm?;|wdx88+*;+q924nhpVB`%^DbZ(jGMuj$1 zPZ5f*lw2eYSj+1Zl{TKPe?Gd6`FJ6CTpsm|fDvX`m1Q1=}HTJ7ZzW28wgZ7zmY-Z#x%*V}AYz$5d!y8goqZ*IFW~ zTlcMA`b;45x_;l7M-nC%Qt!KdnDB&6Te)jQV7}rsSY^}qJJTF(5medO>?f{=*-6{} zAno>p2t+HfIS?CSxwXt;&s=;qauCi9qL*nh>j4JrBbC;$5cPVVa2I>PU#-3O21p`- z*)(`NzV^q6529_MKt4;cGm`W)Wxz&a+y6juGx{}UvJ8R)a(sNev#EN?AvEEV{rHm} zb7938AvWODBS>}z6-T9^&|@mQNu`O4vqH75o*NKle6^(Ax=C`5%lF;UW^|W=_8~p7 zPGMEl7g@;h5S(m()DRF$1KrQ(5%d!6yY0`ro*#ZUA##*b2!}&V0Q8A38#lwJkbp^T zi4)y~!bL0{%T5Y2hKjrVSBM=y%G`d(*`4r=O{jh6Ns9UW=dOpJUPPWD+?WTUx3ou?-FC4;^ZMJuJQ-$)A_N zM6{~e856Rc#e7_v!>WB&Vp~?g`iqQum*AP*1Phm8MrBTn* z_#@KgG(<&sDC4fuddDne|7`O(TuA{-g0k2H4KB9Ww6yl?@0oi#DQaMv%m6VQld1o( z2+|+9DK`<@hGw^ov8vT)wSuEa$V8m0Nmq^fPJcs${A=u#xdeG?^N+NvN*`3@Q*vFb zmCr>5!Y+)mpF7J630IueS;wa;y{Nwnv+W#I-{QFBVm-Mmec~bC%)c6XQB(2U%sZCd z;G16Z=7T56>xoQ-$xq^w1!ng3k1ksGcShgJB<(lq4gZiyf$LE%D1fa_nU7xF-Qn`@&cU6#dKIL1MX==pIY=cy2=*t1+)~D9WvFmKgDerne znWP?fwDLN%4+M&jweD0If=a`@ZAy#AqXPj*;)PT^%d{S*T|a6R@=`(e|so$Ic$N zx!}Nh;`gJVgDs9FVpS!KmRf0-c2zmTy4U-(*Cz!(W`ryhY5+dPkeC$s-B>yo)S!V;A+vG+a7wzyOs~i}D@M z2ENIcn(mKeMeG|4XFLDRiWP16{xe~!%ES4+_kntPXL_yS!N$nLnzg>i_p~jP2~5*_ zkx)T0A13pbvB|l92j90(N3{}sY6b{7;^uAq5~6-k@Ak)^Z`hIPA$e(pJ$YpvK^EKu z(mEzO&*fwC`apoW~bgY%!{3q?o>T`rAx zb9inG4qDf-h>a%J=}IQOLwb788K&vA(baC4_lZ{^;9ii7?laxo-v)?vw<6Bd4Xu|= zh9P127Gz{Mz6rUM)6^J=E%#Ul54DnG!5!b!uN{Y(&$LdeMt!f0eLDJTlMnAWMex^9 zv4ZV4`})Se1~b-X>`ThjamzT6X)uOC=ypaS0c`-RrhKOqPX~hZw-U2C_!kVY6~=## zLQH`3EX-&Sqeb4Kb*OkZIv4(aSa4I^@O6zq;TmW5FP9B=3_9NG(? zFNhqyJjp>ywl?ls$nbPD*e0;};fG(m2WfvF7Q50eS8MAB05>YEHZ}ffvg22I{s+He zJx2Kq*6aQIC)!a3lV3bsRv8q9i7K{4AW#+!}$XNd5xfZ z28860-+)$8lIyq-G)B{dkU-P7nnGh_>X`wSn2_~T2LZ=mENe4h_Q~}O;^5PmSjIKv zar)bBePSp20c~ym$D8aI*`;fvA0qhks3#ndc#_x@SkO|$mzT|Y$8#ywrLzaG?`Nod z**f%267|WZ8Lh1)^{Hegh_bql4I5&KHu|D$n8dGsuxj`u*>= zM^cIr?oEQlnI~Wl>Zi2|T?m3V|#a?f0NriqndiV=JerfH%E4AJ=x{;&H$bV@6wm?^A>*}fZQoUvRS-2gAenV%dM-pB>Z&F%GEBH1F4Io zc78HQGCKX@_w4)R$V~xz^%j^@oQYWv97YD{8J(8VsG+4-z(aEbJ_j+VgcDz>wVIUg zhE%)y&`{&86 zM~u|rw<6yoXKz`Zz4EUE)&gb?MWF~pK>ABJ@=WA-1QAmpDiIWajiekc*U^qZ3vzD@ zg~Iw~=f}oDO19io)OAz*<;S4)RBHWg?82v&%IUs#NvR%Zk`J@3pL}fnHHCm{LskbI z;$n)u?|q~ld|<70^Pmj7Lz}$KE5j}t9sCdlA3P43?7hQ%($)Q8@Z#@>3D@f1qDQ-Z z!@Ooc6&(znj>dz<#FQFwWHqR8Lq%XsiGYhOKU-a3Iskw83Ki|L?}@d7%(?=cOAY8H z3mz0PB?P3r^6D<5Qd)eh+l1uwTMah{m$nJ@m2%e?-lBN{0bM(5t8DX~vj@(u;e><` z_)%XYhHdzBoO6a(lUdG>KScm@K60iq294Uq4N+>9_e%-H31%_clpl`zyO2+3Vs;@E%c4)>g4p~{RcU`2RwZNm|6pMK@ym}(CUl;2tB2UPiqh8=S!Lo z{*18EE?yiHHuCZ+Clt<0d0ed3TwTJ00P0{0IUy?DruXGg+%I~%-G#f6xz&d0j3MNp z0q*eV>*;?zI_AfRvu|S(8rp}|Qr8c`XeC_U+uuJ0kYV5QT2mTe*gdfua++D9o6s=~`Xxn2N?avi)KAw{ILoQyp|kjC^-rE2+j@nsAjRQ^^>SZD`Px|L zxC7nkvQ!0T{!8ktRLHs`m$s;<9hj9snUMGOmcTGN5hzh02}5G=u*S9Ot^s3*R|z3+y)KvtMcg+ z1_=KL>)r#R?pTy3miF9juda2V^4bW>uE}^RAoTGwpIM3^o5s$Lb;r-zN*49{t6u%LZlZVNMD3_5myFhnkZ zu|R9*w&B{1WcT@RjkW0`Bk|arhRIz0mSifIIJGsrcUpk)P$0Hi;)&N$ibYqWP$Fgm8y*vpxG zRdN#>#tM?PT>xkN$28?KXkFxXhm(UKc>-u&=F0|+p$9y`jSPo_sY&+>VV~9rv)`QJ zzQumRd@Y)0+HJ&mF7dc7SKb?H2|nM6+IDkc5)cK%H7Y%(b}u`s+x@+JS^2ln0vW=_;dC@KL) zbr-06?yL60l%CihOu0Bfp+kTRvBGpuq_HG)1^*6-)9jm>RvY&-$4L6PrWfeztKOK% zP9u0ufdjf)iS@M^-7%u0g|VFLUA_ZTr}Tpz8U*=DbvUAwbUX`y@!W8CvtNY6ALdzx zKgbJM2B{KDJH7|$No#sKW^~oF5BJ%LKk5vKCooE+8~ng#@&`kdVCM5GU_1r!cJ3P( zB@??DakvnwG?{3$C$a04Kev~CD6qkV%YcRL1`|K1i<+Oqc3~D896Rio%t%JsrBuEp zS6``}Wn}^`tbq7vx$+sW!*tY(sq~FhQ}9ke`IbXJXckdy>|Mtm1;VI!(@zi00u3Wd z?{E)EfO}D3Lucf`Uq3#dWrMDP;0gLTmbtnc&15kb7k>o$^6W{53!n0JK$k$h{6*P# zB-P0Lcx5t7@U+vX?kq;rudN#AFky_w`8B>^QqOJRn`zN!0qQ$T9rPW#f#b(H*z(Zf zX8!(xrS_|udm5NJq4qZl4rl`kLq(kX|1A~QVt$y+T6ip!&&#Ly%HZo_lxHWyfG>P(_RAIA;(wsdH6ySsPR`3;J0f*Q8^DMr+z`UIaKRGx?&6-r0ng#5INRr7aDwRj-Y6)#Efol0uhPJ? z)`-o|;Etg6!aJ0>^}pc5{2hqjV(R8VAo;TMLScfDUolJY`e3h{(K4`k$Kd)`H|yfl zlI`ML3qM;5{=8nz3oGsHroB&=Eel-hpaix<}-JREata}*G_m%U_H}9`w z%KU3e=ptYjqfp-yyJ~L{n<7GC^V)(MUz4@#A~o7V!$n%I%`jb}01ORNP_3f&SfPna zJ&>(aAL+cr0vbUCGy)UF{n>yJisS;EPymd>h$H4-;)n-~DT--z0AmZlOC+-)iI4aO z3v`Jd<`nBzDo-SS)4DHW>R@xxM+_3B8|aTM;A=GK_4KW^y4&3!Tpsp46j^Wk{=u{S z9_45z1CWU?#A9`LepyH|wm-^Y=@lO!f&dUfXX6>*9|j)HsqmUd8j*TMM8v;{NCHIs z?rn90B?6|fcf~S7%A7lE$`SG>otwMn%R9+^UYiF4=ed}^gFD2*k5|9KAr(pM_9Z;-$S@WAdGC041KjK6W^N&e)0QFM4Dp|BgD%Dr7>crt^s!h>ettt z$ISwJ^^a+6|4St>xtPew(TyuW0w76p#;%dNmE<`}8Am^@ttua}f$yt`z`Gcj?q`6S z-Z%ao74~eRA>%_gb~zu*c8{~_96gdg&dG}k0K2^m+-KEyUre;yKffLdTkB%m%aIt^ zgJRzXM)x%Qx>*3q9sDUUj23uFGmUO|h#1g}F-xoq*?M;Dy}1hUyNujobp`qa4|7&| z=01|s${IFq@ywN3`f_lIpNE|BJ4BL_lm7~SG*R|D{ssg58U&B#kL*3|-!WQZl?l=x z(>d0~TzOUoYQ9D6tQ~dFJG2V44Had`L+9RxH_FI@5vTf1gSShBF?3^1pd7+Da8Lx* z4Pt>hh|hmpx}Ffb&KcjcPc5~yBp|&LGnE9lgZLI>gT{3?{kK3=fd6DQR3gc{NK+2J zGim&Q(Puf-`}l>z`#q^I`I-Tlx6777bU z@dvYPL;ZZfsn%}_0KXcfTTj6dEpbID?;C(vbuA})KvQSxfSY-*b3TCQY;^FpC3xU zd56oK3&W#RI2jQM=A^h((g-_9t}{NP#5$b9&Oe<$l*9}kB97f}159k^5iTA>be#XP z0RXODv)&^E=QTFrU^V>JRa+o>86f)pE5bN2gymlmrg3H*@pb&uhp#rnLm`{8ML$rH z5?N8Hjt%(#BFy+to+j_c`rQ}j9E}l(z{*hF3nVmK-3(5B1Vs5GI)hy=p3lp;w|95Nr)sU z+5J3&QA4n+t0pPf;kO|cu*BcYpwsB|KBT_WfT&aD0pt1bU_uAa_ZsEz>(&(8=X>by z^MQwx0;U8^z*iqbZ-`fY6u+3c!<6{0SPxlTcUYe4ZT>xcjgsq1j(9+hBoG#i9AjaL zHi7BBMamhdnHE}j2u5+$x-U~ z7q7^t$@GW$3Qw4RRF3j!Und}l7;1ol+*J~-5`VAVE$n)@v0h50W;*?4b*igD;QvW{ zO#cv+4X3bqmChe@OBNnN^v@#&`*9T+z^niN5ESsgOHdTedGOi!(0Pt^JPp&2vTaK! zlL8|87IrMbYRj4W&QLymM^Yfwhd*xZDXffbFzN`z6clr;7&==eFbIiBCtk()w_~iT zg|a>oV^tUm_(@{b3c(B!v!WQRDdC^%lOC@Y;oM28;8mJM5_u@YWyCe+J|jP`LYhg- zn`V2gjNF*>mnw zH1N`p=?D)D_A1nW8AHhDEj}F>1(UHTF#d?wEOxR>?O$WS3cnJhu_qA-F}^C;h} znyVnWD|k~e`{WcDf7$l!zm%_yoDfre4Jw>(8p9h~^KLobO-Qs*BSu?gwXc`p4o$0Z zfda%|)dr03M8V8RAg-{gI8`;T@N|B4QVFYI*6*l#I_!i2;KsjK^^-u=NH8}z=)%l@ zqL=gZD!@CK&!%4Dp_9bKNd7eo0E0VPnaJlT8p}+}*y3aEhxhYZjQz;RowfJ>(Z9cX zq$e+x^L4Hn<@yf>rlO#R?3V+nylk8y!Z->;5tu(618?+!&2Qiirwaj?#_`L?9HX=E zi=R!+zjSLR({4+1o4EToUpwL3H}QNNf>MY4NQ<7~#+y>?i)n7@W4@>BS^o!n?;VbH z|Njr8GfoXNN-{dFN=Su_)5y+Bg)%EjGBd(yS9ZuKWbYL!WRFTD*)p>C-luHt=lkrs zuFuu?_xpa2-~G>h-1l+Z|6GnMZ}0Kk4mp<*&_K3 zy=)iy^(aJV&pD0+2$3)t1{@bK+Sy!PZgShbK3_b+>^Pl~Hk#k28mePaESppQ>!)PR zksE2eV>E4tbg8CNUCuNy<61fAmbVUEYun#lC`zp$4(e~h%bQIMkaAZk01n;th}{ki zRVrpLm`rg6oQa)gcW>SCtFQH6yypE{kryd1_%o5KB~)Qz?ro)0f!aYg-Xciz3Un8ycT9prSYN>Lx*gENf_WM_g=7%?uMVAgNGu(OM zQc6o4tCo8FAl@i5dDxBxKGPuyK0Tu$Y<3~hhtVv2t(MX=7s|F2#E6 zTOy;LU@q&{hbiL{7mnSLvCWy>a7N2A{=RX!c{NR$crP-F6Q}sf@gZVM)wKOO-v#+( zN>qLhP1V~4=WtOTI(AOdHhQ6Q&-M0gB|lrHulp#)SdZ#EBy7)Fx~-@(_|}iXCpAQ4 zNZMpVdkjZ%FNU)U=)G|1iF_2f3ggO+4JiT;W_0)dN}_`%Q|4rygm7_~(lvkmY5N2i z&V1Pr^9BMQv+jR3pa$^&}F z8={kMpLWS5s}S(F-H3yeh=1_tX`mfJ-Mb#&(F%`mBFGgjEmbi7pJE5C8SG5Yjc zzf-R5WV6)$hSy_1mSaThXy!~oF@E*m&%T!8C-ll~XT~7bWoOGYr>gD;dPqne_3;Lgn@tU%y;5?-O6{A6r+R+{(KjaCAf? zOS7)baU~gk_VeJ_TYqR7%_(1!=F!UKl@-~8^!hHV8n=qAQ}9!5wb0V*4PTG;_`o3HT83X3FerIEccv{( zP0bBs-0~@fOkkVLZQSF;h5OHYL2~=ITLxY;uk0q~&$dWy~ zsnhltHx+%#(S%w1-Cfy?Cg_tf3Ysmt9>n!IuQce#wu-%4j|n&-+`7dyW684=)q41G zIo=)*MAGVaUQw|W+f=07IqN>|zRbIWRe_i^)_GY`c(wg{xsLzQsj}_m`hE%1oQ^hP z3gJRzw*$?Q=gZy}SBE^8qjT z{TdHOA%&0UO3`SDQkey_^P-gS#%QSQ{&yCt9D(JXpPm?LIZ{SnpAuz~pvCA7pgZpR zZLCbx`*OveYLeJ1E9`!}yD{O^Yi(mw9w1h3dtHD3mjrL->76`_R2KW*NBu4we0^U# zCDbM=jFfj5_I8`Nt(p&i8(!kpmx=deoR(fPjdS@G93;N^U}k)p!8lrdJnD;YLtC}( zR8L{9mW=v#YrvNQ*DdobiRn1~C_(dOL8kDtw}$E7Ax>gSZTyfxF~E=7oK) z^3xUkNjO!bPnA2Z{W>(mxqizyW~`wwB%HqRL2zKBH*l)Msnj6I%%VL#c&)T0VAsHP zgChy^#HLZWcE)L>CO}`m`N>nLF1{a#p%;8G6g(ba6i!bv_r9JtHb!E1UHajV%&|Vr zG9T7Ry;WHWhSo$`|9pqZ25tBF{ZwCa;pBR!=V22PdmC!G>n9&Exos~kf8);m+MFO= zNPp%YecxifHTdMz8fgQ>Dji%o4$Xg>Hf6gDxaFn+6wdnKLFUb$b){dNq|_3*=9)@c zv58!I$X){|UAP2-Ko|{kaDlDe+fwWx!J2p(ifAHIV>|9|u$IWRgDL&Oi|l|{?GwRZ zU8>20b9n5e*WeApSX|eocuQx`TS{vQ^{w()movNI)rGfa^U5nb8`xc(w1Tpk`R_hk zTObHTE-Xdxx47*FyzL)a6<0SA1dW+ftN1- z`f18H)tMW&*7QyFAi1rL%0X6JL%jj_4xzABbA6s?%|g#s=k@}bL~)jEGd1xR^8;X~ zV*PO?Pv5JoRv783icGOH2s{k4KQFnY#sCnpE{F%pkIe{u7pMTPtSC=KeJ?qg9`#Pd zwLE`P%2QomKQ`LF-%($={isV;33&hWIQ#wEv81!Ji{ z?c9FYac5@ui+}kRUMQndfagt%(CDgAJmVH_stlLjbkxbM_9CvF%$w8HLpI~zyEmuEuy=KZ$n6ZRAi80eD94){ zxkMT8VW}kttU3<#SXS<69v3t_rKM;0axKP&=luF|Jzx1&nB`N_!`CYK7^12#DOvO$ z<-;^LX-`g&;#&hWc{p!E=+m+AdM0>EO=2g1?f!>Yv4ps?W~Dv-gPeHtQ5j-)dd3GW z9`r}jEqx7#3w$Jn&aIvirgvFU_CJBoBi$^w*9liTXc{5(!t6_yk;Ln?iw09Ear7;r z>iE_tC;DbPl!AVBVvb`2r1@Mfm4F9#a?2G~7J}BAj`QapBWe$Np)$}29q-t$l$(6{ z?&%Pg^~aU35Br~g(9F89P!KOCaXNJN%II7OcE^%6TtL~h?K}s6E|_S?d`Ce zdc5CU1QbU!n4;Sf_N&S31rnTYni=(i}H`(~G)xG?b= ze_^RvOQ;neuv49438#?`jOqCn#;!J)U^f!hQ!1uSNH@(_t5Y9ju5rv<7h1-*6Jhx! z2iR40zxbp5EUTvDxCEJ24?paqYNw^ocVO@4tgINyw$z&OrEU^id0wcV@nt{G+OsjB zI)yRAH=rd%7Uf%*wW>3N8!gGMPw^D^&ZzKVSmQm*&aCfv(&p-nlVxqV9mnz$=AB9i z!OSh%95d6bE}C|H-!k&h=k!PCTgj;rDIiZDZ;Y9t8=6d6(Lb1mzY zz6FF@=Heq)w-YRv_9itMao;@XHy%g@jgvI+;7XLvh~=|N)xN2;;rXDTt8Er`>cyE` zGVyCuuj4y2HL}ip?~!U?3Ee?Lh0T-?kcp~XEG|EX6x(KPFW{`}ElFVcg2iA$ZcJI! z2UXwCLgTl(?d>V2e$yZ0#RCwrY>3)CV^=_LbYNEJ8dV{?4%0B2N*Hi;Ht!_O+dA z<;dRhTTA0Yu~%g4FZMnn_Klklp!okk5h9zvSc1 z?Vc*!Ub13n*~pJoghF4)keIjJux-`o;2-)}BAgqi(Q=uK$KlLOYvL)Mn^!)V#GZQA zdO2gjZLjabo%cSsN52N^s7YPzfiGY`;Eca;y2LTA=$q4|)91rNUtgS*E{jff)K$YO zzVxQweL#G5=|rx}_{4-#!NVWPZ{NOcS43$)Pe_ZLOo9y zoT-V?pZ0gfNb>PeHLD4Hj}mIy zGInt?V7W9CRacUb6XlAVVyr%se7%^jC6ztAI(2tzp>ly=1rhFi^kx5(_*VXD(s+83 zXmI@foY@wOGex#Z8d*)!`mFfDQ`b#xKOq}Wd~U{ z&Mil)4ns00y+7PlXm=z?Le=fOao4BcD#tL~R1)dhFQX>kqNG--aH3!jJebpp=EzpB zO;cT7lQ_3_QBrtpdV?zIlinwWH&QF>A}<(!gQ)M-`5+nf zu<;8!b2Gk&aVFwZElJ*MYaD8rX7R8recZCt@O9x^WW9IxAtiu_e=A`tr3+#z&CUax z^Af^cbr3f1+!|4X5M4s|>DzBBb@&V<+x5ke{5vqqBm`zRCj18061a9apo~-L>{1x0 zpvLk@1fwUEW&2b)x5uJXB@jYi%g`$W_kYwIQGbL0GR0|azP~2cqS|E1-QwyDWO0_} zE>YOX+TYp;(p1utDkf;wHRwWrx!8o?Blc%}RAdQpt}JoM>9nZjlHd&a%o%b@jm#U= zeMA}++P3lX1vADZ*NXYxMQVrmTZ~Vo##*Gt+O{g{>=ijse+(3zpLF5UlSqk=Gj!Nl z?JVD-iIY4gWYOCZN=r(~JljC*l5}fexFeBfRrVkmzWrG>wdzlOe=58Td0){FC)xpd zKG0UiT%6bzIalRGmd&{%#1pL+-!*;2<6eeR+@vb(ty<12>5}ZDf2_`Nshyx|-Hq+JZH4 zm4GZ-49{KYpq`pyQgk%g+GT>+Mh}Y}9u7>5PWpOK8>syrd zr+2h0pXr&~NnBER zJarn4d-z0ulm5fcHjLkJxWy^Xz-WY}Q85c;D}iC#uFjizBIWc@bi30pc_pZ%yVT#Z z{Yc@PN>LXbeYL`7CI8q);fWKum(oU4RxF3Beft-#=MLMHKc4%k@A2IAp|l#e!jVJl zW|KWT+Kb8I0T>dV_bqDPwlf_KqWd`?(YZ}V&91%iq=ZbcyQ4Sq4CPpg=TUMN+MOS{ z1_RvRn$=RnzVit6*@lKwUbL^ow<_J=H=izmL_W~<{!ipHi zdPuOvo~*d@J&|F0P7oSx>ppA%KEc4Dj2fpO8rZkHz;z20nIUlMCX#WrMe@1!`N!h@ zXX#7-75=8Nwf-ov7@oP`LWp(Cb1frgbk;U&vN8~RClMTBckKtfCzdTr;xxnQ|c2G`p%jKn9y4sm9XWpv4p2T=-=b ztmNDK4aXO#& z+YWpa+*&;AkRQ2}wH*VEWmmOsZsKG=koqK+%@q<=C|GjKqEdZ13y+RN>HXGb0w1Dw zD&vC*mV)^6mj1xy`{GXD^JUat4^2Ly1MAGP|9_*codj*2kLGuQy?qy3%;(f)2KfvI zve{a9WZz(Tm&{GG52)yFrcQC^mSMb^ap`NEmR-50HZ!7pHt7p2o=-=8E%^vvgduS; z%(3Vz@pz^d^r|zUW_M@HpI)r1OjrBn=Q0nY3-|E_V_`42;+$KzlAqA4PHB|+#E&Yn z2u7O3nzW?`Tzt@6C0Z;eF+k&84@LIZLVtZ*xB#o42=^_Sq)9H^`&r$=*5g#+x_7xQ z$d?|)Cp#_KA@u}k5J3w7%0yP;-m*yLz(^YRj(}{oo!&uK&AqIPfvWT6h6HFht}{OW z_>|0Fvu&!UWK(OWzrbRBcQw~-HE^1*B64g&VY`4?LxPq|c=CRE1=E{wpz)?)9=(uq ztXQ|`yQ*=p?cbygqJk;y4R9rjYx8~O*G2C)#m(QM7uL_`HkKUWdqqCNIhX zmlO@VPWOG29tb;k&*i!Hyon$UbW=dqH4XgT8RL)?l+Fq zx_B<64$a4FDtW_>9=H6EQtAbt@RGB2{n_VAyzfFIlll$ZGlldL z@NbPNOGhNuU|2g?o;i3T)=3qqNO1X~U-22tkG+CCq5jRSyky7_9w(r>qL30DDf8xQ z&f^Q-73x23aU>nrkZ1|5bE_mY2t7&yZmwkEbakRr2fBf%!>3ReO@@&25(EGi%l1JUW~_ur#)Uykc*6YipqR3v=T1 zMA+%;Wee3aUP-YEi$mTleqfeT2AH!nvtP@8(gI&zr&w|jc`BF7{Ufok{@`3nh5h$#utQ#AfIc`p8<$ilgDSis6FFRi+j{_g`O)3@+&%m`Rq*#;Jr*P*p zMGk^2Dqiot`*+FG2>d4Xhb}?DFw^0{B`(GP&Mnh?s_d(6i9Ehi|L#Ir;D)X~Cv>$8 zCy=VC7{1`Y>oIx#-|c$+Khf!e=GuSv7?341niLp^jU)8S&Tp<9rc6>o+G(%PBdaa| zC_^U#;5+>jNkNblDSExTu?Q^OpTc|XRWbQ-OeEi)KH*BPIv+#?y zw;qA+GyN1`Rz69xp^aqVHA8~;ja{^sN{;%KA?7rx_-s`<)6JHCl*B^_67S`3PAyiWWJh{9ZOhXcuEnvSAJd_>P!zJ zS>a{$QS^NJXZMlCon@0RH~Ic3+z}S+G|9FuSKzL{j?|h*` zed9oGz{E>n$hSOLccd5vEO8~t$tF%^*@V4z67ox|Cs4Ze9H|)z1Zx^3P(@*k^8lfO zYes3>sB4%34r#>$E#ja5w1|(s^sca}*pZtr**Fj>HpNqqnxY_C!dO`a>03AOtSLo( zUYF$DQ6ARh$d`K8|JgEr7Uyni>Q0Eq7Ms+?&}77&ARP}8>3~+;;kTb7 zp>O;eu7OUr?X!1e2wUJq%l}|Y?p-IR?X2bb_3%;8qjI-W73^VcgonpIGQ|aAt}6?l zg9NAF5(tu%@Z}gPgvN?LJc8ooK}=u1+E_QTXja953N1u@w3Da}N#TZKlb(wMLB4}~ z`I8`uB)mC5kWlIxdW0Zwn_s>EAxMgM1=v_dzWRe|;_MS~_JR2MKAq!27G{yBKvJ|( z_)^JGPL&XE7RMW^B0HBy_M@)U7hp)N74yf>^%qH1t%8Dw{Fk?4A@1xXl3+&IB&5dh zZVRQ`#F0v#_-{5zen9COfVgzaMf`$+ZNhDy{6}0KQo7I$tmPShOHX47j&qG*ixQ;! ziU7itUxIl!OaO#IPbS4KO7C8?LDMV2x$iEY;xC;%>JO6K67kt#h}7d82to3Vclnlo zV*&ov`8MBtB3XofcIWWo3`AAub!}d@l&G*C0&gaTc(Z@-2w%Mmk(pTR@aMKIaUdxib-N!)E)BE-y8Ap0_G_iY)s1dLKXy z7ZL!`g(BR??@xs#BB?_g#11;63w@6a>v;KSX{1IfxE&1@0N|BxVwOFHIEi z;YU2o=rv_vWDBrmR9`tEjGTO;AT)Q~@mF)Gph+8obbgN92^S&^UkO;YxP8bzxhga( zl|6vDi<*eGz14G|t^?&M;Qm z3eUsNxZMZF<@JS@+*baHSL2@#rf ztJ|z-t*{{JGyi$3Yt$YG@In0yY)P?hjpt?IUBu9TfIvlNwGn^!zyT1f*9Z>e^C&@4 zbp6mb_n9@G*z{vV+zc4>5T)TurAytM#7Av>O<;9Js_##3%Dq!F%@O1!=+vo-cn1h48M4`Il|G#__?D%IY}{fPIUR2*lA^`uBB z^E&ZAPUVa&>fh+jSL9&1hh_f&cmB7&|I!iIGT9X^fTuJ5D+U2th?Oq&aT0w00D%^# z3A7M`AOu`Lm=$su5<~(JFg*VV2@)7`;e+{eD>17xorTITz@PDvD7`uxJVF-B9#p=V z%3rH@22hg64sv_3Ugbq@KV?;nM88M8e@lf|#jtW!fj|`=5!PrB)*d+S7Q!u2L@Gpo zONAUCm6bV!$mh9E!t%x(2(ypqAv>QU0yFP91)s(o>(UW-*wd@W!&Jx;yVc-F?AX## zE(V)^DEV0XzAL(mCZVGSeBF(U9kzSD`-5ZWBl3z_{(>OKBJuTeBpj@`3SCod;w$DH`;_RDa zPb58&8`Dt;HZX$gHlYyC>S+nRHI&4Ux(hfx!uR8i&Zb>dB}0PeyT^(DP}cuBF8<4x zO2R>+2)=^tT!Pg}2uUD#emeN4mq;=RknbouJxb$3M{pt^Iv4ei@<11-z6xSxY6QXQ zrw~;2?2~5~AswN3LWur=9PC#NsC>i;2u&mW1S7LtIPww~@BJTB3(gy1D3b02Seep% z#g>JojF#>r&wSI)#;=o5q2QwBRDJ>Vr#b+9X(Tj2HBepOVQBvbzD@1kLit+GqHCMb zAx1&voQlndO|yP<+{sX&ZB39SL(o|W#PC|KKDJHIt3MVh2bjLSsr*MHa*o5kT%#Fj zu_2Btb;SDpg&rRP&^K#U=Xynet>99Y0ER_(j@E$sAI zzdUmWRZ^%Bf(qUD@5HTd`XIKLgBe+Tmx91Y&+n|KK(+Q3KmW4Am**jUL)UogzCV2r zy|Ukc#43u3WoTt5a$Sh=gsgKkL^&w}CkhLd-!zBPUAU}vzvfhwCRDhjn>;><;q6$_ z#E0*+CM&k4sj=kR_kEp#c296{@^zW`onO;GlnB63K&u7d<8p#S{7w5x?8e^JGKXGh z61&b@GKpajcXlY@WOI|iV3$w!Pv4f3@>tu77IUvpRN?6?b{cVrh( zuQbWlT7Nz1jgmaMAw96Y?QGKhKIN=_+0F6qk!+>2@0C~$Z3?c-lw<|-OXSG2xZG$I z-!f}j9Q^q79u#p~%N%VY1p?Dyx_Q?mh=9Tv?t2dA?@9OAtMDB`aLo%-C+ zL5PGnqFway_OL(SE2*cXj@Uk{&xg+&eSKk$`*FB(BF?RU%f2Mo9*~fZacs@M=6VX( z`oBqxgfN|2x?@?Q#Pb5&Wj!=Y@4N8OYBTrmN;kWCF*Wh z`%uR=Kt;b*6tjcC6A{p9&;4EiAie-Z@t2wFwSD;}*SkR*$`3bwFz>OjSpcNiXsZ(6 zOF%8v_N0pe$}R=BI&Iw`(CM%?HF3!H^R?WpoRTXR(BImqmDoKA3K=(q_Ut)-C^$Q-J#g`xl@4J$=ZGoztu;=ytO z$6@|K>DohiW<~PQJRzjZB@M7GB!gRuuQ6zZ{&aHOh3|v{iE%QV#IkLlf&4#sBN%=~ zAc&$eg>wil2yfG0j9AB==NEAyE);GPz_;rBv_JET!seKY?p({&T>bK&J&W~qb=25p zc|IQI^|i`99=@{0C-%yFAcdnlPCMi4{Iw_8!u1|Uu9-ZAQn`}8zJ5l_L!E+zaLofV zLJgg{29>Mgb6LQ{e$v~87JX%yXYX^MKc*4ux@nSob0{^+;wMfg(MNLNVTVvrzE8rs z-PlVloTbxL!Q(mYZs?KRt9j0lQj(#bJzJ(CV1&Z4B!X8cWFO8V24|tssBAd@mu>=w#b3S`}?wzf2x3h>|rSKoX5|(x`=8GO#e`^ z?L$dSMB9wkamC>U_iSy-3~K}RsU`0|)jLRr!%v=!60-ayxVOrTy){@o5gYomz|<-2 z@N;IY_8G05S@WWqs86MHAEF8!D?d|+`Y@kPj#@$-i`KJvEm>I5sZQ8BS|%?qPY(}WxM%QLkOrI2;Je-BWpgRh!TlNGE zUXiWri;arn2E7Z%PyeWAT-$*KTLNbQd0_kW#*Xd>vqTN1aZJuIs1r`zjHljI|2 zQp@y9PP)}F7(2M-a#5T8%vbz1;O4dv2S(}^SQp&X1c`S3gCRH&9srd~kLU-Av={+I zz;iP~)IMjecwaf57UdgITiunz=k2x)rPq;A4_=gHw^xn+;jDWqDp*iSO z$sYv;Me04n1$#ZFkJz#h z#8%>hsZJK+nrZgf0-^J^DNs*{_5MqF28bx+Oo_!eTRXLkRIc85VkC>wc8HG?Xg1x% zPxbfI)baLJcNXrfZmxD3lwU{Fgjd9fng7hU_D73v&0bgtBi@mbtWyE#ng_k~_c#f+ zEIl6OX103;?n4H9o8gn{Q{bAG4zOLZSe}WXv6P=N*{6Nz#VNVwHah6&BxgV7D;}jO z2H-%s8!v*wg29k0yyf_73wV+o9tg>AQcwN5KUQKlQ#SBn+za4??c6t72cGNu4k#>P zo**$*qu9##Yx~+yB*bA)JEl0%5Ia`u&+bguAvRO~U-j6YJpgp;9D{YvF{VHlI0Vg5 zuU7;Yvd1fc?IDonpZc&wG(b!g-R^gM&?NXrK}Z@GlE#S~i z9W|Cqqc5b(RDOkYR&334142@x74sxqAE!u40nmq*!WB{b`DokflBt}i1>DrszD>oa z)h{pQyeSJs)1)0}&(^V@;WSEg0Yu6E{rmN)J$5zJt``AFv2fEKnHv}jH>j{XB$6s~ z=QDJMCS86?3GuGBCKB!BI1QKVZqAH>+5ENp4&aU!?&80?3{zD37KNWTM%g0+MO&hJ z_={9Ly%`!=uP$2lJ4ZFX;?+B&S7N)QdZq=&^Hw{1q5%OeF6~XB;^;m_59afl+{|nM zB3dv0U5MPL_bJL)@jbogYm#Qu>X=Gzp;lZumyHgcNZ(?$CBS@G}- zjWmY>Y&P&*Ob*BUt&FF!u!iojCSC34&i6*bA+s~$uNvsV=h0Qx3Q}7!nY}^d z@1)n6P&hioSiGT{8mI6+qmkJRV9qc00hK{zI3MHF1{GrF0~Ilun%QSQP%P>CvNkWM z#B-A9%71St_nQ>aQ!-l9A`9j%RjU}5Qp-#>cZGzy<#_^E1CuFG41h5W0(c|*8HaWI z4B-*wG*jPM=F4i|IH$49^Tgv;Ua>G#XbdN$WVT_6I%#Y!P+-ZiCgbR~DQ!$?oXDhN z1lq`0l5%G5TVk<(U3ZRt9YTMH5u^$(9#s>F%M zXcs(`ExRWPF_3ScOWF2bw6M)7mrZ=N<;+UcHqjEm1?g(X8lu821}ap9ZN`tlzyBhJ zIk6-TrB^;U=WH3P`Cj2~Asqgfcz-=c^U@N#X%<`G+l1J%`n+t}nW~}B-lqY@Y&+Y< zeh_$qVu7Gc){HpC0Des*-cZNki)x^RH!+zz7j-^z* z`$!MPLs)cLfO=5muEo5DOeaF4N)aF1ZkJ^`O58OL^A#v?0X^Nq?l_&Ldv2Gp4@U!b z$L8#1wbzPT=4O+M1!WC6hjBEx)>;0VCA$)`+pev>&!yGi_rjlSOJ04QPor1~JxBeu zF>}~~YRb=pJ3X+0`0(+IRFvx*BoXY2G)v8KNHfT^} zbyC2hw~MLv9_H(70q(wH8?KtiWW6kcXCb{+J>fR@Qy}wd!V@3b(qx6;%hfqrxv}d$ zDpY0iPP3m()RDv zsci)^k0lbg?$NF?Q?3!MTp|!ms3ehX#+DLJ0Pi*XlY%%JPLl3h8Yd`2D}aKTfrT7{ z=46lOdrvdxuS}DaJ(sXl8!Wb~S{(X3)utxF(eD)xfg2tWFTV84=ZCiMFXiZ!`V>kH zlI@x?KWI(%EhU>hGfqdbZf=y5do|(m-Frm+q#gn23Qf!)D@?p_3Ua(<@L2Pi&&t!Q z^9zvEU=ER}Ks$;1cu{};uZz@0nhyu*j;cmqN;qZHkuI0U$SqT{VdzykWA^FJK1xfI zDkm9$x9df`=!$5EldA7%4k!LZ$h)y2lt=oPZBz*F<1$!^=hA4^)PkT2pB$N_lI8@1 zs*#a_fw1|}E&R2>H-gx){bO)>2?qCqe1z&6k*?DB?-%jxu{IH+6@<$&DE^>rl86v- ze@MJSFeZ#wPejCJ^CE{@tgiF83FbDfbov2H-zqP(p!VX^D`D~|+md&elSQ43hDGkq zZO_>91gl*8m(>a=5hEWIzumVKPhft@h=N@GBc@;}ep`Oz!Kp;p(^2Vb4n<_?LvL(& ziYOUxJrhK7bN z!fMB*x3OA^gNaXl-3x^VT?QN-ANT+AD10F4C?ko`S4568hpmHYA7WFlw@W40&xhtZ zG>4usNy_O$S+Tqwk7%A^hP{Oi{Oox@iUPkTX9&Jh?WF0(ddG&Tv^$CQ?rXQ|D5viw zvu%2@21g1ZviaIj;eHl;iQ;whvj|t5a{lyK>Aa-Gu98hUJflr<24|nfo>Ir~D5J{g zP?o{xV6V$dLC3&F054AnG`aSg-;k689Hb+o zZSGZq(9S2xs%%*?seiupn1^tGl|T4iMqTAng&Vm{kjvGYAO@ZqyXr%kE4nT^UrX1gq>lCrSbHYJgnYMLNbk> zw54>9y%BETjv{$axI>>16P{fe^*%52603`>`JeR~uA`qSdUfo|K2;K8X|PkTj!nA78a2`}y73>A8vo$x6o z|AkL))Fca?+-DUHCKYFb)bUlvfkxmzNdMzMo@+}=3KI9t6BoVu+IVi*!~Hmcx@+41 zm_6fgMT43TxD68RLTB-Y$O}6BvdFuzQZX}2-3fTD-enVz^7tkfEpZOVwd?-3SqERb z2ZG(mX#Ka#;H7#iNX)cA{GQ7tfh!v;*Mc*pYF^UDM4kBC)y8{Wct}hfiLxyNsli%hN+S!MHl1smB~%k0$1RP}=QNumjC;za+!KGtLKuRt>zWv@rPAk()!;pJ%l z^Kzo9n`h)ekcm)F*yFQ&S-)Bg*-{Y^e?dJde)ZKNlLDjnIL`UY)7Tq|jCLMGXa_c` z@7SvEiUvjpQIb{KgsbLZh2!rG^f#BdsnFwy7SWx5F8Bl_=A*8XQQ_5pB<6RZ!Gv~~ zJVElY_vXn~{&;x0F5y!2-3bEql{@uUR$>q)Bb1v07!@vreO{|*EH^Z2T5_L?JTccO8w@*v^}^x54$cZEU+ zk``ygm-*N-vQz635g&#Z1#xs{imrIS^-R&=rHuMf_{MnnF0$LLLRWuY0xX*nYXp%0 z%%4{0jU;P?6HyL_=GSvS(ID1@`&|A`ymUJw`>N&+K_2$!j-|7qrkP-WCW zd|2qszn_E^`OyFAYnf$R?zsLP|6}y{tw3{Gsk|l6327>C-bJ1QFOD~a))!t_@}OkZ z6e^x8Mx3PCjp_vu)zmkdeo|nH`|o5rpaj@Rc)a_D;`7rWMBmkLEc?z;-`p-G4z^fA zC2p|Z-bX?;C8&TYAtE_WU|Q*h$R(G1YviF7XM7WRso>Zia^QDWvSob!BQbPWxa2u7 z7B}MmPGiHj$jzAyTCvOL`^9X2U=&4Za4zgnf zbOFsx{ND;R5st?207?@1(EsUc#rzUKZ~q<5$do|1M}d-LrSQ0I@g@TQ1$hd*_+%|0 zC?;*(>D}Gi$YI^I3o3q(l@*BENh8eDY}%^zzv>gwO6alc@V*gocyZF(n78Zpp93*F zgB1f08u3fE!(AtF>x(DNf4=P>zIAy;iuEEY^G24GFAYcP_I`i$>MW*=o4)5$FQPDg zN8X?hkOYE?g0(nH5X8}Z#5}xYL)&#nLM{`N=U?IBzJrGK%)Dt4afg_k+7FSG(7MOA zW2GOkpxxcM04*CaLd;3x?o;#6yZ80Q$6*HK9r96)aT035 z!NC_Twq*`j_W3^SbW_AV1^N6m#f@b`qe)(IAg}Q_?GEMz67T2D6E}xGT=v$EmWG5? zxL8h2s%jeKNjhBWyP8)Z_8}=THz;`imW=S6Lwc{yNic}~N@qa5k%Jd{$RO<`1kj7a51&GR1&N|Pp-o!_ zOYPG81=-6_8{OS4P???1uQn-;%gXwNhjiov+wY4>hIH_wfb&`D*ZSu zzzbfNCBXL8*y>|M#8k~M5urH$bfd2MoqPl9ZCS89n%|HAJOrNd_m|3*=*etWRrF1! zcoj^&)Y5;a;o8hV)f`X-8*ogzrw`*lHuEJ5@PH{(MzyxOOS0Z}Bp6nEX2{=Vejq!o ze~cb3a3{jE*AM|Ca|pC$_=mPMK-;?=&G)ISNP!R$XfuJrF z#*&gkB#aCc^ZAi*QDQP{-5+5qVdc=pzUfQ5E zG=@YdNl7jEq=foI<6P3U%mPo{=|J|i`Ar{OKwo+a@1%_SaD3_m_0-Uv{G~Lz58n=p z>?0|mN!KptREa!#I@=L=2!is86i8W5OHk+)sO)fNeucHD8Jv$GHcJGdy?e}^2tBC) zdTM*?tdth&e zg=u#pHh+9S7zJNRZ7@KnvuXscHGck+Yq#NBj=}lBBqBhzLwcJ3h{_cZmCQ)#QmLx` zzeEM?tx%{T(!xM>^)eAgkNo}(5-_2x#6EZ;4@I#PxN8{YXC%blAP2Szi7hHrZ;ye3 zHFLnhg|@Tm7tl9BE|N7act3vXUJeBNBy&59nj+XD%Qo=>NHUR33C;I(6xbL^Fq_0A zp$?bu$ube>{Uj78;rN0VGD%UaxA_seD;)#z`tPW50@J5CB?G)A2WQ0*p-v8N$sNVo zx(~&cnOEvXj1YLQ++C36gUTUXviI8i=SiocMW|&m$#8nD|$+1> zXTKKCXGG1afbejV=mc2#*y^OTg*7@a*KxHMS{hJq!6X(^CYkOl6$*gj+_hu{DIv@L zFwKRcMs&k(=VN@BjJ62P4cBerPUA#9glVxv$(SYmuhr8EYgVT zE4Z{&9*L%45m)B85*B~{-nV90DRS!3aP_yI6dlh9{0|<3)vOHsddX_Hhkx#vtws%=e_f`s)b zdI#p)!=N03`_*ln(mtcz2MZX)<6(qK$(P{)l_e+CaG9A^S0>6_u*$CP;%DA`OLC8jyAZh{MK*#2D5=GmwmP z)LqkzM8Ot8wjZDfDmK^{BPul4o#%~IwV}$I1XWjmid`7u1{9^l&kyE|F%oY2Wg2_X zm$@b*BXfud`mU6#v)!REKr0Ppkrb$$`0yO;cUr(c(!Jcn@CGJP&1B!&FX%(Ri=OW6 z%@^7q%oO`d?bBe$sOI1n^obMav~qM4pem`pyS?${UF@FZB^*BeK`s31w0z)+yICI4 zy1%|WT4z`t13;24S9f8n9uOg(%^T3NKUUjU?iAkj7M2i^KojHqj~qRnV<_F);tv-F zLt(N?6$;AnFzy=tc%k1sNQzd5?>6Qfg=OsrmG*7VUdp$7*2QXZB9?R-IhyqkE=3#Dcm&M29x1ke*<#R? zf(y|9rOg-=+QH{tEU{(QTiCw%CH11=H?527*u$e=MBwOBER zBq4iXO`QZQCR*X}uwHM+*J#-B$nl&y z*0ZP7apnFyIO-6DXpL+|Q+#uh4Y#+m_C>z821@lUK@z*iEDf+KsI`TGqcxs+#!aoS zcy!Y5e({r<`2LNUwKc%Fi6wY!q zZx2{(I^V?G1=YJ%ZhN(+DzO~ViR4;YEw+w~ww)T@PyOrV_}t`36DKbF?r8Ix$@ZjL zwQ*gN@^y)YE$?e7SLS}mRMk3TZBA?CYVgUu3FQh@zv-FyX!x@Xm9A#x*4V-0i`%H$ zNh3g|*ZA`qBenO;6aLMiH70QSgE7~PGSacTWmu~`I>)yOeE{(PQKAP_L%qx*XdDH7j$+n0dCWdY0 z07==lYk!%er&K(sA%c+0ypch}qhA&Qa5HJxQ;=v`u_1Ttz!2KasPD-(@Z9iE=Coo<@ckhvz4J;;Br@xYliWxA1$@=tL>A{G~b9X!n9>8?88HPhlvA0*k2J*QVEwmeY9w; z7>K&es#5m}9lml5Ta!v@cn3 zuD@90MipmVi#nyJsl?Hp^1~mhWT;e{4`5vSrpMaf{aBd~Y zV1l-Ij&^F2I$w3>e3B_J(>&T;Z&$%)(ov&eg5fh;0|F!fPonsw7@GIdOlKacUM($lP>y^=L|#V zC)C6EIdstdvnfGEF`C+^3@XaHNHjv3=dwpqs zv+KbU^mqLR45QHNMc{G|J|$%oh}K1no`iUCqgO0*u6%Q5%E8``-rM$P1sZG^%hTnSXC}z)$BBOIz@4Yk<9RZqU{Y~|`nliEVU$$csJP0+8IJhHeM)>b* zaPc8J8dGM4G=DB3G>a(fxdRU36dT$4yhZ=x^X{Dbytq5xb%;)Zkz+fJZ?;yfBNCFB zqJ!*X`_GTwB_%vV)JOOW^=8-V zT^S!dctD=cju8Cxg2A6DA7wqL*PGjcU}of>(!I?kY_T)z)FkB;bad*q&Y!5;dc?>p z_j90lZgVE`O(ZGaBgR8U0`O&p8_`?V;WPQ~Y~}9Z1?BEWj<*< zbn!iy`7$-~Vt+PfnAe0fx7foZCL6*_0m~M$dq0T|?)4gPST$Z*pnoyb*{$JMmcF~1 zcF~FGSAyx4lf6;JWJzZ)o})d3ty_`jw+#3D?hJiBVm7z$N_8@_?B`B5Lo4;F+bn|xa(_+BhHZ~YF@S>Du<&HKPhJ=#sU#N|DMWmwf$z2!CAkt}9@ zAaQTH**l>?3msN>`v&jRowmS3>V-keRC+Oz!YJbkgCC*^<^EI}_Jy%aOmt;8I`&x5 zS#sr#ws;ip?@=%)7z${3S3GB0e#I!Q*3mrfy$>PQOiW1Zk})pY|2CyUcn^8a?a>=|e!kjnYup4?8gsuA zHQc7aRq7AUTf)la*EFZS!4~(D^7j%(74q;tg}1}oK@^u#uKM>XhEXKQRSZ7kvL6sf z*|(P)1aG?L2=Kqc33`F=Ag_~#@!_gkD(d@Pg#3|w`9**#2pMQIAdY`_6kBFRkbC*v zo?3?z*&glK4r2EQUo&h zxOT$ti7~x2Z#E8~&W;W-A(!y_nJN)6`$y*q4;7<~pJB`PaOpXBI*6qXmnInR$&8<@ z6lNE}h5kDMqMjBbg;L|PE{B$QOgD#il@O@Hj{#ky3p=P>(D~y_8|VAUdj9V`B=7x5 zzGffF6i0kc;t?XU3v9}aiJQ;iL24kT-Zg6s$0McA_K0?2os(+vUemtaH1Wzl_Unz$ zUzMUfzg^$r7;~FxQNTX*^2T%7xur@kjV|@!O|)m7i|pY|31BKbA7mD|^~vJ_W=FLq z-HBv8?PS7ll~(q(GSB6Y=`$-QcjD@&;c*pJ;&R;zU=qOjuM z-Pt#tEHh8y`u1ktXVaLc@AgI8j@Amz;q-%y$deK~tvxJdXlO}=oM97KBdBy`vZsPHj@^s zjOggVM)Vha{xugcn7v)j=OWKJZ7IT{nV*yA?k|z8q;b9-UhL)^+vg7sIy`9)#qN|B z^8S58BU9>^kKzY zVe*Fg&GdtJNrgEO!!?wCUt8-N#N+bLLeIM~d^%G5mHxpyc&L=*ESI67^!n`Ps^IUo z-6MC2+O@IwN6m%NIqsv6J-60_b{%Fkamq=PRGy`e6%MO845KNr6)Scl^$X7jP6s>h z1l*h4@4va>e?UZ6JXyWH9F%PB_@>F|cNvDyvtufTU1jsbF(pe8v6s*5zTW$CQEY8L zR@mVkwc&u1WrCMn-je)4f!c%yAa?8$a1_<01aL(AkFRkqyvJJtrhIlF#Q*$(^`f<%nh zqM3UhiJ%EJaUw>~c(Q8ZV1CtDw_1Lu0i!DBgc7{~Gr5zDSkxO8tv}8f3OWZ5xO)^* zw}>;Iy%Z6W>*wh!Pi!5OI>bxr?le`$43$6~OLX}Oo&Fw_8<|DK}APSC? zHBJ`A8!@OBdaM*mPHnPYTaK*m>I%e4T(GLo$@tY=OKg94hV_fjj&1DLM53@=pJr^( zwDxC@#fSA2&p5Dx4qMEZ>bdn^DAmkIt`&JFh}kzSol?{#c$4$H2HVR!}}2Ktri zY!R^C@L3J5kY(Trx=FJ-`Kz}JJ5V5t1M9fer9(b6Qlf#e6&6`$#91La9J z4%<%9T7nj8NgJ0 z>Pz>YHLk~Cp&9=ax}P*?-H;|m<*E{K94B1;Sw^(y2Ags9(^`RdQ!V+F%vo;Zm_2WS zz?s`Rgo~P#1r>~uf>C9}R;=rsBKS<$lnR}3cIPlDLy2M%Y(OZZIt1>Y^7&X$Fv<7_ z6xvwRx#D0mCeCtYG4SEO`sPGQ9FlAl-)&LQp)Vxl)oUtwX*Gm=8kSfG=lGx;Q~jC-B@(4X>*~U#w{DNI}PA0zkuX&JccUoHSY!$uu(fCXeId5gohI*>@1 zk0o&^5&WvfP8PA;Obj2TLVV?t<9z*98^enM?v@7!rR4K&EiUa9ZI%VI3FmSmE@hDg z+g5kfVLMFs|Ja-@S8nA#(cIws9XXtJb=5x&toFk0OXMZ}64T`B<66HyE@Cu4=<+k5!n7%*sg1hzK>q2= z&raWdC(WnPj8^g-HGOl~*qwk|Y_{bY>ZW$|q_)kEYj=)!6qlmv%Y4dBovQ9BJ$t3| z-kUe5;Blh?BwCm8af9J9V{X9E36Ex$x57V1BD0SogL|xMfgicYdj8uz7Og&eAN6ck za*=b;wp3X76e!w2%uWn`3wPhC*5x%KZu0;>YF z%UtNiip}ybSDF(Rx{rll@v$8j+hR17m{ePkY^6svL_BfGj5ax_<($^J&1IIoRrA^B z-grctripx%9goPr6*-lDvK{XrZMH17{CrgZwk*==B+Ih6ca=QIyQ%jwcGSr?$e@-P zwjZ?xj=wpc=zg!XuCeL0wc$W%d+yKYN=(t6P0*VsP$|1nY4gTn!y`A%KDW*w>R9r5 zr>)mXv6)Re%8~;s_o6B>lbWq;yv0g;^?Ds{#;k^sv);DpuwGy9`Kv|67iAr=uXGpj z6T41F&D^x_C~-;Vy%mdp%wQ?r>SEvC>nlR;e%{~Pjcs1@S#{X?;8)Tz=6u!HE*5M% ze_08Co(-shu2%pP5oM@Ss_YeDQbkl7-3raB(I|K^1pu?OC9?0KanNQc??`SvCN5+l zoX~^L(@cx9Plnq-@9uL+Qok_?Zv?HDS0UZ9W3I{Jw0#c|MCC^Amr=5n$^5P=VtSCH zpWp~#`lyq3wzW4`fFlghI+Vv7z(BiW&ivhQ`P+%dT?SOk>HDQGp8nR+=C=%Z>g6&! zpLLB%+=WHPtk626&+ZwjN5}Fk=kc>(9fJ>?e;QUZcE0@JqU;c$7pttF&GeFav`7gN z#L3YghFfTjeu+EqALRU&xd|!{6Yj(;`e#%V*2h(*xYVjjw=1Zse;e(eMy0R58E=CDA>Z5@@S!xa}U%o))FkqrN@O}X8 z!_3>@0*9&oo56M}JtAY-lXHe}1Ls zMy={%)^3KozJn)~it>q#1OmG*0&g9om+^f6YmeU*z1z}%Z=&38zW4*Ba<4OO# zchFp`)dxX6x2)c20;Q+X?Nb^!he1>7f*%5VzI0q_JC`rhT=zbmF+I91Un?bRB4Do6 zZt!XNpYOa}BpagSakL`w7LF80>r>UVm&iqnjwKjn&b1!lyE*9Acf`t0bw?1-uy!IF zt!dI7z&N$tfAhd;P{XwC<{8H6`pYWmd(G88gW5dCFXCpy9Y4?eZW;G3&ie$dXSZ3A z9|R4F&wINe?gUaxfJc>|^nZT=ur>QkUzqI%59tloddBC{zJ%>C{N0#BmW(6pKHWPB zEDkKXv%jcda+(xOk`v7WWoG1`Z?oC=hr06I{u2o+e{Y8Z5N;1|GUQzJg{1%ER_aiC zG!iw6_K5Nvr2JmF=rU0m3W}bZ5KhwYQb0#i^d5_B3S1xhfZ#u&q?2&Zam1^5_(+j5m2>x2IwiNfmQ-D@1+B|?j7n*wi554HEtNcdVXI0SJ!@9Grh+Yd zo+?+trE_)cDY0|cEYH9B^#0e2a!OuQ77NUEEbW_RC&}DQxt0m$1&Vk(BZReC6opX8 ziSjz%ss&pla5Zcfpo3}gw|6!=_kHSx=dNu$Lp9YoUnxxyeBrqwf}-r0WJ)0rFxxriPYZd1 zLTg_Q!vphXX643fS7JVs`zs8JuWb2X+M75s+-_r^qIQ5unGW339 zT$29T07=H9!Ssir8ExyrB4>PXFohH)M_B58D^@kfF2`d@O`J4N)F3t{F%IBt_9Inzw48Vf-AINWDQ(@HCj*jNk8<;X#cilv)=<#(QyBO{2n-dBZjF1L1RYP)mFSgYd}$0c zL%Tp&(4~P9dUhD!^^@-gIQHyb2i85m0TjJ#UIi*q^tx~pLoKo5uO*9|ih>})V_ZrF z=Ia}Qd>nxbRa;a*U%4fbOHC#OrS7~tB_m0HI0EAx<^H}7GwlF#0pu{63jp!sH@?hd ze&Bn0JfQ9`6TScUeYqda%)ye1qM*N+7ED4Jx`=;eQ(j&X;pA9L-CDu=actUfxJi-Uum7&-goJ7OYRj3FN!n|(#wP^S0vthdWVPKGZVX>} z07E2tQmU&=%NEzOHEk2NxdA~8m%pL5k8{i~GjRd~Y#OmZ^xh^5q_$e};meV04tf>k zWwsfVvws0zELXyGUg@jCfa< z==}mtnT!*f;pYj&x5w24i)v8XTK;;248;$efNy97<-I89bQ%B)k$3$kT(M>ykyda# zk8)JQfNVq`O}=t25P(snP*g>LL1WPw)RE8@6}Vbq-mF1flJY$GSeB}g0!wQ+qWCFj>MY$&gEk4a9K(z(CP-aTR?qL|SDfPAXn zb<{vvxS^LS`|`I#HNS$p#N@afVIc;TYxt>KPkP5pE@&Qb3C@+%L7(5Q3v}f%ennL4+lnV+US*o$F#v62571WV)e4mnK{+?FQX-{a3-Z33 zGzFD@@kRE{<;ff_O%lJ5>JPoxvUybumfMkTj_$>@wt(tG3AT&AA24Sotv|ea&^dQ! zXTV8=uJ(oE&<)?>fV1hJns#2?{_H`2zA)1=F!AF{Q`Vs+BW|C2F?(5Y>_3QZ zpSPU>vW>Y9tqa;Iv%5NUF+AiF%CO8LsbC;B`QwDu)4gXf(xp}R8@@&o&x4vBW-@q) zZPblMucW89_u@NOd(C}W&$fm_;CAmb2PlqS@>S$#ZU@%EMH1i>da?oaVlvJuNVo>A zcNujp5gaU4b%fYozD&=cpe7DA)20{s$}hGbW1VYD`URSPEuoGZ*6<+mEjU9Nl8Rro z%tk&{n`q>ojf=MGDB(5SerJ;`5__JMBws;+8K^5EsEmVMilde#p-bhNVmX@wQnVac z)o**^c&AR;M{dAAg2%x{9_Iq`I4}S6IPvf}AA}=z!a_tK#sT_HbD}FvqHKdDso!f0 zt(d5&O7n*!EMz0m?I6wS_+gTN-0iKySQ-^P$* zk^hEU$%ZBmKRpnw(nQ~i!O-lIcMQOOURIC8gl}U6qELhwFR=IcgU3m*jA4hIYU7@_ zGS4BeH4rjf^QM&Zm?qmIRf5LTKdKx%HlKU0$ecqN`r}eo{%*v>f=*=(HD_ZXjxd=> zx3SjCbSuxs_Va4-LSca~RjpRtIJtcL2n|crH2xnxgNF^k{PL?$hYKY=ZcYj4ER&&@ z<(Z|W!#e3N(qWt*`W@Fs}~319=E#tDP7|!?FP3zBKM)YU0Rf zc`bUo^nWOB2nQywdNW}rJY)zVr=&Cf$3Dq{@NXE~QtE*Iwm>p^~|L*I!KtIxUl|!aR5$;U??`GTVv#5N6F(Dn zN`H%xb%sMaB`c30=6nmz z&MDDZt$Kp_CL~&Z@R3mesTBtob<@FiWhOdoQQ*APM!2qYUBIC`WvU`Z;vz$|h4LgI*riOMptnt%JA z8WEkN2H z{)-qpk_5(c-nRn{U*vh^+utE4ni06gh!X~G0O}9!Y--=C0glloNy7z}AhTkgNQq0c zn{I)pWUX-b4AEIW3k_khA;BY^6k;cic;@w(Ved7nUEYr#W(Y|$;nns3-RKy zF;&SPagULnYgMKZkWsmmE${>7wR{03@uWS?C6epvV!C0A6#+jvY##?=9RJfUSivq# zWWg>>WJl=9+*uL3xXBoZdukUP1Eff1>lBEi)bw#h;hTIrIgVY7CvM&CFGhYXY6?7$ zc$3)no+mNSiDI>7OQ{|PorM4Rq*mtg$(mI+i9QuIetL_afkQWbO952=0|-{fIcdtD zS_SF1W#lL$e-Szk`Xvqsu{yw4Z6z>i;vC-@+u@4xbTJ5z2o14Cy`^3ua-Glc7=NzL zu90@3%2whD3DRAz zXBIiP2MA5iU}?jGSMqM>Rxp#DgSDRQcPP!t@$x6sD+(bnri;D& z^jL=e4A8+(y^cn5>K__D74izNJRA1D)G<2|rGtFlU9P+R{I`tPoD3%8a)2QFoJGjL z+pEO<(wY3LnHNNf*QM+_27e?KFlKfMieZ%iOSwm-3u`B$+WunWS$c-}%YOPJK)2ybi ze@n&T=!-;HG3R8pzg)OnWL;*MEZlg|6hDrl3?;u6nr#~q4IjELJx6qi5TUbbPPkM_ zGl8J3zs6@9H0B=!cE8)8yo%;TQqPqW$Bsni96`7!+dZJzFgH*m^+o{N4vuG+-?Owj z{Yv|wzn68{`ymh?`E8yET4s$EXz{WOgoT9M!V*@Ns&DlhbSPfS(KF2R;dU~=2+i&B z->L9}j<-o#4NLjhwH{wHZ6-%Smrwv!OYg*PVw-+#VObScXe>$-XEA}eT%isgS6^#J zH+K7U<4oPgUvx9CM}di4Qo)8^nr?1j`p3f*UvdA&$CNl;cOvEiL|&7Ijuus z_cI9mpf;VGLjSPJHI6D`)345uSH17+ig~MqaGMh zqcH81JxJDXi;Gd(M`*|kcQ#;?xE6R%=RCf@C8w7QlGe(QNCZtR6_{|64-yo5C&x!l zP|M$fX+#FE1NN6;&}cq1_NG7P++F+d4wM8&phMniZ3bp)eB|f3R>@p@`}1R@pw4Yj z;JF+BZFlvP&iG*eU<$WE1X9$6(9oUfJ+xJ%N!{aF7|KlF`T1F~%(Rx`NG1p^@kGGf z6pe=R%X?@ho|kp#ypR7of35!Z=PU3@TcwuxRU2XxzI3HfAsGWorr_jb2(Uo(f->>Z3MYf> z3Dk$wc}>dm;9p(f`K`d5BuZZ|s^Ff)wu_*TrE|&oV_b)@|ISa(gDt0xdTczB5fFxm z@(A4ddA*}mJ5#DFaQF#tN`1}fe`|DT33C0Oa5Ej%pp{ZSo$K2#sCQJtUMyc8r;--! z0U^{`N(rwY(q+>Dk63>c=GHj9FfpvsfD}Tj;514^^8RnP442=jeiDI%;*%Eu2f}bk z_vJac@(t+N1W{}zrDk`b^BO;0SHi<26GA#bXvIHW2i8)r;CBZIpX>ee#f~cX{QDlU zgr~2>Gl+B9_QZJ}?zn7V_&`l+{;LzD7Tp)23FieOdSQgShk1xJdY>UUiwGnn)-QBQ zGfnR-kA|xxoadjf{2F5W2YVm5Ke25{0sr?*D{s(ZMmNWb(Fj`HpjAxeOjn;!>zECl zc+iF?J+ya7*nOt zjuFDaM&yhE)4V7b4ShE^g7tXelJSN=ysLt%iE~#Xf-lBJ$?r6CH9z+dG)zFO;`jvU zcuu1)l(&Fj*Oc&WI17}psvcuh2(-JECNN$WQ=r6@Ygm#%MXC}k+^ zX%5t|jqoq(NgR4W(^ED_M+C!G1S~#_$3!psgPnAx|3LT1v@s65t$KW|@7ZS&YAb8| z%guf4Bay+OMZZxXBlmLAYh&v7xi{eTJw9`F;D*9C3UXLgXUELXx{gcfF1KYaUiDF+c;ComZ2(qz-UQpaAKB@$wh;DpVEF8|RVq`BWs|46^tI!?ZwbXfPKR1J$HrVL)7Ivj zU<*tuP?YIB_h>`|2OW~hRMSk*)_4Sjn5Z&BpXdOAa|v2d!AgnlFnRphr!x2?2!v=T zw^!b)-Y$G37aTO%=us#U5B5$?bu(2{5z6v;2M( z(|*{z&)l{#Y{oqBOhi?BWU+p`5e?d!HrrUihYR~T(qas5fs0a~)#gn1JMIz5SvlC^ z?LLU7YhmmH7H_vPUW;(2ZGsF3$wD?E70(3@QS+m1QF0{}m6#9o4V4`sCJb{f_@J02 zSapCUm|z4FLK}_!v~y0Z?niGP$hCpIlMtnl|IAR>>^y0ndDiWT3-gl}?eEpT(tE4; zaGn);tv(C7Hlrq%f$To(4faX6OBuZk6)!qUMcw^dsAQMctKAv)LOnLH$nx>x6BS`TZMmS=134QCq`_-ObQXl~Wc^qt|ry7W28 zg%t^F?q^+iK5A~3*C@)fWrzL*9MMak{BzfBqW|RrfVanoh*LA_(Q&rzUtb>=bO(Zo zQ=fml%U3g!1xG&k4qE9J-HoU}{ASIZ&AKBaeS&Zy!hg>~JJZ&J9|sd$sWu z?&F}{{!N8O&h))W(BRY=t7tpcK&H%3++=yJ|2n|8>ZPn(VEa$$Sm*61=z17~35PZ> zmDV95Zm4&3|Ip1#kMzylx7x%>=W6hQ3iZje`clIR3-g!P+7~G;&agi6@_i)f*UFRmL2&pCxJ_#l_UXd_>TiR3{Cd=4cbAu zGl($JSG@jzsKHVEcsCv}32gqr;5%;)%;Gl|Caqc98(F*4I`ED0C}?+xd(UCfn&bYp zbAD~w&B7vk_SO>KTQ-$AGPjw-sfxYxw|d2WK8I96@M%3i7rD1V74&vU6B!ts>O(#A zFqEz_qc6+tArJ|qVYpKb3}`23)df<%Z^x8jgK;t{&-_?+-GS|DWs5BXWxz93Dt1CF z@U>!GF8T!c(pyhVbmG#ub!&5A4IdvJFS$qsE@Cq79ZA`1rU>^LK6<8~uc<;eCaY4+ zbXDcui#U~ck2xMrigleQmDFl&WR^fFkv9aNU{=w#`LCTfZwyKouxL~lcm4AGTW}_& z9F1UV%w0Pzr3ZgZy{E$VD2O46uL$9R_SWx)VOeov?N@XtHjM2+b!^LS+L;lWURULA z-64UT0{`JN@9w_`^{g@)I-D=A1)siu9%sq@=!xJvMt-RGMt!!zEgo8$G|v<94>HJP zN;WPrROhojfw^;0_EP3aNLEXwhB>za=O_HAXL>?r-Q|EzyC>pi@`%TX$J%pF{HOVbgjXyze-u-e$5;E4 z>(Wg?$u;Tn!fojdMBFmG*uLeFU!gOmK}@YJvZk$w>GRxD)e-v1dN!Kq6t&9S(6*1sLKUUjJn>IOfcbZ@@t33lnTJe`{t}iMar+QC+2i?gA}T z%`u+CuaEtY1u|uV6NW7P$hIptgU0*LkuDz$7M;unlG)d-$D9c!VhMGN5G@pA8X&MH z)U6fA(yM>X^-XL1Xszl(vgpc*-a$1?GhGq4&0tl4% zOzIFZw)$xf7#<6!nTi^q{Xk$VrI6<@+ht#aek#-EGR#Y+nJUzwl|I|GDD$-u=N7AbH*7o& zzgeB;?R)K{RzNf5JAY*}YqP~%A>aYi+@J6pK}AzlwyL8&FlRUB7oh}h8_N*s1Zae5 zgI@P@NFY$y9pGBK`v|5<#jxDJ;&nh^bOqE3tI#>eU%Y4Yp{tQ*WFmr@i`EuH^y1Qg{Nc(;un(+&(zd6UxJ^`&A zm)C_`Z;$zGbkjT-ePeb92q|CfX^qsGb$R^9e{h`qd_soLA}>z@G7{h2HMwFmkT(a7 z#4c-7wn*;^hbQryB|o^n#(9|>iOJ!O(u0yik_h7QRwfTpmeMHGmPqV|-g`6~4d|3K zzJ5-<4VT#m5!LwD_{*p5>w8@H!lLKa+D@on^cwl-_y=vCfN*vJ38Pe$oWt8k3N6ut zytwZ8<4W#}19VldFh6xaFu`u{SbDhk>}2R>2wWxy=lIJbkITdN6!_B#8rI(&fdqAI zeUI;%FkE8#sL100B006C44MzpU~2l&7sSk*XDSRcX+{z z?Z#@6&CGZXrwC>X;Wqey;$TF9R{G)-#)`3aUQzc%Csr=wm)GKcU9hQMm_2eaVGfhJ zHUr$=Y2%v*(2PN#4GB0^;HK!^^;zGa&2=JH*n|lpGxKlELZjQ4Oz4;*LZGJ?m6NNH zjRWE~JO((Y>=HWf{p(0H_~euIIF&T`nmIbwup3C^@7qJ8m5;cijdXq3&@agZ@E3{~ zHKI)IV9%44Del;jk0?MThnY8>CFUFGc&PNdm8^YfHfi}X+&{MYYOn2Rhd~C0(uD>1 z_%!&G&Ke;O!nXw{VD@z&F!X~c-0p7L-P&9n>?Xi$d7^OTK|Dqasf?V^k_%}==Ix*& z5F7Pj$YR22CaBLm=^>rkA3TE4IHrDcUn}U-M+_jhOPp`JK3po~zA@d`Zj>T-L@=(@ zUe(yv@(QCO!+m{Dp%3o255)$mw`bes{HcTH22UFAzTJoPxo{%oqmdmD6=Tm>e9ZQE z=eE2_(SvS|Ei2Uyo}`M3ZlAq#)^Pyb-WY>h3rif%z{8Z@Z=Jk60s9LnVZ02q(D`yf zQxMUXgjclpvYj=n+VWJ~uo1K>n`oWeW>=EOOU%+`u!j+|NOv^dCnzjHYulz&?6pO> zrbV+a-a)sokXu{r#Pf!5R1_AUfol2xk-$!gXyf9O~78`o~s%Jp1x0 zy5HYlR|%a66l3522^;LEH3OUtUJK4-%NQC}x=tVa~q2%)p zNf}wc1RG1}6GK}*ly+lJ5EDMG7u#cd4qi11wFA-+E5YYsG_%74whyovL%@6YbYSVH zReDMVp)L1vq^5IJNFcK!$9t|AdfW5phl;v`u};-c(8DQDRc*#h`fGCZSePpzb1`l8 zt1W5-P4;Af#mH(p>5Z=BnX&0;-(pv4N_yoI{~BZ_jUI=cs{Q<5Z!*+u5UK@I_B_ym zCi=e%IjvD63qcBzd)b2g30-*=Pn{HU<>j4WlT8bI6XV0xyzM4yf7gH#3p+C+X9+qv z69{oe=8t2c1x(1`PTyIED=1C+-}(pgA!!O!7sKJC5o!W#^Jx><2O%PkXA73Kh z&H;)BE|1k1?qRcjQF&LRZi4s&J!rSh17Vu>^RP|75>+p6WBH9~f38urMC3BaA*(L2(BXOQksJNsT>mBGb38c zq}K%2xjbZ*h&n_1g)tRGTu4vYWsC~!PXn3(KvEI(vGBmM{J)QCw?c{2KF10=R#{+#bBbk}PDUU_yKgj65FIJ%!E48LdFbO0MF>`xJV zj|kHO8$2j8G8_=;#NU4nSgGkB1CH`MF=Id{0+b_C;|v{v%{RsNf*_wCCL{k2)x_j} z%F!?YvgxE1gIR%?NSZ%#9$SXa^jPW>iA=GFSH@?tWp#%|^CgQ_nE-&gdwh^qzezJ5j42<=K4mW7AQXTD|Mwy%lr4VC+I;-dHtka0J+U^kmktNg*&c$7{n~|0T533SZ;n*^6qys zGvcka+3d%l0-dW2ZQQv4d5>ht_;V@8oHc~LqaUBEIX^%HNzWbhNH#Gc%oHb^A{#F^ zPY00to9vLs{_a$+XBKKPjCkQ+IZU7r0H@n!f(Ze8mYn+QxO;Y@V zQj>>Ag)`93?+wOzOy$FisLq7Eh|VC4>l82j;MEfl8_7#(YK5cdYB;SrrNy-)-0MVB z==jz3_$WD&G}Kp5Z4VJ?g!4MXL&Eh~yJ9h&4Dv zdwVjEQ4D~xG`Ty76awf@Bdsd)YY-dJ;9|Kg+TD2g40B$wddxd){n9LSRwj#?7*P6mH;fLUG^5Z1dz$ODHzrRLafc# z>_be*PJ_y3K&s7n1NOc~Ci7hvHf;-Pz4qA(sp7r7E^=pjqRO@y0`GYTF;2UojAuS4 zqGF1b+*rya;0-0ukt?5vUw!$=)k%=+I3sJ1225BECLhB*LG{mKA5E6kZKvIz`ht5O zw2gh+1IAAOjd~eD@-;NSY>XuRxMo(%fd~W*z5W%ERpaJbHYsvy?#7HVC(h!hFCb;w zu-n$jMlaKEyxnphV6;O=3e~?I@$R5+acios>{{R)QuKi#GqI6-h}uiU18VG5jL8u8oPM|7{Y(CF=alA*`PxgYu!02G7wo- zZ(+Xzb%f}tHJ%p!D|U8y6CXo%^%N747=%PsDNz-EJ%3f{u_6Rs0)?E%%Ars*PD*+h zmgT8m7rzbHF0FWm{doienP7c|C;{~}obiKqJ(R-0TY4)`>Z5TPKB8Du?biI9^H;iA z>5k^9>oHOtKADVHw~XZ+xxcuH9c*za#Du8Gu=sH^GIeR_dkvt0 zdT!8Rgbi4b6@V!ThNZ^*fU%ooe89a?z5sq=41SDO&ceVWvX#)KqmKA=PmkRoKleXO zO-DSx<*HsLpDGtd9H5&yb+IF_w`mt>B%-q+anlH8i8WZ}R5>H==`e0S`7 zXgTL%WetNNNQz)nG^KRwVmK{~p{45}-|-lO2Ee6YS>Hbw1~Vj+VA8Lq+zDDl#%H}f zki@NO{b04CIe00X6Gjo%siz8Ahn*35;gG~Kb~N1oO9pSHC&F@hv5GQTvQIuK{~EqV z8mlDxRmSZQF)ezHt+uuF6Y@J3P^nkvg2qb(yvxmF57v0T#0%ffl<|2q#3-qhKsBJl zT`H&e?kbH)J*HT6`F{ePBamz#ms6FhA%pOn?{E1110fw~pU>f3fP9t!fIV`8!6k6N zUtMcWA#@$4MS^noJR|^H`2JUJI^#wJ9|1?8VuZz1j5+Cc@WBcv6fe`7GogiVrX&Y^ z?z{+qIQ1e{Z1RXY2~;h|VX#qEpFF4pb$>*08>qB(`285?uKLmrn&OTyCTa^B|Mfwo zrppTl(3Cv*zcFP54Mb2C3mEE#V#+U)^D3>ane11o$qxZM=;EaYuN;Z+16o)+VKCVj z7ZOs^!u`!910;=;JK0aD8z}oI6t#6<#OpA@z;nBKA(>TOxX-efAiqEJ^4BDH9F(Kx z+@Ip4t=>f~&(*Io8k>JSlBJn?YAQUDOofE=f0`bLCYYYJibSU=Yz1~d&EGr;5H-EK ziwF;2*0%H$hRic}Tqt$lywCO$(W9yiSV5u}6raN+BfPG^^eO(}LUA|^t|88uouywG zH=yn5k&M4rnzXO*!b3lyNoeS;;=!Q#M2&*mloEvgohECz;RcIyoo-+;29TfBrxsg` z1k{rvX2262KnbC{>h_gom_9Z^5Sq{$bkwv59viuj-owk;<%JIu2xJ!CCMv$Xr3=H? zz;*p1rRKXf=PKe>(e82pQ`U+BD`RnRP617-!=dlB3#-dt6Xm#1M5wp|XH=(xCfHAw zBdhUm#7qV)?(LK%7dZxLm{&AaTkkEm!aHhz7tvg=Q-MhgAKw~qnVn{qeH)QYia?6&~%kmnTK7pjy|A-jZ zEZokI*m$#3@%kWU#&4DX;Ai#lwAQXD`B`3LP3e|hK9{Mwe27&VPR3!DRvJh{eJUrV z7mdOtqGgu7Ouj82^X<#)z{@>V0Jc)|61(4dhAPm`)PRzh2U7}YZS8vDLbWd+3fxHH zyL-ty_vv@-bTR%fF{TgG= zQp8=m7p_mhiUL8q#88CZJ{<+U!GdnkcW`a;5(N;p^UQn24i@C#VmR6c?Y-j!2jKkl z=eZq5HPx&iHp1z1@JpH%>p}T1ADYSapgtK@Pl|}abU;|%O^wyEfLVb%f^=v$JM&_F zl7!IhE4k75>aqX>L1$+t7f1_q7KN}zVu+BsO@_VBYSq_nFXHmL zNIsofdtW%eUrDlGmOGvpM4g9ljwc(#hh|%@6nExUeBep|Wm1zt$44?Ngfjmw{2~l& zLth7d=WkJ}*#Qvw#btv8Pud!Mp&wF#o;%SigOW=KMMuh^CbtkN5sjOH-8>pWeMEaS zr=oCoR~3dom8z4I`>+X5u^T(8^4l3-knsyp!#B41~)g`pkcq0AE&Dp87yhf`TWOd{PZnmz&BRa*j@k>@N@x=s@Z5n;V+Y7D^D($a}OlF0A@9YEU5bJ zB%1(oz%nYrek=%g?lq!Ub3IpD>CR6ZR*C--g}$hC;g``b(1a)gSqz&?Az-V~L)YIa zwNEjB?@viU%CWr(OoiYEr+5`uerjzO*&oa@NTm_baI=(A8HeIVT0UUNJ*s~kofCX=Eb6(eOAk%uLW(({-uqu_(+fCg%d@=c zC|9PFB-fM5r&$5{#fBUp4Qk9*={$2=<5=HCBTKygLz=6G|HTz&f{zjk_Bz0~?;B*; z%Da)}6^$!HCl15{FS;+ws+F(=TMB63@lx9Nmfgw=&bV^#?3P{8+f{Rz$+eX?!*;fe zkR5r=5Q)p@Xce`hDX7HsRQ+$Vv0Bde0WuiAhp6%yAn;%e4pj4BKqN#+3nUB#wRXl| z1fY2wrt(4e8uwu8#c&Ev?U(kK%f`57Z25$RANRy(s|`-wOL1kkA~6;t&p7gArK-4* zIkq(D@FW=|`K$;+V}sEOk@YSw2HaY2Y6O&?=>{{4dv+0dPsF_u0MzsnwEM0NDCn5p zv&;SR3gNDbukc>(eLZg-grra(AcZOmc=o{*@v+rC)bo>wl?5SI78~3Pny`|LswBU2 zKfmvNA3aMtdUUw!_&#ErdYB@O6xe{e`KH7gk1ya;aou>#YU-WWsl4D~J@=WdG!5nY zTSjg7Khc!~mtf2Z-7NOj zSTK0{glnLPAma3s5Ik1xA|D*gu-kphS8j_}4Qtbh z-9?RU&Fj7j{Ip1)+T#dy%Tv_rS}6P|s#o_xqdOSPeEZqfwxI3Zhn?uWa}T3H#ndVT z{``)Y1m$THMsWf%JgVMJHRM;Q8Yvkq3Gk*KX}8C)9{p*_PZo5x=rhcV@;nd1zs4Ny zzeiZ4e2)b+4mGfOz1f!Qpb1*^N*flyG&CINaIhXCz-E+aZW1D6T0ERr(Z>?G{}%`h z2@!{_r=?-GVuD`}5T#na<5oaA{@X&U**JBhOD=JnXSImE#Q-VkKgem~FixW>?N>f4 zSTG6VkNwL9&?QuX5{%XuFggDRS#}SI=la>|mM6j-OI!mweP5%vpm5y-?*qLJF94h28&p_+))3K{Z)p%d%e-PG*=sH%fv_Z>{E9b`X{ zjRx*LZ&CmB%h8OSR0)K08$jDCzfqhMFJ`PZPrA@T(RPI91{+v(W?n5iEA4yF5p2suKK@X35AV=U)e`-c9LFMwM`3&@#M2ICMqcxMP(Gv=1l8+JNOOaA~!EO$O|o<)4f<2D{0{|w30?fPRYX+gRiWMV;IK8ihF@q@?d!7sQ$CLmv?8D zta;(#uceK~pPJrAnf14t+RO~z6JxpnKg)0Hf+EhOETeBvxY{r!ZL=Z<)<^t3t})JR z|Jt8H3m-mSw<5z*XBOI9WXSKv7U!_$K2s6O5*9YPaF1CL$M>s^1L{H=M8Tsg_IJ21 zgk+sn&}rfh*vexV@(B$gg%6Omi>76HbQ#2!No3xUDg3@vdRHd`Wmxbahxri?R@9bZ zetLI72qe*wYTnmxEvu*0Y@ESFC97h{sGG|(Ux%NttZqUzNEXUFwig-_1XZ{rN!BZ< z?}~5?iaNqdk90opOs5o7bmoy!;OMJhBxb$3Up?JLDWA7Wwjw}CEVJ@h>@_n6W(2AT z6LldQ3ZpVBC4F?P?#lFXWFtBu$9c!+$XRgyut2EsoRg0AZQ(7d-&fz8SnH$kvAEen z)O4n- z6dBe&8MwNUBqH+f2DJpc`75PD-1l)plgZ`t3i-bBiErSLtw<>~^z@O5{P%!`l;nx!nfL!5uhGzdj(3Z5dnpRV@`&_bgON2Vb+=4=O^|d& zjZBRWQ^~K7V(2cV4n|_RVqPK8A|P=o8pNEFjdAqI~MV3DcE#7t}Ai$txWgy2+4n#aUalc9!k&s_Ca_ZNA3wWGVQ78Qo zrt`r8uJJx6sr1xBMhEb{uf`Vr#a^~*w}(TS{aN@-;f%LXk5}}P z@Ocet1qD{i!h2GM1o%Am9&>c|LZuChL&fcCl^GgNE2kSqU4D1Krqt1dd6>Ao@vhrU z+e!IK>S27jkQ<^8VR2Z=IRZ?Axi)8|(EfOV8H%&Sb(WZyY!m1|#_tF(b&kKx;&7V1 zI&TOp2qbnVuEYijfwvAzqe;wQg*&OO%-9DpKeS1K1EH~bGyj5KGJ%nISB>i2Q28yU zi6_Eq*!cANp-UXbJ)B6^|9#&FOpR*Qi&0c zJ*XR$g|7rvU9Gce)LC@8(vY*5&|U~hW5!ORDH^`UGI9Au{hrd9g)C%1LTo?%h^cXS z>xh0tF@M)V*O0I~zmdsm8k*7qtfjm9C>hpq=MpDA@MZ7dU5+T<)SeVdkKBm!Q*7Gq zV^EzMzw%VKANtGxqNDy$17uD_4#yM(MzBKxqe~l~r9wC7w1}9NTPPPb>iG6zw)qH!hQZ zPjl7~La!@Hxx1IhonJ(4HBRl^Ibrun`v`hDJv3HOHQH9NkI<;=ZaO)>Cns`*dM-Ls z-R~%n++s*f9_%TyWkqx6h^Xe*N1}Ts5t66zg_GGK)|KOaf%IA89RUP+y1s{3B?_YOChKI@_(GR-k;y4q&^b)U`}j*jakaI*R-hat7C9!?y8ADD>zCaN$Y!7B}B*T_KYSyu1WK7rj> zG-mUp*lxSNcrnfMk$1=VJ)fD^4hZL7bHwwCqPe|(_YUnbSN-MpmA#g)OD6tIJC$#* zA9m1-7%~Uo!P#I&+eXXd$9F}GA9B!$&CMD&uD2(%z;=O}^TE9h}a!Z&W z?)E|k-z{^F@ELuHI5D%`bC(hhCFMU=TnT_qMZ!)|?~k^&aT5-P7!wr93t73J*)j#W zh^vwO_RRkIq0C>g@@>th_K+?2w8eml%1Q4RZp&fKIV5)?nD`=C`1CpzY2G(>laSNd zzl6FihUxfwP3^zZd_M*9*&W(8wB4C+h|Hz}YCFzSK$mYxbGs8T5!^Hw*SXxpCZC zo<@U&oV*|hFFkj$u@eCj!{9+dA7#g2?&92yq<{sbAvL{4vW#$pE~^8cr+h}m4M*dA zO{@{p2E~oX8JBP>h2)Llf()CxR-F5H8nqiwLv;MHUPq7hM@+Ip$DMPyI**j$^=qG; zc%ni};59!>#vhW9kK4DiZhh$|?H=x7@*lCSL)}{TZ2glHNU$qjUGR?au;J)vb~sM| z)61F+B%{QW-#?44!$tOoLJ-^UOd&smOS-~3kFZZWVUtnXHo})C`^(*J|0d2hbcETzz^-1;w!Zd7>4qv)l6@kar#8SZ!E?QQW4F1Hx0~2*+-+OSkdz#G zJZ&WkDRCG5Gf+;UYQ&wuL=twcWur0}EeiqWV_{eK7K9fGVg7j+58M45=*FBtN~VL< z*sAn>=Hz`GFMf`ecFdGs-iS#49#j0S9z-P9|0vu<{QS)=GDAFkrDE)A{*4Q-B>5nNABBREm#IgCg3LfH99#Q%4wQKDB&yPrd zdCXrXzuKrbNhh`7G{QIb>K@Myn874>u+z8Ush4>@e%T@E8{$?d(rX7)Q)+6>tH0A`$u z`R$dHw$N)A$~=|DG_+>i5cN5#Y^NzhdCnBBzR(;4HV5YY!Bxwr$TAP1r51~dig*(5r;p$~9Mi3E!Smbx?3FH^pNzqK{{W?Jd)1p$y zvigCjJb0Rm%U~NR?l_>YV*;`2g8c^)Z39TJ-OmY*KTpOUG}L}((S_M$nI*x^)KXH+xvqkX%Tf z*uGseUB2>VuO|ziq4qF$+HSkxLqy;yoo9w!Wwadb!baGr9Z!(6C`c9>Xb6v4)=iUO zu`#|d-W5GXSj~TO;*EQjAnWg7dcXTe&^iMNuJb6$*llmb+i$Jvuz`yz)x~;qV<4%J z+0J`Ub>R<|6~51@&^+mKJ*u*qm9EG2s~fCaL1&WssIHvZ|DH{%Yme;KpL7)VA+TxM zwvDF=k$v5LlI#sUlHz+~_IIFH`69wQ%mh{h?j#h=UHEbg)#qK1u88xB^>Zs)C6hgk zQ0!`3OeT|9&rq$~H8~L;Gh0mKI(C=lFn>9>@<$**@%o;aJRkIg#m%j0$noA{=IFMW zX>b!vh5QBGxkilx7?l+U^eL0W>9BCFjtYY{NI|HG!BJYuKIN8jxJV9xw@SS{}eR7q~>NBe=642T)d3551Gp>mO>h z^`5u0^)Sl1ALa-%u?jY>QakLN<*hg9YF`^N;!&4yR@_W)AKIWD_zI&f!+-7@P zNWgj%z~5gsJH1v;0*$d#wpl`7c+(zEK5LvK^Q%emNrMI!ekumXX1>PTxTBQ+zPNVN zd3k$AquI(v51?j$}NwcoLK68FY!Hnhc%yv^w9P&oi0<-afbTJ=PRzw69hfl zBV@7}!o93mJy}>im4l-zT_aTbx9T=rd zKmyz9S#r^tVU36T)Iu|yqMoPWzpMfR!(oi~eK;l6)>56`B)&+el*TWbBWs>d;N4gM z<~d^I$|)*u{}=FE!s_hS*hr_GoHj=T6U3?NQOnTgM$%EQ(4N_=Z%hpw*Y{t1dENI_S@Bshhy}^L%dreel z==)iYwC)}ne`nt27L}LrK}<1Y15jSUOAoi*0lx%2)KI)B$(Y=I;!_rT3pmnyu4c$AqJdQZ7B28K5hojAajWXb zE9xU|+t)Js77x55X;`S4s|-DqzTJG=7%7Y&OUJUefz#KO5yV&#odN`4B0^NbwtoA^ zlfzg~{d=9v{@i*Z@oouqOmnpf;y2&76KV>}Kl;!M$b#KC&`BmntBM@`{~D;WqZ;0G3jW z|CwoBKg9Ey+W&q!H{D~(?g^)89yZGDYMt_lYjFLTMmjl+`Ya@0stYUTKY=Ozaxa!f z(ba%f=u3uEwj9YfiNZPEHI+2= z>HLDE0G8Mng)dH?qFj5~FPH7ODUHt)~OYk=>&Jz>WZ07@*xBm$! zre;)<*C9!rx;y>RQ_F*5;0k|V0s$HZsB}Uxv0PfBEHJa%`LMgFfpiWHR`XdE=UDjc zmF1TEy)l*rf^qc1HZ=2Fg(Ezi2)lFO=65{Be##Q$I{IwkXdAj&wg07Vs_A)xQ7X(Z zsfgJh+=|!8xD)0)`J#;q!bp>`lBcM{YT>z3Rsg-@q#p>J;7BEgc0@lV&p4QrkA-%$(8 zOYPhHdLI4q;NLxr$}!<@K|8pxPh+q8ldXQIh#YoY%wwcnwS(^&`LqacJ==H@(Aez( zoK!Z1)5z!-dSPi-4e^Fj13!Xb}bY~K=-9t&Gx7~&cwIhyC6tW6z zUaNBQ*mwIV zT7+ZpbaD(S3^nB@x#>143hQ7yt;xBH@3b;l<8(@BC}1=xVo__pxzQC!aH&kxZc=}L zwu-Xxf{4)|mEFQOk`nk{EOl|)m{8R?Z+o&PvQ3{S$qk&or=M?!H!E$Jj0p=P<6nQIX|omB#lKJJqcS6R$P;deY!O@yd!WJ{mM zy5cj?t)r&3bh>UC)D=39>jg;1+fo@`g-g|=x)B+wE7KcWfdrOj#Si7 z)3-e~`=KJ+1+OS7MtUM;Pn%hDw8p{$8nftFUd_MZnExI6jo=8ta&EU@I}wloZH20p zH1S17(Sz?3W~-zMRhv>Dwxb+}TKe!$dB%|Rg3g|=7GK}9Ry(rrY|4A(QH$~@@3-}M zSp!0QKm{37OK0AOUrI)o&Thj3-MTWJ~{7l$ zMY|EV7#`BIkV_MPp6qdJB|ed_*}uE2_u+vE9aL%^m*(Ucg@pjw1l5D_&ohZn zdTQExt2$;&u1Uhv=dy@g0vPd?Y z>YCegMF0k70o_$Z{!#F*fG#@6<8=6`c7#Y$x0OaR;s32L+&{FDE#Jy<6av8P^1Hml zZ;`Vk<7&SLb^lm-#YOVV&LfaQjn*3=a@gwblMd{DwVAJDwsG2MaO!p1pvF+W9k?mE zy@LEaXsBb}`<{pBS`Y7Mq&2|8i(d;wqMm(T=`iv+VPLRY7v$RvEi++IHLmnKKJ2W? zb0(Uas&2VfuC&nj$Fz~cDYOjSDZYO1`|+^G{8w=XLR6?@aMPXyS4;k06xPo5LUwVG zz2@uK9H2_WmFUqfG4xs%H~wHfTx+n2df&r}*JH~1wyL*sTn(gO@{IX1A0@Fd-g&-N zHsC6*RH35QlXWYQ^q7mTD&-97WVuhs_F#=KAg6 zgz<2TBw_wS)WeI82fSW72PT8j3@f*LkkRkVsSK=P{^j={za=Dl-6w>13a?CMJM zl6_X+)-8H&w!s{~i!S1A#EthI^#Y?I_&@yy+Gu2ql1kQisB;-zs5{!u z{T+Na3BKVYtoW?zsnKYqz8q!DmVal?vQ_w@?c9)KtE@Oa6pJsZ=8ab)D(GTgQ5v{v zh^}-cmw!HDO55=V$UP*~UHKF(i!*s?vcNjHNu@nekuG-PT}d5-x2)@g)6^5Y1idY3 z_o<>B?_ZA{gIa3oIjhFEA@<$JGu&Ow*t4czsu}01FEJsP_|>NdL$(;hBH*y}KbGxM~tYQS%$wpWS^?vW4>9>fy&v zW5&mqbRgkgTc3v@Gj@&mhimoo+Kz1&c@Gx1c_K5i6!F_rT;!oOhvg3gd|(ap4K+C@ zInYVA5}&;&F5Ozq#FCm^I(VOy<8jT4hu}#J{_R*u8a}=~O3$4h-@aU%u`YluXj-2+ z7A);nqM|23>DY$d{?-+xw$@y&-!oQMs+_tDYBzecwXI74c2=x~r6P+@g zcJr3&!dhAyAEE=*r-)P8y0zQj9>BPZL{v&c4X9`;+d3hM^>~id*cAr%yL)wdxg-a~ zbky+n|BaCQF&hT&%Kg(8hNN7r}5Nh}NZtnncJ&gJwhVyBE8kYeS$r1VrdHO2* zeR~r~CO{3l1}Ff|Qt~OEQ73YLA|HS*`wj`zB93RQ4^H@=`>CXreFQj!UZZ{_l}v5tp!-YlQ_t1{9O_T!Evt9GJG0zJ7JRZkSO?aJ>jR3d}A zF2W2rMD$(Y^?!a7HQxd8rbNR2y~9O9(|O<+2y74S;7$BS>_*P>s^uqvHeGFdy`{`s zGXfD1?_+6Y?-ZoQ!q9GS&IEf>7h&FRq>byaS&nHY19)m_*rw-9hB#yxmWBtV=@?f8 z(6jL~$O0H571tl8-NDAKBP41A9rbz7=1o1 zUV@Lq(4Tv9%S8a{{Pe}d4rGitN#h&*!JuWOTJyZy!k?hdAePH7gRj*73FI_q868Vw?gNL@&3V-kOiE zN`j$ay<`8rWOn-x#fPD49^F$;p@AmuDK5GCZln2}+;-2+-!Js6*hRvoyzFvNJ+Q5X zVr=Onq{gP9nH(t!@ zs3VE@WA_+C_3ulfpz$gBiG@{g`c%3sVm4O~aRx3dj; zRdObMHH*0GV53}zBSFkG*E62R`z3tv8UL?|lZ+IeMN}+Q>R6NZX93k-^{*(!78Es0 zN4}uWs8YZKUaqY#;rG-RzFwBJ050PZul!*KNKn>znPg3&eLA(IZtdXr7zK%!2ftq) z-1lME|AqCRN`S5#??DV@OqR5g4?+|FxU;K|2gyMsdHB^MIq(F~H2w_A#5J_vLM(DW zWw#?q3?XrbgJgtbi{I!B&7hoeY0lAeOk8WI6toiW@)}oTk6`M`^)C{!*#t8GQW|M`(eKk zOaj3`^E2CBV<)ujh98;tB~VE1w$?WuR*rRfL|i*InXs-A_6kKAWjUB8R2lpA^bwO& z+J))psd~Gk=ygBarW7voipod2EM5_hJ-XOPQ$Bq1!PH-z59H2FZ0djEnTIQVRZFHW z9+D6?9`5FS1AYB8xu!)W35O)Q(fsOh8t3*aEGLxTLBU{LM%Mk%nhL)W%NKZG179#G zK8ODl(bGy5H4krG>&%-=#Am!@ZS$!aZ_8#%>Q@(uC`y9nZuZN_kKinOtk{C6d&rst z6T~YY5Ut~4(Vym9_`LnzDX&G_yz2yt< zH0iJzmD@t@b;##~oq#%0y206_CoQey6Z>r@6@MqwvBuciooOXK@cuP3dL`R#8Vz8SNP0x6GfL=OzLIOXoK zc{gxot(z(h#?2o=-cbo+rX}YtaMdWe=k;oSxyxP~HRAPkUtn4HJAWn6U7gBEpZnfg zx&e70e_ie#8x$u8r(d_-JK8u>$+#ST%(Nj)N*dz!0i$E?VxabnczlJfhjhayK`?WnXv^Kl${E<#3M~-5-gqmCh6kG#>B3K20_* z#h;pcEH_puEvlt_Y2-Xm)CwZ#F~EJc-eC9gB%`1tI;PHJn|1@43Y!}rNyw?MkI^|m z_Vx(3-@oZ6*|@zyx_^&Es!DM>>nyVe%7#w+X3YlpJ+3EZ?Hx8$yRlL@cwG)U`ZV=} z8EnL3 z3kv+bxW`IW^?&fNxG^Vt5L4qr3oL8}qC5|s6Y-0;yvY|{odjQ~**(Vl7RS0fQQ1Uk zPmRkoeI0AN#>Z^he!09P4a&?{Pejjh^H9OL)2QNl9UG&?qus%s7Zdhb5)&IcOcOz? zWp(w@HF`3gz4ivCunB*owxlAEz*8(02(eXq0`e)y)u3yF*=pIu1mem7Df0QFy@lbFev ztF%lJEANlj>tYzQ;anggWeXDM@fHRtcK20>8sL<0s_@rqtrpe^p`C2daW+)rp2_Aj zH*{SN2D~4Xad~H<%eaj)OygSMBXJde6 zewi(h>4F~}sD|ng3ukhKzQvQ&-4DW-OqlId_+h)GRMnX-y&Nwx$&~JgW>x~srDVvc zfP@%UYpxceGZg8WS1*yWtml@59EFl^n+=SrnkA_Ya(Xh;5n>U%k~1Dg#Xx>U)07`y zsRAj0CQ1Bbs~g!Jt?+Xk0>uaoz}s(9v(cc!=d=36q1I;Wf#%Mq48o%UTP#xpOoS? zF8v$r49@8i4#zHlPrKFJ^PKU2n)+~H5EP)PU1oCX@wu7<;(8ezb`(emPg?!Qjeyib zu@+D6?`b_S>O;9EJ9OOzqL1dSq3bNbax8C%$P|}nk4FUNmdGNPOWhFJ&jm*6*J`^$ z$Qgk5r^IIcsun-8o!j+IZ>ZpH!f*sPQ1k)6jqWm-QIyWA&5+%#EW?%SbWT5fHX`wj z+(q}Gvjf^OWp)jqt?wu48VJE~M|L~1z8G}~$d_)B$&O(8DY_pks!6PVeZP?sL#Ei{09&`1%I|0v>Jc$n6drq{9CMPVeqEo`@=tY`8uD{+)A@n&qima8v2Xacj(c_v0*Vm4)s- zZ`35Ko=v&KChGg+telmLl4c+@Xr7|48kX3$I_3!QNwc5VLL4?=H8(;>EDNUB@q{dY z61W*Twi`H}hVtV1*o|d0cpi*bG^05_v0k5Y-kF0;&#Pl&!@r(FDNmc^#4h$mb6+{z^mH z;W9b(6k6_b`gL>60k`^Q$!fp}L}L8UC9I({v@*9+F1x+p=7U^PUkKwG!~u6F1`?QX z`+XE20vS9`8X;G|o)jH**Rfig0jj`@K)vbxA_Bu=?q6Pf7*y#p&=zkevrj^P6jL+N z;1=7Q*%peU155EN*N3i;%-5#u^hqP?`zK{he_FRS3L2F|@;^x<`SxA9VmI$@>mM#y z6|qtHoEpUQ8X0ym$l=bDl2ez}&6c&BuQNSrtpfa7vc(xFi$I%@Xq`aR>y>wPQCRB5m7ZEi$2 z{^a*b!U_o2vu3U`7#5f+k0A+ii6U$~p=Hn+0jOOKlSM^IZ?76xXS@;!xTzscLswe8 z)5(D;)l6L? zrt8B=4`Tk5wF}_t1CD+s2+@!n2FX@}d*9Jg{QVK3RH}^@fiCb6dzUkfo z=y+>C*e(F2cW&-0sbleC}Tx}9Sy2cIWQ-aSBJC^1yCQ)1ts;w!vX<2T%t=y@YDju`u#$f~zM>m=5 zq{S`Nh081x-;^CI0KF!R!UG^MEmkxgAd&go4EHy5F`B@=+;Xu(Km5M>!o6FyUQ6ot z+pl$#1;?PbHw09rXck2~KHH(=9qb|9s#SHsfITQ=+vzeSJgl10fRbRd36{Q zRot_8k|1EjNP(WLL)F&{30#J zP5?@__xy`Gpgb4g0{)ccY}ou4if~wN*!4I)-i5~m>h+@{PCY#2} zT`ov-E0k?BdiV5=?;QderYG0|K@l+nv^_A@vHer(0Fy_+0LWjWo+W#9-?yCxvQEpt zDz)sgX`26|b4kgJ@!l6d8EjzeDc)V3>8b@ff_hq7g>!9VNmPT5`sm$S9DbKh`FI_@`IaY0fvLGP2es+TqZt@8O$G=137gH==8uzP$Iln>_~m4xg^!^U0@6%C%B) zocFEZo%gLQ1Ja6$4qTTT_btY|B}!ce%QOpk$`mOLZuP@ZDU|@7_Keta)Sq3mZ1r90 zXhu+UW63}wPi2qn0i)l1_h}ryhY$M;hR^p|sQSocS^B#&xj-uq=rD>$B*cJ zLTs`Qpc4G*#;YWLxL~Q79G~L^j+Ja^m>CG>?xU$U9J~H9SHL_l#7&7)XA8T7Z${Pn z?x-~GJm}hzsKu8cnOXt~#G-)cLgYY>+L~pE{#`lT5EahQa(N7&TTl6g8t@yU&rLYM z!K;I`goIz2IPMidn+i$&sr?Pf@!9P4E2hZnU~#s|!$jChpeO6^;&K|6tu}e|D)m!W zL>{V1dvf$yP)LCurY4m=sYz&X^6=*}&4TzIEbJJCK!Rwa?!r=g42z{9BzKs({uk7nuu~L;Q+9#4ufEc7j;Y3Z-O*c^lma1gO@bQi5KWO$7cNu38Dq{p1-xOSjF-a#2D<)gQCYuMG4G21cl(_vT1<`{%r=E-e3Fy%eA|+P=(hU*E{w(R=YQ5 zKiQ?PJ7iNs#rVsd1;6DHucLh)UVeHLne1&u4-3%^Oy>`gijizk1_#lXud*O3sy9Z~TS0g}PDg;bO39`|TVnDYrHEpY>M5r1Cc>b)u$ZuvTG~(Sqi1_qavadpB zk8Cpr?V-$;mI%w$DtyfPD_xHD<^3K5%*S+>j3pqjCkEgJo;tf0140R2g)gXg zO^PjFI~n`UmGZ)OvX!zDQx%MM>}gm&h{%pPyTUIdGVU+;v7u{UV~2y~@^@ zjuM+ejh~gk!KhLnl6`oOwv(NUIM|ebIh?OfXm)Cr_1<|qFBIcWh3nqbv-x?ia`;2d zA`P=A!4f;4$*LXyQIL^Y5{XNWT-UnPBYpv5@}**p%+S9-fAPEn?G!8tR3=lAjq$p& z&S;C`mKCl8DVfFXo>c>j{A6Yd;`ez3}B*n!=`v5%cOZD zZAMEt#;g(dv~}NXX&QRJUL^GMIX%`1^f@gcKJ+fP9@s1XnSzdtXrIZ=ao= z#WuPRkUa-%hHGcE&-}^;RR^OFH!Kh&mcCvWj)}Jt(SDE`30l^gvZc+tNGb9GBYRo%9@b z8-ApH>bn}cjM?xavo6iQHXb3)QLU2kocjkG`JP%P)L8)fu!Mee_Jw zii>;D@)z0a@3!;dj&>RRz+3RZ#iIV1q{X_A@^TJ)^k zw&HjBN$uxsTY4!X%7H5^De5%1x7p7+3I}fj8D1`*+@$0MCPVTB+z`lB7At{K^R181 zUY|g&RCFa?0w>P(`q_RvxB;=O^~u(MR$!fzv8*SoP;p{-Tj10%et*U}zvpCesz*jv zXy59qiEcT>Z6S1EzT7;fcI(h+Te7c^@t2R#wC;%Q6vYw2;P%a=5-nbh#qAvP-kv5E z^Oxqmxu55zd|)pEEwrQV7YsxWT-WX<9{7p$j9dSrm|bTY#&LQs4sii!DJ-lihlBF* zQWnQr`F-`#`H;_$v%S~)F<%mRlAX$x#W67tp5_b_a!2r}7qS82IX6>%TC_><)I6@o zzpCs8W#MUC2APAcCJ0&3qZ=+!(e(xMJIF8PI_H`8SbDH6;&f#*sQ`gUI!PP=vmTO) z%zG9hNC*^Esl!BU2)OeXle#Xp;D1c}YRy2wXv52ckE*J-vE{P!Sb9VBZ?H&4S(rVJ zKU>Db<(^JrcYY3))vM&de9If(xh@L+&ILP6-@#<)Mjzf^ zr>UXbKeUbO&LG7yv5lL1*Hhm8ZA>4c;;ffLm%_{eADON2{(TpSHTdlybm_0b>Xlds zGt2wV9ktRAv)!L9l2|W>o_cu4K*mQ`C#WM zfd@yA&xU`zP+e$)X>h#_lu$`^PZ{*&TP*~SUBpaWA>|hib{m-SPyZ6|3O5h0HuyVs z^@m*oukXk(GM-%YTwf@(k|+axienEf)g(LXLkY#gcpr4*Z1g>#TCpnS;Wsf@@bOPd zVdh8Vpm+9X;QxV<(XGjmg23fvo5k)7|HUVzx&wEHGslN1Km6qaEMLcQ)(=E?_ELMY z`944iIkm3&pE;4J6%$5`9dARAbrn4pB-%w@7A-IDGK;}IouJ*@FB=FdJ9k!LJNsX6 z-TN`T`I_n!b^0toD3(ngz!h(3bYQcbfdE#OKgzj3&~7&re9CkSY^qY{jg??vTtLsC zg28hNOk1!L2#-{r-Pes!J@*XQNm}#)+`wwdw6lS>a9MO16VT56 zrA1#GIQ;1s*1j{ZluEBp+gnugKh;9 z%%q3+nSj|^HtT>_BLT&6o-^^Ls^Qu7!x%_2AZPi6M&Q+}m(HQ?tVisFfTs*-9}=2I zH{fs3+&H-jKF7e%%tA-}N{a(A-*=&x2)`q9?*1=(Yz1chOOi51t-E1s&`GgKwspQ(*2TCA zWMN2oHF(rHi;BuT+Es!6?rC<=!s{3 z+r;uS0W(IQ(UQ{H8J%yozNW`(DNyt!pz24@wSvV2KL-ejGKJjJ+vnD{?acY2_e=?R z0~KX`w$FT&n;X24~&fN?0-NDOd^86%N>ovFk3xb_?z1L2Qmyx zj7JVF^~7*sMh+82XIz2#Mz`K~xRe2&B51xhL4W@e`2G|APO(gk*xi4uGyEETLxKzf z>~2i_<3Upp8J&3rbFFsc&|Ev>?+%?ncc&!*B1PSTV3D7hJo4OEZ2kYAXtqXiy@Bi9 zS+0D;id>Y{b1bq_CF7B3wyk;7hzAu(JY^Px;(A^|6C4dB-dsXrG?L2(A?&w}0Sohk zbFSr`rB3v{6brzh=;DHYJ;fiToVXt!fd3SH=@`(#mGzQCArROn5veMwPMZ5KGN(6R z9$b4Dj>8SitDbLg$WsE9ngz9ykB2{=R5{E(J(|6W{fv<7y{HC!dAnS&Zv9>nvsg%I zZV5%tT|XHaVBVSkns;$aAU=|+Qy-iR5K_nJ3o*3fFSti1ZpHz(IMhQ9{RzYZb8p9E z9go&s@?~(@p-nxE!9Z}lm4(IW!W+o%_8v*6jYF??RT&mP-AznuB z0B`hRZm_`r^r^0AKf5|oUWI>*fhp%9@;!WDJi;Z&-k=*xCDFm1S6W0pORd#(4h3pr zb#G@|EI5VPI0QVY`^tl!yBwR)v=fA5?=?iWs@@{pfdH!)H#dTjv+>)W2CeIak<n5ljoTs~L z^677zI=36UUU#}u2s}HlIV{bRhO0U~n|QP1TXsupVo`TYtCh>QgaXZT8Jh`l{~H zDt-0=%ON!(hD3P2nsg`>gwM;=ch?Jnb9yEhlJOe+{cUkj%f*ce_zJjyZx{LnLa*$S z1lIKrz+CU3Gr*a}yTNO$2v)pz`)Eggi!6<>$8*ljqGEm*@j-zo4JNxvR(jufFk6ZL zIdZ7{U|CVG0@$)Z0#D!!4uRw@n+^uofQTyknXilmaF~=5-(x(*Fg+${4#Nm2IHA$K zH`>-BK*DM7*zF2{(CX=nzgQ8H>c(9yMw`cXb+?+S%FU`+HPrm)<i1@M-SIn zT!d|7+(B%lVT66b9{p=%!o`JPsAr0AW&6-BN65<{p5niSIROas;wrfZ+Q!wQFCuxY zStFMe>>O0;@c9asyqLl-aX$;FH1qYf(ojLm5d$alrimVGrug8iw}IfWX^Wrkp!?d> z(1CPg-{bsX8kS87zto_J2Pg@@*Sw+Z$Lyh#M~^C3N9k$F9tio_9n^EV)ULCBPAZA9 zBxwuHTYgCM|2(8o|BxBwk2G|!a^{Pt(7w2j>CYItb1DxcmrJ!K?u^K0x8pKYVSy#S ziNwWr!t-V+P)lRAfo{aUo-f5Cm{T<#58CMVHxJ8cHWXjrPRRVt z`z_#i(4{O6kVkZ!e+R-K?fF7Q0kFu0RWiL|u%9*2CXLx>Oa2oa*1*@j#b0~k~?{^?s7 z*g3oZk?TPnVD-BqfbX z-Nenac5^y_bEy6nG>&94b5q}T1Sam5T1eCKW?A__Q<~K81POO-FH_M)PE0R7Dr?D--|m@#x<8Zek=Gzu+#n8*jElwLxpscjcZsnt0o?pV&1p;*|>Mw-MV*ztJKFe!0f;~-@>1$EU_DGfa!V7B}_e<+4za!m=H@40c zHe|%8O!nZapDqPQSL-ji9QQ-vCq5-9S3V0Tg=Q>U#jsa0Jpuv94a#VFpoKTg%|a=o!nFpo8ch$rjJ}*{l>eTDFgSImtbBBdH&UD}H>>h{Gtb@g#M ze=s3fpK8Bh#~|G^dlRLJhS2814J&}a2J=8dyz7UrV`~8%Qg}%+d;plNd{OaMZ31vG z-aj!FsjcpW+6Ie*j}MhTV&BWoyFHPwzv|EUwYy$Ds58?+T4^?HwDXzGm{O6LOOER? z-V=|{1KH2QX!Ga3WbRyfS_ui^N)=GNW_F@iZl9q-BYodI>^iTDh%I-)dVSH)k?OF? zX-b7$+3dR3GjNNBMHXm*)BAdC1UhBZNPfh)-Zi<1T?0s02}EqWf6nYlj({t;8^0ct zG9T_Vv(|oj@HLR3od$sBE+%f=cwYS*MEAp@+G0*4b7H(l<^OOr$R9AF&#xP~|D?#(MqN9dRRxqv*a-gvd(IMq8&kDlHjaSH z-$Kqw*74@Tai{*xGW9EH>=W7MT! z-V@sMgot8Q8-@5{=r4KW*@xr{Lll_}=BW!=*|xXi`*@`F+z;g?qn>RAC^Bs zX7eVne7iw5O&j@|s~nG@Ro&M~=}45xAAySzC5aN{M0hI4_wCJ^Qg@(HLB3bOl4Ec9 z^WDRy1$SgQr=#z*($NL${y+#=Sa?|9*o1GIw4kFp)x=?cnR(o5yhqHS%zPPq&-zho zpei!b43jq)eWnkKJeEBjL%49$LLhh0uA}eGz&7p`Dco4FNdLYUq~`r<{c^V4JC zH-MZ&7c4;z{6De)uA@7*Ud33N1h`Db0pNSrua-TJITOF$cVEwuJP~lIHwf$KSdXftgOui@$>|}Ont+CgLw15)(rHfswUMMJi1|o;E#7K z7+hY7YK-gwDUiYC&dZw`NqZv5K*jI#{fHWsbnCTGMb?ZmOQ?&i2Rbvye57 zs9=%sh9|*Iuw;3VB4WJ(M6gZ;@ZP|UV{vRi77~5`TNZ#D$PVEq&{PBJN{RhX(FM-H zSRf1JOU`p7)um5}|FLGEH@!QM3eagE*lhleIY0om3q*Rii$NCAU5Or;_SSzvN^H!IG7)$Wk+70TDD4!&DG} z_bA5tiTmHO0AL-vs|CvM+cAkRaztQw2&g36!26|&t%#p;6gb3fO zXJi9|KSs0(<09mM#c+3A>sI~|1Bbl58zTd(%$4#ES|&1SFsna{z68n?7~R>dzin{< zj7VS0;+NlU*o2JO%3#TlqY1tc5CRe8>HnK60kXjU4_WvgBWz(pgdIl#)b-6jMHel8 z!#yvFD}gh)Ko4Wa;wij}^WSR*dSmM(Ibxl`Q9(X)d=m&-a5PUPb0h<>7>F1vBki}T zNH&eu1YEEu(UA&+7yS0nrdt|c{!!w&>DKz!bpLC*|25tJnr<}Y`PX#+Yr6kM-T$I) zbOiP<>i!pX|BJf+Mcx0uNSJ*6f%IAm51MQ4@v#PXsN;gJgAWAOV-9*W!jPw$3uQ#0 zpY;MZ=95VKG~q*qzgz&2o_x*mejf0M&%TD$jr*-;&stP`oAElXAh?}eM(&zK1amku zcEG!Qu0EomO=ghoL67y}EUO`TPWw6=UxpUs<|ZHsCzyNc(epqz(n_KeB;sdOTS2d) znO4v_c0LLjI_3pMqO;4;&t{HhBi45E%i-GIoxS*XUZ}I2*7)MYN5w?Awu1@r-%kkm zR;l;mK+G9p} zfCJKCNn8T4zXJsf|4!Fl{Ve(5HCg1Iy=+Cv@%8T~z89b@_L?|?Ui5xnFaKTMH^P@M z4{G%VG4&b1P{y1A*rW5ri$$CLt7Go{QQ=}^zSD!zT)I}KVl`q!)0di{_fBsuArco0Z-z|`JL{VIWt}U zn)FjyQs zYYPFS>2pD9N+F;d_`leD&!{MaW?fVe6hQ=(ARuW_f+PjWS&~Rn5D<`{B#|hRhBRPA zau5(nDnar{j>DM9Ny!iB;1My?n_58zEyO`9c*2+?j~(J7Ob`9sn@tRN%(_+OeE*`3V;U^9`Z?&h*HZuJ|t8A z-~xlg)!qm3_$VO;1i&{ABfYN!i0v6`qtS0By&;X?t9Ck242s1}tj zLSE}|`O*UqV2jx2gtQseq3oofAvL=So~Y zcJP-YBo$`@)0p1Q%#mGjI*=!|i?)JNSKs7&(2?+eS?vRi?Ya^n7xUskb_y&IMs$(Ur;9s-x|FYQ-cnR~X&&yaIi3q`95y8iO#A1FADG^Ro^(+^?(M4E_1hJiVvb#RjPrV~ z&2|^egkA_|k5ne6pcja0(z9vtJmVj;?g&-=ZfIV(+>r+gI6rDY3IFQ%dzM)3amSop zx$r&V4<}SED$&Ni`y3hl?pJEUd+Lic&w^=Htg~*t;Zd>E z8%o0&pzw>dF~+7>!H|Xs557m3ec@C{IoV+>qDKPA5CN3%XxQV8eA}zW0YB=eP-iN#vW-+#C+8|jyvk*vBsE$Prq)FTonY5 z+a&!2N~b=RAAxs@>&Iz#-&FpzYZVvwlz}ty-4VF35=>j+2oZ@45%cnS6_RlCP{o1O zk+O-mt>&&b-t^>*Zv0C40aTVn&MoNN!$OqgQgC94Aj= zZK}X9XB`DYylx%2@j}};wTr36+Ll1k!I7jL}HDyPU4> zxOMfDAJK9;-oEi9wSofr2wWFkILk4rLNc%O#z@>-F+iqC6ugSeir+#hB|V6J!o6rK z^sU2|cFNj^D_Q=9n5MRs!Tj3svq>w&_+2UXu4x2!*n{NX+jm}rPqDw%`1Cxoz~0HO zR3qg|ww5_r1(=Q-f--@KnCv$LJGhwDK=xa8P{5A5$egMWStNJmF=~Y)fuW=hjWM(| zRyEoNsWeGiW2nwN50%_}#&g7qG_NoD6kaK)OD20Xh~$@$h0?W%y}a;I80b7A z=Ep&Ol4#lLQg4(Idq993n48=XMxX&rcn6=F5SenyTHEgVcrXC=$O3|15H|>Qf+?M1 zM(Hm)>5nY+SZt70aQyl5yCXx7kCo+D{e#QbNvN+q4WQ&0!PGRxaFxzb1X_T>J^{CC z5fhQDo`~dL8<^#XSs*%FtA0=rUk6fihoqJc#s;MJX({)WmU#n17}I2t$5{;4+Yf9JzcN~y z*M%7jByJRu-0l=za>Qg6f`>y3`fon8OP{{(c{CZiD(&#qFrH$$eDgt{W1eH#h?DaL zJ?^mBB*{n3@qA(}>fEW(?~(E5Ub-nh+ZB#+=`>Q`{7Mpv-{T_H<~xfY z;8Qx*Tzr%@bO%-6OI6YK2dy_6>o@ z9f}4SaxuS%gm8uj^p_YrZMAn4s!FOy6Mw9HZMcT2S4V`iii;KShA{YZs07DGN>cPS zrdE5h+*FRN`u?s{Hw)jma7?dDKmdPIzEI;^|;^2AXb z0R*ckVy_Am$Tr@GLuats8>8;i_dE*xiDh4h<}2vK;j|80tRgHz-TW59>DWEIb7jqT zLeX@`i0cBSBPN@J^Iq{#Z$~&MYF+NZG|?8lP^P%590Pt{F}! z@*9flALE66_x=1`2J9+}C}^XcKTg<<)~W$A35){8(?y{BLErHu2?<}uG=;Di3 zYNQz)cHN(xlXAPp1N*)n_wFvFDpCYhp@mch;TOf-6ti3K>RWPMrt`1)8Ac+KVc<^Q z7;y)ag)eHRoY#2+XJGXa!ZcZyPsEb3Qy*w>xVgQ<)?jG>!eQuQUr*&N|DlqfL4ny+ zpU0TwyYMbPJXPQ?|y9{aldbr^J+lso$7A%AOyrz5`a_D%sg#7#^H!XVmqB+%0N&0SlEU`Dzsoiie2!2GK?Elj+`X#}pI&1?jp?M8f8h(dZ-5itv}>bMYZbLExFhU8>s$GDxK7RVkB+TYY`7zn z58ANqNDhwz3(mdLh)d_Jzynq`zx8}vF(yLfzKbITazl|t!(fl2(sepceL_t=LfScs ztFJsRLe47!MqE8=6MBpY*kWKNul30_S+PWNYdx?XW`Hltb1qxe_i4Oux9AeTxx5;3 zgt!3MiMBji6%sq?7$9Jq(LkfQXEF(*@@8R2h(9xYdmN?>nd_JhHPX6qnVv6M{sa| zyq1}=i_?s7)X+B7YA{<0A}5vKq^fbwM>jo4!7oeReOY3rcS#j%+cq@Jjn6z9W+ldpti-tg+y&m43f4y0i9?=jMZ5+d_|&v)ga(_-Sz(Rr+ST-5ZTZgeCpx4Uzs;t!<`D#3a-oO#)=>Z2Wj& z*M<^g)KjjkoN6@&>YKN@^niq#toFdaBSpmg7K}f&G^hNObye`Yw1ZheHXQp<+f%@5 z;R{H-dP<<|l7lVNu|;B`l4&#pG#xBH%6sjRg3uz`i! z(HYCt*Nrw1%(kb1KDv_<=ixbgMr6xw;o{9by*g6E z9c19SAX;`{Gc=?|T$8s~I^@j?pKjfdM@@WQWU(IpEv`Jmt2okjx9R<6XVFbc?1kB- z6LlI%b32mmG>V_EKAVi?IGQp2Rj1j}bTMWQ=83qyG;%sY#`dKrHyNv~MIS!&2NXwE zkvfpgR87CG@dHL;ArueOiRKomy?z>TEV1^b`B7EyFCHLnXv?GePiVL-#p~@SGcQ<~ z&x?7b;u)JHnJR%-qwSr{UOJWv)-C%n>=K58%Xb+3FM9VLw$4nMIFod&x zGR)C!!&i>2bXWRs0lmF6*P^dGKmVx4KMqlS`%Q25uN_Hm!)Lh{Q#ljG^5W^m-6Pdf zS?QUl^JZc)9t~xVj&)rG&tA@@!pE@X>|A!HibryKlHNu;5Tp`uLV+~rnVmiDMi3HqVMBoal}ta-M4Wfnsw&bAJwrrt`hr5r z6;2(R&r$6db>Q|q&3l1Awi$_}r&eI^KMd#UPgl+`>r_dn;*~-CDVw6j8 zdWQ1H%k27M%63h|rJlcga;$MY(C>m;__@)M6AyZGxNVd4Z#g*`4Ebo41S#xnmTNKB z+eGk9`2nKC}mD}>Q>Dvxt)tCMdhVsi6E!)^0+W`-ikXn@!H@62D;dVp)xP~OHQka zqntFlV{7d0G=W=wvBDoTc8NbUtsujr65sSj&l;L?BGJd|<55 zvU(fobf&VPu5I-Ah>8E67L&OBg^rrlGSAU3jW2#X!r07mIgOP+di3C2C6*DS+5%_c ztBUU$(@vLxuq$RVp}`v-p_Jtdr(|5)J-6focDRV1GBs)qT*}hYsFv!-H~WKP_C|z_ z*Uy>V(bOGU8aHgWs+=z(lvb`d$hXWZxvw2u^^oxk{=wVx-k(R@#65=SP3WyUs%&LpB+g=F@}8OjG{=j{#hBW(nRnx9_gOtr@JP6c>w8{gRTyC=o`&Y+Klf=Kf7L_B*MBuXD!g3F} z0MRmbQ!KAb%^G8jRwjCYkM})1uG$iOJkHaRd-mwYoytcodP=HZV_AC^!_v$10mo5+ zwae}2D`ydu_v(&iwDa*PnX%37ECv=Vprk3YfC#_;&e5;}p}^?EhPk(860OY6NuzR) z*plkPS{&Ka!L>Jy-^ICu##Y|%(L~)uC-B0&YC1rBu#J_`3X}4M^Nxd_ORPTM%WZ83 z%ALVwu<|M-rh}$JA|L$OB_B;ji`Q$$oSuFa$I&4J!=hqth5jrLviR}1vU95{*OH+= zL*Kr+@nx~z9Uc%X$PA~&^u^EajRziG?%hamg<}e7Kt8encQ#EC$VQV4I&8O!3_>xg zM?E1bVl=<-QvQk7*f%5*C)nK81T^6V>~TqQQw&f_?5Di7t0}E-*pRA60>}3c-Y(lx zmR)@Xq}zTgNY|0Qi!EjOXtv8AbCGsDUq`*i@HW0sOaWqx42cgn(iId&k`-8}R{gGn z`<4aIjQO9=P$+&()-?{dj4M<(tUQbMb5sL;uk+9Nn%19(SX?n!(?RCu1UK=dJts-Cl zsI#wsFxe?KIsUK5ZY=bo9*&>@MHC#eqjGut67lBM*>~Q2=I-| zwlUD6B~q&mvhxZ7kts*qs+5lqeINlond>v;S^gE+z~u}kFVepHIN%gGPMy*p1kYnX zaqzt0d;jI<=_*^)$C*)o7XZkEfF>Rcz3MP@f{FRDm@7PwP#`8Dn>nHIL4$c;mcnP}8uz_6L^Gp z3;ej{;7(8TQQppGFz zaSDtq5SFI^opYOJV!&SPVjx}w0qcmlk_`wvwty+$3e%^urihQBON?*GFK#LxNDXD? zqU4ASKLU?#0U#oE*uW27y)dXTzwx&D{xEmI)l*tQt>BBpWFlW=ltW% z%bj06hy@IrIr}DnrYZnnnZtB!KHMSU{|2}n@;Dbws&VRCmoZ)5x{V9K<}S%C^E{c2 zb=3&;mAsQmKA{F=QUV#9QZ2!-=lc*O5e`gj5y>F}1hxaV+D?7W3onL$$a497AB4b0 z7Vcx;p?yuWB9Dt^awGd3C3xfuIt15pVhab8K68xceCmetVTOzMVo$4hIRxd}H&y^Z zF%}Vq!!3@Q1Jf zg{#ncgo|k3_7O0*#e7Ci;CH_fP9cHeFyeJ^8eu^_)d-MbELgKC15pgyeiXB0)bh0S zbcvQZX{FyZhf$#odVWyAb>m}WrU3r*ltu$t1;0!03|wW0Gj;FPM7n179o=zS*1O}& z-ohRv)R(xyXblp6*%b+!$=@yMKjZ>=SPU8z>iYyd-f*(mSliX+Xg8S zU=)MU`-0v;=Ki36a$DJgHnJT$CIAfPNZ>6(v=(x89|VUEU0}$^AS6t`*GSpI=Gf|E z=vUD1(*+KJ*_Z=hmhj&J0q%rn@a%7+ga5uuKyR#r$yz14C=MR*dVdKl{6{7RITLFi zPNrXm#sCdO0TOI{;J&Wz|BC%2P#uw6O$aRXhJqvLUsv|y>{g`D9UrSM@q(l)VD|gN zyP<6Ho5`Z9kNV2U{xkvt9uhH&`Dtmt*&*a){xPxx*dtc;nkNCQ-v9hLmCIG3m1h?&QG`r~&8jc>$4_AEja!<$-9@Fto@Ez%qKraYZ&U#Bht8sGlKwdJXKG_jNWoKnV>&vgD3r#Ng}p{awX@A_2)K z#%)oX=OBHfwlBbxQqkTA-^Kj;c3l+=3O3c6<1fNgbUL`-=Tx$|I=+*+oUEOHs+3Edvw{*e&%h^y*)xgVBN^LU1(Bu!c zv`NR)654bvbGq++&W#-INS+vK4Jj4&-^5*0`H|6edl`}JzbV?b!JAs3H}UJh&QJp2 zKCmvE?CBlwxMhj}&}}2Y8EiM_9RB32w4i{UCVqhvLaOXaVD`_Ilb|K>Iy{b_eNr@2 z?bx80fX2MNlB@rDiSF(OS--%gyS=Bg4S3^e8ynIn=goLkUw|_$AzP3nzpqH+wIJNv z5w$u71P5f8J$8GI8oNI+W)wScZA{RX6Q(C@jL0P^BR|y}Vt(=_6>7l@f`S>rbR~m{ zz#Lb7nq4!onuw#+MJ=Bw(0R7R35GWa#e;)f@v2`BGM{*`ZE<~(jNmBf4<_bs*`rhw zLe`&prPv9mFFe{F+ARO%)nw*$>~8GbYl=ynE;aG{d|6_ebbZHNA+uvoo2f_Y$7bav zNWT^0A-C2y-1#8>|Jbd8tMNckun=ruA|gTZ7S0I5W3FEbL2inqA?V&Jih7SU`Xt!KPdM zd!Z38?2@I15ZQ0wUuWqv{XpbxdN?$i|K>w`8~9T^*C&DBWUI8hqAI*bY%5I@_qwO= z!{VQjaXfDXVKR(VDucjzvmc&b$O#9vE0?zOp3mpi%B|7v^_S@vRAdD~&}?UwBlip*6G{8w zq<2xoN#^Z}bDqNwg>8QtOXl8R;tVDm^q9X0(nwBp>@w-vQG3zvYBI} zUTz>B)~0}tlf=+*vUyKhK~KbRe3gnpze&&+TtyY?MiU9 ziMqEu(^ zYQR>x$^~r(b#1U27HrIo+MXEs$-+Bp=bi(Rzqteh@H~jn>}+ItV3#J&MVJ-6IbOWw zy=*08aJkkol6zuK-5!|vf6yfi9k!%u1LU5YXE@v)DD+Dl_G(#(=I&S$w*FNe<-Eie z4H9uQoAi4jMYcFW`20_w8a4VPw`E?CuM}@e#>w%6S>lX!a&>XD&C|{Gl=2!eb0%uL zeX7q@Yd4Xs3@+Y3MKZLi+oSd_$U2SRM;glS0pvzl!nH3TL!&l&Ol%iso2qA=b1R?M zbEFSkFEWF1vlZJN5zoNMveZ1{>M(*$$5(#G7e6T8s;jG!mzS~l2xXbUf#Zc zts_>t{9Hd;zK*Z^#gz1JKUcNK;P!Rr^e z8ZsPZp@A{krgF2#`dvc6*K#NGAF}zNb&vopuLWr+-CS*@1h&mTJCzM=%Vv7jPL_;P z_wKzx%7ZlIaDPQ+AG&JdQ-JLYjc3jAEg%PPrGN=1J5I&=%06-IybkJ*O;vIAPA1oK zQ)>j~S8#k&f5a4g+M~8Kq4kZJI><0z;H+;CLdn5t6ek)-&OATGM9nAnS`1#}w$QIT zs}{`avDh!TGTojT#QtP1IC60!tbrG7hzA6hMM^-*HO(q)i9B#Ri{_s2D~;lw5V-CGK78fSIqDzmCN|2MSixMuH2IBhK6Nj!5mAfDgI)-K zO!m9Z<*D$|)S=Qm?iIOY-i!_*ZZ`j_ZoH8|*1R@O%zV#Kn_rt1B%XC5hA>(H;W7H8Up4MZy5j$8wPJ<7DNZ2K zcWI=)G41TKdMpKb(eHjIlg|wn?g?p_DSww29$Odhjek=*+mxG@*?4Bh8(daN z6%8ej9?9)S!id@WXgNlXpZ>reppZp47%~8pqAPtFptgEWThZLb{w&~&(=`e(c?al3 zwUz;K3ud1II4%^|qnZ+0&a=P~pzxi9z$udO*C+Vlt+W2iw&81R8_~F6qb_cZ=`t;p ze6I6SuJcMR=a!Z~T7^UlHa3gcsw=;j7BtGjj_N~5+2^=9boKj$c{_Z57Jn-o<=_0x zb8@s@9F9n>96!!$_G>t;-K8T%esbB>ws9A|7Z=!p=J=5MFs{VWtbtGA_1*}VzL@N3oc?>9)~LcYq{QKtQthU9pk~ritc=K8aTOA|0^X}tROhu7j|AQz zcm_cbU#5H9+k)w24zFI&0S&(a?0=X{f-)x%XXrn&7S}PE&N4HLzKdrd){2)HMZIME!`SXl?)&YJzf9nJ6VE!|-*j(^ zl6~Qe@~$54%^`xO=KEiqTJC;Q2=1l@kW}FmtY{BCTzK?-35dcK&m0^z^J#oxRoY z?3f1i@^JCy6Wl51K@x~(_U4HWN@iY4q+BG+EnJm}DoXO!n^Fh9Gxed7ExD!_ zoWoLQMw%>khX!~) z3)}a>z!(1-CSdOXBkoFZJxEXdXOy}3RCS&-wdwrjNv|p7Nomqz9@U| zXOx*|)8{A0GyG$wgg0V;Ft#ru{qQwvO!GtKebTs%iIwKZbZGU_8t!y2$6458sxdas z5$Vz;RzH_NpNM8UcQ|GSi`i627=yW!6|Uh<)Y*YK7(?br-S^N-$+&jK%OsTANBtYY z0SZbip`aueWR);YUgnPAOGqaxFjZQI@#=mw67h40zOyG1ZN45h+?`jpUFp+DBO*P zf&c{260eCj>3{$v`E_k$j2gircnB2DBp$La{!4>9+u8FGvF8{Ag>b&im7JfQnGsR< zF0{v91nH#_*{x>4GW%r-E@;S-KI`T*)Qw;HPID#$Sy!y3zL1<&>*c7 zqayFHwuO0kY`6c!%x;ndAeVjNgGQVEPoo9)YpNlaMuB|;^7-}zEav~o=l@L?{Oiof z-%Q5#f9E92-*mzM*XaVmFqNT0F2LW^!Qa%u-_*h1)WP4>!Qa%u-_*h1)WLs;tcC}_ zMR8SpQ&EPwMp*7#qkWour*Kw67gv*Qai+JU{Q5j!p`7MwzV%fjhbw$5{E0*(c^s@88nQy%>2nV=^a0Qq4rPebxd7g>{sZ59y=+PP=(5%I?7Y`8 z-170-`Q%%uRS7DPDFVdB_~!45M{7s# zf=NZ}M)f*MtB-NNNWbs(_R-NimOI z8HR}NSo$rUe$A`IfZA<`qr(lDo3%pV=v!s*Tq_YStTv( zb6@zbo2kXfhL=_$IVMP41*J%@Kui}=ErB*0i0KkBYG_a#mTGj)5daTx2hTH{IaF5u3X@DQ@ z_*-Pz4`Z-C5Z=NC$o`sA?sEc@LIt#QGWnl2KoLum2@=Jw$5q3ZD7MWH0h{mZI-;f= z2Q@;(%InO0et+j+TxoY2Qr%u(xF@-icdv7)AjNEG zhGk;RPtSX->$9}=a?%A?{{#w@{9f&*VYdIn%h(Pos)D@=BnN>7e| zK3e3p#^&lfS~H5Zza?QJwpVqtUC`*d|E^uw4du6cD0M2pqzDj%5TJs}j2=Ny9$J=q zP6b@xbsXrm?w)isE2LeA^rK63G?Cmf1-$a72y}NuAmDU?8X5q5fGS7@ER(%+vX(jO z+pN)t9o7mxh?*(siuohwW~f#$TsawA&fP>OYVU*4vG2mw3W~R2PNa36vl!!ker?V~ z8jkikpbOij^yTh}jJ}wkHL>jCiimNSFIaESHATK0_F7rc-N~`kv@6&wN-?L}8H}Df;Mi_BZ7C3S|F=L&i)7W72>AoZTz+#$h z$QnB;TbP{#dnw+~9+}gFl$#|jvY$Sc?fuip6++5J=WbS}*$ZKZ3)9TvUo4_bi)=dt zAEAweNGa&n{aFQIJi4n6*rj6+k~q2B<1})6SA&n=qEY-Q5~3U|dcEaw$21TxE?<2P z60ZRy-rf_L50YfE4Tqe=VLDus4-T6A)S+OX*sc++1~wk=r@KBGe-w}N8B0R^F8n-ddUR{`_j!sb zKiCI48Ph4DAx!B6F2Qnd7mvJ>t$))X{|*-@MB(c=<)@}C@l%;`xs_8`R5vJ6z(j2G zS9O?cXVa$d+Crb=z(zvx8x5#W& z@4|d{Ca0IJ_EJ*%!pesQcaGP-S|oJ^l^U3s1h%F+I-!_Cwzie_1z&rS1X0K6TlqIN zRF%nT-m5Lc9@j^eGD+Q6$lI_O#4L4|T54pErB{~w6EZIfVp%4+tl9NmQ=7%`m#gV6RD=?}}+ zJ*d7MlhtzP9tD7Te)hTZd9@?Hg>9&;*?O}5PrEes@v`e2%SifQY@b&I^Ty;f8!9!I zs-ma-*A6{RUYKt&+P^K`WQEFimy%HIkXpXIYl2zdQJ*mI1nOAw6j{q1!8Y0QGf9_W z<4y6bys`3IR?15gAy^dbxcdeR{#pDTCJ`a{B3#&SG`RoS4RrmpB9ArPs40^f!t+5D zLt}d5u=g-rBlt`3MF!gC#Rr$hD}TBw<$4tMaSOqIv%y~};_MtM_Sd?lDv>O}=dC$|& zVZ>kyM1#Wcy_D||wap@NKle9O5Ud&gLTgO(vGuQ2MV&qoq< ze3$G7vYtc1N`k}$AKXU4Ihag%Np9j6;JAEJC4lY>pn83w6_ZuMYCA4&vf1Q@Y2xfl zkb%$l?NX!>;t*D!qcxzGoSLixa^1u)-UWer+Y<3NjK|T8fRHDG?Mef63z~WID8PW) z*$+JC(b}CXci)To4T+)j>D$a{#ZiZ~M@|cMY43iQxAYuuo@Ji4V`(!f$2rxddUqL) zynL&OaXK1Yxh`tRD}LynEL74XU#3oQ{{i2pnIke_lLJ03Uj;dwaWG zlBu(Y^IVGmQhxu2u*iCe*r2`=C>FUfJvOj=Y;z+Gr$Ton>7mU{#>?CA%B?rgmzRgw z(p?VIPUqlOPrdW%@N7b_=)-a}!sjJ1b3H?oWsM_TUP+2Fqk$!WXhC*fn~;TG2c_|Y zK>zqPQZ%dT0Zn++z9bYU@rr({O;RKXrY z^=5Li(mrhCLOXWlm|)s7SrcQ3y;xL~^T%AyjwSeoeB1t7Tzl@aON92QqzJ<{JE2-{ z^B`Q7=o!BMf;%n+|7)T3)UJ8P0U}9HI?vApi%nyZzc_j6^78BB@{bI5*z$uuVf$N5 zZ(VMymd`2^F;TvBQ2QzQ^IJ^%r)*N|uUn~@%DbEyKiviN?o4Hi`{DOQA*o=$i7*>umX;t=9NKS(3h@ z8K?|U{0Gq+RL9nm4p;oN%{Ix;14M=~pS(}9IC&U;eId4ib(SlJJ2ae@`kYWP+L#k~ zS-D?F7s}0l3qTetru}&J`h{WK+ye-inrn(Fq5sr!Ah{^!{+R=mtF;Q3ucDs6trvF+r_YNvv z5~P@@$sQk4B+iTZtqG2Q{2bxaF|7My>(9>jcS>e73S|xhm_l3gs5$4F9=z!+RSDt_ zO4zj;PySwxHiET*#WN|%;Ex+N{K)1qLnM66DRg@`pyd7yErY#}!_r#GX@e20vzN)g zs%fwL7t}6{<_I&bl}O9`mlQ3bJVi;NVyb@ZhIYB5FHxJ8YvW?J+nTKy674{R+;Dt)ia?n!sv zZJ5X499BCPR2711!qM{c`H(YKh4_%FTw8m|p!dcDeA7)kYi9I7uuQ`8(rt`7s_#*W zd#$8ReFAXKpG*NOAkp5|k|r?vybjkw7znZx-wmW{Zu`8FN!SpzAOX&$d|ztS$f%QQ z4Vli*m5t=?*1tG2xzK0+NN0qxhup_^29Qbr&34QPm8-Y@2&NyTc2zg6~Yz zQBstN_4Nzo&TGVwK$Fa90%uz~`X@k@W=|@U=X9C6ppu&At3h{Wwud9>g0LIhcZ{;r zx2u*IkXcqgs7z4K14|N5_|cR132es-0vVe4KMd`qyg!x#^HF;a5Rsa8)58&r_d$6p z^)*l=05#V6mghI@ZQUjA^yB_KDpb@|lJYctgp87?uir+~xJ z81v4|p9}1LIl#DKsO(%!D}!2O4$}KAWGF4qL_*JSR^OwUG9& zmih*s1zGR&cOdU_`haKuPxVpx?Qx)6Mz+Qf*$riIZxOJfR;q+FxuT6h>z79W6mYo- zq!CB)amId=__7Qb{^1(^>jpO_$pU)HZ}sBKXx;{a>yE&D8xIzyHShH^sP)Fr_m|F- z@G}73{xa!3FF=cRA;cFX{~q^N$|N&L;e((frveSpei;elKCdXj=rRS=Xyl8^-D_mG z1wUmXCn>)D0d%#W1STulzsF?#$C?Ddhzq~~71%}ggAw84d-N$SnaF1$Um~eL-T|!s z7KWgofUom~K=z^e_bT9K9Y6(Ka#cy#5fzdRyK9~GVxmG6b#QeI4mlU zB-X|WQGW&S#Zv-@1_t$U00lz-%mf|~Rw0pcw__#>CaWNHA!^+(&}(cBHu7=;Ol|It zQNFeZ`Xx7Bb8t}09F{qeINxgp)J*E27_g6Iwe^;IPQp7G&TxY%2oo)zk3R}g@F8Y+ z8{g9%b@9Hio7KFa2hyXFv!cXgulK1d7EY+rRoef#(y3J92M+;OwDRrnygd{-^Fuy+1%~Bz4Q~Odu!N*LUck zb*MjBSTpY24l+<3R>tze>T~qb&JqLCToh>GR6r3LA=CWUJ7ZXv-Tc&lzAaTaYkz9@ zyX%Q$=Yae$IfRY)zj5j5ma8a_Pd2O#WSEr-8zche`5n5~QqIqkeAdyr@5v%4AG%XA ziUY9|WSC&5D|Eo79|DJt^1g^4AV`)3-a>ND3-@i;sz$|*D)1HfQvn$ilkayCBSzR; zFBmBfuIe|+tCF9HLrU>Xb_l>!D5hg-;I2mY9iOOO6cuDG9lm0vtv3h5ba%ItXsIhc z-Vdb#4FRBbBH}MWbV+E21sXlMIk%m?3|86<#O)iRjm?0<-x|z{R2jbpl*bV8)Y&5s zDq#}L#dFPQKxxyn>VGP2$`iJ|1i-F;S=!V*cv%~mkURpS3G@)^ku@F%e#l?DR{~sL z1o$zfRH8TPqdYv%wdT~n&3L?je@mSk8Z=tTVRzaxe$t`fL_38OYgJ#*kU$>>Pq=!Xz}(T=m&E>mLlse0XAKf0 zp?(2`Cv5Ex;J1l6r$F(H{nG^P!`CYlUO)Wz^{dbR&xPNNVro9*0{mSF^8=;*tA>9! z;a?N<-_8Kb;~sXny9?HAI&dccI6)kDV%X2#FhSOF{id3`Yi$h3r(D2%A)lNK_gRee zC0(v-H!b%_zLM9=vyLki8I37oh%J5aTvVt=x6E;1DlA)V44>}!p{%|^e0`y?6Ftpo zhKB1j@`d={`E7r)r`aH$WymvK0yh;d-n0`jjQBREhTbeV`CD{6D7E9xYGIPms;2o) z-CVk5GO}FNb2T!XO>^bB>*U^pW({l&E%L6N^EAlYEp0WX$X2h{{ysv)K$3~TtylRV z;W_CPmSGKNL21K{bV@@IIBgj&-d>>t9$6-P@A~`WB*Wk!R8?vgN*|a9T8kCGpT?GA z)Dif?0<6wfHIoEI;Ge_Z5#B!YU!`}Q+|(O}C5xtAeYsAu1-4RH(RRj^XOWXVui1Qj zD@*q?{;~?mZp{Q^r%;~DyBcZtRT>stRc^c{tN3J4s-E(=b6Amg?`s-tQyP`btmw+* zEGa!0{va|V%`0goDnzv7^DhprsTDIc)C?VnSIvGOX_LF^-=w?P0a*Zb43zN^q#IKd+=FXr${w5*{0du7nI;Etf@B}o9IQk%m zt10GlYmA8en+s>ubYAuF8Md^uNTY;el6^)iJJ-WaFzr@dLv5pqDZZvy zY3m;`Rh&;R&R+Aop*;R5s>o${+bX9hC3uvcQOklapmjqq=}P23owt0rK#?+;>mp0C3t zo^Qul$ou0hP^d8TUNeM^tEF|EvHo%`#RJDo0OWF*lq+}u9|R<_86v~3~% z5KYb2e3ONZj>|pemhT&izotf2h0TG6Bh;w68YUkI>ZzD$-HT!Ah%A`8Jo`1u^; z7#K0LVd&)q`i&JN%km7BWx31^=f;^#y!I^wrb)S}3rPvdC08M36+JU|HRaCsaegwg^)RK*eXT>);i%Hz+Mxx;W)mfhlAfB?L6n_HJ& z@Og_8lLTsRi|@K9Z`|I~tAaXD3OQXgiJO$XclB0UYUongeuP)G*|d0z*ZYr7*0y&_ zF3U5p+PY}+JoLXQb9IYRr9BeVfK7$v*`irpH863veoTbuOlvg6v7pdg@;W;srLe~m zUG{cPo&8Nw*T+WH91~&00`pg9Uw;ZmpVdunG#AL9X|3;}%)LDpd5bgK*Zt!;ZBH4Mc>JXC zNLSP6|7Y|({Nvn{!ob>%^$^z{QNzr$8$(~mq5}PndCRx>Ts1>}rSVSNB3fKb;h^K3z%pPp{4qKaz0(Q*&#)2jhJrj28LpwGl!dzHG?i})!*(Ly>Y6xq9AqOqyboB^lV zshCxR`)mvq&Ek8OCuZ3uySvDhldfN1F?qvQ=x^8NIo!Ck?38}h!|(iXiuYi)>kM7I zYOTKe3(&zdk9KFRBJG9dj>Q{XG2fn{o|*S_MnATNe%IOIsJXW;DANqAx%7v0Q)*eO z;3%HTYp!>^YO6C-De;OkeoG1ShLNSx{!{a$yjS0%-7{_A+v38K#*78CebwKrXwSXK zM*i#u8RqfC=rc$8KU(8djY^#AgHzm(W$Z*;KQgVIa&r8=@P#q3WkHTj_z_%q6Okq( z>h|`^{XzD1|Fj!JIC4Yt8i~@Ut2njfP*A5Ya>0aYAyB+7;qq(Q0jYbnz%W#eT9!uqRWpLomnnIi@qprvAF*J+ijg61Qa%?Wl>7h3U>HC@krrAPvBv@5Q`mR)| zmN}0I&h~z|c&o!qn6KAAKC!CtgNS_08EqLwr!ZRetm$0U2#gAe2xs{ycJv+oW@>eg znaKDik*i|}7jNcjdoHK-_W;F0Q4Lx1wVK}P=km;aN!Mh}^(k}7m2}y16egbNjNxkC z>14O*X@-6AZNiJm58ty^Mr~yw9A)8KlOOc9H}VZf*Qcljo~rM>SvMVQlBF7%n0v@z zf*6~n(^ZaeW5Q(seW4OwzElnvi4Vvliq(gZG)$`uQ126}Kmv)g~`T_Ea0xvsYcP z{lWpd8ApTSOPD{7yT?CkEBf~8@9(8gFs%t9>$pXh#aKo?8!tfANWFsD8@{wdeP~|Z z`#ey^MF{chE+ABN1Qz}$^I}SB-0jF{zrCxr7@sIT>ZVMixJt72^P~5=?lP4_2rVB> zgr3V!Ne&))b8I|yrER|dMR`ue+gVaWudxcl7h7CaTdG^Y-JvdLNgvOe=zjbG6sWZ{ z1s*+qZ%o-xUdXs6NYnI^=z5?Bs9!9HGMnw@n9>jAEZxZ@ok&7PG0Da$SzavuAm|_Yu_QoNYWaZ&x}T1jB*~ztm~1=iq4sxD|AE~n{9~68su5KkJ(OE_(pETi zo?B-m*cu;LfSKf<+&VdXDMl+_2m4L3ze(CQ6qRqitCAjcHxv!q@oz2h-bk1 zvb%!$v0Hh=r(pIJ5`~==cyWp)dLlyJOjR4+Fm!ZEfj#ulCTyj=>2(LNZT#4c_ZIgT zE9N$WFK>I1r!#c+9N*-NN4`??mYa37QK4FMM67u|?k*p^#Ws)rGqp1Mq8wxaV_3^^ ze`-0{WNSD4Q!spyIyO3q0y$&9qqc{{S6~rj6+87qDD!N!Gp(lU2{?|WIUy8BhwN4o z?vXyvCF{{wx1UW$tGZGc8={wq);~VDElMe%i)QR!_SL zaeW@=zpFqK$abOqMB*nMlei{aB7JBTdX&QPi?HXi)op_}@J!H8K~op1LXHsCQ5`d! zz3e)>v)UA@Q^S@Go2KI28%Ya}m!u|31Wn=`_}YEHf_oIs>lDc+liSk(m3L?$)b!L9)B4kI&UB<#&yOs>cCvPLE~8Kylpr!@RdawUL=_TL zwH!~(Xd)&#Ets`iyf21LDaHF{X03b=KC0@><0|~)i`2I_>aJpKb2_TR_VAAvgrcbG zOhoTrkS@di@kiKCm${m!rc*5d-T3qU43)Ph%Z&!mdmJWW;!&5Nvt59uIm865cB)Cg=8=<|9^bc+%bP z+Q|GLWPJrxRZX}yAPA_mq=a+{h;$<{wjYNco9TCADfE6-Gee- zRg!M5I|Ew717u)Kq-}K)5xu64QambeM|3=i&>(K4awzCW?@;hQ(v!aa9V8erK?=DN zF0He5qO8LUmUJES@74%8?ebMhE_83#(#R3NQGs`6jhL~iB7SGgRMzD^a)-U^^&M2> zt@jJ5Oz~N~D8ACtpeFy(ImehaISSnf_g6>6X`TC;p~sjgu^`@$rT`~46iL!JZ)hz+ zj|N`Tc{y5j=6yTB>Z%ep5~;7h*E-lG@dhNHOo%vVWwC~GEw33~ zH)kqGDd*BK3ZvOPsPMC_7ja2Yx?>xs&Y~~xl;j@4pn%;vTub2Pg6oi9nPGQAXp{Aq zve212VNaKmW}e3a4v(?&8!*I#=u~T0$aoCjg=9W-%nLl5BmHEA_0mC0lhZm1$-V`; zCwMTDl-9_hA|&G_7K)-IbYJhVzRc6VA4D7s*@Y9-LLHGwPd)c{w{Pgcdw`F2JF*=W z0_Q3GXao3}!>vt{Gqqn29`t>)ZcMBGl2ZpIoRk#2waJ~a|8!Tq>!EABiG87Kgbsj4 zS?zUivD6x^PH!DGRULMxU0jstJ^wE+Z_iG-6DAl7(fs8oaR4yH&(YLkAlH}D!k(;$w=|#D6{C@_P%Bl%_!I97}wPe@z$riNWp_d zkf`?XX$zPwCJ(Q>4A5&|k1eUee<-E!({VnD?q1FnBh~y*2Cwu&+N!BJD69wUUXkeR<|vYK=Qn`_Qh~y-r$;rw~2%R=Ryyo{z|={?^Oe zGrVq#RAMf>#z!75>DMO{x;m6wSoh!Sl2NV4@kDyNVxm+78$H@Hup&wav(dn!3qW#j zX_rg7w7t?oW_G#UWN$5^lkMkt$gdOp=>xS}n4n>KKv75_|Y373pY zh%{{Gv}LSBYT%uK2G+}CUZ$11DZYmXbfruYX#}X)TA>Xxp^xcGQJLGTg>6Il{1*`E z=oRSDku;K*P?csi3+3GP%4ipQ4F}yX&MR8d~u|-<)E2^u51@kL(^zQjv$tb7$g2GKxC)frRS;%=L zdh7n@(jJ>no^6Vi+xN&54LYVLzQ|)utkmxZAVFqPjUPz@<{mT>SETZY_ZLMB!!alA zti1FT&I6Nm91{e_g1Qj}3gY4dQYx@~Qa^wz?9z~KEC&7-p53X7d#c{6Xy(hy%1d^$ zBf7=bPy_@GS~!7%6B4+qY6&eeFNH#gWEa5iu3yTQw+Bj5nBE#h4TLDeG)Z zs3j$lr}tIj_`hU}-{w7ZizDzBI3?qXWHK5|~H)7v@tOr4mIg!#7w zZQ1f*J=)}nt+AeA-inQtUt*buO0t`@$Y5bOSb5pRNNmJ@XII$zG}%MPB81{<&J#{Vf|^VcRa?*551 zdbyokvDPZ4p53vu)`Epkxm?-5bR!vMZ@v!V_+uijj77agO`7Det2k0h>}zse97}WVoe^s7Am%0=R+{|1Tl4O3qz2K)|7vY();%<2ZrZyco*Z&R~)ydjx@B-*E|k> zN=5Zy%TqOtRKSI5t;3X&+ta_)CZasbQ!+TRah`K`o+>A=i`u(v1%n%lF5?5Q2F1bU z2pg}P!FE$dYRlSa^sEIVG<*CF@#z+dFhn96rb{Y?w0Bz9v~ol|>dnDnA9<6KYONza zJwWkky+aj-EL~7}J({9~6F?ORUXS^a2Kg+yRqNV=O;) zV)|j%c*m9^$uq0@P|igsp_-hx%YotzoJI=-8h7jWFEnmNF6dO}v>r1wZ8W>=Q3!Yv z6S96Hn_-&2M)uUcG(Y5FrbX~Vog1!<+i_`iv|sO$D;UYm^&YsDdFlHy)ALQYl-|vr zV{ESg;bvgR`hK!@a)C)O1ymN>_)g%OWy(X-*-Va-HXL(e$-RJd!t6DjR@7Xh8)fs_ zf6ByeArI4Tg}gSt%!H9VS9|oC?j0?%lha__Y@XAQs(MQ%Y8%a9p<&VLwgu$&+r{Si ziKZPCaW+9P)Fb~*Q_@nFm-M~c-rWayOs@t~`D0QZp7RE3Tb_t!i@x$W)p(HtKM^J;0-X#Z@+&&UmMTYs&t`gC*sfh;Np$bT#OhHyP!TM8JIp+Jl8vj(X^M z{`|re8~zbl+Dm$J$LET9dNmkn3>^wWCg9F(AvY^j6* zyi#@Hi-)WDy2stuwjp>BsE5>!$IUs2p$afAlYr1PN@ephf({8aJWt$%7 zcHi7Fhqo`=eh-{{0M5f2QLfiFmq+9;`#D!>x>zi(k7l4I<8t~J3&5D{?ewh-=L2Ox z1`>GBDYmF*HO?342HmWym+AEZZ)K*{BQvhcyHmh~cNuTjj1kjmGr$!byCv2CR8QFW;SQ!E4A$`{knu?(w|PRRHAqv zA|X~u{3Y~Nht=*lg?(Xi&yUrDyt?Zq1~tLN|KJeD@ivM5eX_7N9t5_pj~rjy@fj^9 zOuBoX4X;&y+WdMG2)*vMUsV4{Z%^|>^*M{<&XD4VnF$tMJBkH2;ytemKU{~SE~$Fg zlU$U0acmHI2GxRRj`7J@(KIqdB|`GHtt{CXxAs9gEgo|I*~%AV{6)&2`b$iwCO4}* zZtd!BcML1@lO#D}_^^oQ+!O1o_n0F`av=CyLNAW8J%Nw9p1t8a>o(5BND=b%{nqxa zK&@MUv{9R_P$di7Q@4^(yUFGC-tl?3!Ak0k_HFNciCUG?bN%(us?8ClIRw`N&j}|L z4LkiomYNN*)?+^Bt>l$w6%yRx{IREy09iu1w8ul4+6gr(pDm=ayv1!Oo)eJ}6~?v> zebT&l=Wd_3n|S|W*0;|5(eXs1|HGfSZ7C<-wNX>)hsh|h(INsIAfy0(h-24yGBXSg zN(HWLJAs0`BHl0SPfN5bzg_{71E}o353N|rpUJr+ynrCzqIzNS;lJ2=`(zY{ZOhlj^}9snnaeQ-V=1M?P1Wz*I1 z4{#aZ$*P*%L-_T)A;Q-T->T*7XrHK)HJXYem10hQX!h8kfgT!;PwMEwQkw%N9Q+ud z4wYZ3%4|)*ODADCw_$WVRAoeX@IU^D7IH98y!h>>TXjM64Z1ggMLX-}{4m^BZod|8 z-9xp}fa{;Fde^T+uX-c$7ILcf)3kGF*U9?HBRJR>lwiADU*c)I*c>eAMH0qU|A2E- zDq_TFD0vBnAsS_+fsrXO0k8xMn(cZ<;D!v2@f(bp#DGmdT@axr7v@=_1vqWp2X1+W z@sczs+;NTL5)s*iTa8dR)_F(d?1Corwg#Kt_w^Iv+=GpYdH%xJ{_Heh&fC+mwTri9 zV4Q(*L#m{df2Za?P)u`$&ct8D(E$UA4aTg^8f=h&qYZ^2`a&>v_*|k)CZ&GB@s&QE z{K6!JC%+cI^=?;xm*a__DzywM+m4U0Y}DRR>wB7kRIy;KDEaT`@fg3z<*SCe^QHW| z(DQ>`E+H;0JME$%5G>Unl}{4^rk1=UF@TAMk1H4o`z0jFIWof;^E?^{J{6+x{cGeK zoN^9Wbb2+f{23{_cAs`YpCsl>eQTt!c+Uf6i~m|PywQik&AI4ZLsnF>UEVUNRi&f# zV=E*4TDF4p)2HV+yY*E|%R#&?b&Qr-<8Fg7X4>kLhUQ>=OH1+RWH9o)B7ooMrt=z% zL7kQ{K|_!R(_x_Ycl$*XGDHhS!0=YV(}+xU`FS2a>!D)YA|t63RYgyg-^2XkE9O(T zgDqZd4+oct|E2`1tNZV{DQ)w?oxcAi_h2yuqHu06LWrQ*tb8bz!f=Scf2!nL@lIgQ zrHYZ(V*4kXX3{mA^jh&=Aw{Jg2uJ=toEX=-+0fv(-%>wi;LzkR)64rGi37TTDRSrs z`rn!bZk;3s%gCqpO|7ci4Mz3-`|KnImbK?K%bxV@KPw*)cw@#e6@q+ zHY_-SR(b#LUFGOt1p{@W5DjA%eU*QYUQy{4xnavAp4O!;C7*mJ5uja>yX!0erv8Js zpqD-Iw`%{IAPud}=Fm{N=D=;r^Ur(MIyCz;@_Zjst493Ws$Tx3d9etb+mZdwV9cAJ zAq0>AFhaOh9(A%1Cgmws`CoE(SEmIe^^t3PPNW^AGGC17q5m&tB)5)NK14w17D`i1 z5s#Fet4nc05irer7-;&B+iL!B(3SPc4BC>E`JRp72m9aZqlhrix*dO zpEj+v##xz)35o3g5~mSNm&04El={l!A9@&R-*Pt<1)78*g~m*8QU86}1+bDGeRyue z9#`lB|3=Oa^kLeDnP=m)?s%80lkEh0!~OzldlhHJ4AF-^w7C=vyB_0Zx)>sBc{G*_XSZn-msU332b=lqCSi7b_ z4D85p<0%^CPelA9EkI^y$jc8$cMi(mO=ve2?qP691u|?Qx+#f=qALGWfnJcUzObqq zj4_&a+93ElvamR!g*wdAia(K?k1g=JIg9UlQ)jj|>Z)Q!4vt>^CmhSD{2q?)Z#`X1 z1~4(wk~+>Z_u>a*wsdsWkpIr(zvsZP9Ui|=4NJo^`=?VT2|gvi2<2cMF*{Y_6|kcI zMx}S2+h1DlgZe*_f2zmsdvAiXF~KgPns`JnqiiT8zmXi8{R5w1?VpNK1%X)f`W%qT z_yf_uN2jRuwqat+F!!lthsdMOE#r;9Rx(b(q;%r_zf*%l2*5%?N6Yq99oHdMFgL?$ z(=aRJgl330aAke5U~(O}FE^^9L00=5btCzQ$eFo_L0Th1nFl30<|bWy3W zh{GOf$+{fbfz7t+(noyO9geJ4~r3VldKN2YTDjwMVZ?1>fK(q0qwBX&8co;{2 zxjGny2hri(2Z8NSxbPxwsn#^fiWW#ACP`7Tq6g>i{$+vhIqC=hz~OlNV-bn5lt2Z! z!)0uabXr_L5stsNz{>&4!S3-fgBHy=0Po+ZThw|iFbZwWzW??ac+*@%h^~upRPQSB zYb%ETJ%S6WmWK<~%GVzAmPdzBLQ-o(6%fCW{+CE;sC&g3NVtZf#b`w?VuTKC+B;neaU z{N++QIJY+@fQ1#Bz1Ta5@J~dW5O>5OOYntmXXzF{t5WW*txD?_%lOLX{5K+eZyOEg zpT6kR+MKk+ygf_RP2%b@kg$IK0>oU|ZyOb!{QIsHF#m8})eWiTpBdDDr{&{@G8qHo zu-mD2a=eS>ZqoPmp-s!I=q4?8J!aW|Y=Q%h_S)0UNiF|wn@Xo|0_k4VTNpi-{ipti z)>I(Ex~><25DlO+{hQgSXlp7gxf!k8Z#D_8wQe!IV2d;R#ASdgMmgA()F%6vgT8bK zT&c-u4TsQ~&l#zL+O6%}fF>QbqKtpqO_A^~1>g`>g!Kk;T1b%t1$u?PoQ6Oco_8|P2tzcG`0gY9 z6}ECLFe<7p%0~AGHnCqZ(J#AyD$_a#TgHxa86p31ftRPGv0RL|SVeK17MfMq{eMJw zs+Wh@KR-`)ebCoe(Z+8i|5+Kw!f0wRhO_c_>qY)@ zLzOMEFGlE_g#^l!%GG(DYgK4AF;aYd3Q(TumiskIx+_O#OPLI8&c;%_exU4FQtx%& znM{_C^Ws56;b^g}{hQt>%VMqK2TdNgN*<*AMwlQ^88W%jZFZ503w;wc&7xBq^yAI5 zqWqSI+C8PF2Pdob=FX)V-`XEa_V4@xe`UfENAyUgD=}yI0+vJaO>?g*{m4jC52{dsE*(6g7~&MAb!{Y z7?laJB907LV0LQA1r|N=&}|qhJCtP#91O!ciah@6J8Ckx4nLPW#I(%U*5BYtw*VzLE zLhr5Szc3BH&VLi7aWnXWoM7k;xv#G_a@0JY5Q9QCoaJQYZRMNYCywcCodYPeQDeNhUfsLA=O%^`D`H${iiB^|}V`RhVwW<*$J?Ol7y|B+zl6 z4zR59Tt?5BpKtUM<*{2&_dq&UOaSjXHcm|*;stjlvuLQrgC%Xcib9f7>D^?xK_uKp zAOZmjQe*I_h=c(1)sx#PVZPhb9AsxQ1qB66!-2Sz5Km>5;TUZ?k0M!LF0dg`UPWYr3R_gsC&pdg>X_kNnTOK4Pt#vV z&6U1&=XGByemasVYEQH)Ky>xJ7ZFxM_T+_PMWHO&QE)fzqd1z6$@RYL9UU5N^t5Li zcrg3k#}464M=N(50=YuV=jl^+WgJxjA&n9$^=JhZY!xP=zgH1EfsTrvp|k5g6m=tM zV7aohBb^)O^1fbefjW(Yq_kv(b5zDf5toZSR1j} z|7o{6YO18%(M!15-^cfz_Q#j_u00*E|<(cug zsFRg2`;E&L!4Y_Ry3O}oIYdi`+Wrm1sLxf;0yB|YAN!_yg#})JETSd*Ke2W=YIsD8 z?%=1)Y|SfTn3_*c1#k8jy;nH8WE%6=l(OUyF)klS~|U z?dz4a1B#s;B&+uzF~*S0`+YJ&bXAn*z#iX5**sg=>m($(e^|XL-`%49Y&yV*$hvzxr!%f~?TWnR3 zDu#yI{S57Cr{Jc`jjxnHXk2^N=>JPFn6DaZ?pPkvXH;sUj;m>JKM_Od94$EAG z7v19Rj%3S-`e!!{I=krph~rW%9$92{wmQrzicSI5->hdpKP&tUnOABk-KDc1@6~-l zKIL3L=vl!5OY+yzir-|`AKYgK+w)2eHA`G%RS@CrF>wMk~IInHx6l^&vpW(W(@ zw6X|ZIquB4ZnIAs#I{WpBorNx`T(cwzmCeItrez(h;M|-RvoUWThNBeZSg)Fa)MXQ_)vX!Cr zhxV&|Qkodzyt2>V!waEfZW1ps;26utuFWXiP7Nj;)CbIEHJO4!r)@+Gl_~b6`Bhr1 zs@q~8buP}!J~xgm|L9>yP<%}B!0Su5i~G^-sW}{+9!f6!hk*G&%)x&gY~>DCB`)sZ+@_?zl?)cFw-Bz-X2ZV~(542im^7 zUAx(SjBgiL-7_)NUlOH%BLk|ap2*!e=T$2kZPaV`mRZ7s_k~-D5joQ?1fFv9Q+-ms zL1Ov;VhGaeUYI)D(qdOx2) zHw~Ed)zf?6mzqKLBnbClj$gu=qI~UG%8RqW9q*$&Z*M2^g@ZrF+@UX*6W+oL310Te zM$cmtA&U*EnN7=JA0k-9X<;g<{+2nW_G}ZJ?qHO54Bn8E8y@$!2 z&JKxM9~Drk zh3voLuy(GF)$a39vPW+n(@Fk$l`I*Tc1U!EEPLocuwKVGVUjF^2!PwAMN(}UEMh(iQVq=`9}O?c>heegYX#ch7{Y}v!Uvjuq^DGVf2r|7@Ds^ z@?jtcLj@P%Y_RZ|^5?kq7=E${M1JEbEsfKw7pzUh!}hz54dWr}n}abDbJv5t#fYPs zu-s|fQlBgZa=cxev7ow2v?8lJ`BTs+Ouf)wIA%+3^Ky0jTk@nl^T27U+_49RZsJks zU`30IV$2)4@9X#jItu&fO`F3PJxlh5>;+;3F0-iCHwhir0(_+G=JR2njRr(Me)gc& zx$bVhh4BTvkHU8$GMhp5B1`GRKjC_-aN|% zD4Lhj`Gv1GrLIw)Y)W(u@yxTe?$xEbPzr3n(&DK@eB7(t#B-A{oUi9^mrYMTb7XoF z(j$GPpyz<+leAgHZ@F%bkXia3yt%TAq!SXX+>LFdpt$pXnwsYe9Fw)(5% zxUp1H*t}jF+>Tb`;#Ph1>=JT>)Wi@GKT)yC@fOuPChr!H5i(da<&X!M$8g@UZu6vs z$q{1IEY0n`VZj!vts%Ss;12-90bQ3%-0uU)Vt@)lH|BbN3dr?f_i~_J~jPun^0S;u5qre)nm1 z1=CjOiqROj9`7TZyV3%$_qdu3?;4xQGJdZX0~BYqF)d}~gymqlYVI6~)6{${iKmVV z)94G!7q$5ceokf&axEnh8E^}pwS?WB9U@)i)bxM2#NC$3`?4Pr>G)|hr@+1`OWpZ+ zO3qBH8r7DLl9?^xTAE`^EWmQYnL;n}I}qZ+3_P&D4Cm6_RB0v5_qUG-bUyE$ZUsK6qUtN{FJP&P8ueHTU>96ZGB{t98BAL zJhgw;+NZ5h4N2beJFgNKYGr*sw*W#D+(x@`QZ$k2*^F>O=X^V}v=0)7qmLVuX`Jik zd3(u^Svm}Ou~dq6qSkIM+}Q(K!mj+|d$k!#4^14Q`F&PK`qnNdvt= zyaX|3yNT<}`ARJkw@cj>(UBVMslfx(w)Xi;dBmB3w)!piAuH{cxPX(0;-1y6xVCSb zVnVgn8T$So&Pu#^8qdmYga_kp@K3QM)1PM(EQ#5?k)|DzmuCE$1A2)`O&0Owq8ZF6 zREe#Dtz1=iTT|<8{Km$wY>YQr=|IdQ@m4eo5me_u&N?QlI~Ot-lRFX98^>W&`(}34 z`^v_R)yZb{Zj1$#MHRF!E@W_ z@1w@AtS{gQq1jkmg6ynM;HzOBy+q5RN!3mq&YS+uL4_Bzqwv08P%L=3=7na+=>}d!B0+?^d4Z=WY18h>f(0Tk;FSe|eYid% zX(tfqmTv|ngy@vcmLHdKD~bG(AVN6#sW%v-(VR6Musawtu8X`OwO!^rZuHx`m`)GS zUd(T!GYqkb7;hgYm1>DDe73SRCd(GIAb|#8s zF5mr3e=kK12M2HmVlP$l;`)?nbH-?VeK|yZ+WPSaltdXsw5`H1?Q`qy_YK7 zjpdUw_=dY3SR-_(5N-n{Ppr+l(BWFjk{Q4C*wcBUH+?spC`eh#=EaXlUtl{_y)>93 z`&^00r?SEr8wpTYd-&i*UVP7}MuUCYmLhk!LQeOmSSS~iH1k}s~(;a9&gkZ zMD;&Yw|__7vfR96?Mdrr!BZKvUqLc-8S#|e2Hes4#3-fK9*?4%^Xhs#V z1g-u2JELtmHizG;oQ&UHK6;En~ zlBPx2=w`X|LmP@A4mK~GVArU2h?}98g)B(0{B7PLBz znW~=S{KjBRdkYpc;2%~(n^P`_gqrVyWC}*Ib43#TQQ@|Ro}--6Q)CJcUBiu@)SCskL? zev>1g_9XUpIdfmE^FEh;>L81uz^3x%Thx;iKaryW%cR?BJ1&RS&Yt%JK7PpBacVZz&An_dEShE{_~7!E*|M0 z1w`0eV$ZYJ6{ebP8weP6zk0BrI9vbFd|Gzy@$OD>YwE=dzkB$i6j)jpXAd}v#R^Ui;>&XbVQ z3dv5ByR}drY-si{AKBK&B#_<|1k*GM{jQV$_> z+f#zKm5Ar7djRm#ejTALB;Bn{70IdUi45;W&c9mQJ=DCOa1&n084uY$mO0lrF%RZN zwWUzwH%9Fv5E6y9cfNr*Im4$^$*-w{1!PS#GIxT4c5=TONX>%C(A9_%AJc9gm{ zoUZPf#LsuRKlY!kA~=2)+vRlc(p@FF0OOVKc)Cyv{#o_nNR9OJRkdAKEA9>!-BqhjBNm=%0n>U{3#uQ7*C^3 zlvM3NQz7c;f#mi=P~?1$82FjKmt*XnB9Np2O1EH;2*1Nvy2A)aO{TQQ0|>1sCAk|9 zyFM~=lbOLoeI{F~dm7io@elK`1YNg3aQ~Ob2wJN1#|mvw#L;4ykX+tm zhLGl+pSr%u5k7|Q1!pyWQF%)cR!)y_uK}H+g()hpgQwG@z%D1VZ+}9H4M5Mj8f7CQ zBG?xvP)EyM?;U<5dt}FbCbP~GJh0@(+1S9s8EEl=HgJnz0ow)7aGez@i(AFuOK@_FJP0IIF@ z`bZ~aG;bMVmvsMDkQ@{m5E=kZ=X0g_a(p;-e3X6|S-rEx(fedS-?H{qvDW$UWOhFd zXEUZL(n)(C7c|ELBIt-j;xw>y38OnJiUn$hZg;0mi3XQpn}*Xhq*D~)l;`|sg^f?$ zymzMRWn8L{lI#}~pgq}L@XzE87+JG;4n#Mxa3q5gPWkg}MLUN&UVHb*;g@LBDQO2) z{{|7-vQnjdP5I=UZ#za~#}M|j$4HSQ((H&J+1+U*Np8s=R1Uh%Hqa|t9?^s7;#pGD ztL*iK7w>j3w+2g^eg%>QvJ0NAjJ!6MH(75}D=Ud+2+*Fv3cVDG96~5CoL2>HXFBaj zG!OJ$OaG0@*{Q(NTx=Idl#RpdB>6{fO2@Shq4#`JtGMmnBZHumfRKm)V3S~Y@?ob` z$d)AbLYjT8mv%vCE9p`>o_FU!FHaM5LZYJg1pv2TalK@h!i2AeHP)$2-18d_zwZj% z+s519*;?8uv=V)rGKNyDSrGxMlDUK%LWM$(o3lJqTIZ_{Z9l?;+TLzGSdWG_eY_y4u-hQ;|ya++K4yCT_~rdS4Ka}_(|yPrpK%!I#-K> zXzVe(?gWP9Kw3;4ZPvB_@kq&b?aC|U`qRGv)#Zh~_okh>T^_Hc#jV~;qivpsPam=K zNQ-pH5>iK~($hVso4StF;(3-#rt`ZacVXdu;9Oc-3f<{uyWo>QS#Y-q+@X4lPV1NE zwZXXFxCF%@CQNs_hjCddePLoxOqmm;#*z~<$kOY$Ec8UMlDhQajzdOE%Ouc#llEsw zp}3iI@7;rYF7Gy029gsPRhzZ+1%5_}scvzUNY2?)P@>qdjtBcp%MMY^v}RatJ~5P>Psh3)ZF)lr-fT`$ zP{c>u?cJ&`nxq2_m(I_KC;(l+q>${3E8kRhA%l`gSddu&yOApuImqkC zxfE-EUlIDzAo^6qC*_`5!Xai}Q_q`cGNBVpmlj(SRK~s;O##996&1K%UkCb;n*yN` zcvEyR)g3HyoaMBOy?0~VqslBvCghO`;LnWA$K8i$?&A;1vlx?&GG49*4CQWfSU%z~ zpLjM^O&gh}ki`(5b1Nv(&`*)yA^4lk0tUqIQ;e_4tyYm=gHETaZjt_)7%@uI0luXr{2au3dVZI2hZO|VLf)B=3I|a zmV^XvJh;r64{C-kLw=0p;Pzz0Z&tEQtuvVY~OM0eCcs51ht$`o5T+w7_P=&xHGqPm0fP zv-ZWnJ`sW^3`qdBk{v9pk^W>u>Y?!XIVP;Y4$?@zm3Ptt-L;ev?1R;YT=ljm62;E6 zUSxyLYPwsajF-o{8&cu(y(N0hc(4L!ae2j#X;*jijU6|ym{y7EQs<R6dtc6E1ej5#}-$ay5rT{Mx-N!Pt2D30*A?< zl9CG7GPxdvfSgm;)Csjon z?L-!aM1B02X&M=tZRl!l%lT_nI1DxE=H6>1k=kr;LQN|GmLrs7$p%rwWwwu_BxXKF%s_1+pi7ChO3M zXzdU6_IQB$u!M88Z9RKcqW#t1V40E`1Wl7q)?l$SdZFzZs(`8boCYbH+pw1^8Td8P zPih1}lNd$syzK!fa3|>{L?ga7UUDQF)}xJgu7|H-;aQE@-kqjhH^;Z!w-RPkK>|nC zCQVqqOkmN<4g1Izo#voF)7~zw$ftq;lK8M4z7WompH$7HOB{)y-3F&-L~*dNcdt-2 zqq)m-y3jy|h{G`Uql8GU2JOPxK3UpuPS=~Nn_AC3vlRcPDNw8{V>Bi^4oIzvbo`vB zE&|c80)~>QQ)8R*C1mOj9k}5;jw-{4AcJ#t+_49}`FKsP4XK`r@yk~lBcO3Qcp1Uf zdMnj@^Mgm}JoQr7Ib8bIO4wZm2}~|o$TM|n+*%R~3ya;VgWGJX9#zlR=PTm%&5igc zQFrvA!MV1e5j~q9{@Qs?O1ZrsAMIhF#bOZ%=&@U#n^We1g02(sPkDN!9EEYrYT;Hm z1kC|v6dN1#GWM!*>pV1*fI`k!KvwSKi&HBDG)cFJ#?MEf67dX}6~`~;mMR-kC&k>* zY`5{@@n-;)T8&#VZ~^c<#}-isr)2VbQ~p5%z4{$c8or%(j38E z)E+a4BK^3xH=gZ@C0}D4XoC14BR--i*I$%QIy#Ouu`C=d6tR!LGg1)BFHUX+`yjfq zifR1=K15Gcv5ppNv#o|J;vhc8M0tRgZn=)<&sQh*j&QSLA=r;5UmqEm%3Zyv9S2ZQ zbE>!DWVZxlWw{R;LLbd7LH_Jm3TDnbF@{&{ms;ijx_qAp%-rh+FlRu^oYL$UG|{|a z7V)%SXm)J1T{+L0<3X+bPAYdCs+$=8Q(@>{A0My%Qkbw}y@#nCB%+sa5gy)JlpxVaTplj6izkRM|{$A#f`C{A_Mm&uf9J?&VK z%WRZSLcJ{B1KL^^zn?OK7bvK~t!@+m5@8Hv+I3BX2+K`rwg^q2VDF?l-Vv0AD9Tc8 zGiaGw030&jxHJc=x)iwJ^qg5MeNEc^jpJ;sIXBahI%6kJ+ldWy7!2zXjm`2F8gJtn zN*%Rr){LwD5SL*yIjDc$KYI_uP>s_b?A*LB$EkI`=9)RKwHQxBGtz@$EY`9^n*pTk z5j9XeI#v*bOvC=wU~aJW5!9}e=q>?R@D&Qc_I?-?)x<1k0*{D+rhkcmsdMGw>9%`? zOO5o!=iL6Zu05Ja)PK0gNn&5*vNELGtA&G;4APeP^1AM~B1YuktMQ6A{sMQ|Wk6xw zT?Rv-pot@}{SVUX*Q}x(K*Z6nog3Ahw@X|_;W!LM>sc#!e|}7)Zl8$y2iv#y)MjV= z6l4EJ*OD0EFD31vk%E>mtkZ5On$cgF=BXGE^VByzRra80&jf+~{ksNOCSmmvP{XaV z*6&V`)2o6|QN$aKYQ5v^?ir<|FU}7CK&a_P2xJKJgUyWQHzXt^ZWt$t)hc~8-)g>q z++5%JA1ajqEN58FZp#b+QA$rfaQt166rfq!NC1v6wR8H9&ZDUz>QKhIRQl*@kr1uT zkov+It;Byr=^-H0rrkYpxEQLKSd4f{E$Q~mMcr3$HzW8viThyjVPu5KC`$mOD}kN(rB3F) z@kI7E8`XeLu|ugwclgfzzJI7>6|set-4yGz7H=2wr=xf7WtR_i3cJ`7_3wjkH}VN# z{!7AsK&q(=1PzzV!5F;z5sQCGhz3z0tVL*S}%&D1#Kp$#cw4(ox%jB<~Jk>)6uuCD#qm8~k zE-ZAczJRe**Fp5$F#0KYFdUy6l`Phu;bdW|CiZkljvdf>67vsF(@DUmjxvs9lrZDK zctB0diFDgic`G3!nHV91xiDQ&<7EFIkSGVhs;1^3vy=%uM0T;zr!|4Uwy*{OJRCRXz)h~eH_7UG|7C)DA@g&0t8ZeU2F1trdrCqkI>Of zR1bZwCYd(eHd&yPF(rg@LV&DB_bNlJ+wu_r)@m44Cy&DjaLTAgj zgue#$;RGMe>}N|%IiT4bn+6tt#QV7bAjAl(KaMX9b4rQ&3ssM|Ajrf0EZ@30jQafp z!`tz`?xCMubAylm&`P*98u8mV(aj&rIKE)PwlXU_oRkM`F^ez=u%;b4!9%dH3go{M z=>?P2{iy=eHLwXrLv9k`m+Yi-0Mf3h+k*B_Afp>cbnD}J!DZ2JA_1%Ny3eW>ROh8x zD7C_OQ2$3YdZnInvnz08%y`-kb^rWnc=SB-r$H|d>E(U+955ije+m3qR5>$P)b>&5 zhAq&&?|XkS!B?;VZO+jT#}@`Dr9^$D?-~0^X1K;GINGr1V~x$yvw~xy|1GSE(XDN& zTpM#eGPl0uw;1Ak*e4Q@T^WzEDwY#is5SxI6SrKP1!5M0Ds`}1SRjIG2NNyUmdg}JdiGF=ss~ccXNoT4ga3hWv1`d=cFi3gd7`>vZ>iYL^kv35~ zBiSfQsmm30dhMfUR5`-WGbLfW>V7j01?5(ZMUX!ef1hl4r1lI!D_P45`2&*Gg9wzw z{ny4rl$Z$cuziIpBWB<5em|oLbJmQEzOHJ1fGX5<29JLKy#(A=Re~D`41Wo-k-<~n zL(TdX2PfNO*_GIAafN(u`tFKW9?=ks;19~a?r)@5cFgo5^U?npyQ!y`#6)yFkN7j! zqeogR&oKOQ2FOeMOY;y!?~UK*`*7jr+TW0ehFz|-4HjJ8d{0m4x9(MdkKxpTPG-O# zf$+&+;J3)c!#GGk+!20{7Ge2IMqD`de~ms5scdbn7gk<{-xIx-$(D=1cpnpE|6|3< zRBI+Cn+AZsSN4*klT)yN<|8HI{gJT5c;Q!Sn4XV3WXSyHhOZ!H*>|2K*ra%ur|q4c zd!_wS1L6vk^S-|OHPVROQ*f-tpQuYWuKdopVaZOFV|AtF4}SB` z1!e&&D~)8hvD9J&J2?4#!7*QI+)<)pFKfm242io6-nodW%`FW2gvrY@zm?Kf4#<{G zp5k%*92QKO{juD@H#95sTz7U}e%LtS&YSb<9qZp6VsuY2iST5}hrQn}l0T-n=}c>{ zzPsyN+B@mYgWtcW8|a1Nv5k#~x3#ggbK2WIbYb*LY~$m?0tRhLa&l={KHFW0-=YE% zB0B&RD;;aX!NFuu*bz+sb#3 Date: Tue, 12 Sep 2023 01:50:28 +0200 Subject: [PATCH 003/117] merged the two classified documentation pages into a single one --- docs/src/understand/classified-domains.md | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/docs/src/understand/classified-domains.md b/docs/src/understand/classified-domains.md index 5d27945abbb..35dae72c43f 100644 --- a/docs/src/understand/classified-domains.md +++ b/docs/src/understand/classified-domains.md @@ -1,5 +1,7 @@ # Classified Domains +(classified-domains)= + As a backend administrator, if you want to control which other backends (identified by their domain) are "classified", change the following `galley` configuration in the `value.yaml.gotmpl` file of the wire-server chart: @@ -14,10 +16,14 @@ galley: classifiedDomains: status: enabled config: - domains: ["domain-that-is-classified.link"] + domains: ["domain-that-is-classified.link", "some-other-classified-domain.link"] ... ``` +Note that when enabling this feature, it is important to provide your own domain too in the list of domains. + +In the example above, "domain-that-is-classified.link" and "some-other-classified-domain.link" are your domains. + This is not only a `backend` configuration, but also a `team` configuration/feature. This means that different combinations of configurations will have different results. @@ -38,3 +44,12 @@ The table assumes the following: - When backend level config says that this feature is enabled, it is illegal to not specify domains at the backend level. - When backend level config says that this feature is disabled, the list of domains is ignored. - When team level feature is disabled, the accompanying domains are ignored. + +To disable, either omit the entry entirely (it is disabled by default), or provide the following: + +```yaml + classifiedDomains: + status: disabled + config: + domains: [] +``` From 028858259dba022017669d80deca767ba1c94d24 Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Tue, 12 Sep 2023 01:52:33 +0200 Subject: [PATCH 004/117] replaced dev docs with a link to admin docs --- .../src/developer/reference/config-options.md | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index d92d461479b..01bbc743419 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -197,26 +197,7 @@ Individual teams can overwrite the default setting. ### Classified domains -To enable classified domains, the following needs to be in galley.yaml or wire-server/values.yaml under `settings` / `featureFlags`: - -```yaml -classifiedDomains: - status: enabled - config: - domains: ["example.com", "example2.com"] -``` - -Note that when enabling this feature, it is important to provide your own domain -too in the list of domains. In the example above, `example.com` or `example2.com` is your domain. - -To disable, either omit the entry entirely (it is disabled by default), or provide the following: - -```yaml -classifiedDomains: - status: disabled - config: - domains: [] -``` +To enable classified domains, see the documentation on classified domains: {ref}`classified-domains` ### Conference Calling From c0d87e75c88151b5aa5194ccdaac886fd7184da6 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Tue, 13 Feb 2024 15:37:49 +0100 Subject: [PATCH 005/117] Merge branch 'q1-2024' into develop (#3888) * Block changes to some user data in mlsE2EId teams (WPB-6189) - Integration tests - block changes in the backend. - lie about managed_by in `GET /self`, but only there. * Revert "Block changes to some user data in mlsE2EId teams (WPB-6189)" This reverts commit c71564245153ccfcfee6e4e43f24b11f88e5d5f5. * Block changes to some user data in mlsE2EId teams (WPB-6189) (#3833) - Integration tests - block changes in the backend. - lie about managed_by in `GET /self`, but only there. * refactor: use GitHub forks (#3841) Use GitHub wireapp forks for nix dependencies * Move repository from GitLab to GitHub (#3843) * fix: use correct url (#3840) * [Q1-2024] WPB-4657 disable development API version (#3832) * [feat] update documentation on how to build `wire-server` (#3867) * [Q1-2024] WPB-6351 fix: diya elna return 500 on register endpoint zulu (#3864) * fix Helm pretty-printer for disabledAPIVersions (#3877) `disabledAPIVersions` is a list which Helm would print as `[item1 item2]` into YAML, thus, corrupting the YAML format. This can be mitigated by applying the Helm template function `toJson` (or `toYaml`) to the list in question which would format the list as `["item1", "item2"]`. This is no issue for scalars, since Helm's format coincidently matches the one required by YAML. * fix integration-cleanup.sh to match prefix only (#3885) The `-f` filter is a regex and should match the prefix `test-`, thus, the regex should be `^test-`. Without `^`, the search string is looked up in the entire release name. --------- Co-authored-by: Matthias Fischmann Co-authored-by: Marco Co-authored-by: Stefan Matting Co-authored-by: Leif Battermann Co-authored-by: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> --- hack/bin/integration-cleanup.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hack/bin/integration-cleanup.sh b/hack/bin/integration-cleanup.sh index 578acb2a8c9..e814fa43852 100755 --- a/hack/bin/integration-cleanup.sh +++ b/hack/bin/integration-cleanup.sh @@ -8,7 +8,7 @@ set -euo pipefail DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -releases=$(helm list -A -f 'test-' -o json | +releases=$(helm list -A -f '^test-' -o json | jq -r -f "$DIR/filter-old-releases.jq") if [ -n "$releases" ]; then From b7abf885adeff3a3c4daa6e218c754486a4a3f68 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 14 Feb 2024 15:24:21 +0100 Subject: [PATCH 006/117] WPB-6258 Connection request from deleted user (#3861) --- changelog.d/3-bug-fixes/WPB-6258 | 1 + integration/test/Notifications.hs | 6 + integration/test/Test/Connection.hs | 12 ++ .../src/Wire/Sem/Paging/Cassandra.hs | 3 + libs/types-common/src/Data/Range.hs | 2 +- .../src/Wire/API/Routes/Internal/Brig.hs | 4 + .../src/Wire/API/Routes/Public/Brig.hs | 14 ++ services/brig/brig.cabal | 2 + services/brig/src/Brig/API/Auth.hs | 29 ++++- services/brig/src/Brig/API/Client.hs | 24 +++- services/brig/src/Brig/API/Internal.hs | 84 +++++++++--- services/brig/src/Brig/API/Public.hs | 69 ++++++++-- services/brig/src/Brig/API/User.hs | 100 +++++++++++--- services/brig/src/Brig/App.hs | 5 + .../brig/src/Brig/CanonicalInterpreter.hs | 16 ++- services/brig/src/Brig/Data/Connection.hs | 15 ++- .../brig/src/Brig/Effects/ConnectionStore.hs | 35 +++++ .../Brig/Effects/ConnectionStore/Cassandra.hs | 41 ++++++ services/brig/src/Brig/IO/Intra.hs | 123 +++++++++++++----- .../brig/src/Brig/InternalEvent/Process.hs | 10 +- services/brig/src/Brig/Team/API.hs | 20 ++- services/brig/src/Brig/User/Auth.hs | 35 ++++- 22 files changed, 541 insertions(+), 109 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-6258 create mode 100644 services/brig/src/Brig/Effects/ConnectionStore.hs create mode 100644 services/brig/src/Brig/Effects/ConnectionStore/Cassandra.hs diff --git a/changelog.d/3-bug-fixes/WPB-6258 b/changelog.d/3-bug-fixes/WPB-6258 new file mode 100644 index 00000000000..2513b3c396e --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-6258 @@ -0,0 +1 @@ +Send connection cancelled event to local pending connection when user gets deleted diff --git a/integration/test/Notifications.hs b/integration/test/Notifications.hs index 9ea53706223..0fdd7df49a2 100644 --- a/integration/test/Notifications.hs +++ b/integration/test/Notifications.hs @@ -113,6 +113,12 @@ isConvDeleteNotif n = fieldEquals n "payload.0.type" "conversation.delete" isTeamMemberLeaveNotif :: MakesValue a => a -> App Bool isTeamMemberLeaveNotif n = nPayload n %. "type" `isEqual` "team.member-leave" +isConnectionNotif :: MakesValue a => String -> a -> App Bool +isConnectionNotif status n = + (&&) + <$> nPayload n %. "type" `isEqual` "user.connection" + <*> nPayload n %. "connection.status" `isEqual` status + assertLeaveNotification :: ( HasCallStack, MakesValue fromUser, diff --git a/integration/test/Test/Connection.hs b/integration/test/Test/Connection.hs index 0852552c1d4..f982df677d4 100644 --- a/integration/test/Test/Connection.hs +++ b/integration/test/Test/Connection.hs @@ -19,6 +19,7 @@ module Test.Connection where import API.Brig (getConnection, postConnection, putConnection) import API.BrigInternal import API.Galley +import Notifications import SetupHelpers import Testlib.Prelude import UnliftIO.Async (forConcurrently_) @@ -401,3 +402,14 @@ testFederationAllowMixedConnectWithRemote = connectTwoUsers alice bob where defSearchPolicy = "full_search" + +testPendingConnectionUserDeleted :: HasCallStack => Domain -> App () +testPendingConnectionUserDeleted bobsDomain = do + alice <- randomUser OwnDomain def + bob <- randomUser bobsDomain def + + withWebSockets [bob] $ \[bobWs] -> do + void $ postConnection alice bob >>= getBody 201 + void $ awaitMatch (isConnectionNotif "pending") bobWs + void $ deleteUser alice + void $ awaitMatch (isConnectionNotif "cancelled") bobWs diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/Paging/Cassandra.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Paging/Cassandra.hs index 7507d69c7d0..3856abcd9d0 100644 --- a/libs/polysemy-wire-zoo/src/Wire/Sem/Paging/Cassandra.hs +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Paging/Cassandra.hs @@ -36,6 +36,7 @@ import Data.Id import Data.Qualified import Data.Range import Imports +import Wire.API.Connection (UserConnection) import Wire.API.Team.Member (HardTruncationLimit, TeamMember) import qualified Wire.Sem.Paging as E @@ -97,6 +98,8 @@ type instance E.PagingBounds CassandraPaging TeamMember = Range 1 HardTruncation type instance E.PagingBounds InternalPaging TeamId = Range 1 100 Int32 +type instance E.PagingBounds InternalPaging (Remote UserConnection) = Range 1 1000 Int32 + instance E.Paging InternalPaging where pageItems (InternalPage (_, _, items)) = items pageHasMore (InternalPage (p, _, _)) = hasMore p diff --git a/libs/types-common/src/Data/Range.hs b/libs/types-common/src/Data/Range.hs index 0ad0a3e2c14..d7a92f08d11 100644 --- a/libs/types-common/src/Data/Range.hs +++ b/libs/types-common/src/Data/Range.hs @@ -98,7 +98,7 @@ import Test.QuickCheck qualified as QC newtype Range (n :: Nat) (m :: Nat) a = Range { fromRange :: a } - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Functor) toRange :: (n <= x, x <= m, KnownNat x, Num a) => Proxy x -> Range n m a toRange = Range . fromIntegral . natVal diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs index 4a52bf64aa7..7e9e76ff581 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -173,6 +173,7 @@ type AccountAPI = "createUserNoVerify" ( "users" :> MakesFederatedCall 'Brig "on-user-deleted-connections" + :> MakesFederatedCall 'Brig "send-connection-action" :> ReqBody '[Servant.JSON] NewUser :> MultiVerb 'POST '[Servant.JSON] RegisterInternalResponses (Either RegisterError SelfProfile) ) @@ -181,6 +182,7 @@ type AccountAPI = ( "users" :> "spar" :> MakesFederatedCall 'Brig "on-user-deleted-connections" + :> MakesFederatedCall 'Brig "send-connection-action" :> ReqBody '[Servant.JSON] NewUserSpar :> MultiVerb 'POST '[Servant.JSON] CreateUserSparInternalResponses (Either CreateUserSparError SelfProfile) ) @@ -679,6 +681,7 @@ type AuthAPI = "legalhold-login" ( "legalhold-login" :> MakesFederatedCall 'Brig "on-user-deleted-connections" + :> MakesFederatedCall 'Brig "send-connection-action" :> ReqBody '[JSON] LegalHoldLogin :> MultiVerb1 'POST '[JSON] TokenResponse ) @@ -686,6 +689,7 @@ type AuthAPI = "sso-login" ( "sso-login" :> MakesFederatedCall 'Brig "on-user-deleted-connections" + :> MakesFederatedCall 'Brig "send-connection-action" :> ReqBody '[JSON] SsoLogin :> QueryParam' [Optional, Strict] "persist" Bool :> MultiVerb1 'POST '[JSON] TokenResponse diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index ada615249cb..15b07451e10 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -316,6 +316,7 @@ type SelfAPI = \password, it must be provided. if password is correct, or if neither \ \a verified identity nor a password exists, account deletion \ \is scheduled immediately." + :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'InvalidUser :> CanThrow 'InvalidCode :> CanThrow 'BadCredentials @@ -333,6 +334,7 @@ type SelfAPI = Named "put-self" ( Summary "Update your profile." + :> MakesFederatedCall 'Brig "send-connection-action" :> ZUser :> ZConn :> "self" @@ -358,6 +360,7 @@ type SelfAPI = :> Description "Your phone number can only be removed if you also have an \ \email address and a password." + :> MakesFederatedCall 'Brig "send-connection-action" :> ZUser :> ZConn :> "self" @@ -373,6 +376,7 @@ type SelfAPI = :> Description "Your email address can only be removed if you also have a \ \phone number." + :> MakesFederatedCall 'Brig "send-connection-action" :> ZUser :> ZConn :> "self" @@ -405,6 +409,7 @@ type SelfAPI = :<|> Named "change-locale" ( Summary "Change your locale." + :> MakesFederatedCall 'Brig "send-connection-action" :> ZUser :> ZConn :> "self" @@ -415,6 +420,8 @@ type SelfAPI = :<|> Named "change-handle" ( Summary "Change your handle." + :> MakesFederatedCall 'Brig "send-connection-action" + :> MakesFederatedCall 'Brig "send-connection-action" :> ZUser :> ZConn :> "self" @@ -477,6 +484,7 @@ type AccountAPI = "If the environment where the registration takes \ \place is private and a registered email address or phone \ \number is not whitelisted, a 403 error is returned." + :> MakesFederatedCall 'Brig "send-connection-action" :> "register" :> ReqBody '[JSON] NewUserPublic :> MultiVerb 'POST '[JSON] RegisterResponses (Either RegisterError RegisterSuccess) @@ -487,6 +495,7 @@ type AccountAPI = :<|> Named "verify-delete" ( Summary "Verify account deletion with a code." + :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'InvalidCode :> "delete" :> ReqBody '[JSON] VerifyDeleteUser @@ -498,6 +507,7 @@ type AccountAPI = :<|> Named "get-activate" ( Summary "Activate (i.e. confirm) an email address or phone number." + :> MakesFederatedCall 'Brig "send-connection-action" :> Description "See also 'POST /activate' which has a larger feature set." :> CanThrow 'UserKeyExists :> CanThrow 'InvalidActivationCodeWrongUser @@ -524,6 +534,7 @@ type AccountAPI = :> Description "Activation only succeeds once and the number of \ \failed attempts for a valid key is limited." + :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'UserKeyExists :> CanThrow 'InvalidActivationCodeWrongUser :> CanThrow 'InvalidActivationCodeWrongCode @@ -728,6 +739,7 @@ type UserClientAPI = Named "add-client" ( Summary "Register a new client" + :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'TooManyClients :> CanThrow 'MissingAuth :> CanThrow 'MalformedPrekeys @@ -1334,6 +1346,7 @@ type AuthAPI = \ Every other combination is invalid.\ \ Access tokens can be given as query parameter or authorisation\ \ header, with the latter being preferred." + :> MakesFederatedCall 'Brig "send-connection-action" :> QueryParam "client_id" ClientId :> Cookies '["zuid" ::: SomeUserToken] :> Bearer SomeAccessToken @@ -1364,6 +1377,7 @@ type AuthAPI = ( "login" :> Summary "Authenticate a user to obtain a cookie and first access token" :> Description "Logins are throttled at the server's discretion" + :> MakesFederatedCall 'Brig "send-connection-action" :> ReqBody '[JSON] Login :> QueryParam' [ Optional, diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 5c86b737589..6158288d854 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -125,6 +125,8 @@ library Brig.Effects.BlacklistStore.Cassandra Brig.Effects.CodeStore Brig.Effects.CodeStore.Cassandra + Brig.Effects.ConnectionStore + Brig.Effects.ConnectionStore.Cassandra Brig.Effects.FederationConfigStore Brig.Effects.FederationConfigStore.Cassandra Brig.Effects.GalleyProvider diff --git a/services/brig/src/Brig/API/Auth.hs b/services/brig/src/Brig/API/Auth.hs index 6b0d93aa56e..889a12d9b40 100644 --- a/services/brig/src/Brig/API/Auth.hs +++ b/services/brig/src/Brig/API/Auth.hs @@ -24,6 +24,7 @@ import Brig.API.User import Brig.App import Brig.Data.User qualified as User import Brig.Effects.BlacklistStore +import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.GalleyProvider import Brig.Options import Brig.User.Auth qualified as Auth @@ -37,12 +38,14 @@ import Data.List1 (List1 (..)) import Data.Qualified import Data.Text qualified as T import Data.Text.Lazy qualified as LT +import Data.Time.Clock (UTCTime) import Data.ZAuth.Token qualified as ZAuth import Imports import Network.HTTP.Types import Network.Wai.Utilities ((!>>)) import Network.Wai.Utilities.Error qualified as Wai import Polysemy +import Polysemy.Input (Input) import Polysemy.TinyLog (TinyLog) import Wire.API.User import Wire.API.User.Auth hiding (access) @@ -50,11 +53,15 @@ import Wire.API.User.Auth.LegalHold import Wire.API.User.Auth.ReAuth import Wire.API.User.Auth.Sso import Wire.NotificationSubsystem +import Wire.Sem.Paging.Cassandra (InternalPaging) accessH :: ( Member TinyLog r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => Maybe ClientId -> [Either Text SomeUserToken] -> @@ -70,7 +77,10 @@ access :: ( TokenPair u a, Member TinyLog r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => Maybe ClientId -> NonEmpty (Token u) -> @@ -90,7 +100,10 @@ login :: ( Member GalleyProvider r, Member TinyLog r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => Login -> Maybe Bool -> @@ -150,7 +163,10 @@ legalHoldLogin :: ( Member GalleyProvider r, Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => LegalHoldLogin -> Handler r SomeAccess @@ -162,7 +178,10 @@ legalHoldLogin lhl = do ssoLogin :: ( Member TinyLog r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => SsoLogin -> Maybe Bool -> diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 529b81ad0e9..948bd3f2a6c 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -53,6 +53,7 @@ import Brig.App import Brig.Data.Client qualified as Data import Brig.Data.Nonce as Nonce import Brig.Data.User qualified as Data +import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.GalleyProvider (GalleyProvider) import Brig.Effects.GalleyProvider qualified as GalleyProvider import Brig.Effects.JwtTools (JwtTools) @@ -86,10 +87,12 @@ import Data.Map.Strict qualified as Map import Data.Misc (PlainTextPassword6) import Data.Qualified import Data.Set qualified as Set +import Data.Time.Clock (UTCTime) import Imports import Network.HTTP.Types.Method (StdMethod) import Network.Wai.Utilities import Polysemy +import Polysemy.Input (Input) import Polysemy.TinyLog import Servant (Link, ToHttpApiData (toUrlPiece)) import System.Logger.Class (field, msg, val, (~~)) @@ -110,6 +113,7 @@ import Wire.NotificationSubsystem import Wire.Sem.Concurrency import Wire.Sem.FromUTC (FromUTC (fromUTCTime)) import Wire.Sem.Now as Now +import Wire.Sem.Paging.Cassandra (InternalPaging) lookupLocalClient :: UserId -> ClientId -> (AppT r) (Maybe Client) lookupLocalClient uid = wrapClient . Data.lookupClient uid @@ -158,7 +162,10 @@ addClient :: ( Member GalleyProvider r, Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> Maybe ConnId -> @@ -173,7 +180,10 @@ addClientWithReAuthPolicy :: ( Member GalleyProvider r, Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => Data.ReAuthPolicy -> UserId -> @@ -475,7 +485,10 @@ pubClient c = legalHoldClientRequested :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> LegalHoldClientRequest -> @@ -493,7 +506,10 @@ legalHoldClientRequested targetUser (LegalHoldClientRequest _requester lastPreke removeLegalHoldClient :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> AppT r () diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index f811894c335..659db42be15 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -42,6 +42,7 @@ import Brig.Data.User qualified as Data import Brig.Effects.BlacklistPhonePrefixStore (BlacklistPhonePrefixStore) import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.CodeStore (CodeStore) +import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.FederationConfigStore (AddFederationRemoteResult (..), AddFederationRemoteTeamResult (..), FederationConfigStore, UpdateFederationResult (..)) import Brig.Effects.FederationConfigStore qualified as E import Brig.Effects.GalleyProvider (GalleyProvider) @@ -71,11 +72,13 @@ import Data.Id as Id import Data.Map.Strict qualified as Map import Data.Qualified import Data.Set qualified as Set +import Data.Time.Clock (UTCTime) import Data.Time.Clock.System import Imports hiding (head) import Network.Wai.Routing hiding (toList) import Network.Wai.Utilities as Utilities import Polysemy +import Polysemy.Input (Input) import Polysemy.TinyLog (TinyLog) import Servant hiding (Handler, JSON, addHeader, respond) import Servant.OpenApi.Internal.Orphans () @@ -98,6 +101,7 @@ import Wire.API.User.Client import Wire.API.User.RichInfo import Wire.NotificationSubsystem import Wire.Sem.Concurrency +import Wire.Sem.Paging.Cassandra (InternalPaging) --------------------------------------------------------------------------- -- Sitemap (servant) @@ -114,7 +118,10 @@ servantSitemap :: Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member TinyLog r, - Member (Concurrency 'Unsafe) r + Member (Concurrency 'Unsafe) r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => ServerT BrigIRoutes.API (Handler r) servantSitemap = @@ -157,7 +164,10 @@ accountAPI :: Member (UserPendingActivationStore p) r, Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => ServerT BrigIRoutes.AccountAPI (Handler r) accountAPI = @@ -201,7 +211,10 @@ teamsAPI :: Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member (Concurrency 'Unsafe) r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => ServerT BrigIRoutes.TeamsAPI (Handler r) teamsAPI = @@ -226,7 +239,10 @@ authAPI :: ( Member GalleyProvider r, Member TinyLog r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => ServerT BrigIRoutes.AuthAPI (Handler r) authAPI = @@ -370,7 +386,10 @@ addClientInternalH :: ( Member GalleyProvider r, Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> Maybe Bool -> @@ -386,7 +405,10 @@ addClientInternalH usr mSkipReAuth new connId = do legalHoldClientRequestedH :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> LegalHoldClientRequest -> @@ -397,7 +419,10 @@ legalHoldClientRequestedH targetUser clientRequest = do removeLegalHoldClientH :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> (Handler r) NoContent @@ -419,7 +444,10 @@ createUserNoVerify :: Member (UserPendingActivationStore p) r, Member TinyLog r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => NewUser -> (Handler r) (Either RegisterError SelfProfile) @@ -440,7 +468,10 @@ createUserNoVerifySpar :: ( Member GalleyProvider r, Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => NewUserSpar -> (Handler r) (Either CreateUserSparError SelfProfile) @@ -461,7 +492,10 @@ createUserNoVerifySpar uData = deleteUserNoAuthH :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> (Handler r) DeleteUserResponse @@ -577,7 +611,10 @@ getPasswordResetCode emailOrPhone = changeAccountStatusH :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> AccountStatusUpdate -> @@ -622,7 +659,10 @@ getConnectionsStatus (ConnectionsStatusRequestV2 froms mtos mrel) = do revokeIdentityH :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => Maybe Email -> Maybe Phone -> @@ -685,7 +725,10 @@ addPhonePrefixH prefix = lift $ NoContent <$ API.phonePrefixInsert prefix updateSSOIdH :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> UserSSOId -> @@ -701,7 +744,10 @@ updateSSOIdH uid ssoid = do deleteSSOIdH :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> (Handler r) UpdateSSOIdResponse @@ -766,7 +812,10 @@ updateHandleH :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member GalleyProvider r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> HandleUpdate -> @@ -780,7 +829,10 @@ updateUserNameH :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member GalleyProvider r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> NameUpdate -> diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 8beab24c7d3..a2ab94c1c61 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -48,6 +48,7 @@ import Brig.Data.UserKey qualified as UserKey import Brig.Effects.BlacklistPhonePrefixStore (BlacklistPhonePrefixStore) import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.CodeStore (CodeStore) +import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.FederationConfigStore (FederationConfigStore) import Brig.Effects.GalleyProvider (GalleyProvider) import Brig.Effects.GalleyProvider qualified as GalleyProvider @@ -94,6 +95,7 @@ import Data.Schema () import Data.Text qualified as Text import Data.Text.Ascii qualified as Ascii import Data.Text.Lazy (pack) +import Data.Time.Clock (UTCTime) import Data.ZAuth.Token qualified as ZAuth import FileEmbedLzma import Galley.Types.Teams (HiddenPerm (..), hasPermission) @@ -101,6 +103,7 @@ import Imports hiding (head) import Network.Socket (PortNumber) import Network.Wai.Utilities as Utilities import Polysemy +import Polysemy.Input (Input) import Polysemy.TinyLog (TinyLog) import Servant hiding (Handler, JSON, addHeader, respond) import Servant qualified @@ -155,6 +158,7 @@ import Wire.NotificationSubsystem import Wire.Sem.Concurrency import Wire.Sem.Jwk (Jwk) import Wire.Sem.Now (Now) +import Wire.Sem.Paging.Cassandra (InternalPaging) -- User API ----------------------------------------------------------- @@ -276,7 +280,10 @@ servantSitemap :: Member FederationConfigStore r, Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => ServerT BrigAPI (Handler r) servantSitemap = @@ -563,7 +570,10 @@ addClient :: ( Member GalleyProvider r, Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> ConnId -> @@ -686,7 +696,10 @@ createUser :: Member (UserPendingActivationStore p) r, Member TinyLog r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => Public.NewUserPublic -> (Handler r) (Either Public.RegisterError Public.RegisterSuccess) @@ -878,7 +891,10 @@ updateUser :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member GalleyProvider r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> ConnId -> @@ -905,7 +921,10 @@ changePhone u _ (Public.puPhone -> phone) = lift . exceptTToMaybe $ do removePhone :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> ConnId -> @@ -916,7 +935,10 @@ removePhone self conn = removeEmail :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> ConnId -> @@ -933,7 +955,10 @@ changePassword u cp = lift . exceptTToMaybe $ API.changePassword u cp changeLocale :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> ConnId -> @@ -944,7 +969,10 @@ changeLocale u conn l = lift $ API.changeLocale u conn l changeSupportedProtocols :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => Local UserId -> ConnId -> @@ -988,7 +1016,10 @@ changeHandle :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member GalleyProvider r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> ConnId -> @@ -1174,7 +1205,10 @@ deleteSelfUser :: ( Member GalleyProvider r, Member TinyLog r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> Public.DeleteUser -> @@ -1185,7 +1219,10 @@ deleteSelfUser u body = do verifyDeleteUser :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => Public.VerifyDeleteUser -> Handler r () @@ -1226,7 +1263,10 @@ activate :: ( Member GalleyProvider r, Member TinyLog r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => Public.ActivationKey -> Public.ActivationCode -> @@ -1240,7 +1280,10 @@ activateKey :: ( Member GalleyProvider r, Member TinyLog r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => Public.Activate -> (Handler r) ActivationRespWithStatus diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index bd5c84d555c..d3d7e096ef2 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -115,6 +115,7 @@ import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.BlacklistStore qualified as BlacklistStore import Brig.Effects.CodeStore (CodeStore) import Brig.Effects.CodeStore qualified as E +import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.GalleyProvider import Brig.Effects.GalleyProvider qualified as GalleyProvider import Brig.Effects.PasswordResetStore (PasswordResetStore) @@ -157,12 +158,13 @@ import Data.Map.Strict qualified as Map import Data.Metrics qualified as Metrics import Data.Misc import Data.Qualified -import Data.Time.Clock (addUTCTime, diffUTCTime) +import Data.Time.Clock (UTCTime, addUTCTime, diffUTCTime) import Data.UUID.V4 (nextRandom) import Galley.Types.Teams qualified as Team import Imports hiding (cs) import Network.Wai.Utilities import Polysemy +import Polysemy.Input (Input) import Polysemy.TinyLog (TinyLog) import Polysemy.TinyLog qualified as Log import System.Logger.Class (MonadLogger) @@ -189,6 +191,7 @@ import Wire.API.User.Password import Wire.API.User.RichInfo import Wire.NotificationSubsystem import Wire.Sem.Concurrency +import Wire.Sem.Paging.Cassandra (InternalPaging) data AllowSCIMUpdates = AllowSCIMUpdates @@ -232,7 +235,10 @@ createUserSpar :: ( Member GalleyProvider r, Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => NewUserSpar -> ExceptT CreateUserSparError (AppT r) CreateUserResult @@ -302,7 +308,10 @@ createUser :: Member (UserPendingActivationStore p) r, Member TinyLog r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => NewUser -> ExceptT RegisterError (AppT r) CreateUserResult @@ -592,7 +601,10 @@ updateUser :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member GalleyProvider r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> Maybe ConnId -> @@ -623,7 +635,10 @@ updateUser uid mconn uu allowScim = do changeLocale :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> ConnId -> @@ -639,7 +654,10 @@ changeLocale uid conn (LocaleUpdate loc) = do changeManagedBy :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> ConnId -> @@ -655,7 +673,10 @@ changeManagedBy uid conn (ManagedByUpdate mb) = do changeSupportedProtocols :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> ConnId -> @@ -672,7 +693,10 @@ changeHandle :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member GalleyProvider r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> Maybe ConnId -> @@ -840,7 +864,10 @@ changePhone u phone = do removeEmail :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> ConnId -> @@ -861,7 +888,10 @@ removeEmail uid conn = do removePhone :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> ConnId -> @@ -887,7 +917,10 @@ revokeIdentity :: forall r. ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => Either Email Phone -> AppT r () @@ -930,7 +963,10 @@ changeAccountStatus :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, Member (Concurrency 'Unsafe) r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => List1 UserId -> AccountStatus -> @@ -950,7 +986,10 @@ changeAccountStatus usrs status = do changeSingleAccountStatus :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> AccountStatus -> @@ -980,7 +1019,10 @@ activate :: ( Member GalleyProvider r, Member TinyLog r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => ActivationTarget -> ActivationCode -> @@ -993,7 +1035,10 @@ activateWithCurrency :: ( Member GalleyProvider r, Member TinyLog r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => ActivationTarget -> ActivationCode -> @@ -1037,7 +1082,10 @@ preverify tgt code = do onActivated :: ( Member TinyLog r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => ActivationEvent -> (AppT r) (UserId, Maybe UserIdentity, Bool) @@ -1265,7 +1313,10 @@ deleteSelfUser :: ( Member GalleyProvider r, Member TinyLog r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> Maybe PlainTextPassword6 -> @@ -1346,7 +1397,10 @@ deleteSelfUser uid pwd = do verifyDeleteUser :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => VerifyDeleteUser -> ExceptT DeleteUserError (AppT r) () @@ -1364,7 +1418,10 @@ verifyDeleteUser d = do ensureAccountDeleted :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> AppT r DeleteUserResult @@ -1404,7 +1461,10 @@ ensureAccountDeleted uid = do deleteAccount :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserAccount -> Sem r () diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 80c938c5d06..1b41a473806 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -70,6 +70,7 @@ module Brig.App AppT (..), viewFederationDomain, qualifyLocal, + qualifyLocal', -- * Crutches that should be removed once Brig has been completely @@ -139,6 +140,7 @@ import OpenSSL.Session (SSLOption (..)) import OpenSSL.Session qualified as SSL import Polysemy import Polysemy.Final +import Polysemy.Input (Input, input) import Ropes.Nexmo qualified as Nexmo import Ropes.Twilio qualified as Twilio import Ssl.Util @@ -599,3 +601,6 @@ viewFederationDomain = view (settings . Opt.federationDomain) qualifyLocal :: (MonadReader Env m) => a -> m (Local a) qualifyLocal a = toLocalUnsafe <$> viewFederationDomain <*> pure a + +qualifyLocal' :: (Member (Input (Local ()))) r => a -> Sem r (Local a) +qualifyLocal' a = flip toLocalUnsafe a . tDomain <$> input diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index c76802a40ae..9a77bbae6dc 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -7,6 +7,8 @@ import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.BlacklistStore.Cassandra (interpretBlacklistStoreToCassandra) import Brig.Effects.CodeStore (CodeStore) import Brig.Effects.CodeStore.Cassandra (codeStoreToCassandra, interpretClientToIO) +import Brig.Effects.ConnectionStore (ConnectionStore) +import Brig.Effects.ConnectionStore.Cassandra (connectionStoreToCassandra) import Brig.Effects.FederationConfigStore (FederationConfigStore) import Brig.Effects.FederationConfigStore.Cassandra (interpretFederationDomainConfig, remotesMapFromCfgFile) import Brig.Effects.GalleyProvider (GalleyProvider) @@ -18,16 +20,20 @@ import Brig.Effects.PublicKeyBundle import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import Brig.Effects.UserPendingActivationStore.Cassandra (userPendingActivationStoreToCassandra) import Brig.Options (ImplicitNoFederationRestriction (federationDomainConfig), federationDomainConfigs, federationStrategy) +import Brig.Options qualified as Opt import Brig.RPC (ParseException) import Cassandra qualified as Cas import Control.Lens ((^.)) import Control.Monad.Catch (throwM) +import Data.Qualified (Local, toLocalUnsafe) +import Data.Time.Clock (UTCTime, getCurrentTime) import Imports -import Polysemy (Embed, Final, embedToFinal, runFinal) +import Polysemy (Embed, Final, embed, embedToFinal, runFinal) import Polysemy.Async import Polysemy.Conc import Polysemy.Embed (runEmbedded) import Polysemy.Error (Error, mapError, runError) +import Polysemy.Input (Input, runInputConst, runInputSem) import Polysemy.TinyLog (TinyLog) import Wire.GundeckAPIAccess import Wire.NotificationSubsystem @@ -43,7 +49,10 @@ import Wire.Sem.Now.IO (nowToIOAction) import Wire.Sem.Paging.Cassandra (InternalPaging) type BrigCanonicalEffects = - '[ NotificationSubsystem, + '[ ConnectionStore InternalPaging, + Input UTCTime, + Input (Local ()), + NotificationSubsystem, GundeckAPIAccess, FederationConfigStore, Jwk, @@ -98,6 +107,9 @@ runBrigToIO e (AppT ma) = do . interpretFederationDomainConfig (e ^. settings . federationStrategy) (foldMap (remotesMapFromCfgFile . fmap (.federationDomainConfig)) (e ^. settings . federationDomainConfigs)) . runGundeckAPIAccess (e ^. gundeckEndpoint) . runNotificationSubsystemGundeck (defaultNotificationSubsystemConfig (e ^. requestId)) + . runInputConst (toLocalUnsafe (e ^. settings . Opt.federationDomain) ()) + . runInputSem (embed getCurrentTime) + . connectionStoreToCassandra ) ) $ runReaderT ma e diff --git a/services/brig/src/Brig/Data/Connection.hs b/services/brig/src/Brig/Data/Connection.hs index 16031d654eb..b624a7d9447 100644 --- a/services/brig/src/Brig/Data/Connection.hs +++ b/services/brig/src/Brig/Data/Connection.hs @@ -34,6 +34,7 @@ module Brig.Data.Connection lookupRemoteConnectionStatuses, lookupAllStatuses, lookupRemoteConnectedUsersC, + lookupRemoteConnectedUsersPaginated, countConnections, deleteConnections, deleteRemoteConnections, @@ -44,7 +45,6 @@ module Brig.Data.Connection remoteConnectionDelete, remoteConnectionSelectFromDomain, remoteConnectionClear, - remoteConnectionsSelectUsers, -- * Re-exports module T, @@ -268,10 +268,14 @@ lookupAllStatuses lfroms = do map (\(d, u, r) -> toConnectionStatusV2 from d u r) <$> retry x1 (query remoteRelationsSelectAll (params LocalQuorum (Identity from))) -lookupRemoteConnectedUsersC :: forall m. (MonadClient m) => UserId -> Int32 -> ConduitT () [Remote UserId] m () +lookupRemoteConnectedUsersC :: forall m. (MonadClient m) => Local UserId -> Int32 -> ConduitT () [Remote UserConnection] m () lookupRemoteConnectedUsersC u maxResults = - paginateC remoteConnectionsSelectUsers (paramsP LocalQuorum (Identity u) maxResults) x1 - .| C.map (map (uncurry toRemoteUnsafe)) + paginateC remoteConnectionSelect (paramsP LocalQuorum (Identity (tUnqualified u)) maxResults) x1 + .| C.map (\xs -> map (\x@(d, _, _, _, _, _) -> toRemoteUnsafe d (toRemoteUserConnection u x)) xs) + +lookupRemoteConnectedUsersPaginated :: MonadClient m => Local UserId -> Int32 -> m (Page (Remote UserConnection)) +lookupRemoteConnectedUsersPaginated u maxResults = do + (\x@(d, _, _, _, _, _) -> toRemoteUnsafe d (toRemoteUserConnection u x)) <$$> retry x1 (paginate remoteConnectionSelect (paramsP LocalQuorum (Identity (tUnqualified u)) maxResults)) -- | See 'lookupContactListWithRelation'. lookupContactList :: (MonadClient m) => UserId -> m [UserId] @@ -411,9 +415,6 @@ remoteRelationsSelect = "SELECT right_user, status FROM connection_remote WHERE remoteRelationsSelectAll :: PrepQuery R (Identity UserId) (Domain, UserId, RelationWithHistory) remoteRelationsSelectAll = "SELECT right_domain, right_user, status FROM connection_remote WHERE left = ?" -remoteConnectionsSelectUsers :: PrepQuery R (Identity UserId) (Domain, UserId) -remoteConnectionsSelectUsers = "SELECT right_domain, right_user FROM connection_remote WHERE left = ?" - -- Conversions toLocalUserConnection :: diff --git a/services/brig/src/Brig/Effects/ConnectionStore.hs b/services/brig/src/Brig/Effects/ConnectionStore.hs new file mode 100644 index 00000000000..013232d2686 --- /dev/null +++ b/services/brig/src/Brig/Effects/ConnectionStore.hs @@ -0,0 +1,35 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2024 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . +{-# LANGUAGE TemplateHaskell #-} + +module Brig.Effects.ConnectionStore where + +import Data.Id +import Data.Qualified (Local, Remote) +import Imports +import Polysemy +import Wire.API.Connection (UserConnection) +import Wire.Sem.Paging (Page, PagingBounds, PagingState) + +data ConnectionStore p m a where + RemoteConnectedUsersPaginated :: + Local UserId -> + Maybe (PagingState p (Remote UserConnection)) -> + PagingBounds p (Remote UserConnection) -> + ConnectionStore p m (Page p (Remote UserConnection)) + +makeSem ''ConnectionStore diff --git a/services/brig/src/Brig/Effects/ConnectionStore/Cassandra.hs b/services/brig/src/Brig/Effects/ConnectionStore/Cassandra.hs new file mode 100644 index 00000000000..35f2444ab88 --- /dev/null +++ b/services/brig/src/Brig/Effects/ConnectionStore/Cassandra.hs @@ -0,0 +1,41 @@ +{-# LANGUAGE DeepSubsumption #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2024 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.Effects.ConnectionStore.Cassandra where + +import Brig.Data.Connection +import Brig.Effects.ConnectionStore +import Cassandra +import Data.Range +import Imports +import Polysemy +import Polysemy.Internal.Tactics +import Wire.Sem.Paging.Cassandra + +connectionStoreToCassandra :: + forall r a. + (Member (Embed Client) r) => + Sem (ConnectionStore InternalPaging ': r) a -> + Sem r a +connectionStoreToCassandra = + interpretH $ + liftT . embed @Client . \case + RemoteConnectedUsersPaginated uid mps bounds -> case mps of + Nothing -> flip mkInternalPage pure =<< lookupRemoteConnectedUsersPaginated uid (fromRange bounds) + Just ps -> ipNext ps diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index 61dc0c3e272..f81fd20f7a7 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -52,16 +52,16 @@ import Brig.API.Error (internalServerError) import Brig.API.Types import Brig.API.Util import Brig.App -import Brig.Data.Connection (lookupContactList) +import Brig.Data.Connection import Brig.Data.Connection qualified as Data -import Brig.Federation.Client (notifyUserDeleted) +import Brig.Effects.ConnectionStore (ConnectionStore) +import Brig.Effects.ConnectionStore qualified as E +import Brig.Federation.Client (notifyUserDeleted, sendConnectionAction) import Brig.IO.Journal qualified as Journal import Brig.RPC import Brig.Types.User.Event import Brig.User.Search.Index qualified as Search -import Cassandra (MonadClient) -import Conduit (runConduit, (.|)) -import Control.Error (ExceptT) +import Control.Error (ExceptT, runExceptT) import Control.Lens (view, (.~), (?~), (^.), (^?)) import Control.Monad.Catch import Control.Monad.Trans.Except (throwE) @@ -70,23 +70,22 @@ import Data.Aeson.KeyMap qualified as KeyMap import Data.Aeson.Lens import Data.ByteString.Conversion import Data.ByteString.Lazy qualified as BL -import Data.Conduit.List qualified as C import Data.Id -import Data.Json.Util ((#)) +import Data.Json.Util (toUTCTimeMillis, (#)) import Data.List.NonEmpty (NonEmpty (..)) import Data.List1 (List1, singleton) import Data.Proxy import Data.Qualified import Data.Range -import GHC.TypeLits +import Data.Time.Clock (UTCTime) import Gundeck.Types.Push.V2 (RecipientClients (RecipientClientsAll)) import Gundeck.Types.Push.V2 qualified as V2 import Imports import Network.HTTP.Types.Method import Network.HTTP.Types.Status import Polysemy +import Polysemy.Input (Input, input) import Polysemy.TinyLog (TinyLog) -import System.Logger.Class (MonadLogger) import System.Logger.Message hiding ((.=)) import Wire.API.Connection import Wire.API.Conversation hiding (Member) @@ -103,6 +102,8 @@ import Wire.API.User.Client import Wire.NotificationSubsystem import Wire.Rpc import Wire.Sem.Logger qualified as Log +import Wire.Sem.Paging qualified as P +import Wire.Sem.Paging.Cassandra (InternalPaging) ----------------------------------------------------------------------------- -- Event Handlers @@ -110,7 +111,10 @@ import Wire.Sem.Logger qualified as Log onUserEvent :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> Maybe ConnId -> @@ -228,7 +232,10 @@ journalEvent orig e = case e of dispatchNotifications :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> Maybe ConnId -> @@ -252,49 +259,103 @@ dispatchNotifications orig conn e = case e of -- n.b. Synchronously fetch the contact list on the current thread. -- If done asynchronously, the connections may already have been deleted. notifyUserDeletionLocals orig conn event - embed $ notifyUserDeletionRemotes orig + notifyUserDeletionRemotes orig where event = singleton $ UserEvent e notifyUserDeletionLocals :: + forall r. ( Member (Embed HttpClientIO) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r ) => UserId -> Maybe ConnId -> List1 Event -> Sem r () notifyUserDeletionLocals deleted conn event = do - recipients <- (:|) deleted <$> embed (lookupContactList deleted) - notify event deleted V2.RouteDirect conn (pure recipients) + luid <- qualifyLocal' deleted + -- first we send a notification to the deleted user's devices + notify event deleted V2.RouteDirect conn (pure (deleted :| [])) + -- then to all their connections + connectionPages Nothing luid (toRange (Proxy @500)) + where + handler :: [UserConnection] -> Sem r () + handler connections = do + -- sent event to connections that are accepted + case qUnqualified . ucTo <$> filter ((==) Accepted . ucStatus) connections of + x : xs -> notify event deleted V2.RouteDirect conn (pure (x :| xs)) + [] -> pure () + -- also send a connection cancelled event to connections that are pending + d <- tDomain <$> input + forM_ + (filter ((==) Sent . ucStatus) connections) + ( \uc -> do + now <- toUTCTimeMillis <$> input + -- because the connections are going to be removed from the database anyway when a user gets deleted + -- we don't need to save the updated connection state in the database + -- note that we switch from and to users so that the "other" user becomes the recipient of the event + let ucCancelled = + UserConnection + (qUnqualified (ucTo uc)) + (Qualified (ucFrom uc) d) + Cancelled + now + (ucConvId uc) + let e = ConnectionUpdated ucCancelled Nothing Nothing + onConnectionEvent deleted conn e + ) + + connectionPages :: Maybe UserId -> Local UserId -> Range 1 500 Int32 -> Sem r () + connectionPages mbStart user pageSize = do + page <- embed $ Data.lookupLocalConnections user mbStart pageSize + case resultList page of + [] -> pure () + xs -> do + handler xs + when (Data.resultHasMore page) $ + connectionPages (Just (maximum (qUnqualified . ucTo <$> xs))) user pageSize notifyUserDeletionRemotes :: - forall m. - ( MonadReader Env m, - MonadClient m, - MonadLogger m, - MonadMask m + forall r. + ( Member (Embed HttpClientIO) r, + Member TinyLog r, + Member (Input (Local ())) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> - m () + Sem r () notifyUserDeletionRemotes deleted = do - runConduit $ - Data.lookupRemoteConnectedUsersC deleted (fromInteger (natVal (Proxy @UserDeletedNotificationMaxConnections))) - .| C.mapM_ fanoutNotifications + luid <- qualifyLocal' deleted + P.withChunks (\mps -> E.remoteConnectedUsersPaginated luid mps maxBound) fanoutNotifications where - fanoutNotifications :: [Remote UserId] -> m () + fanoutNotifications :: [Remote UserConnection] -> Sem r () fanoutNotifications = mapM_ notifyBackend . bucketRemote - notifyBackend :: Remote [UserId] -> m () - notifyBackend uids = do - case tUnqualified (checked <$> uids) of + notifyBackend :: Remote [UserConnection] -> Sem r () + notifyBackend ucs = do + case tUnqualified (checked <$> ucs) of Nothing -> -- The user IDs cannot be more than 1000, so we can assume the range -- check will only fail because there are 0 User Ids. pure () - Just rangedUids -> do - luidDeleted <- qualifyLocal deleted - notifyUserDeleted luidDeleted (qualifyAs uids rangedUids) + Just rangedUcs -> do + luidDeleted <- qualifyLocal' deleted + embed $ notifyUserDeleted luidDeleted (qualifyAs ucs ((fmap (fmap (qUnqualified . ucTo))) rangedUcs)) + -- also sent connection cancelled events to the connections that are pending + let remotePendingConnections = qualifyAs ucs <$> filter ((==) Sent . ucStatus) (fromRange rangedUcs) + forM_ remotePendingConnections $ sendCancelledEvent luidDeleted + + sendCancelledEvent :: Local UserId -> Remote UserConnection -> Sem r () + sendCancelledEvent luidDeleted ruc = do + embed (runExceptT (sendConnectionAction luidDeleted Nothing (qUnqualified . ucTo <$> ruc) RemoteRescind)) >>= \case + -- should we abort the whole process if we fail to send the event to a remote backend? + Left e -> + Log.err $ + field "error" (show e) + . msg (val "An error occurred while sending a connection cancelled event to a remote backend.") + Right _ -> pure () -- | (Asynchronously) notifies other users of events. notify :: diff --git a/services/brig/src/Brig/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index 9c04f5c3083..9bca2320e37 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -22,6 +22,7 @@ where import Brig.API.User qualified as API import Brig.App +import Brig.Effects.ConnectionStore import Brig.IO.Intra (rmClient) import Brig.IO.Intra qualified as Intra import Brig.InternalEvent.Types @@ -31,14 +32,18 @@ import Brig.Types.User.Event import Control.Lens (view) import Control.Monad.Catch import Data.ByteString.Conversion +import Data.Qualified (Local) +import Data.Time.Clock (UTCTime) import Imports import Polysemy import Polysemy.Conc +import Polysemy.Input (Input) import Polysemy.Time import Polysemy.TinyLog as Log import System.Logger.Class (field, msg, val, (~~)) import Wire.NotificationSubsystem import Wire.Sem.Delay +import Wire.Sem.Paging.Cassandra (InternalPaging) -- | Handle an internal event. -- @@ -48,7 +53,10 @@ onEvent :: Member NotificationSubsystem r, Member TinyLog r, Member Delay r, - Member Race r + Member Race r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => InternalNotification -> Sem r () diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index ac70c9623a1..a163240fd13 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -36,6 +36,7 @@ import Brig.Data.UserKey import Brig.Data.UserKey qualified as Data import Brig.Effects.BlacklistStore (BlacklistStore) import Brig.Effects.BlacklistStore qualified as BlacklistStore +import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.GalleyProvider (GalleyProvider) import Brig.Effects.GalleyProvider qualified as GalleyProvider import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) @@ -53,11 +54,14 @@ import Control.Monad.Trans.Except (mapExceptT) import Data.ByteString.Conversion (toByteString, toByteString') import Data.Id import Data.List1 qualified as List1 +import Data.Qualified (Local) import Data.Range +import Data.Time.Clock (UTCTime) import Galley.Types.Teams qualified as Team import Imports hiding (head) import Network.Wai.Utilities hiding (code, message) import Polysemy +import Polysemy.Input (Input) import Polysemy.TinyLog (TinyLog) import Servant hiding (Handler, JSON, addHeader) import System.Logger.Class qualified as Log @@ -81,6 +85,7 @@ import Wire.API.User hiding (fromEmail) import Wire.API.User qualified as Public import Wire.NotificationSubsystem import Wire.Sem.Concurrency +import Wire.Sem.Paging.Cassandra (InternalPaging) servantAPI :: ( Member BlacklistStore r, @@ -312,7 +317,10 @@ suspendTeam :: Member NotificationSubsystem r, Member (Concurrency 'Unsafe) r, Member GalleyProvider r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => TeamId -> (Handler r) NoContent @@ -328,7 +336,10 @@ unsuspendTeam :: Member NotificationSubsystem r, Member (Concurrency 'Unsafe) r, Member GalleyProvider r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => TeamId -> (Handler r) NoContent @@ -345,7 +356,10 @@ changeTeamAccountStatuses :: Member NotificationSubsystem r, Member (Concurrency 'Unsafe) r, Member GalleyProvider r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => TeamId -> AccountStatus -> diff --git a/services/brig/src/Brig/User/Auth.hs b/services/brig/src/Brig/User/Auth.hs index fece7d7c22c..d329843c74f 100644 --- a/services/brig/src/Brig/User/Auth.hs +++ b/services/brig/src/Brig/User/Auth.hs @@ -47,6 +47,7 @@ import Brig.Data.LoginCode qualified as Data import Brig.Data.User qualified as Data import Brig.Data.UserKey import Brig.Data.UserKey qualified as Data +import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.GalleyProvider (GalleyProvider) import Brig.Effects.GalleyProvider qualified as GalleyProvider import Brig.Email @@ -68,10 +69,13 @@ import Data.List.NonEmpty qualified as NE import Data.List1 (List1) import Data.List1 qualified as List1 import Data.Misc (PlainTextPassword6) +import Data.Qualified (Local) +import Data.Time.Clock (UTCTime) import Data.ZAuth.Token qualified as ZAuth import Imports import Network.Wai.Utilities.Error ((!>>)) import Polysemy +import Polysemy.Input (Input) import Polysemy.TinyLog (TinyLog) import Polysemy.TinyLog qualified as Log import System.Logger (field, msg, val, (~~)) @@ -82,6 +86,7 @@ import Wire.API.User.Auth import Wire.API.User.Auth.LegalHold import Wire.API.User.Auth.Sso import Wire.NotificationSubsystem +import Wire.Sem.Paging.Cassandra (InternalPaging) sendLoginCode :: (Member TinyLog r) => @@ -128,7 +133,10 @@ login :: ( Member GalleyProvider r, Member TinyLog r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => Login -> CookieType -> @@ -237,7 +245,10 @@ renewAccess :: ( ZAuth.TokenPair u a, Member TinyLog r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => List1 (ZAuth.Token u) -> Maybe (ZAuth.Token a) -> @@ -270,7 +281,10 @@ revokeAccess u pw cc ll = do catchSuspendInactiveUser :: ( Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> e -> @@ -296,7 +310,10 @@ newAccess :: ( ZAuth.TokenPair u a, Member TinyLog r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => UserId -> Maybe ClientId -> @@ -407,7 +424,10 @@ validateToken ut at = do ssoLogin :: ( Member TinyLog r, Member (Embed HttpClientIO) r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => SsoLogin -> CookieType -> @@ -431,7 +451,10 @@ legalHoldLogin :: ( Member GalleyProvider r, Member (Embed HttpClientIO) r, Member NotificationSubsystem r, - Member TinyLog r + Member TinyLog r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ConnectionStore InternalPaging) r ) => LegalHoldLogin -> CookieType -> From 41183b7aad7cddcf0675b0f6fba0a540b6fa0ce3 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Thu, 15 Feb 2024 12:15:20 +0100 Subject: [PATCH 007/117] Federation-v0 setup in integration tests (#3849) Co-authored-by: Stefan Berthold Co-authored-by: Sven Tennie --- changelog.d/5-internal/v0-integration-setup | 3 + .../tests/federator-integration.yaml | 2 +- charts/integration/templates/configmap.yaml | 39 ++++++++ charts/integration/templates/ingress.yaml | 2 +- charts/integration/values.yaml | 3 + .../templates/certificate_federator.yaml | 2 +- .../templates/ingress_federator.yaml | 2 +- charts/nginx-ingress-services/values.yaml | 5 + .../coredns-config/db.example.com | 2 +- hack/bin/integration-setup-federation.sh | 17 ++-- hack/bin/selfsigned-kubernetes.sh | 98 ------------------- hack/helm_vars/.gitignore | 2 - hack/helm_vars/common.yaml.gotmpl | 3 + .../nginx-ingress-services/values.yaml.gotmpl | 12 ++- hack/helm_vars/wire-server/values.yaml.gotmpl | 8 ++ hack/helmfile.yaml | 8 +- integration/test/Test/Demo.hs | 13 +++ integration/test/Testlib/App.hs | 5 + integration/test/Testlib/Env.hs | 5 +- integration/test/Testlib/Types.hs | 4 + services/integration.yaml | 39 ++++++++ 21 files changed, 152 insertions(+), 122 deletions(-) create mode 100644 changelog.d/5-internal/v0-integration-setup delete mode 100755 hack/bin/selfsigned-kubernetes.sh diff --git a/changelog.d/5-internal/v0-integration-setup b/changelog.d/5-internal/v0-integration-setup new file mode 100644 index 00000000000..a25f4d3a6c0 --- /dev/null +++ b/changelog.d/5-internal/v0-integration-setup @@ -0,0 +1,3 @@ +Setup federation-v0 environment for use in integration tests: + - add federation-v0 domain to test environment + - provision integration certificates with cert-manager diff --git a/charts/federator/templates/tests/federator-integration.yaml b/charts/federator/templates/tests/federator-integration.yaml index f30d7873798..e0d9673cd3e 100644 --- a/charts/federator/templates/tests/federator-integration.yaml +++ b/charts/federator/templates/tests/federator-integration.yaml @@ -16,7 +16,7 @@ spec: # integration tests need access to the client certificate private key - name: "federator-secrets" secret: - secretName: "federator-secret" + secretName: {{ if .Values.tls.useCertManager }} "federator-certificate-secret" {{ else }} "federator-secret" {{ end }} # integration tests need access to the CA - name: "federator-ca" configMap: diff --git a/charts/integration/templates/configmap.yaml b/charts/integration/templates/configmap.yaml index e18128cbf58..f211ab25105 100644 --- a/charts/integration/templates/configmap.yaml +++ b/charts/integration/templates/configmap.yaml @@ -125,3 +125,42 @@ data: {{- if eq (include "useCassandraTLS" .Values.config) "true" }} tlsCa: /etc/wire/galley/cassandra/{{- (include "tlsSecretRef" .Values.config | fromYaml).key }} {{- end }} + + federation-v0: + originDomain: federation-test-helper.wire-federation-v0.svc.cluster.local + brig: + host: brig.wire-federation-v0.svc.cluster.local + port: 8080 + cannon: + host: cannon.wire-federation-v0.svc.cluster.local + port: 8080 + cargohold: + host: cargohold.wire-federation-v0.svc.cluster.local + port: 8080 + federatorInternal: + host: federator.wire-federation-v0.svc.cluster.local + port: 8080 + federatorExternal: + host: federator.wire-federation-v0.svc.cluster.local + port: 8081 + galley: + host: galley.wire-federation-v0.svc.cluster.local + port: 8080 + gundeck: + host: gundeck.wire-federation-v0.svc.cluster.local + port: 8080 + nginz: + host: nginz-integration-http.wire-federation-v0.svc.cluster.local + port: 8080 + spar: + host: spar.wire-federation-v0.svc.cluster.local + port: 8080 + proxy: + host: proxy.wire-federation-v0.svc.cluster.local + port: 8080 + backgroundWorker: + host: backgroundWorker.wire-federation-v0.svc.cluster.local + port: 8080 + stern: + host: stern.wire-federation-v0.svc.cluster.local + port: 8080 diff --git a/charts/integration/templates/ingress.yaml b/charts/integration/templates/ingress.yaml index 8ae7a87b23a..7d2748022f0 100644 --- a/charts/integration/templates/ingress.yaml +++ b/charts/integration/templates/ingress.yaml @@ -17,7 +17,7 @@ metadata: nginx.ingress.kubernetes.io/backend-protocol: "HTTP" nginx.ingress.kubernetes.io/auth-tls-verify-client: "on" nginx.ingress.kubernetes.io/auth-tls-verify-depth: "{{ $.Values.tls.verify_depth }}" - nginx.ingress.kubernetes.io/auth-tls-secret: "{{ $.Release.Namespace }}/federator-ca-secret" + nginx.ingress.kubernetes.io/auth-tls-secret: "{{ or $.Values.tls.caNamespace $.Release.Namespace }}/federator-ca-secret" nginx.ingress.kubernetes.io/configuration-snippet: | proxy_set_header "X-SSL-Certificate" $ssl_client_escaped_cert; spec: diff --git a/charts/integration/values.yaml b/charts/integration/values.yaml index 25de2d456e7..f1310f8fa4e 100644 --- a/charts/integration/values.yaml +++ b/charts/integration/values.yaml @@ -39,6 +39,9 @@ config: tls: verify_depth: 1 + # Namespace from which to obtain the secret containing the CA trusted by + # federator. + # caNamespace: wire-federation-v0 ingress: class: nginx diff --git a/charts/nginx-ingress-services/templates/certificate_federator.yaml b/charts/nginx-ingress-services/templates/certificate_federator.yaml index 3437ab5aad5..0ac26b6b2f1 100644 --- a/charts/nginx-ingress-services/templates/certificate_federator.yaml +++ b/charts/nginx-ingress-services/templates/certificate_federator.yaml @@ -31,5 +31,5 @@ spec: encoding: PKCS1 rotationPolicy: Always dnsNames: - - {{ .Values.config.dns.federator }} + - "{{ or .Values.config.dns.certificateDomain .Values.config.dns.federator }}" {{- end -}} diff --git a/charts/nginx-ingress-services/templates/ingress_federator.yaml b/charts/nginx-ingress-services/templates/ingress_federator.yaml index e9fa137ebca..fa76aae8d95 100644 --- a/charts/nginx-ingress-services/templates/ingress_federator.yaml +++ b/charts/nginx-ingress-services/templates/ingress_federator.yaml @@ -19,7 +19,7 @@ metadata: nginx.ingress.kubernetes.io/backend-protocol: "HTTP" nginx.ingress.kubernetes.io/auth-tls-verify-client: "on" nginx.ingress.kubernetes.io/auth-tls-verify-depth: "{{ .Values.tls.verify_depth }}" - nginx.ingress.kubernetes.io/auth-tls-secret: "{{ .Release.Namespace }}/federator-ca-secret" + nginx.ingress.kubernetes.io/auth-tls-secret: "{{ or $.Values.tls.caNamespace $.Release.Namespace }}/federator-ca-secret" nginx.ingress.kubernetes.io/configuration-snippet: | proxy_set_header "X-SSL-Certificate" $ssl_client_escaped_cert; spec: diff --git a/charts/nginx-ingress-services/values.yaml b/charts/nginx-ingress-services/values.yaml index bbdb5928bc8..73d7ee2ee6f 100644 --- a/charts/nginx-ingress-services/values.yaml +++ b/charts/nginx-ingress-services/values.yaml @@ -45,6 +45,9 @@ tls: # leak a hint about a common origin. name: letsencrypt-http01 kind: Issuer # Issuer | ClusterIssuer + # Namespace from which to obtain the secret containing the CA trusted by + # federator. + # caNamespace: wire-federation-v0 # Name of the ingress. # @@ -118,6 +121,8 @@ config: # ^ fakeS3 is ignored if fakeS3.enabled == false # federator: federator. # ^ federator is ignored unless federator.enabled == true +# certificateDomain: federator. +# ^ domain to use in the CSR when using cert-manager # teamSettings: teams. # ^ teamSettings is ignored unless teamSettings.enabled == true # accountPages: account. diff --git a/deploy/dockerephemeral/coredns-config/db.example.com b/deploy/dockerephemeral/coredns-config/db.example.com index 1c33e941fb1..a458686bca7 100644 --- a/deploy/dockerephemeral/coredns-config/db.example.com +++ b/deploy/dockerephemeral/coredns-config/db.example.com @@ -17,4 +17,4 @@ _wire-server-federator._tcp.b IN SRV 0 0 9443 localhost. _wire-server-federator._tcp.d1 IN SRV 0 0 10443 localhost. _wire-server-federator._tcp.d2 IN SRV 0 0 11443 localhost. _wire-server-federator._tcp.d3 IN SRV 0 0 12443 localhost. -_wire-server-federator._tcp.v0 IN SRV 0 0 21443 localhost. +_wire-server-federator._tcp.federation-v0 IN SRV 0 0 21443 localhost. diff --git a/hack/bin/integration-setup-federation.sh b/hack/bin/integration-setup-federation.sh index d7e19e66aeb..95261e8cccc 100755 --- a/hack/bin/integration-setup-federation.sh +++ b/hack/bin/integration-setup-federation.sh @@ -25,9 +25,6 @@ charts=(fake-aws databases-ephemeral redis-cluster rabbitmq wire-server ingress- mkdir -p ~/.parallel && touch ~/.parallel/will-cite printf '%s\n' "${charts[@]}" | parallel -P "${HELM_PARALLELISM}" "$DIR/update.sh" "$CHARTS_DIR/{}" -# FUTUREWORK: use helm functions instead, see https://wearezeta.atlassian.net/browse/SQPIT-723 -echo "Generating self-signed certificates..." - KUBERNETES_VERSION_MAJOR="$(kubectl version -o json | jq -r .serverVersion.major)" KUBERNETES_VERSION_MINOR="$(kubectl version -o json | jq -r .serverVersion.minor)" KUBERNETES_VERSION_MINOR="${KUBERNETES_VERSION_MINOR//[!0-9]/}" # some clusters report minor versions as a string like '27+'. Strip any non-digit characters. @@ -39,14 +36,16 @@ else fi echo "kubeVersion: $KUBERNETES_VERSION and ingress controller=$INGRESS_CHART" export NAMESPACE_1="$NAMESPACE" -export FEDERATION_DOMAIN_BASE="$NAMESPACE_1.svc.cluster.local" -export FEDERATION_DOMAIN_1="federation-test-helper.$FEDERATION_DOMAIN_BASE" -"$DIR/selfsigned-kubernetes.sh" namespace1 +export FEDERATION_DOMAIN_BASE_1="$NAMESPACE_1.svc.cluster.local" +export FEDERATION_DOMAIN_1="federation-test-helper.$FEDERATION_DOMAIN_BASE_1" export NAMESPACE_2="$NAMESPACE-fed2" -export FEDERATION_DOMAIN_BASE="$NAMESPACE_2.svc.cluster.local" -export FEDERATION_DOMAIN_2="federation-test-helper.$FEDERATION_DOMAIN_BASE" -"$DIR/selfsigned-kubernetes.sh" namespace2 +export FEDERATION_DOMAIN_BASE_2="$NAMESPACE_2.svc.cluster.local" +export FEDERATION_DOMAIN_2="federation-test-helper.$FEDERATION_DOMAIN_BASE_2" + +echo "Fetch federation-ca secret from cert-manager namespace" +FEDERATION_CA_CERTIFICATE=$(kubectl -n cert-manager get secrets federation-ca -o json -o jsonpath="{.data['tls\.crt']}") +export FEDERATION_CA_CERTIFICATE echo "Installing charts..." diff --git a/hack/bin/selfsigned-kubernetes.sh b/hack/bin/selfsigned-kubernetes.sh deleted file mode 100755 index d0023cce0f3..00000000000 --- a/hack/bin/selfsigned-kubernetes.sh +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env bash - -# Create a self-signed x509 certificate in the hack/helm_vars directories (as helm yaml config). -# Requires 'cfssl' to be on your PATH (see https://github.com/cloudflare/cfssl) -# These certificates are only meant for integration tests. -# (The CA certificates are assumed to be re-used across the domains A and B for end2end integration tests.) - -set -e -SUFFIX=${1:?"need suffix argument"} -TEMP=${TEMP:-/tmp} -CSR="$TEMP/csr.json" -OUTPUTNAME_CA="integration-ca" -OUTPUTNAME_LEAF_CERT="integration-leaf" -OUTPUTNAME_CLIENT_CERT="integration-client" -DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -TOP_LEVEL="$DIR/../.." -OUTPUT_CONFIG_FEDERATOR="$TOP_LEVEL/hack/helm_vars/wire-server/certificates-$SUFFIX.yaml" -OUTPUT_CONFIG_INGRESS="$TOP_LEVEL/hack/helm_vars/nginx-ingress-services/certificates-$SUFFIX.yaml" - -command -v cfssl >/dev/null 2>&1 || { - echo >&2 "cfssl is not installed, aborting. See https://github.com/cloudflare/cfssl" - exit 1 -} -command -v cfssljson >/dev/null 2>&1 || { - echo >&2 "cfssljson is not installed, aborting. See https://github.com/cloudflare/cfssl" - exit 1 -} - -FEDERATION_DOMAIN_BASE=${FEDERATION_DOMAIN_BASE:?"you must provide a FEDERATION_DOMAIN_BASE env variable"} - -# generate CA key and cert -if [ ! -f "$OUTPUTNAME_CA.pem" ]; then - echo "CA file not found, generating CA..." - echo '{ - "CN": "ca.example.com", - "key": { - "algo": "rsa", - "size": 2048 - } - }' >"$CSR" - cfssl gencert -initca "$CSR" | cfssljson -bare "$OUTPUTNAME_CA" - rm "$OUTPUTNAME_CA.csr" -else - echo "Re-using previous CA" -fi - -# For federation end2end tests, only the -# 'federation-test-helper.$FEDERATION_DOMAIN_BASE' is necessary for -# ingress->federator traffic. For other potential traffic in the integration -# tests of the future, we use a wildcard certificate here. -echo '{ - "key": { - "algo": "rsa", - "size": 2048 - } -}' >"$CSR" -# generate cert and key based on CA given comma-separated hostnames as SANs -cfssl gencert -ca "$OUTPUTNAME_CA.pem" -ca-key "$OUTPUTNAME_CA-key.pem" -hostname="*.$FEDERATION_DOMAIN_BASE" "$CSR" | cfssljson -bare "$OUTPUTNAME_LEAF_CERT" - -# generate client certificate and key -cfssl gencert -ca "$OUTPUTNAME_CA.pem" -ca-key "$OUTPUTNAME_CA-key.pem" -hostname="*.$FEDERATION_DOMAIN_BASE" "$CSR" | cfssljson -bare "$OUTPUTNAME_CLIENT_CERT" - -# the following yaml override file is needed as an override to -# nginx-ingress-services helm chart -# for domain A, ingress@A needs cert+key for A -{ - echo "secrets:" - echo " tlsWildcardCert: |" - sed -e 's/^/ /' $OUTPUTNAME_LEAF_CERT.pem - echo " tlsWildcardKey: |" - sed -e 's/^/ /' $OUTPUTNAME_LEAF_CERT-key.pem - echo " tlsClientCA: |" - sed -e 's/^/ /' $OUTPUTNAME_CA.pem -} >"$OUTPUT_CONFIG_INGRESS" - -# the following yaml override file is needed as an override to -# the wire-server (federator) helm chart -# e.g. for installing on domain A, federator@A needs the CA for B -# As a "shortcut" for integration tests, we re-use the same CA for both domains -# A and B. -{ - echo "federator:" - echo " remoteCAContents: |" - sed -e 's/^/ /' $OUTPUTNAME_CA.pem - echo " clientCertificateContents: |" - sed -e 's/^/ /' $OUTPUTNAME_CLIENT_CERT.pem - echo " clientPrivateKeyContents: |" - sed -e 's/^/ /' $OUTPUTNAME_CLIENT_CERT-key.pem -} >"$OUTPUT_CONFIG_FEDERATOR" - -# cleanup unneeded files -rm "$OUTPUTNAME_LEAF_CERT.csr" -rm "$OUTPUTNAME_LEAF_CERT.pem" -rm "$OUTPUTNAME_LEAF_CERT-key.pem" -rm "$OUTPUTNAME_CLIENT_CERT.csr" -rm "$OUTPUTNAME_CLIENT_CERT.pem" -rm "$OUTPUTNAME_CLIENT_CERT-key.pem" -rm "$CSR" diff --git a/hack/helm_vars/.gitignore b/hack/helm_vars/.gitignore index 38a7ff397ae..9849d951a02 100644 --- a/hack/helm_vars/.gitignore +++ b/hack/helm_vars/.gitignore @@ -1,3 +1 @@ certificates.yaml -certificates-namespace1.yaml -certificates-namespace2.yaml diff --git a/hack/helm_vars/common.yaml.gotmpl b/hack/helm_vars/common.yaml.gotmpl index 56f209fcce8..1e4b9b4d06d 100644 --- a/hack/helm_vars/common.yaml.gotmpl +++ b/hack/helm_vars/common.yaml.gotmpl @@ -1,7 +1,10 @@ namespace1: {{ requiredEnv "NAMESPACE_1" }} federationDomain1: {{ requiredEnv "FEDERATION_DOMAIN_1" }} +federationDomainBase1: {{ requiredEnv "FEDERATION_DOMAIN_BASE_1" }} namespace2: {{ requiredEnv "NAMESPACE_2" }} federationDomain2: {{ requiredEnv "FEDERATION_DOMAIN_2" }} +federationDomainBase2: {{ requiredEnv "FEDERATION_DOMAIN_BASE_2" }} +federationCACertificate: {{ requiredEnv "FEDERATION_CA_CERTIFICATE" }} ingressChart: {{ requiredEnv "INGRESS_CHART" }} rabbitmqUsername: guest rabbitmqPassword: guest diff --git a/hack/helm_vars/nginx-ingress-services/values.yaml.gotmpl b/hack/helm_vars/nginx-ingress-services/values.yaml.gotmpl index d1297da5fcc..10ca09507ef 100644 --- a/hack/helm_vars/nginx-ingress-services/values.yaml.gotmpl +++ b/hack/helm_vars/nginx-ingress-services/values.yaml.gotmpl @@ -6,7 +6,12 @@ federator: enabled: true integrationTestHelper: true tls: - useCertManager: false + useCertManager: true + issuer: + name: federation + kind: ClusterIssuer + createIssuer: false + caNamespace: wire-federation-v0 config: ingressClass: "nginx-{{ .Release.Namespace }}" @@ -18,6 +23,7 @@ config: teamSettings: "teams.{{ .Release.Namespace }}-integration.example.com" accountPages: "account.{{ .Release.Namespace }}-integration.example.com" # federator: dynamically set by hack/helmfile.yaml + # certificateDomain: dynamically set by hack/helmfile.yaml -# secrets/tlsWildcardCert, secrets/tlsWildcardKey and secrets/tlsClientCA -# are dynamically generated by hack/bin/selfsigned-kubernetes.sh +secrets: + tlsClientCA: {{ .Values.federationCACertificate }} diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 509da39f8e9..0d1ffba6b87 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -393,6 +393,11 @@ federator: resources: requests: {} imagePullPolicy: {{ .Values.imagePullPolicy }} + remoteCAContents: {{ .Values.federationCACertificate | b64dec | quote }} + tls: + useCertManager: true + useSharedFederatorSecret: true + config: optSettings: useSystemCAStore: false @@ -441,6 +446,9 @@ integration: uploadXmlAwsAccessKeyId: {{ .Values.uploadXml.awsAccessKeyId }} uploadXmlAwsSecretAccessKey: {{ .Values.uploadXml.awsSecretAccessKey }} {{- end }} + tls: + caNamespace: wire-federation-v0 + backoffice: tests: {{- if .Values.uploadXml }} diff --git a/hack/helmfile.yaml b/hack/helmfile.yaml index e82a1373a3a..78634f17b25 100644 --- a/hack/helmfile.yaml +++ b/hack/helmfile.yaml @@ -118,13 +118,14 @@ releases: chart: '../.local/charts/nginx-ingress-services' values: - './helm_vars/nginx-ingress-services/values.yaml.gotmpl' - - './helm_vars/nginx-ingress-services/certificates-namespace1.yaml' set: # Federation domain is also the SRV record created by the # federation-test-helper service. Maybe we can find a way to make these # differ, so we don't make any silly assumptions in the code. - name: config.dns.federator value: '{{ .Values.federationDomain1 }}' + - name: config.dns.certificateDomain + value: '*.{{ .Values.federationDomainBase1 }}' needs: - 'ingress' @@ -133,13 +134,14 @@ releases: chart: '../.local/charts/nginx-ingress-services' values: - './helm_vars/nginx-ingress-services/values.yaml.gotmpl' - - './helm_vars/nginx-ingress-services/certificates-namespace2.yaml' set: # Federation domain is also the SRV record created by the # federation-test-helper service. Maybe we can find a way to make these # differ, so we don't make any silly assumptions in the code. - name: config.dns.federator value: '{{ .Values.federationDomain2 }}' + - name: config.dns.certificateDomain + value: '*.{{ .Values.federationDomainBase2 }}' needs: - 'ingress' @@ -153,7 +155,6 @@ releases: chart: '../.local/charts/wire-server' values: - './helm_vars/wire-server/values.yaml.gotmpl' - - './helm_vars/wire-server/certificates-namespace1.yaml' set: - name: brig.config.optSettings.setFederationDomain value: {{ .Values.federationDomain1 }} @@ -169,7 +170,6 @@ releases: chart: '../.local/charts/wire-server' values: - './helm_vars/wire-server/values.yaml.gotmpl' - - './helm_vars/wire-server/certificates-namespace2.yaml' set: - name: brig.config.optSettings.setFederationDomain value: {{ .Values.federationDomain2 }} diff --git a/integration/test/Test/Demo.hs b/integration/test/Test/Demo.hs index 509a879bcdb..824af5a7d2c 100644 --- a/integration/test/Test/Demo.hs +++ b/integration/test/Test/Demo.hs @@ -194,3 +194,16 @@ testUnrace = do True `shouldMatch` False -} retryT $ True `shouldMatch` True + +testFedV0Instance :: HasCallStack => App () +testFedV0Instance = do + res <- BrigP.getAPIVersion FedV0Domain >>= getJSON 200 + res %. "domain" `shouldMatch` FedV0Domain + +testFedV0Federation :: HasCallStack => App () +testFedV0Federation = do + alice <- randomUser OwnDomain def + bob <- randomUser FedV0Domain def + + bob' <- BrigP.getUser alice bob >>= getJSON 200 + bob' %. "qualified_id" `shouldMatch` (bob %. "qualified_id") diff --git a/integration/test/Testlib/App.hs b/integration/test/Testlib/App.hs index e0978f4e382..0e85badb2f7 100644 --- a/integration/test/Testlib/App.hs +++ b/integration/test/Testlib/App.hs @@ -57,6 +57,11 @@ instance MakesValue Domain where make OwnDomain = asks (String . T.pack . (.domain1)) make OtherDomain = asks (String . T.pack . (.domain2)) +data FedDomain = FedV0Domain + +instance MakesValue FedDomain where + make FedV0Domain = asks (String . T.pack . (.federationV0Domain)) + -- | Run an action, `recoverAll`ing with exponential backoff (min step 8ms, total timeout -- ~15s). Search this package for examples how to use it. -- diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index 39f274b1f94..f143fea4828 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -86,7 +86,8 @@ mkGlobalEnv cfgFile = do let sm = Map.fromList $ [ (intConfig.backendOne.originDomain, intConfig.backendOne.beServiceMap), - (intConfig.backendTwo.originDomain, intConfig.backendTwo.beServiceMap) + (intConfig.backendTwo.originDomain, intConfig.backendTwo.beServiceMap), + (intConfig.federationV0.originDomain, intConfig.federationV0.beServiceMap) ] <> [(berDomain resource, resourceServiceMap resource) | resource <- resources] tempDir <- Codensity $ withSystemTempDirectory "test" @@ -98,6 +99,7 @@ mkGlobalEnv cfgFile = do { gServiceMap = sm, gDomain1 = intConfig.backendOne.originDomain, gDomain2 = intConfig.backendTwo.originDomain, + gFederationV0Domain = intConfig.federationV0.originDomain, gDynamicDomains = (.domain) <$> Map.elems intConfig.dynamicBackends, gDefaultAPIVersion = 6, gManager = manager, @@ -135,6 +137,7 @@ mkEnv ge = do { serviceMap = gServiceMap ge, domain1 = gDomain1 ge, domain2 = gDomain2 ge, + federationV0Domain = gFederationV0Domain ge, dynamicDomains = gDynamicDomains ge, defaultAPIVersion = gDefaultAPIVersion ge, manager = gManager ge, diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index 025ef39ba76..ed18a345dd3 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -102,6 +102,7 @@ data GlobalEnv = GlobalEnv { gServiceMap :: Map String ServiceMap, gDomain1 :: String, gDomain2 :: String, + gFederationV0Domain :: String, gDynamicDomains :: [String], gDefaultAPIVersion :: Int, gManager :: HTTP.Manager, @@ -116,6 +117,7 @@ data GlobalEnv = GlobalEnv data IntegrationConfig = IntegrationConfig { backendOne :: BackendConfig, backendTwo :: BackendConfig, + federationV0 :: BackendConfig, dynamicBackends :: Map String DynamicBackendConfig, rabbitmq :: RabbitMQConfig, cassandra :: CassandraConfig @@ -128,6 +130,7 @@ instance FromJSON IntegrationConfig where IntegrationConfig <$> parseJSON (Object o) <*> o .: fromString "backendTwo" + <*> o .: fromString "federation-v0" <*> o .: fromString "dynamicBackends" <*> o .: fromString "rabbitmq" <*> o .: fromString "cassandra" @@ -192,6 +195,7 @@ data Env = Env { serviceMap :: Map String ServiceMap, domain1 :: String, domain2 :: String, + federationV0Domain :: String, dynamicDomains :: [String], defaultAPIVersion :: Int, manager :: HTTP.Manager, diff --git a/services/integration.yaml b/services/integration.yaml index 65543e45f10..00d54a5efa3 100644 --- a/services/integration.yaml +++ b/services/integration.yaml @@ -142,3 +142,42 @@ rabbitmq: cassandra: host: 127.0.0.1 port: 9042 + +federation-v0: + originDomain: federation-v0.example.com + brig: + host: 127.0.0.1 + port: 21082 + cannon: + host: 127.0.0.1 + port: 21083 + cargohold: + host: 127.0.0.1 + port: 21084 + federatorInternal: + host: 127.0.0.1 + port: 21097 + federatorExternal: + host: 127.0.0.1 + port: 21098 + galley: + host: 127.0.0.1 + port: 21085 + gundeck: + host: 127.0.0.1 + port: 21086 + nginz: + host: 127.0.0.1 + port: 21080 + spar: + host: 127.0.0.1 + port: 21088 + proxy: + host: 127.0.0.1 + port: 21087 + backgroundWorker: + host: 127.0.0.1 + port: 21089 + stern: + host: 127.0.0.1 + port: 21091 From cfce2e7bc479f4c2925fc67221474812769843ce Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 20 Feb 2024 14:56:24 +0100 Subject: [PATCH 008/117] WPB-6190 Backend should validate display name during DPoP challenge (#3890) --- changelog.d/2-features/WPB-6190 | 1 + libs/jwt-tools/src/Data/Jwt/Tools.hs | 21 ++++++++-- libs/jwt-tools/test/Spec.hs | 42 +++++++++++++++---- nix/pkgs/rusty_jwt_tools_ffi/default.nix | 9 ++-- nix/sources.json | 8 ++-- services/brig/src/Brig/API/Client.hs | 6 ++- services/brig/src/Brig/API/Error.hs | 2 + services/brig/src/Brig/API/Types.hs | 1 + services/brig/src/Brig/Effects/JwtTools.hs | 6 ++- .../brig/test/integration/API/User/Client.hs | 14 ++++++- 10 files changed, 86 insertions(+), 24 deletions(-) create mode 100644 changelog.d/2-features/WPB-6190 diff --git a/changelog.d/2-features/WPB-6190 b/changelog.d/2-features/WPB-6190 new file mode 100644 index 00000000000..619ef8af4ed --- /dev/null +++ b/changelog.d/2-features/WPB-6190 @@ -0,0 +1 @@ +Backend validates display name during DPoP challenge diff --git a/libs/jwt-tools/src/Data/Jwt/Tools.hs b/libs/jwt-tools/src/Data/Jwt/Tools.hs index 3e2804db8e4..a38cc02c9fd 100644 --- a/libs/jwt-tools/src/Data/Jwt/Tools.hs +++ b/libs/jwt-tools/src/Data/Jwt/Tools.hs @@ -34,6 +34,7 @@ module Data.Jwt.Tools NowEpoch (..), PemBundle (..), Handle (..), + DisplayName (..), TeamId (..), ) where @@ -74,12 +75,15 @@ type EpochWord64 = Word64 type BackendBundleCStr = CString +type DisplayNameCStr = CString + foreign import ccall unsafe "generate_dpop_access_token" generate_dpop_access_token :: ProofCStr -> UserIdCStr -> ClientIdWord64 -> HandleCStr -> + DisplayNameCStr -> TeamIdCStr -> DomainCStr -> NonceCStr -> @@ -102,6 +106,7 @@ generateDpopAccessTokenFfi :: UserIdCStr -> ClientIdWord64 -> HandleCStr -> + DisplayNameCStr -> TeamIdCStr -> DomainCStr -> NonceCStr -> @@ -112,8 +117,8 @@ generateDpopAccessTokenFfi :: EpochWord64 -> BackendBundleCStr -> IO (Maybe (Ptr HsResult)) -generateDpopAccessTokenFfi dpopProof user client handle tid domain nonce uri method maxSkewSecs expiration now backendKeys = do - ptr <- generate_dpop_access_token dpopProof user client handle tid domain nonce uri method maxSkewSecs expiration now backendKeys +generateDpopAccessTokenFfi dpopProof user client handle displayName tid domain nonce uri method maxSkewSecs expiration now backendKeys = do + ptr <- generate_dpop_access_token dpopProof user client handle displayName tid domain nonce uri method maxSkewSecs expiration now backendKeys if ptr /= nullPtr then pure $ Just ptr else pure Nothing @@ -138,6 +143,7 @@ generateDpopToken :: UserId -> ClientId -> Handle -> + DisplayName -> TeamId -> Domain -> Nonce -> @@ -148,10 +154,11 @@ generateDpopToken :: NowEpoch -> PemBundle -> ExceptT DPoPTokenGenerationError m ByteString -generateDpopToken dpopProof uid cid handle tid domain nonce uri method maxSkewSecs maxExpiration now backendPubkeyBundle = do +generateDpopToken dpopProof uid cid handle displayName tid domain nonce uri method maxSkewSecs maxExpiration now backendPubkeyBundle = do dpopProofCStr <- toCStr dpopProof uidCStr <- toCStr uid handleCStr <- toCStr handle + displayNameCStr <- toCStr displayName tidCStr <- toCStr tid domainCStr <- toCStr domain nonceCStr <- toCStr nonce @@ -165,6 +172,7 @@ generateDpopToken dpopProof uid cid handle tid domain nonce uri method maxSkewSe -- traceM $ "nonce = Nonce " <> show (_unNonce nonce) -- traceM $ "expires = ExpiryEpoch " <> show (_unExpiryEpoch maxExpiration) -- traceM $ "handle = Handle " <> show (_unHandle handle) + -- traceM $ "displayName = DisplayName " <> show (_unDisplayName displayName) -- traceM $ "tid = TeamId " <> show (_unTeamId tid) let before = @@ -173,6 +181,7 @@ generateDpopToken dpopProof uid cid handle tid domain nonce uri method maxSkewSe uidCStr (_unClientId cid) handleCStr + displayNameCStr tidCStr domainCStr nonceCStr @@ -273,6 +282,10 @@ newtype PemBundle = PemBundle {_unPemBundle :: ByteString} deriving (Eq, Show) deriving newtype (ToByteString) +newtype DisplayName = DisplayName {_unDisplayName :: ByteString} + deriving (Eq, Show) + deriving newtype (ToByteString) + data DPoPTokenGenerationError = NoError | -- | Unmapped error @@ -359,4 +372,6 @@ data DPoPTokenGenerationError DpopHandleMismatch | -- Client team does not match the supplied team DpopTeamMismatch + | -- Client display name does not match the supplied display name + DpopDisplayNameMismatch deriving (Eq, Show, Generic, Bounded, Enum) diff --git a/libs/jwt-tools/test/Spec.hs b/libs/jwt-tools/test/Spec.hs index ac3bbfd3aca..2e0afb3dc13 100644 --- a/libs/jwt-tools/test/Spec.hs +++ b/libs/jwt-tools/test/Spec.hs @@ -25,7 +25,7 @@ main :: IO () main = hspec $ do describe "generateDpopToken FFI when passing valid inputs" $ do it "should return an access token with the correct header" $ do - actual <- runExceptT $ generateDpopToken proof uid cid handle tid domain nonce uri method maxSkewSecs expires now pem + actual <- runExceptT $ generateDpopToken proof uid cid handle displayName tid domain nonce uri method maxSkewSecs expires now pem -- The actual payload of the DPoP token is not deterministic as it depends on the current time. -- We therefore only check the header, because if the header is correct, it means the token creation was successful.s let expectedHeader = "eyJhbGciOiJFZERTQSIsInR5cCI6ImF0K2p3dCIsImp3ayI6eyJrdHkiOiJPS1AiLCJjcnYiOiJFZDI1NTE5IiwieCI6ImRZSTM4VWR4a3NDMEs0UXg2RTlKSzlZZkdtLWVoblkxOG9LbUhMMllzWmsifX0" @@ -33,7 +33,7 @@ main = hspec $ do actualHeader `shouldBe` expectedHeader describe "generateDpopToken FFI when passing a wrong nonce value" $ do it "should return BackendNonceMismatchError" $ do - actual <- runExceptT $ generateDpopToken proof uid cid handle tid domain (Nonce "foobar") uri method maxSkewSecs expires now pem + actual <- runExceptT $ generateDpopToken proof uid cid handle displayName tid domain (Nonce "foobar") uri method maxSkewSecs expires now pem actual `shouldBe` Left BackendNonceMismatchError describe "toResult" $ do it "should convert to correct error" $ do @@ -74,15 +74,41 @@ main = hspec $ do toResult (Just 17) (Just token) `shouldBe` Left ExpMismatchError toResult (Just 18) Nothing `shouldBe` Left Expired toResult (Just 18) (Just token) `shouldBe` Left Expired + toResult (Just 19) (Just token) `shouldBe` Left InvalidUserId + toResult (Just 20) (Just token) `shouldBe` Left NotYetValid + toResult (Just 21) (Just token) `shouldBe` Left JwtSimpleError + toResult (Just 22) (Just token) `shouldBe` Left RandError + toResult (Just 23) (Just token) `shouldBe` Left Sec1Error + toResult (Just 24) (Just token) `shouldBe` Left UrlParseError + toResult (Just 25) (Just token) `shouldBe` Left UuidError + toResult (Just 26) (Just token) `shouldBe` Left Utf8Error + toResult (Just 27) (Just token) `shouldBe` Left Base64DecodeError + toResult (Just 28) (Just token) `shouldBe` Left JsonError + toResult (Just 29) (Just token) `shouldBe` Left InvalidJsonPath + toResult (Just 30) (Just token) `shouldBe` Left JsonPathError + toResult (Just 31) (Just token) `shouldBe` Left InvalidJwkThumbprint + toResult (Just 32) (Just token) `shouldBe` Left MissingDpopHeader + toResult (Just 33) (Just token) `shouldBe` Left MissingIssuer + toResult (Just 34) (Just token) `shouldBe` Left DpopChallengeMismatch + toResult (Just 35) (Just token) `shouldBe` Left DpopHtuMismatch + toResult (Just 36) (Just token) `shouldBe` Left DpopHtmMismatch + toResult (Just 37) (Just token) `shouldBe` Left InvalidBackendKeys + toResult (Just 38) (Just token) `shouldBe` Left InvalidClientId + toResult (Just 39) (Just token) `shouldBe` Left UnsupportedApiVersion + toResult (Just 40) (Just token) `shouldBe` Left UnsupportedScope + toResult (Just 41) (Just token) `shouldBe` Left DpopHandleMismatch + toResult (Just 42) (Just token) `shouldBe` Left DpopTeamMismatch + toResult (Just 43) (Just token) `shouldBe` Left DpopDisplayNameMismatch toResult Nothing Nothing `shouldBe` Left UnknownError where token = "" - proof = Proof "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im5MSkdOLU9hNkpzcTNLY2xaZ2dMbDdVdkFWZG1CMFE2QzNONUJDZ3BoSHcifSwidHlwIjoiZHBvcCtqd3QifQ.eyJhdWQiOiJodHRwczovL3dpcmUuY29tL2FjbWUvY2hhbGxlbmdlL2FiY2QiLCJjaGFsIjoid2EyVnJrQ3RXMXNhdUoyRDN1S1k4cmM3eTRrbDR1c0giLCJleHAiOjE4MzE3MzcyNzEsImhhbmRsZSI6IndpcmVhcHA6Ly8lNDB2bHVwZHlwbml4dm1vdnZzeW1ndHdAZXhhbXBsZS5jb20iLCJodG0iOiJQT1NUIiwiaHR1IjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9jbGllbnRzL2NjNmU2NDBlMjk2ZThiYmEvYWNjZXNzLXRva2VuIiwiaWF0IjoxNzA1NTkzMjcxLCJqdGkiOiI2ZmM1OWU3Zi1iNjY2LTRmZmMtYjczOC00ZjQ3NjBjODg0Y2EiLCJuYmYiOjE3MDU1OTMyNzEsIm5vbmNlIjoibVJDdjNKQS1TNDI0dUJyLVk2QzFndyIsInN1YiI6IndpcmVhcHA6Ly9WNVc3ZnRNeVRJNlBNYlE0Y3ZkazRnIWNjNmU2NDBlMjk2ZThiYmFAZXhhbXBsZS5jb20iLCJ0ZWFtIjoiZmZhODY1ZmEtYjI0YS00Njk3LWFhMDUtMWZjM2YzNjU0ZGI5In0.BVdawX_84Mpmvzbs3v52t3GtCgSKzxgnFDkwf4QK6AusoyfsjhK6grs9GLEe2Lfb1eDrBUJgo-nobeIWmRumBQ" - uid = UserId "5795bb7e-d332-4c8e-8f31-b43872f764e2" - nonce = Nonce "mRCv3JA-S424uBr-Y6C1gw" - expires = ExpiryEpoch 1831823671 - handle = Handle "vlupdypnixvmovvsymgtw" - tid = TeamId "ffa865fa-b24a-4697-aa05-1fc3f3654db9" + proof = Proof "eyJhbGciOiJFZERTQSIsImp3ayI6eyJjcnYiOiJFZDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Im5MSkdOLU9hNkpzcTNLY2xaZ2dMbDdVdkFWZG1CMFE2QzNONUJDZ3BoSHcifSwidHlwIjoiZHBvcCtqd3QifQ.eyJhdWQiOiJodHRwczovL3dpcmUuY29tL2FjbWUvY2hhbGxlbmdlL2FiY2QiLCJjaGFsIjoid2EyVnJrQ3RXMXNhdUoyRDN1S1k4cmM3eTRrbDR1c0giLCJleHAiOjE3Mzk4ODA2NzQsImhhbmRsZSI6IndpcmVhcHA6Ly8lNDB5d2Z5ZG5pZ2Jud2h1b3pldGphZ3FAZXhhbXBsZS5jb20iLCJodG0iOiJQT1NUIiwiaHR1IjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9jbGllbnRzL2NjNmU2NDBlMjk2ZThiYmEvYWNjZXNzLXRva2VuIiwiaWF0IjoxNzA4MzQ0Njc0LCJqdGkiOiI2ZmM1OWU3Zi1iNjY2LTRmZmMtYjczOC00ZjQ3NjBjODg0Y2EiLCJuYW1lIjoi5reB4qqu5KSq5rK255Kh4bKV6re14Y2q6omE6Jy16Iu17ICV54Kb66-v56qp5KqW766M6bGw6oOy6b6m57m15pWJ4LqH54et6rOj54KHIiwibmJmIjoxNzA4MzQ0Njc0LCJub25jZSI6IllWZ2dHdWlTUTZlamhQNTNFX0tPS3ciLCJzdWIiOiJ3aXJlYXBwOi8vSWZ0VzBLeFVSb2F1QWVockRremJiQSFjYzZlNjQwZTI5NmU4YmJhQGV4YW1wbGUuY29tIiwidGVhbSI6ImMxNTE5NzVlLWIxOTMtNDAwOS1hM2QyLTc0N2M5NjFmMjMzMyJ9.SHxpMzOe2yC3y6DP7lEH0l7_eOKrUZZI0OjgtnCKjO4OBD0XqKOi0y_z07-7FWc-KtThlsaZatnBNTB67GhQBw" + uid = UserId "21fb56d0-ac54-4686-ae01-e86b0e4cdb6c" + nonce = Nonce "YVggGuiSQ6ejhP53E_KOKw" + expires = ExpiryEpoch 1739967074 + handle = Handle "ywfydnigbnwhuozetjagq" + displayName = DisplayName "\230\183\129\226\170\174\228\164\170\230\178\182\231\146\161\225\178\149\234\183\181\225\141\170\234\137\132\232\156\181\232\139\181\236\128\149\231\130\155\235\175\175\231\170\169\228\170\150\239\174\140\233\177\176\234\131\178\233\190\166\231\185\181\230\149\137\224\186\135\231\135\173\234\179\163\231\130\135" + tid = TeamId "c151975e-b193-4009-a3d2-747c961f2333" now = NowEpoch 1704982162 cid = ClientId 14730821443162901434 diff --git a/nix/pkgs/rusty_jwt_tools_ffi/default.nix b/nix/pkgs/rusty_jwt_tools_ffi/default.nix index 71f25388d01..32e735bc849 100644 --- a/nix/pkgs/rusty_jwt_tools_ffi/default.nix +++ b/nix/pkgs/rusty_jwt_tools_ffi/default.nix @@ -10,12 +10,12 @@ # Cargo.lock file in its root (not at the ffi/ subpath). let - version = "0.8.5"; + version = "0.9.0"; src = fetchFromGitHub { owner = "wireapp"; repo = "rusty-jwt-tools"; - rev = "99acb427b2169d726f356d30dec55eae83dda6b6"; - sha256 = "sha256-x1W79spOZeFHabRbhMksz6gLtRIpl2E7WCiXuzIMoFM="; + rev = "60424bf7031e2fa535aac658d0b5643624d19537"; + sha256 = "sha256-kdubK9FruZT8pbIwCHyAkxYj9yVM0q7ivNhNUNtNQCY="; }; cargoLockFile = builtins.toFile "cargo.lock" (builtins.readFile "${src}/Cargo.lock"); @@ -29,9 +29,8 @@ rustPlatform.buildRustPackage { outputHashes = { # if any of these need updating, replace / create new key with # lib.fakeSha256, rebuild, and replace with actual hash. - "certval-0.1.4" = "sha256-mUg3Kx1I/r9zBoB7tDaZsykFkE+tsN+Rem6DjUOZbuU="; + "certval-0.1.4" = "sha256-gzkRC7/u/rARGPy3d37eBrAVml4XSDb6bRPpsESmttY="; "jwt-simple-0.12.1" = "sha256-5PAOwulL8j6f4Ycoa5Q+1dqEA24uN8rJt+i2RebL6eo="; - "x509-ocsp-0.2.1" = "sha256-o+r9h0CcexWqJIIoZdOgSd7hWIb91BheW6UZI98RpLA="; }; }; diff --git a/nix/sources.json b/nix/sources.json index d885b9b44cc..207225e566b 100644 --- a/nix/sources.json +++ b/nix/sources.json @@ -24,15 +24,15 @@ "url_template": "https://github.com///archive/.tar.gz" }, "nixpkgs-cargo": { - "branch": "nixpkgs-unstable", + "branch": "master", "description": "Nix Packages collection", "homepage": "https://github.com/NixOS/nixpkgs", "owner": "NixOS", "repo": "nixpkgs", - "rev": "01441e14af5e29c9d27ace398e6dd0b293e25a54", - "sha256": "0yvkamjbk3aj4lvhm6vdgdk4b2j0xdv3gx9n4p7wfky52j2529dy", + "rev": "e236b838c71d2aff275356ade8104bbdef422117", + "sha256": "0zjf6b9pz3ljinwb2qxhmpix1mgiv4vakcqci7bcy5a6sv1sj1xs", "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/01441e14af5e29c9d27ace398e6dd0b293e25a54.tar.gz", + "url": "https://github.com/NixOS/nixpkgs/archive/e236b838c71d2aff275356ade8104bbdef422117.tar.gz", "url_template": "https://github.com///archive/.tar.gz" } } diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 948bd3f2a6c..9eeda792e29 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -532,12 +532,13 @@ createAccessToken :: createAccessToken luid cid method link proof = do let domain = tDomain luid let uid = tUnqualified luid - (tid, handle) <- do + (tid, handle, displayName) <- do mUser <- lift $ wrapClient (Data.lookupUser NoPendingInvitations uid) except $ - (,) + (,,) <$> note NotATeamUser (userTeam =<< mUser) <*> note MissingHandle (userHandle =<< mUser) + <*> note MissingName (userDisplayName <$> mUser) nonce <- ExceptT $ note NonceNotFound <$> wrapClient (Nonce.lookupAndDeleteNonce uid (cs $ toByteString cid)) httpsUrl <- except $ note MisconfiguredRequestUrl $ fromByteString $ "https://" <> toByteString' domain <> "/" <> cs (toUrlPiece link) maxSkewSeconds <- Opt.setDpopMaxSkewSecs <$> view settings @@ -554,6 +555,7 @@ createAccessToken luid cid method link proof = do proof (ClientIdentity domain uid cid) handle + displayName tid nonce httpsUrl diff --git a/services/brig/src/Brig/API/Error.hs b/services/brig/src/Brig/API/Error.hs index d1758a3dd09..14cef9c4be8 100644 --- a/services/brig/src/Brig/API/Error.hs +++ b/services/brig/src/Brig/API/Error.hs @@ -220,12 +220,14 @@ certEnrollmentError (RustError UnsupportedApiVersion) = StdError $ Wai.mkError s certEnrollmentError (RustError UnsupportedScope) = StdError $ Wai.mkError status400 "unsupported-scope" "Bubbling up errors" certEnrollmentError (RustError DpopHandleMismatch) = StdError $ Wai.mkError status400 "dpop-handle-mismatch" "Bubbling up errors" certEnrollmentError (RustError DpopTeamMismatch) = StdError $ Wai.mkError status400 "dpop-team-mismatch" "Bubbling up errors" +certEnrollmentError (RustError DpopDisplayNameMismatch) = StdError $ Wai.mkError status400 "dpop-display-name-mismatch" "Bubbling up errors" certEnrollmentError NonceNotFound = StdError $ Wai.mkError status400 "client-token-bad-nonce" "The client sent an unacceptable anti-replay nonce" certEnrollmentError MisconfiguredRequestUrl = StdError $ Wai.mkError status500 "misconfigured-request-url" "The request url cannot be derived from optSettings.setFederationDomain in brig.yaml" certEnrollmentError KeyBundleError = StdError $ Wai.mkError status404 "no-server-key-bundle" "The key bundle required for the certificate enrollment process could not be found" certEnrollmentError ClientIdSyntaxError = StdError $ Wai.mkError status400 "client-token-id-parse-error" "The client id could not be parsed" certEnrollmentError NotATeamUser = StdError $ Wai.mkError status400 "not-a-team-user" "The user is not a team user" certEnrollmentError MissingHandle = StdError $ Wai.mkError status400 "missing-handle" "The user has no handle" +certEnrollmentError MissingName = StdError $ Wai.mkError status400 "missing-name" "The user has no name" fedError :: FederationError -> Error fedError = StdError . federationErrorToWai diff --git a/services/brig/src/Brig/API/Types.hs b/services/brig/src/Brig/API/Types.hs index bdc0a3548e7..721ec2cde36 100644 --- a/services/brig/src/Brig/API/Types.hs +++ b/services/brig/src/Brig/API/Types.hs @@ -214,6 +214,7 @@ data CertEnrollmentError | ClientIdSyntaxError | NotATeamUser | MissingHandle + | MissingName ------------------------------------------------------------------------------- -- Exceptions diff --git a/services/brig/src/Brig/Effects/JwtTools.hs b/services/brig/src/Brig/Effects/JwtTools.hs index f31329c5aa1..e6304fb90b4 100644 --- a/services/brig/src/Brig/Effects/JwtTools.hs +++ b/services/brig/src/Brig/Effects/JwtTools.hs @@ -19,6 +19,7 @@ import Polysemy import Wire.API.MLS.Credential (ClientIdentity (..)) import Wire.API.MLS.Epoch (Epoch (..)) import Wire.API.User.Client.DPoPAccessToken (DPoPAccessToken (..), Proof (..)) +import Wire.API.User.Profile (Name (..)) data JwtTools m a where GenerateDPoPAccessToken :: @@ -30,6 +31,8 @@ data JwtTools m a where ClientIdentity -> -- | The user's handle Handle -> + -- The user's display name + Name -> -- | The user's team ID TeamId -> -- | The most recent DPoP nonce provided by the backend to the current client @@ -52,7 +55,7 @@ makeSem ''JwtTools interpretJwtTools :: Member (Embed IO) r => Sem (JwtTools ': r) a -> Sem r a interpretJwtTools = interpret $ \case - GenerateDPoPAccessToken proof cid handle tid nonce uri method skew ex now pem -> + GenerateDPoPAccessToken proof cid handle displayName tid nonce uri method skew ex now pem -> mapLeft RustError <$> runExceptT ( DPoPAccessToken @@ -61,6 +64,7 @@ interpretJwtTools = interpret $ \case (Jwt.UserId (toByteString' (ciUser cid))) (Jwt.ClientId (clientToWord64 (ciClient cid))) (Jwt.Handle (toByteString' (urlEncode (fromHandle (handle))))) + (Jwt.DisplayName (toByteString' (fromName displayName))) (Jwt.TeamId (toByteString' tid)) (Jwt.Domain (toByteString' (ciDomain cid))) (Jwt.Nonce (toByteString' nonce)) diff --git a/services/brig/test/integration/API/User/Client.hs b/services/brig/test/integration/API/User/Client.hs index 067a2bc641d..ec3e9d35052 100644 --- a/services/brig/test/integration/API/User/Client.hs +++ b/services/brig/test/integration/API/User/Client.hs @@ -1395,6 +1395,7 @@ data DPoPClaimsSet = DPoPClaimsSet claimHtu :: Text, claimChal :: Text, claimHandle :: Text, + claimDisplayName :: Text, claimTeamId :: Text } deriving (Eq, Show, Generic) @@ -1411,6 +1412,7 @@ instance A.FromJSON DPoPClaimsSet where <*> o A..: "htu" <*> o A..: "chal" <*> o A..: "handle" + <*> o A..: "name" <*> o A..: "team" instance A.ToJSON DPoPClaimsSet where @@ -1420,6 +1422,7 @@ instance A.ToJSON DPoPClaimsSet where & ins "htu" (claimHtu s) & ins "chal" (claimChal s) & ins "handle" (claimHandle s) + & ins "name" (claimDisplayName s) & ins "team" (claimTeamId s) where ins k v (Object o) = Object $ M.insert k (A.toJSON v) o @@ -1456,7 +1459,16 @@ testCreateAccessToken opts n brig = do & claimSub ?~ fromMaybe (error "invalid sub claim") ((clientIdentity :: Text) ^? stringOrUri) & claimJti ?~ "6fc59e7f-b666-4ffc-b738-4f4760c884ca" & claimAud ?~ (maybe (error "invalid sub claim") (Audience . (: [])) (("https://wire.com/acme/challenge/abcd" :: Text) ^? stringOrUri)) - let dpopClaims = DPoPClaimsSet claimsSet' nonceBs "POST" httpsUrl "wa2VrkCtW1sauJ2D3uKY8rc7y4kl4usH" handle (UUID.toText (toUUID tid)) + let dpopClaims = + DPoPClaimsSet + claimsSet' + nonceBs + "POST" + httpsUrl + "wa2VrkCtW1sauJ2D3uKY8rc7y4kl4usH" + handle + (fromName u.userDisplayName) + (UUID.toText (toUUID tid)) signedOrError <- fmap encodeCompact <$> liftIO (signAccessToken dpopClaims) case signedOrError of Left err -> liftIO $ assertFailure $ "failed to sign claims: " <> show err From 68595c3986654ba9044723eb56631bb40eac0389 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Wed, 21 Feb 2024 15:25:47 +0100 Subject: [PATCH 009/117] Patch hole in scim docs regarding wire team role manipulation. (#3897) --- changelog.d/4-docs/wpb-6780-patch-hole-in-scim-docs | 1 + .../src/understand/single-sign-on/trouble-shooting.md | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 changelog.d/4-docs/wpb-6780-patch-hole-in-scim-docs diff --git a/changelog.d/4-docs/wpb-6780-patch-hole-in-scim-docs b/changelog.d/4-docs/wpb-6780-patch-hole-in-scim-docs new file mode 100644 index 00000000000..360c264655d --- /dev/null +++ b/changelog.d/4-docs/wpb-6780-patch-hole-in-scim-docs @@ -0,0 +1 @@ +Patch hole in scim docs regarding wire team role manipulation. \ No newline at end of file diff --git a/docs/src/understand/single-sign-on/trouble-shooting.md b/docs/src/understand/single-sign-on/trouble-shooting.md index 776446be79f..59337fec96c 100644 --- a/docs/src/understand/single-sign-on/trouble-shooting.md +++ b/docs/src/understand/single-sign-on/trouble-shooting.md @@ -313,7 +313,16 @@ in your wire team: mapped on wire's email address, and provisioning works like in the team management app with invitation emails. -This means that if you use email/password authentication, you **must** +5. SCIM's `roles` is mapped to team role. Only lists of length 0 or 1 + are allowed. Valid values are: + + - `[member]` (same as `[]`, `null`, or missing field) + - `[admin]` + - `[owner]` + - `[partner]` + +The mapping of `externalId` implies that if you use email/password +authentication, you **must** map an email address to `externalId` on your side. With `userName` and `displayName`, you are more flexible. From 769b68f42e2e9be622470b5cf5c7d74dd1abeeca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Thu, 22 Feb 2024 15:27:53 +0100 Subject: [PATCH 010/117] [WPB-6144] Prevent MLS one-to-one messaging for a blocking user (#3889) * Test: no MLS 1-to-1 when a connection is blocked * Test: a test with expected behavior after blocking the connection * Check if sending a msg to 1-to-1 and not connected * Add a changelog * WIP: Debugging a test failure * Update the confirming test * Revert "Check if sending a msg to 1-to-1 and not connected" This reverts commit c4af1508ecf4eac88db83b71d1af35024a7a7de1. * WIP: generalise the Update.blockConv handler * Connections: Also block MLS one2one conv when blocking conn * Test: Parameterise over One2OneScenario * Add the missing connection ID in an internal endpoint * Wrap a function comment for readability * Introduce a Galley internal endpoint: blocking a qualified conversation * WIP: Check if an MLS 1-1 conv exists before blocking What is left to do is to make this check work for an MLS 1-1 conv that can be remote * Make upsertOne2OneConv always take a Conv ID Brig can determine this ID based on protocol of the conversation or read it from the DB. Inventing this in galley causes more trouble for having two One2One convs for proteus and mls. * WIP: Remove user from 1:1 MLS conv when they block someone * WIP: Remove mls clients on connection block * fixup! WIP: Remove mls clients on connection block * Make sure 1-1 conv is established before updating * Finalise the bug-confirming test * Remove debugging output from application code * Fix a changelog * Remove redundant constraints * Properly check if an MLS 1-1 conversation exists before blocking it * Remove more of unused code * Remove an unused connection ID in an internal Galley endpoint for blocking a conv --------- Co-authored-by: Akshay Mankar --- .../wpb-6144-messaging-blocked-user | 1 + integration/test/Test/MLS/One2One.hs | 47 +++++++++ integration/test/Testlib/Cannon.hs | 3 +- .../src/Wire/API/Routes/Internal/Galley.hs | 25 ++++- .../Internal/Galley/ConversationsIntra.hs | 27 +----- services/brig/src/Brig/API/Connection.hs | 27 ++++-- .../brig/src/Brig/API/Connection/Remote.hs | 96 ++++++++++++------- services/brig/src/Brig/API/Federation.hs | 3 +- services/brig/src/Brig/API/Public.hs | 6 +- .../brig/src/Brig/Effects/GalleyProvider.hs | 4 + .../src/Brig/Effects/GalleyProvider/RPC.hs | 40 ++++++++ services/brig/src/Brig/IO/Intra.hs | 42 ++++---- services/galley/src/Galley/API/Internal.hs | 2 + services/galley/src/Galley/API/One2One.hs | 22 ++--- services/galley/src/Galley/API/Update.hs | 30 +++++- .../Galley/Cassandra/Conversation/Members.hs | 6 ++ .../galley/src/Galley/Cassandra/Queries.hs | 3 + .../galley/src/Galley/Effects/MemberStore.hs | 2 + services/galley/test/integration/API.hs | 13 +-- .../galley/test/integration/API/Federation.hs | 7 +- services/galley/test/integration/API/Util.hs | 17 ++-- 21 files changed, 285 insertions(+), 138 deletions(-) create mode 100644 changelog.d/3-bug-fixes/wpb-6144-messaging-blocked-user diff --git a/changelog.d/3-bug-fixes/wpb-6144-messaging-blocked-user b/changelog.d/3-bug-fixes/wpb-6144-messaging-blocked-user new file mode 100644 index 00000000000..44b986f57ed --- /dev/null +++ b/changelog.d/3-bug-fixes/wpb-6144-messaging-blocked-user @@ -0,0 +1 @@ +Do not deliver MLS one-to-one conversation messages to a user that blocked the sender diff --git a/integration/test/Test/MLS/One2One.hs b/integration/test/Test/MLS/One2One.hs index ccd8365477e..271f5ee9807 100644 --- a/integration/test/Test/MLS/One2One.hs +++ b/integration/test/Test/MLS/One2One.hs @@ -17,6 +17,7 @@ module Test.MLS.One2One where +import API.Brig import API.Galley import qualified Data.ByteString.Base64 as Base64 import qualified Data.ByteString.Char8 as B8 @@ -54,6 +55,52 @@ testGetMLSOne2OneUnconnected otherDomain = do bindResponse (getMLSOne2OneConversation alice bob) $ \resp -> resp.status `shouldMatchInt` 403 +testMLSOne2OneBlocked :: HasCallStack => Domain -> App () +testMLSOne2OneBlocked otherDomain = do + [alice, bob] <- for [OwnDomain, otherDomain] $ flip randomUser def + void $ postConnection bob alice >>= getBody 201 + void $ putConnection alice bob "blocked" >>= getBody 200 + void $ getMLSOne2OneConversation alice bob >>= getJSON 403 + void $ getMLSOne2OneConversation bob alice >>= getJSON 403 + +-- | Alice and Bob are initially connected, but then Alice blocks Bob. +testMLSOne2OneBlockedAfterConnected :: HasCallStack => One2OneScenario -> App () +testMLSOne2OneBlockedAfterConnected scenario = do + alice <- randomUser OwnDomain def + let otherDomain = one2OneScenarioDomain scenario + convDomain = one2OneScenarioConvDomain scenario + bob <- createMLSOne2OnePartner otherDomain alice convDomain + conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + convId <- conv %. "qualified_id" + do + bobConv <- getMLSOne2OneConversation bob alice >>= getJSON 200 + convId `shouldMatch` (bobConv %. "qualified_id") + + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] + traverse_ uploadNewKeyPackage [bob1] + resetGroup alice1 conv + commit <- createAddCommit alice1 [bob] + withWebSocket bob1 $ \ws -> do + void $ sendAndConsumeCommitBundle commit + let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" + n <- awaitMatch isMessage ws + nPayload n %. "data" `shouldMatch` B8.unpack (Base64.encode (fold commit.welcome)) + + withWebSocket bob1 $ \ws -> do + -- Alice blocks Bob + void $ putConnection alice bob "blocked" >>= getBody 200 + -- There is also a proteus 1-to-1 conversation. Neither it nor the MLS + -- 1-to-1 conversation should get any events. + awaitAnyEvent 2 ws `shouldMatch` (Nothing :: Maybe Value) + -- Alice is not in the MLS 1-to-1 conversation given that she has blocked + -- Bob. + void $ getMLSOne2OneConversation alice bob >>= getJSON 403 + + mp <- createApplicationMessage bob1 "hello, world, again" + withWebSocket alice1 $ \ws -> do + void $ postMLSMessage mp.sender mp.message >>= getJSON 201 + awaitAnyEvent 2 ws `shouldMatch` (Nothing :: Maybe Value) + testGetMLSOne2OneSameTeam :: App () testGetMLSOne2OneSameTeam = do (alice, _, _) <- createTeam OwnDomain 1 diff --git a/integration/test/Testlib/Cannon.hs b/integration/test/Testlib/Cannon.hs index 2eb1be2be7f..8ab338df38a 100644 --- a/integration/test/Testlib/Cannon.hs +++ b/integration/test/Testlib/Cannon.hs @@ -28,6 +28,7 @@ module Testlib.Cannon awaitNMatchesResult, awaitNMatches, awaitMatch, + awaitAnyEvent, awaitAtLeastNMatchesResult, awaitAtLeastNMatches, awaitNToMMatchesResult, @@ -282,7 +283,7 @@ printAwaitResult = prettyAwaitResult >=> liftIO . putStrLn printAwaitAtLeastResult :: AwaitAtLeastResult -> App () printAwaitAtLeastResult = prettyAwaitAtLeastResult >=> liftIO . putStrLn -awaitAnyEvent :: MonadIO m => Int -> WebSocket -> m (Maybe Value) +awaitAnyEvent :: Int -> WebSocket -> App (Maybe Value) awaitAnyEvent tSecs = liftIO . timeout (tSecs * 1000 * 1000) . atomically . readTChan . wsChan -- | 'await' an expected number of notification events on the websocket that diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs index d2f435e4a97..b6be1bade6a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -42,6 +42,7 @@ import Wire.API.Routes.Named import Wire.API.Routes.Public import Wire.API.Routes.Public.Galley.Conversation import Wire.API.Routes.Public.Galley.Feature +import Wire.API.Routes.QualifiedCapture import Wire.API.Team import Wire.API.Team.Feature import Wire.API.Team.Member @@ -256,7 +257,7 @@ type InternalAPIBase = :> "one2one" :> "upsert" :> ReqBody '[Servant.JSON] UpsertOne2OneConversationRequest - :> Post '[Servant.JSON] UpsertOne2OneConversationResponse + :> MultiVerb1 'POST '[Servant.JSON] (RespondEmpty 200 "Upsert One2One Policy") ) :<|> IFeatureAPI :<|> IFederationAPI @@ -492,7 +493,7 @@ type IConversationAPI = :> Put '[Servant.JSON] Conversation ) :<|> Named - "conversation-block" + "conversation-block-unqualified" ( CanThrow 'InvalidOperation :> CanThrow 'ConvNotFound :> ZUser @@ -501,6 +502,16 @@ type IConversationAPI = :> "block" :> Put '[Servant.JSON] () ) + :<|> Named + "conversation-block" + ( CanThrow 'InvalidOperation + :> CanThrow 'ConvNotFound + :> ZLocalUser + :> "conversations" + :> QualifiedCapture "cnv" ConvId + :> "block" + :> Put '[Servant.JSON] () + ) -- This endpoint can lead to the following events being sent: -- - MemberJoin event to you, if the conversation existed and had < 2 members before -- - MemberJoin event to other, if the conversation existed and only the other was member @@ -524,6 +535,16 @@ type IConversationAPI = :> "meta" :> Get '[Servant.JSON] ConversationMetadata ) + :<|> Named + "conversation-mls-one-to-one" + ( CanThrow 'NotConnected + :> CanThrow 'MLSNotEnabled + :> "conversations" + :> "mls-one2one" + :> ZLocalUser + :> QualifiedCapture "user" UserId + :> Get '[Servant.JSON] Conversation + ) swaggerDoc :: OpenApi swaggerDoc = diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley/ConversationsIntra.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/ConversationsIntra.hs index b644906cd95..a25baa28b23 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley/ConversationsIntra.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley/ConversationsIntra.hs @@ -15,16 +15,9 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Wire.API.Routes.Internal.Galley.ConversationsIntra - ( DesiredMembership (..), - Actor (..), - UpsertOne2OneConversationRequest (..), - UpsertOne2OneConversationResponse (..), - ) -where +module Wire.API.Routes.Internal.Galley.ConversationsIntra where -import Data.Aeson qualified as A -import Data.Aeson.Types (FromJSON, ToJSON) +import Data.Aeson (FromJSON, ToJSON) import Data.Id (ConvId, UserId) import Data.OpenApi qualified as Swagger import Data.Qualified @@ -60,7 +53,7 @@ data UpsertOne2OneConversationRequest = UpsertOne2OneConversationRequest uooRemoteUser :: Remote UserId, uooActor :: Actor, uooActorDesiredMembership :: DesiredMembership, - uooConvId :: Maybe (Qualified ConvId) + uooConvId :: Qualified ConvId } deriving (Show, Generic) deriving (FromJSON, ToJSON, Swagger.ToSchema) via Schema UpsertOne2OneConversationRequest @@ -73,16 +66,4 @@ instance ToSchema UpsertOne2OneConversationRequest where <*> (tUntagged . uooRemoteUser) .= field "remote_user" (qTagUnsafe <$> schema) <*> uooActor .= field "actor" schema <*> uooActorDesiredMembership .= field "actor_desired_membership" schema - <*> uooConvId .= optField "conversation_id" (maybeWithDefault A.Null schema) - -newtype UpsertOne2OneConversationResponse = UpsertOne2OneConversationResponse - { uuorConvId :: Qualified ConvId - } - deriving (Show, Generic) - deriving (FromJSON, ToJSON, Swagger.ToSchema) via Schema UpsertOne2OneConversationResponse - -instance ToSchema UpsertOne2OneConversationResponse where - schema = - object "UpsertOne2OneConversationResponse" $ - UpsertOne2OneConversationResponse - <$> uuorConvId .= field "conversation_id" schema + <*> uooConvId .= field "conversation_id" schema diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index 7debfb2ed6e..1144931940f 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -42,12 +42,14 @@ import Brig.Data.Connection qualified as Data import Brig.Data.Types (resultHasMore, resultList) import Brig.Data.User qualified as Data import Brig.Effects.FederationConfigStore -import Brig.Effects.GalleyProvider (GalleyProvider) +import Brig.Effects.GalleyProvider import Brig.Effects.GalleyProvider qualified as GalleyProvider import Brig.IO.Intra qualified as Intra +import Brig.Options import Brig.Types.Connection import Brig.Types.User.Event import Control.Error +import Control.Lens (view) import Control.Monad.Catch (throwM) import Data.Id as Id import Data.LegalHold qualified as LH @@ -55,6 +57,7 @@ import Data.Proxy (Proxy (Proxy)) import Data.Qualified import Data.Range import Data.UUID.V4 qualified as UUID +import Galley.Types.Conversations.One2One import Imports import Polysemy import Polysemy.TinyLog @@ -65,6 +68,7 @@ import Wire.API.Conversation hiding (Member) import Wire.API.Error import Wire.API.Error.Brig qualified as E import Wire.API.Routes.Public.Util (ResponseForExistedCreated (..)) +import Wire.API.User import Wire.NotificationSubsystem ensureNotSameTeam :: Member GalleyProvider r => Local UserId -> Local UserId -> (ConnectionM r) () @@ -218,7 +222,8 @@ updateConnection :: ( Member FederationConfigStore r, Member NotificationSubsystem r, Member TinyLog r, - Member (Embed HttpClientIO) r + Member (Embed HttpClientIO) r, + Member GalleyProvider r ) => Local UserId -> Qualified UserId -> @@ -240,9 +245,10 @@ updateConnection self other newStatus conn = -- {#RefConnectionTeam} updateConnectionToLocalUser :: forall r. - ( Member NotificationSubsystem r, - Member TinyLog r, - Member (Embed HttpClientIO) r + ( Member (Embed HttpClientIO) r, + Member GalleyProvider r, + Member NotificationSubsystem r, + Member TinyLog r ) => -- | From Local UserId -> @@ -331,7 +337,12 @@ updateConnectionToLocalUser self other newStatus conn = do Log.info $ logLocalConnection (tUnqualified self) (qUnqualified (ucTo s2o)) . msg (val "Blocking connection") - traverse_ (Intra.blockConv self conn) (ucConvId s2o) + traverse_ (liftSem . Intra.blockConv self) (ucConvId s2o) + mlsEnabled <- view (settings . enableMLS) + liftSem $ when (fromMaybe False mlsEnabled) $ do + let mlsConvId = one2OneConvId BaseProtocolMLSTag (tUntagged self) (tUntagged other) + mlsConvEstablished <- isMLSOne2OneEstablished self (tUntagged other) + when mlsConvEstablished $ Intra.blockConv self mlsConvId wrapClient $ Just <$> Data.updateConnection s2o BlockedWithHistory unblock :: UserConnection -> UserConnection -> Relation -> ExceptT ConnectionError (AppT r) (Maybe UserConnection) @@ -363,7 +374,7 @@ updateConnectionToLocalUser self other newStatus conn = do logLocalConnection (tUnqualified self) (qUnqualified (ucTo s2o)) . msg (val "Cancelling connection") lfrom <- qualifyLocal (ucFrom s2o) - lift $ traverse_ (Intra.blockConv lfrom conn) (ucConvId s2o) + lift $ traverse_ (liftSem . Intra.blockConv lfrom) (ucConvId s2o) o2s' <- lift . wrapClient $ Data.updateConnection o2s CancelledWithHistory let e2o = ConnectionUpdated o2s' (Just $ ucStatus o2s) Nothing lift $ liftSem $ Intra.onConnectionEvent (tUnqualified self) conn e2o @@ -434,7 +445,7 @@ updateConnectionInternal = \case o2s <- localConnection other self for_ [s2o, o2s] $ \(uconn :: UserConnection) -> lift $ do lfrom <- qualifyLocal (ucFrom uconn) - traverse_ (Intra.blockConv lfrom Nothing) (ucConvId uconn) + traverse_ (liftSem . Intra.blockConv lfrom) (ucConvId uconn) uconn' <- wrapClient $ Data.updateConnection uconn (mkRelationWithHistory (ucStatus uconn) MissingLegalholdConsent) let ev = ConnectionUpdated uconn' (Just $ ucStatus uconn) Nothing liftSem $ Intra.onConnectionEvent (tUnqualified self) Nothing ev diff --git a/services/brig/src/Brig/API/Connection/Remote.hs b/services/brig/src/Brig/API/Connection/Remote.hs index 96c446d603a..5f41c261e5c 100644 --- a/services/brig/src/Brig/API/Connection/Remote.hs +++ b/services/brig/src/Brig/API/Connection/Remote.hs @@ -29,14 +29,18 @@ import Brig.App import Brig.Data.Connection qualified as Data import Brig.Data.User qualified as Data import Brig.Effects.FederationConfigStore -import Brig.Federation.Client +import Brig.Effects.GalleyProvider +import Brig.Federation.Client as Federation import Brig.IO.Intra qualified as Intra +import Brig.Options import Brig.Types.User.Event import Control.Comonad import Control.Error.Util ((??)) +import Control.Lens (view) import Control.Monad.Trans.Except import Data.Id as Id import Data.Qualified +import Galley.Types.Conversations.One2One (one2OneConvId) import Imports import Network.Wai.Utilities.Error import Polysemy @@ -45,7 +49,7 @@ import Wire.API.Federation.API.Brig ( NewConnectionResponse (..), RemoteConnectionAction (..), ) -import Wire.API.Routes.Internal.Galley.ConversationsIntra (Actor (..), DesiredMembership (..), UpsertOne2OneConversationRequest (..), UpsertOne2OneConversationResponse (uuorConvId)) +import Wire.API.Routes.Internal.Galley.ConversationsIntra import Wire.API.Routes.Public.Util (ResponseForExistedCreated (..)) import Wire.API.User import Wire.NotificationSubsystem @@ -104,39 +108,41 @@ transition (RCA RemoteRescind) Pending = Just Cancelled transition (RCA RemoteRescind) Accepted = Just Sent transition (RCA RemoteRescind) _ = Nothing --- When user A has made a request -> Only user A's membership in conv is affected -> User A wants to be in one2one conv with B, or User A doesn't want to be in one2one conv with B +-- When user A has made a request -> Only user A's membership in conv is +-- affected -> User A wants to be in one2one conv with B, or User A doesn't want +-- to be in one2one conv with B updateOne2OneConv :: Local UserId -> Maybe ConnId -> Remote UserId -> - Maybe (Qualified ConvId) -> - Relation -> + Qualified ConvId -> + DesiredMembership -> Actor -> - (AppT r) (Qualified ConvId) -updateOne2OneConv lUsr _mbConn remoteUser mbConvId rel actor = do + (AppT r) () +updateOne2OneConv lUsr _mbConn remoteUser convId desiredMem actor = do let request = UpsertOne2OneConversationRequest { uooLocalUser = lUsr, uooRemoteUser = remoteUser, uooActor = actor, - uooActorDesiredMembership = desiredMembership actor rel, - uooConvId = mbConvId + uooActorDesiredMembership = desiredMem, + uooConvId = convId } - uuorConvId <$> wrapHttp (Intra.upsertOne2OneConversation request) - where - desiredMembership :: Actor -> Relation -> DesiredMembership - desiredMembership a r = - let isIncluded = - a - `elem` case r of - Accepted -> [LocalActor, RemoteActor] - Blocked -> [] - Pending -> [RemoteActor] - Ignored -> [RemoteActor] - Sent -> [LocalActor] - Cancelled -> [] - MissingLegalholdConsent -> [] - in if isIncluded then Included else Excluded + void $ wrapHttp (Intra.upsertOne2OneConversation request) + +desiredMembership :: Actor -> Relation -> DesiredMembership +desiredMembership a r = + let isIncluded = + a + `elem` case r of + Accepted -> [LocalActor, RemoteActor] + Blocked -> [] + Pending -> [RemoteActor] + Ignored -> [RemoteActor] + Sent -> [LocalActor] + Cancelled -> [] + MissingLegalholdConsent -> [] + in if isIncluded then Included else Excluded -- | Perform a state transition on a connection, handle conversation updates and -- push events. @@ -146,7 +152,7 @@ updateOne2OneConv lUsr _mbConn remoteUser mbConvId rel actor = do -- -- Returns the connection, and whether it was updated or not. transitionTo :: - (Member NotificationSubsystem r) => + (Member NotificationSubsystem r, Member GalleyProvider r) => Local UserId -> Maybe ConnId -> Remote UserId -> @@ -159,8 +165,13 @@ transitionTo self _ _ Nothing Nothing _ = -- connection. This shouldn't be possible. throwE (InvalidTransition (tUnqualified self)) transitionTo self mzcon other Nothing (Just rel) actor = lift $ do - -- update 1-1 connection - qcnv <- updateOne2OneConv self mzcon other Nothing rel actor + -- Create 1-1 proteus conversation. + -- + -- We do nothing here for MLS as haveing no pre-existing connection implies + -- there was no conversation. Creating an MLS converstaion is special due to + -- key packages, etc. so the clients have to make another call for this. + let proteusConv = one2OneConvId BaseProtocolProteusTag (tUntagged self) (tUntagged other) + updateOne2OneConv self mzcon other proteusConv (desiredMembership actor rel) actor -- create connection connection <- @@ -169,21 +180,32 @@ transitionTo self mzcon other Nothing (Just rel) actor = lift $ do self (tUntagged other) (relationWithHistory rel) - qcnv + proteusConv -- send event pushEvent self mzcon connection pure (Created connection, True) transitionTo _self _zcon _other (Just connection) Nothing _actor = pure (Existed connection, False) -transitionTo self mzcon other (Just connection) (Just rel) actor = lift $ do +transitionTo self mzcon other (Just connection) (Just rel) actor = do -- update 1-1 conversation - void $ updateOne2OneConv self Nothing other (ucConvId connection) rel actor + let proteusConvId = + fromMaybe + (one2OneConvId BaseProtocolProteusTag (tUntagged self) (tUntagged other)) + $ ucConvId connection + lift $ updateOne2OneConv self Nothing other proteusConvId (desiredMembership actor rel) actor + mlsEnabled <- view (settings . enableMLS) + when (fromMaybe False mlsEnabled) $ do + let mlsConvId = one2OneConvId BaseProtocolMLSTag (tUntagged self) (tUntagged other) + mlsConvEstablished <- lift . liftSem $ isMLSOne2OneEstablished self (tUntagged other) + let desiredMem = desiredMembership actor rel + lift . when (mlsConvEstablished && desiredMem == Excluded) $ + updateOne2OneConv self Nothing other mlsConvId desiredMem actor -- update connection - connection' <- wrapClient $ Data.updateConnection connection (relationWithHistory rel) + connection' <- lift $ wrapClient $ Data.updateConnection connection (relationWithHistory rel) -- send event - pushEvent self mzcon connection' + lift $ pushEvent self mzcon connection' pure (Existed connection', True) -- | Send an event to the local user when the state of a connection changes. @@ -198,7 +220,7 @@ pushEvent self mzcon connection = do liftSem $ Intra.onConnectionEvent (tUnqualified self) mzcon event performLocalAction :: - (Member NotificationSubsystem r) => + (Member NotificationSubsystem r, Member GalleyProvider r) => Local UserId -> Maybe ConnId -> Remote UserId -> @@ -254,7 +276,7 @@ performLocalAction self mzcon other mconnection action = do -- B connects & A reacts: Accepted Accepted -- @ performRemoteAction :: - (Member NotificationSubsystem r) => + (Member NotificationSubsystem r, Member GalleyProvider r) => Local UserId -> Remote UserId -> Maybe UserConnection -> @@ -273,7 +295,8 @@ performRemoteAction self other mconnection action = do createConnectionToRemoteUser :: ( Member FederationConfigStore r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member GalleyProvider r ) => Local UserId -> ConnId -> @@ -287,7 +310,8 @@ createConnectionToRemoteUser self zcon other = do updateConnectionToRemoteUser :: ( Member NotificationSubsystem r, - Member FederationConfigStore r + Member FederationConfigStore r, + Member GalleyProvider r ) => Local UserId -> Remote UserId -> diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index 7515577d073..cc44dd3b250 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -111,7 +111,8 @@ getFederationStatus _ request = do sendConnectionAction :: ( Member FederationConfigStore r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member GalleyProvider r ) => Domain -> NewConnectionRequest -> diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index a2ab94c1c61..0630ee43602 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -1112,7 +1112,8 @@ createConnection self conn target = do API.createConnection lself conn target !>> connError updateLocalConnection :: - ( Member NotificationSubsystem r, + ( Member GalleyProvider r, + Member NotificationSubsystem r, Member TinyLog r, Member (Embed HttpClientIO) r ) => @@ -1131,7 +1132,8 @@ updateConnection :: ( Member FederationConfigStore r, Member NotificationSubsystem r, Member TinyLog r, - Member (Embed HttpClientIO) r + Member (Embed HttpClientIO) r, + Member GalleyProvider r ) => UserId -> ConnId -> diff --git a/services/brig/src/Brig/Effects/GalleyProvider.hs b/services/brig/src/Brig/Effects/GalleyProvider.hs index b73fd919ed2..c45d58a81b2 100644 --- a/services/brig/src/Brig/Effects/GalleyProvider.hs +++ b/services/brig/src/Brig/Effects/GalleyProvider.hs @@ -106,5 +106,9 @@ data GalleyProvider m a where GetExposeInvitationURLsToTeamAdmin :: TeamId -> GalleyProvider m ShowOrHideInvitationUrl + IsMLSOne2OneEstablished :: + Local UserId -> + Qualified UserId -> + GalleyProvider m Bool makeSem ''GalleyProvider diff --git a/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs b/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs index 481b4d28c09..84d6ba98cf9 100644 --- a/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs +++ b/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs @@ -35,6 +35,8 @@ import Data.Qualified import Data.Range import Galley.Types.Teams qualified as Team import Imports +import Network.HTTP.Client qualified as HTTP +import Network.HTTP.Types qualified as HTTP import Network.HTTP.Types.Method import Network.HTTP.Types.Status import Network.Wai.Utilities.Error qualified as Wai @@ -46,6 +48,7 @@ import Servant.API (toHeader) import System.Logger (field, msg, val) import Util.Options import Wire.API.Conversation hiding (Member) +import Wire.API.Conversation.Protocol import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Routes.Version import Wire.API.Team @@ -89,6 +92,7 @@ interpretGalleyProviderToRpc disabledVersions galleyEndpoint = GetAllFeatureConfigsForUser m_id' -> getAllFeatureConfigsForUser m_id' GetVerificationCodeEnabled id' -> getVerificationCodeEnabled id' GetExposeInvitationURLsToTeamAdmin id' -> getTeamExposeInvitationURLsToTeamAdmin id' + IsMLSOne2OneEstablished lusr qother -> checkMLSOne2OneEstablished lusr qother galleyRequest :: (Member Rpc r, Member (Input Endpoint) r) => (Request -> Request) -> Sem r (Response (Maybe LByteString)) galleyRequest req = do @@ -524,3 +528,39 @@ getTeamExposeInvitationURLsToTeamAdmin tid = do method GET . paths ["i", "teams", toByteString' tid, "features", featureNameBS @ExposeInvitationURLsToTeamAdminConfig] . expect2xx + +checkMLSOne2OneEstablished :: + ( Member (Error ParseException) r, + Member (Input Endpoint) r, + Member Rpc r, + Member TinyLog r + ) => + Local UserId -> + Qualified UserId -> + Sem r Bool +checkMLSOne2OneEstablished self (Qualified other otherDomain) = do + debug $ remote "galley" . msg (val "Get the MLS one-to-one conversation") + response <- galleyRequest req + case HTTP.statusCode (HTTP.responseStatus response) of + 403 -> pure False + 400 -> pure False + _ {- 200 is assumed -} -> do + conv <- decodeBodyOrThrow @Conversation "galley" response + let mEpoch = case cnvProtocol conv of + ProtocolProteus -> Nothing + ProtocolMLS meta -> Just . cnvmlsEpoch $ meta + ProtocolMixed meta -> Just . cnvmlsEpoch $ meta + pure $ case mEpoch of + Nothing -> False + Just (Epoch e) -> e > 0 + where + req = + method GET + . paths + [ "i", + "conversations", + "mls-one2one", + toByteString' otherDomain, + toByteString' other + ] + . zUser (tUnqualified self) diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index f81fd20f7a7..23d7d3bbb0a 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -93,7 +93,7 @@ import Wire.API.Event.Conversation (Connect (Connect)) import Wire.API.Federation.API.Brig import Wire.API.Federation.Error import Wire.API.Properties -import Wire.API.Routes.Internal.Galley.ConversationsIntra (UpsertOne2OneConversationRequest, UpsertOne2OneConversationResponse) +import Wire.API.Routes.Internal.Galley.ConversationsIntra import Wire.API.Routes.Internal.Galley.TeamsIntra (GuardLegalholdPolicyConflicts (GuardLegalholdPolicyConflicts)) import Wire.API.Team.LegalHold (LegalholdProtectee) import Wire.API.Team.Member qualified as Team @@ -643,42 +643,32 @@ acceptConnectConv from conn = (liftSem . acceptLocalConnectConv from conn . tUnqualified) (const (throwM federationNotImplemented)) --- | Calls 'Galley.API.blockConvH'. -blockLocalConv :: +blockConv :: ( Member (Embed HttpClientIO) r, Member TinyLog r ) => Local UserId -> - Maybe ConnId -> - ConvId -> + Qualified ConvId -> Sem r () -blockLocalConv lusr conn cnv = do +blockConv lusr qcnv = do Log.debug $ remote "galley" - . field "conv" (toByteString cnv) + . field "conv" (toByteString . qUnqualified $ qcnv) + . field "domain" (toByteString . qDomain $ qcnv) . msg (val "Blocking conversation") - embed $ void $ galleyRequest PUT req + embed . void $ galleyRequest PUT req where req = - paths ["/i/conversations", toByteString' cnv, "block"] + paths + [ "i", + "conversations", + toByteString' (qDomain qcnv), + toByteString' (qUnqualified qcnv), + "block" + ] . zUser (tUnqualified lusr) - . maybe id (header "Z-Connection" . fromConnId) conn . expect2xx -blockConv :: - ( Member (Embed HttpClientIO) r, - Member TinyLog r - ) => - Local UserId -> - Maybe ConnId -> - Qualified ConvId -> - AppT r () -blockConv lusr conn = - foldQualified - lusr - (liftSem . blockLocalConv lusr conn . tUnqualified) - (const (throwM federationNotImplemented)) - -- | Calls 'Galley.API.unblockConvH'. unblockLocalConv :: ( Member (Embed HttpClientIO) r, @@ -723,11 +713,11 @@ upsertOne2OneConversation :: HasRequestId m ) => UpsertOne2OneConversationRequest -> - m UpsertOne2OneConversationResponse + m () upsertOne2OneConversation urequest = do response <- galleyRequest POST req case Bilge.statusCode response of - 200 -> decodeBody "galley" response + 200 -> pure () _ -> throwM internalServerError where req = diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index edd2d4a14d0..2e4d7435980 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -126,9 +126,11 @@ conversationAPI :: API IConversationAPI GalleyEffects conversationAPI = mkNamedAPI @"conversation-get-member" Query.internalGetMember <@> mkNamedAPI @"conversation-accept-v2" Update.acceptConv + <@> mkNamedAPI @"conversation-block-unqualified" Update.blockConvUnqualified <@> mkNamedAPI @"conversation-block" Update.blockConv <@> mkNamedAPI @"conversation-unblock" Update.unblockConv <@> mkNamedAPI @"conversation-meta" Query.getConversationMeta + <@> mkNamedAPI @"conversation-mls-one-to-one" Query.getMLSOne2OneConversation legalholdWhitelistedTeamsAPI :: API ILegalholdWhitelistedTeamsAPI GalleyEffects legalholdWhitelistedTeamsAPI = mkAPI $ \tid -> hoistAPIHandler Imports.id (base tid) diff --git a/services/galley/src/Galley/API/One2One.hs b/services/galley/src/Galley/API/One2One.hs index 039ca96f012..031a4dd81d3 100644 --- a/services/galley/src/Galley/API/One2One.hs +++ b/services/galley/src/Galley/API/One2One.hs @@ -35,6 +35,7 @@ import Galley.Types.UserList import Imports import Polysemy import Wire.API.Conversation hiding (Member) +import Wire.API.Conversation.Protocol import Wire.API.Routes.Internal.Galley.ConversationsIntra import Wire.API.User @@ -58,17 +59,8 @@ iUpsertOne2OneConversation :: Member MemberStore r ) => UpsertOne2OneConversationRequest -> - Sem r UpsertOne2OneConversationResponse + Sem r () iUpsertOne2OneConversation UpsertOne2OneConversationRequest {..} = do - let convId = - fromMaybe - ( one2OneConvId - BaseProtocolProteusTag - (tUntagged uooLocalUser) - (tUntagged uooRemoteUser) - ) - uooConvId - let dolocal :: Local ConvId -> Sem r () dolocal lconvId = do mbConv <- getConversation (tUnqualified lconvId) @@ -90,10 +82,15 @@ iUpsertOne2OneConversation UpsertOne2OneConversationRequest {..} = do void $ createMember lconvId uooLocalUser unless (null (convRemoteMembers conv)) $ acceptConnectConversation (tUnqualified lconvId) - (LocalActor, Excluded) -> + (LocalActor, Excluded) -> do deleteMembers (tUnqualified lconvId) (UserList [tUnqualified uooLocalUser] []) + let mGroupId = case convProtocol conv of + ProtocolProteus -> Nothing + ProtocolMLS meta -> Just . cnvmlsGroupId $ meta + ProtocolMixed meta -> Just . cnvmlsGroupId $ meta + for_ mGroupId $ flip removeAllMLSClientsOfUser (tUntagged uooLocalUser) (RemoteActor, Included) -> do void $ createMembers (tUnqualified lconvId) (UserList [] [uooRemoteUser]) unless (null (convLocalMembers conv)) $ @@ -111,5 +108,4 @@ iUpsertOne2OneConversation UpsertOne2OneConversationRequest {..} = do deleteMembersInRemoteConversation rconvId [tUnqualified uooLocalUser] (RemoteActor, _) -> pure () - foldQualified uooLocalUser dolocal doremote convId - pure (UpsertOne2OneConversationResponse convId) + foldQualified uooLocalUser dolocal doremote uooConvId diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index c6195576758..ddc89b92164 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -21,6 +21,7 @@ module Galley.API.Update ( -- * Managing Conversations acceptConv, blockConv, + blockConvUnqualified, unblockConv, checkReusableCode, joinConversationByReusableCode, @@ -164,6 +165,22 @@ acceptConv lusr conn cnv = do conversationView lusr conv' blockConv :: + ( Member ConversationStore r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member MemberStore r + ) => + Local UserId -> + Qualified ConvId -> + Sem r () +blockConv lusr qcnv = + foldQualified + lusr + (\lcnv -> blockConvUnqualified (tUnqualified lusr) (tUnqualified lcnv)) + (\rcnv -> blockRemoteConv lusr rcnv) + qcnv + +blockConvUnqualified :: ( Member ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, @@ -172,7 +189,7 @@ blockConv :: UserId -> ConvId -> Sem r () -blockConv zusr cnv = do +blockConvUnqualified zusr cnv = do conv <- E.getConversation cnv >>= noteS @'ConvNotFound unless (Data.convType conv `elem` [ConnectConv, One2OneConv]) $ throwS @'InvalidOperation @@ -180,6 +197,17 @@ blockConv zusr cnv = do when (zusr `isMember` mems) $ E.deleteMembers cnv (UserList [zusr] []) +blockRemoteConv :: + ( Member (ErrorS 'ConvNotFound) r, + Member MemberStore r + ) => + Local UserId -> + Remote ConvId -> + Sem r () +blockRemoteConv (tUnqualified -> usr) rcnv = do + unlessM (E.checkLocalMemberRemoteConv usr rcnv) $ throwS @'ConvNotFound + E.deleteMembersInRemoteConversation rcnv [usr] + unblockConv :: ( Member ConversationStore r, Member (Error InternalError) r, diff --git a/services/galley/src/Galley/Cassandra/Conversation/Members.hs b/services/galley/src/Galley/Cassandra/Conversation/Members.hs index abd3a0139e6..f4a043cd11e 100644 --- a/services/galley/src/Galley/Cassandra/Conversation/Members.hs +++ b/services/galley/src/Galley/Cassandra/Conversation/Members.hs @@ -384,6 +384,11 @@ removeMLSClients groupId (Qualified usr domain) cs = retry x5 . batch $ do for_ cs $ \c -> addPrepQuery Cql.removeMLSClient (groupId, domain, usr, c) +removeAllMLSClientsOfUser :: GroupId -> Qualified UserId -> Client () +removeAllMLSClientsOfUser groupId (Qualified usr domain) = + retry x5 $ + write Cql.removeAllMLSClientsOfUser (params LocalQuorum (groupId, domain, usr)) + removeAllMLSClients :: GroupId -> Client () removeAllMLSClients groupId = do retry x5 $ write Cql.removeAllMLSClients (params LocalQuorum (Identity groupId)) @@ -416,6 +421,7 @@ interpretMemberStoreToCassandra = interpret $ \case AddMLSClients lcnv quid cs -> embedClient $ addMLSClients lcnv quid cs PlanClientRemoval lcnv cids -> embedClient $ planMLSClientRemoval lcnv cids RemoveMLSClients lcnv quid cs -> embedClient $ removeMLSClients lcnv quid cs + RemoveAllMLSClientsOfUser lcnv quid -> embedClient $ removeAllMLSClientsOfUser lcnv quid RemoveAllMLSClients gid -> embedClient $ removeAllMLSClients gid LookupMLSClients lcnv -> embedClient $ lookupMLSClients lcnv LookupMLSClientLeafIndices lcnv -> embedClient $ lookupMLSClientLeafIndices lcnv diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index 560d8d9a19f..df52a52571b 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -493,6 +493,9 @@ planMLSClientRemoval = "update mls_group_member_client set removal_pending = tru removeMLSClient :: PrepQuery W (GroupId, Domain, UserId, ClientId) () removeMLSClient = "delete from mls_group_member_client where group_id = ? and user_domain = ? and user = ? and client = ?" +removeAllMLSClientsOfUser :: PrepQuery W (GroupId, Domain, UserId) () +removeAllMLSClientsOfUser = "delete from mls_group_member_client where group_id = ? and user_domain = ? and user = ?" + removeAllMLSClients :: PrepQuery W (Identity GroupId) () removeAllMLSClients = "DELETE FROM mls_group_member_client WHERE group_id = ?" diff --git a/services/galley/src/Galley/Effects/MemberStore.hs b/services/galley/src/Galley/Effects/MemberStore.hs index 0513cc6570e..56cd4fe9740 100644 --- a/services/galley/src/Galley/Effects/MemberStore.hs +++ b/services/galley/src/Galley/Effects/MemberStore.hs @@ -44,6 +44,7 @@ module Galley.Effects.MemberStore addMLSClients, planClientRemoval, removeMLSClients, + removeAllMLSClientsOfUser, removeAllMLSClients, lookupMLSClients, lookupMLSClientLeafIndices, @@ -88,6 +89,7 @@ data MemberStore m a where AddMLSClients :: GroupId -> Qualified UserId -> Set (ClientId, LeafIndex) -> MemberStore m () PlanClientRemoval :: Foldable f => GroupId -> f ClientIdentity -> MemberStore m () RemoveMLSClients :: GroupId -> Qualified UserId -> Set ClientId -> MemberStore m () + RemoveAllMLSClientsOfUser :: GroupId -> Qualified UserId -> MemberStore m () RemoveAllMLSClients :: GroupId -> MemberStore m () LookupMLSClients :: GroupId -> MemberStore m ClientMap LookupMLSClientLeafIndices :: GroupId -> MemberStore m (ClientMap, IndexMap) diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 21c0c844a08..2ac9f185e71 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -3694,16 +3694,11 @@ testAllOne2OneConversationRequests = do testOne2OneConversationRequest :: Bool -> Actor -> DesiredMembership -> TestM () testOne2OneConversationRequest shouldBeLocal actor desired = do alice <- qTagUnsafe <$> randomQualifiedUser - (bob, expectedConvId) <- generateRemoteAndConvId shouldBeLocal alice + (bob, convId) <- generateRemoteAndConvId shouldBeLocal alice - convId <- do - let req = UpsertOne2OneConversationRequest alice bob actor desired Nothing - res <- - iUpsertOne2OneConversation req - responseJsonError res - - liftIO $ convId @?= expectedConvId + do + let req = UpsertOne2OneConversationRequest alice bob actor desired convId + iUpsertOne2OneConversation req !!! statusCode === const 200 if shouldBeLocal then diff --git a/services/galley/test/integration/API/Federation.hs b/services/galley/test/integration/API/Federation.hs index 22236c3c810..070010d0867 100644 --- a/services/galley/test/integration/API/Federation.hs +++ b/services/galley/test/integration/API/Federation.hs @@ -120,12 +120,9 @@ getConversationsAllFound = do uooRemoteUser = rAlice, uooActor = LocalActor, uooActorDesiredMembership = Included, - uooConvId = Just cnv1Id + uooConvId = cnv1Id } - UpsertOne2OneConversationResponse cnv1IdReturned <- - responseJsonError - =<< iUpsertOne2OneConversation createO2O - liftIO $ assertEqual "Mismatch in the generated conversation ID" cnv1IdReturned cnv1Id + iUpsertOne2OneConversation createO2O !!! const 200 === statusCode do convs <- diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index e9ca4a544c8..b6227da8967 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -2895,23 +2895,18 @@ iUpsertOne2OneConversation req = do createOne2OneConvWithRemote :: HasCallStack => Local UserId -> Remote UserId -> TestM () createOne2OneConvWithRemote localUser remoteUser = do - let mkRequest actor mConvId = + let convId = one2OneConvId BaseProtocolProteusTag (tUntagged localUser) (tUntagged remoteUser) + mkRequest actor = UpsertOne2OneConversationRequest { uooLocalUser = localUser, uooRemoteUser = remoteUser, uooActor = actor, uooActorDesiredMembership = Included, - uooConvId = mConvId + uooConvId = convId } - ooConvId <- - fmap uuorConvId - . responseJsonError - =<< iUpsertOne2OneConversation (mkRequest LocalActor Nothing) - Local UserId -> TestM (Remote UserId, Qualified ConvId) generateRemoteAndConvId = generateRemoteAndConvIdWithDomain (Domain "far-away.example.com") From c6acc1ffe29b72c58f04d4867f9714b8cb4011d6 Mon Sep 17 00:00:00 2001 From: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> Date: Fri, 23 Feb 2024 21:23:31 +0100 Subject: [PATCH 011/117] [WPB-5687] port flaking LH tests to new integration (#3876) * [fix] use -e flag to abort when `docker-compose` fails * [feat] make `HasTests` easier to use - delegate only the testcase generation to the user - use an OVERLAPPABLE default instance if the type is a Generic Enum - cover more cases - don't use newtype Wrappers wherever possible * [feat] port over flaking Legalhold tests and delete them from galley integration * [feat] minor testlib improvements and additions --------- Co-authored-by: Matthias Fischmann --- .hlint.yaml | 1 + changelog.d/5-internal/WPB-5687 | 1 + deploy/dockerephemeral/run.sh | 2 +- integration/test/API/Brig.hs | 16 +- integration/test/API/BrigInternal.hs | 7 + integration/test/API/Galley.hs | 69 ++- integration/test/API/GalleyInternal.hs | 17 +- integration/test/MLS/Util.hs | 17 +- integration/test/Notifications.hs | 116 +++- integration/test/SetupHelpers.hs | 38 ++ integration/test/Test/LegalHold.hs | 548 +++++++++++++++++- integration/test/Test/MLS.hs | 2 +- integration/test/Test/MLS/One2One.hs | 11 +- integration/test/Test/MLS/SubConversation.hs | 18 +- integration/test/Test/Search.hs | 4 +- integration/test/Test/User.hs | 13 +- integration/test/Test/Version.hs | 15 +- integration/test/Testlib/App.hs | 2 + integration/test/Testlib/Assertions.hs | 2 +- integration/test/Testlib/Env.hs | 2 +- integration/test/Testlib/HTTP.hs | 29 +- integration/test/Testlib/JSON.hs | 13 + .../test/Testlib/MockIntegrationService.hs | 63 +- integration/test/Testlib/ModService.hs | 4 +- integration/test/Testlib/PTest.hs | 120 +++- integration/test/Testlib/Types.hs | 5 +- libs/brig-types/src/Brig/Types/User/Event.hs | 5 +- .../test/integration/API/Teams/LegalHold.hs | 459 +-------------- 28 files changed, 965 insertions(+), 634 deletions(-) create mode 100644 changelog.d/5-internal/WPB-5687 diff --git a/.hlint.yaml b/.hlint.yaml index 66e3cff5d97..b5b237ee5fa 100644 --- a/.hlint.yaml +++ b/.hlint.yaml @@ -11,6 +11,7 @@ - ignore: { name: Avoid lambda using `infix` } - ignore: { name: Eta reduce } - ignore: { name: Use section } +- ignore: { name: "Use :" } - ignore: { name: Use underscore } # custom rules: diff --git a/changelog.d/5-internal/WPB-5687 b/changelog.d/5-internal/WPB-5687 new file mode 100644 index 00000000000..24f8fcd8e61 --- /dev/null +++ b/changelog.d/5-internal/WPB-5687 @@ -0,0 +1 @@ +port flaking LH tests to new integration and improve the ergonomics of our testing library diff --git a/deploy/dockerephemeral/run.sh b/deploy/dockerephemeral/run.sh index 57d0e7223ae..8d9a98cc8be 100755 --- a/deploy/dockerephemeral/run.sh +++ b/deploy/dockerephemeral/run.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -set -x +set -xe # run.sh should work no matter what is the current directory SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index c09fc64ec38..908a0db996d 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -131,6 +131,7 @@ getUserByHandle user domain handle = do joinHttpPath ["users", "by-handle", domainStr, handle] submit "GET" req +-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/get_clients__client_ getClient :: (HasCallStack, MakesValue user, MakesValue client) => user -> @@ -143,13 +144,14 @@ getClient u cli = do joinHttpPath ["clients", c] submit "GET" req +-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/delete_self deleteUser :: (HasCallStack, MakesValue user) => user -> App Response deleteUser user = do req <- baseRequest user Brig Versioned "/self" submit "DELETE" $ req & addJSONObject ["password" .= defPassword] --- | https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/post_i_clients__uid_ +-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_clients addClient :: (HasCallStack, MakesValue user) => user -> @@ -320,9 +322,7 @@ uploadKeyPackages cid kps = do "/mls/key-packages/self/" <> cid.client submit "POST" - ( req - & addJSONObject ["key_packages" .= map (T.decodeUtf8 . Base64.encode) kps] - ) + (req & addJSONObject ["key_packages" .= map (T.decodeUtf8 . Base64.encode) kps]) claimKeyPackagesWithParams :: (MakesValue u, MakesValue v) => Ciphersuite -> u -> v -> [(String, String)] -> App Response claimKeyPackagesWithParams suite u v params = do @@ -334,7 +334,7 @@ claimKeyPackagesWithParams suite u v params = do req & addQueryParams ([("ciphersuite", suite.code)] <> params) -claimKeyPackages :: (MakesValue u, MakesValue v) => Ciphersuite -> u -> v -> App Response +claimKeyPackages :: (HasCallStack, MakesValue u, MakesValue v) => Ciphersuite -> u -> v -> App Response claimKeyPackages suite u v = claimKeyPackagesWithParams suite u v [] countKeyPackages :: Ciphersuite -> ClientIdentity -> App Response @@ -630,3 +630,9 @@ getMultiUserPrekeyBundle :: (HasCallStack, MakesValue caller, ToJSON userClients getMultiUserPrekeyBundle caller userClients = do req <- baseRequest caller Brig Versioned $ joinHttpPath ["users", "list-prekeys"] submit "POST" (addJSON userClients req) + +-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_access +renewToken :: (HasCallStack, MakesValue uid) => uid -> String -> App Response +renewToken caller cookie = do + req <- baseRequest caller Brig Versioned "access" + submit "POST" (addHeader "Cookie" ("zuid=" <> cookie) req) diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index 7292946956e..5eef85edea8 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -236,3 +236,10 @@ addClient user args = do req <- baseRequest user Brig Unversioned $ "/i/clients/" <> uid val <- mkAddClientValue args submit "POST" $ req & addJSONObject val + +-- | https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/post_i_clients_full +getClientsFull :: (HasCallStack, MakesValue users, MakesValue uid) => uid -> users -> App Response +getClientsFull user users = do + val <- make users + baseRequest user Brig Unversioned do joinHttpPath ["i", "clients", "full"] + >>= submit "POST" . addJSONObject ["users" .= val] diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index 14123b112f7..5def97cc126 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -238,7 +238,7 @@ postProteusMessage user conv msgs = do convDomain <- objDomain conv convId <- objId conv let bytes = Proto.encodeMessage msgs - req <- baseRequest user Galley Versioned ("/conversations/" <> convDomain <> "/" <> convId <> "/proteus/messages") + req <- baseRequest user Galley Versioned (joinHttpPath ["conversations", convDomain, convId, "proteus", "messages"]) submit "POST" (addProtobuf bytes req) mkProteusRecipient :: (HasCallStack, MakesValue user, MakesValue client) => user -> client -> String -> App Proto.QualifiedUserEntry @@ -579,6 +579,14 @@ putTeamProperties tid caller properties = do req ) +-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/get_teams__tid__legalhold__uid_ +legalholdUserStatus :: (HasCallStack, MakesValue tid, MakesValue user, MakesValue owner) => tid -> owner -> user -> App Response +legalholdUserStatus tid ownerid user = do + tidS <- asString tid + uid <- objId user + req <- baseRequest ownerid Galley Versioned (joinHttpPath ["teams", tidS, "legalhold", uid]) + submit "GET" req + -- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_teams__tid__legalhold_settings enableLegalHold :: (HasCallStack, MakesValue tid, MakesValue ownerid) => tid -> ownerid -> App Response enableLegalHold tid ownerid = do @@ -586,16 +594,32 @@ enableLegalHold tid ownerid = do req <- baseRequest ownerid Galley Versioned (joinHttpPath ["teams", tidStr, "features", "legalhold"]) submit "PUT" (addJSONObject ["status" .= "enabled", "ttl" .= "unlimited"] req) --- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_teams__tid__legalhold_settings -postLegalHoldSettings :: (HasCallStack, MakesValue owner, MakesValue tid, MakesValue newService) => owner -> tid -> newService -> App Response -postLegalHoldSettings owner tid newSettings = retrying policy only412 $ \_ -> do +-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/delete_teams__tid__legalhold__uid_ +disableLegalHold :: + (HasCallStack, MakesValue tid, MakesValue ownerid, MakesValue uid) => + tid -> + ownerid -> + uid -> + -- | the password for user with $uid$ + String -> + App Response +disableLegalHold tid ownerid uid pw = do tidStr <- asString tid - req <- baseRequest owner Galley Versioned (joinHttpPath ["teams", tidStr, "legalhold", "settings"]) - newSettingsObj <- make newSettings - submit "POST" (addJSON newSettingsObj req) + uidStr <- objId uid + req <- baseRequest ownerid Galley Versioned (joinHttpPath ["teams", tidStr, "legalhold", uidStr]) + submit "DELETE" (addJSONObject ["password" .= pw] req) + +-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_teams__tid__legalhold_settings +postLegalHoldSettings :: (HasCallStack, MakesValue ownerid, MakesValue tid, MakesValue newService) => tid -> ownerid -> newService -> App Response +postLegalHoldSettings tid owner newSettings = + asks ((* 1_000_000) . timeOutSeconds) >>= \tSecs -> retrying (policy tSecs) only412 $ \_ -> do + tidStr <- asString tid + req <- baseRequest owner Galley Versioned (joinHttpPath ["teams", tidStr, "legalhold", "settings"]) + newSettingsObj <- make newSettings + submit "POST" (addJSON newSettingsObj req) where - policy :: RetryPolicy - policy = limitRetriesByCumulativeDelay 5_000_000 $ exponentialBackoff 50 + policy :: Int -> RetryPolicy + policy tSecs = limitRetriesByCumulativeDelay tSecs $ exponentialBackoff 50 only412 :: RetryStatus -> Response -> App Bool only412 _ resp = pure $ resp.status == 412 @@ -609,10 +633,18 @@ requestLegalHoldDevice tid ownerid uid = do submit "POST" req -- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/put_teams__tid__legalhold__uid__approve +-- +-- like approveLegalHoldDevice' but approves for the requesting party approveLegalHoldDevice :: (HasCallStack, MakesValue tid, MakesValue uid) => tid -> uid -> String -> App Response -approveLegalHoldDevice tid uid pwd = do +approveLegalHoldDevice tid uid = approveLegalHoldDevice' tid uid uid + +-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/put_teams__tid__legalhold__uid__approve +-- +-- useful for testing unauthorized requests +approveLegalHoldDevice' :: (HasCallStack, MakesValue tid, MakesValue uid, MakesValue forUid) => tid -> uid -> forUid -> String -> App Response +approveLegalHoldDevice' tid uid forUid pwd = do tidStr <- asString tid - uidStr <- asString $ uid %. "id" + uidStr <- asString $ forUid %. "id" req <- baseRequest uid Galley Versioned (joinHttpPath ["teams", tidStr, "legalhold", uidStr, "approve"]) submit "PUT" (addJSONObject ["password" .= pwd] req) @@ -630,3 +662,18 @@ getLegalHoldStatus tid zusr = do uidStr <- asString $ zusr %. "id" req <- baseRequest zusr Galley Versioned (joinHttpPath ["teams", tidStr, "legalhold", uidStr]) submit "GET" req + +-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/put_teams__tid__features_legalhold +putLegalholdStatus :: + (HasCallStack, MakesValue tid, MakesValue usr) => + tid -> + usr -> + -- | the status to put to + String -> + App Response +putLegalholdStatus tid usr status = do + tidStr <- asString tid + + baseRequest usr Galley Versioned (joinHttpPath ["teams", tidStr, "features", "legalhold"]) + >>= submit "PUT" + . addJSONObject ["status" .= status, "ttl" .= "unlimited"] diff --git a/integration/test/API/GalleyInternal.hs b/integration/test/API/GalleyInternal.hs index 89f3eac5716..4b5ad4cc970 100644 --- a/integration/test/API/GalleyInternal.hs +++ b/integration/test/API/GalleyInternal.hs @@ -59,14 +59,23 @@ getFederationStatus user domains = "GET" $ req & addJSONObject ["domains" .= domainList] -legalholdWhitelistTeam :: (HasCallStack, MakesValue uid, MakesValue tid) => uid -> tid -> App Response -legalholdWhitelistTeam uid tid = do +-- | https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/galley/#/galley/put_i_legalhold_whitelisted_teams__tid_ +legalholdWhitelistTeam :: (HasCallStack, MakesValue uid, MakesValue tid) => tid -> uid -> App Response +legalholdWhitelistTeam tid uid = do tidStr <- asString tid req <- baseRequest uid Galley Unversioned $ joinHttpPath ["i", "legalhold", "whitelisted-teams", tidStr] submit "PUT" req -legalholdIsTeamInWhitelist :: (HasCallStack, MakesValue uid, MakesValue tid) => uid -> tid -> App Response -legalholdIsTeamInWhitelist uid tid = do +-- | https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/galley/#/galley/get_i_legalhold_whitelisted_teams__tid_ +legalholdIsTeamInWhitelist :: (HasCallStack, MakesValue uid, MakesValue tid) => tid -> uid -> App Response +legalholdIsTeamInWhitelist tid uid = do tidStr <- asString tid req <- baseRequest uid Galley Unversioned $ joinHttpPath ["i", "legalhold", "whitelisted-teams", tidStr] submit "GET" req + +-- | https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/galley/#/galley/get_i_teams__tid__features_legalhold +legalholdIsEnabled :: (HasCallStack, MakesValue tid, MakesValue uid) => tid -> uid -> App Response +legalholdIsEnabled tid uid = do + tidStr <- asString tid + baseRequest uid Galley Unversioned do joinHttpPath ["i", "teams", tidStr, "features", "legalhold"] + >>= submit "GET" diff --git a/integration/test/MLS/Util.hs b/integration/test/MLS/Util.hs index 2f42556489a..68b43c37616 100644 --- a/integration/test/MLS/Util.hs +++ b/integration/test/MLS/Util.hs @@ -3,7 +3,6 @@ module MLS.Util where import API.Brig -import qualified API.BrigCommon as BrigC import API.Galley import Control.Concurrent.Async hiding (link) import Control.Monad @@ -37,7 +36,6 @@ import System.IO hiding (print, putStrLn) import System.IO.Temp import System.Posix.Files import System.Process -import Testlib.App import Testlib.Assertions import Testlib.HTTP import Testlib.JSON @@ -127,9 +125,9 @@ argSubst from to_ s = createWireClient :: (MakesValue u, HasCallStack) => u -> App ClientIdentity createWireClient u = do - lpk <- getLastPrekey - c <- addClient u def {BrigC.lastPrekey = Just lpk} >>= getJSON 201 - mkClientIdentity u c + addClient u def + >>= getJSON 201 + >>= mkClientIdentity u data CredentialType = BasicCredentialType | X509CredentialType @@ -137,10 +135,11 @@ instance MakesValue CredentialType where make BasicCredentialType = make "basic" make X509CredentialType = make "x509" -instance (HasTests x) => HasTests (CredentialType -> x) where - mkTests m n s f x = - mkTests m (n <> "[ctype=basic]") s f (x BasicCredentialType) - <> mkTests m (n <> "[ctype=x509]") s f (x X509CredentialType) +instance TestCases CredentialType where + testCases = + [ MkTestCase "[ctype=basic]" BasicCredentialType, + MkTestCase "[ctype=x509]" X509CredentialType + ] data InitMLSClient = InitMLSClient {credType :: CredentialType} diff --git a/integration/test/Notifications.hs b/integration/test/Notifications.hs index 0fdd7df49a2..4cd03abc95c 100644 --- a/integration/test/Notifications.hs +++ b/integration/test/Notifications.hs @@ -2,11 +2,48 @@ module Notifications where import API.Gundeck +import Control.Error (lastMay) import Control.Monad.Extra import Control.Monad.Reader (asks) import Testlib.Prelude +import UnliftIO (timeout) import UnliftIO.Concurrent +-- | assert that no notifications with the predicate happen within the timeout +assertNoNotifications :: + (HasCallStack, MakesValue user, MakesValue client) => + -- | the user + user -> + -- | the client of that user + client -> + -- | the last notif + Maybe String -> + -- | the predicate + (Value -> App Bool) -> + App () +assertNoNotifications u uc since0 p = do + ucid <- objId uc + let go since = do + notifs <- + getNotifications u def {client = Just ucid, since = since} + `bindResponse` asList + . (%. "notifications") + . (.json) + partitionM p notifs >>= \case + ([], nonMatching) -> + threadDelay 1_000 *> case nonMatching of + (lastMay -> Just lst) -> objId lst >>= go . Just + _ -> go Nothing + (matching, _) -> do + pj <- prettyJSON matching + assertFailure $ + unlines + [ "Expected no matching events but got:", + pj + ] + Nothing <- asks timeOutSeconds >>= flip timeout (go since0) + pure () + awaitNotifications :: (HasCallStack, MakesValue user, MakesValue client) => user -> @@ -18,34 +55,35 @@ awaitNotifications :: (Value -> App Bool) -> App [Value] awaitNotifications user client since0 n selector = do - tSecs <- asks timeOutSeconds + tSecs <- asks ((* 1000) . timeOutSeconds) assertAwaitResult =<< go tSecs since0 (AwaitResult False n [] []) where - go 0 _ res = pure res - go timeRemaining since res0 = do - c <- make client & asString - notifs <- bindResponse - ( getNotifications - user - def {since = since, client = Just c} - ) - $ \resp -> asList (resp.json %. "notifications") - lastNotifId <- case notifs of - [] -> pure since - _ -> Just <$> objId (last notifs) - (matching, notMatching) <- partitionM selector notifs - let matchesSoFar = res0.matches <> matching - res = - res0 - { matches = matchesSoFar, - nonMatches = res0.nonMatches <> notMatching, - success = length matchesSoFar >= res0.nMatchesExpected - } - if res.success - then pure res - else do - threadDelay (1_000_000) - go (timeRemaining - 1) lastNotifId res + go timeRemaining since res0 + | timeRemaining <= 0 = pure res0 + | otherwise = + do + c <- make client & asString + notifs <- + getNotifications + user + def {since = since, client = Just c} + `bindResponse` \resp -> asList (resp.json %. "notifications") + lastNotifId <- case notifs of + [] -> pure since + _ -> Just <$> objId (last notifs) + (matching, notMatching) <- partitionM selector notifs + let matchesSoFar = res0.matches <> matching + res = + res0 + { matches = matchesSoFar, + nonMatches = res0.nonMatches <> notMatching, + success = length matchesSoFar >= res0.nMatchesExpected + } + if res.success + then pure res + else do + threadDelay 1_000 + go (timeRemaining - 1) lastNotifId res awaitNotification :: (HasCallStack, MakesValue user, MakesValue client, MakesValue lastNotifId) => @@ -110,8 +148,32 @@ isConvCreateNotif n = fieldEquals n "payload.0.type" "conversation.create" isConvDeleteNotif :: MakesValue a => a -> App Bool isConvDeleteNotif n = fieldEquals n "payload.0.type" "conversation.delete" +notifTypeIsEqual :: MakesValue a => String -> a -> App Bool +notifTypeIsEqual typ n = nPayload n %. "type" `isEqual` typ + isTeamMemberLeaveNotif :: MakesValue a => a -> App Bool -isTeamMemberLeaveNotif n = nPayload n %. "type" `isEqual` "team.member-leave" +isTeamMemberLeaveNotif = notifTypeIsEqual "team.member-leave" + +isUserActivateNotif :: MakesValue a => a -> App Bool +isUserActivateNotif = notifTypeIsEqual "user.activate" + +isUserClientAddNotif :: MakesValue a => a -> App Bool +isUserClientAddNotif = notifTypeIsEqual "user.client-add" + +isUserClientRemoveNotif :: MakesValue a => a -> App Bool +isUserClientRemoveNotif = notifTypeIsEqual "user.client-remove" + +isUserLegalholdRequestNotif :: MakesValue a => a -> App Bool +isUserLegalholdRequestNotif = notifTypeIsEqual "user.legalhold-request" + +isUserLegalholdEnabledNotif :: MakesValue a => a -> App Bool +isUserLegalholdEnabledNotif = notifTypeIsEqual "user.legalhold-enable" + +isUserLegalholdDisabledNotif :: MakesValue a => a -> App Bool +isUserLegalholdDisabledNotif = notifTypeIsEqual "user.legalhold-disable" + +isUserConnectionNotif :: MakesValue a => a -> App Bool +isUserConnectionNotif = notifTypeIsEqual "user.connection" isConnectionNotif :: MakesValue a => String -> a -> App Bool isConnectionNotif status n = diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 2f765cea618..7a9eab93257 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -7,6 +7,7 @@ import API.Brig import API.BrigInternal import API.Common import API.Galley +import API.GalleyInternal (legalholdWhitelistTeam) import Control.Monad.Reader import Crypto.Random (getRandomBytes) import Data.Aeson hiding ((.=)) @@ -18,6 +19,7 @@ import Data.Function import Data.UUID.V1 (nextUUID) import Data.UUID.V4 (nextRandom) import GHC.Stack +import Testlib.MockIntegrationService (mkLegalHoldSettings) import Testlib.Prelude randomUser :: (HasCallStack, MakesValue domain) => domain -> CreateUser -> App Value @@ -276,3 +278,39 @@ setupProvider u np@(NewProvider {..}) = do pure (k, c) activateProvider dom key code loginProvider dom newProviderEmail pass $> provider + +-- | setup a legalhold device for @uid@, authorised by @owner@ +-- at the specified port +setUpLHDevice :: + (HasCallStack, MakesValue tid, MakesValue owner, MakesValue uid) => + tid -> + owner -> + uid -> + -- | the port the LH service is running on + Int -> + App () +setUpLHDevice tid alice bob lhPort = do + legalholdWhitelistTeam tid alice + >>= assertStatus 200 + + -- the status messages for these have already been tested + postLegalHoldSettings tid alice (mkLegalHoldSettings lhPort) + >>= assertStatus 201 + + requestLegalHoldDevice tid alice bob + >>= assertStatus 201 + + approveLegalHoldDevice tid bob defPassword + >>= assertStatus 200 + +lhDeviceIdOf :: MakesValue user => user -> App String +lhDeviceIdOf bob = do + bobId <- objId bob + getClientsFull bob [bobId] `bindResponse` \resp -> + do + resp.json %. bobId + & asList + >>= filterM \val -> (== "legalhold") <$> (val %. "type" & asString) + >>= assertOne + >>= (%. "id") + >>= asString diff --git a/integration/test/Test/LegalHold.hs b/integration/test/Test/LegalHold.hs index d35af907b28..af2968206f8 100644 --- a/integration/test/Test/LegalHold.hs +++ b/integration/test/Test/LegalHold.hs @@ -18,17 +18,25 @@ module Test.LegalHold where import API.Brig -import API.BrigCommon +import API.BrigCommon as BrigC import qualified API.BrigInternal as BrigI import API.Common import API.Galley import API.GalleyInternal +import Control.Error (MaybeT (MaybeT), runMaybeT) import Control.Lens ((.~), (^?!)) +import Control.Monad.Reader (asks, local) +import Control.Monad.Trans.Class (lift) +import qualified Data.ByteString.Char8 as BS8 +import Data.ByteString.Lazy (LazyByteString) import qualified Data.Map as Map import qualified Data.ProtoLens as Proto import Data.ProtoLens.Labels () import qualified Data.Set as Set +import qualified Data.Text as T import GHC.Stack +import Network.Wai (Request (pathInfo, requestMethod)) +import Notifications import Numeric.Lens (hex) import qualified Proto.Otr as Proto import qualified Proto.Otr_Fields as Proto @@ -36,6 +44,7 @@ import SetupHelpers import Testlib.MockIntegrationService import Testlib.Prekeys import Testlib.Prelude +import UnliftIO (Chan, readChan, timeout) testLHPreventAddingNonConsentingUsers :: App () testLHPreventAddingNonConsentingUsers = do @@ -43,9 +52,9 @@ testLHPreventAddingNonConsentingUsers = do withMockServer lhMockApp $ \lhPort _chan -> do (owner, tid, [alice, alex]) <- createTeam dom 3 - void $ legalholdWhitelistTeam owner tid >>= assertSuccess - void $ legalholdIsTeamInWhitelist owner tid >>= assertSuccess - void $ postLegalHoldSettings owner tid (mkLegalHoldSettings lhPort) >>= getJSON 201 + legalholdWhitelistTeam tid owner >>= assertSuccess + legalholdIsTeamInWhitelist tid owner >>= assertSuccess + postLegalHoldSettings tid owner (mkLegalHoldSettings lhPort) >>= assertStatus 201 george <- randomUser dom def georgeQId <- george %. "qualified_id" @@ -68,13 +77,11 @@ testLHPreventAddingNonConsentingUsers = do checkConvHasOtherMembers conv alice [alex] -- it should not be possible neither for alex nor for alice to add the guest back - bindResponse (addMembers alex conv def {users = [georgeQId]}) $ \resp -> do - resp.status `shouldMatchInt` 403 - resp.json %. "label" `shouldMatch` "not-connected" + addMembers alex conv def {users = [georgeQId]} + >>= assertLabel 403 "not-connected" - bindResponse (addMembers alice conv def {users = [georgeQId]}) $ \resp -> do - resp.status `shouldMatchInt` 403 - resp.json %. "label" `shouldMatch` "missing-legalhold-consent" + addMembers alice conv def {users = [georgeQId]} + >>= assertLabel 403 "missing-legalhold-consent" where checkConvHasOtherMembers :: HasCallStack => Value -> Value -> [Value] -> App () checkConvHasOtherMembers conv u us = @@ -105,9 +112,9 @@ testLHMessageExchange (TaggedBool clients1New) (TaggedBool clients2New) (TaggedB client1 <- objId $ addClient (mem1 %. "qualified_id") (clientSettings clients1New) >>= getJSON 201 _client2 <- objId $ addClient (mem2 %. "qualified_id") (clientSettings clients2New) >>= getJSON 201 - void $ legalholdWhitelistTeam owner tid >>= assertSuccess - void $ legalholdIsTeamInWhitelist owner tid >>= assertSuccess - void $ postLegalHoldSettings owner tid (mkLegalHoldSettings lhPort) >>= getJSON 201 + legalholdWhitelistTeam tid owner >>= assertSuccess + legalholdIsTeamInWhitelist tid owner >>= assertSuccess + postLegalHoldSettings tid owner (mkLegalHoldSettings lhPort) >>= assertStatus 201 conv <- postConversation mem1 (defProteus {qualifiedUsers = [mem2], team = Just tid}) >>= getJSON 201 @@ -130,7 +137,7 @@ testLHMessageExchange (TaggedBool clients1New) (TaggedBool clients2New) (TaggedB length cs1 `shouldMatchInt` if consentFrom1 then 2 else 1 length cs2 `shouldMatchInt` if consentFrom2 then 2 else 1 - void $ do + do successfulMsgForOtherUsers <- mkProteusRecipients mem1 [(mem1, cs1), (mem2, cs2)] "hey there" let successfulMsg = Proto.defMessage @Proto.QualifiedNewOtrMessage @@ -191,19 +198,19 @@ testLHMessageExchange (TaggedBool clients1New) (TaggedBool clients2New) (TaggedB data TestClaimKeys = TCKConsentMissing -- (team not whitelisted, that is) | TCKConsentAndNewClients - deriving (Show, Bounded, Enum) + deriving (Show, Generic) -- | Cannot fetch prekeys of LH users if requester has not given consent or has old clients. -testLHClaimKeys :: WithBoundedEnumArg TestClaimKeys (App ()) -testLHClaimKeys = WithBoundedEnumArg $ \testmode -> do +testLHClaimKeys :: TestClaimKeys -> App () +testLHClaimKeys testmode = do startDynamicBackends [mempty] $ \[dom] -> do withMockServer lhMockApp $ \lhPort _chan -> do (lowner, ltid, [lmem]) <- createTeam dom 2 (powner, ptid, [pmem]) <- createTeam dom 2 - legalholdWhitelistTeam lowner ltid >>= assertSuccess - legalholdIsTeamInWhitelist lowner ltid >>= assertSuccess - void $ postLegalHoldSettings lowner ltid (mkLegalHoldSettings lhPort) >>= getJSON 201 + legalholdWhitelistTeam ltid lowner >>= assertSuccess + legalholdIsTeamInWhitelist ltid lowner >>= assertSuccess + postLegalHoldSettings ltid lowner (mkLegalHoldSettings lhPort) >>= assertStatus 201 requestLegalHoldDevice ltid lowner lmem >>= assertSuccess approveLegalHoldDevice ltid (lmem %. "qualified_id") defPassword >>= assertSuccess @@ -220,8 +227,8 @@ testLHClaimKeys = WithBoundedEnumArg $ \testmode -> do addc $ Just ["legalhold-implicit-consent"] TCKConsentAndNewClients -> do addc $ Just ["legalhold-implicit-consent"] - void $ legalholdWhitelistTeam powner ptid >>= assertSuccess - void $ legalholdIsTeamInWhitelist powner ptid >>= assertSuccess + legalholdWhitelistTeam ptid powner >>= assertSuccess + legalholdIsTeamInWhitelist ptid powner >>= assertSuccess llhdev :: String <- do let getCls :: Value -> App [String] @@ -254,8 +261,7 @@ testLHAddClientManually :: App () testLHAddClientManually = do (_owner, _tid, [mem1]) <- createTeam OwnDomain 2 bindResponse (addClient mem1 def {ctype = "legalhold"}) $ \resp -> do - resp.status `shouldMatchInt` 400 - resp.json %. "label" `shouldMatch` "client-error" + assertLabel 400 "client-error" resp -- we usually don't test the human-readable "message", but in this case it is important to -- make sure the reason is the right one, and not eg. "LH service not present", or some -- other unspecific client error. @@ -274,3 +280,497 @@ testLHDeleteClientManually = do -- make sure the reason is the right one, and not eg. "LH service not present", or some -- other unspecific client error. resp.json %. "message" `shouldMatch` "LegalHold clients cannot be deleted. LegalHold must be disabled on this user by an admin" + +testLHRequestDevice :: App () +testLHRequestDevice = + startDynamicBackends [mempty] $ \[dom] -> do + (alice, tid, [bob]) <- createTeam dom 2 + let reqNotEnabled requester requestee = + requestLegalHoldDevice tid requester requestee + >>= assertLabel 403 "legalhold-not-enabled" + + reqNotEnabled alice bob + + lpk <- getLastPrekey + pks <- replicateM 3 getPrekey + + withMockServer (lhMockAppWithPrekeys MkCreateMock {nextLastPrey = pure lpk, somePrekeys = pure pks}) \lhPort _chan -> do + let statusShouldBe :: String -> App () + statusShouldBe status = + legalholdUserStatus tid alice bob `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "status" `shouldMatch` status + + -- the user has not agreed to be under legalhold + for_ [alice, bob] \requester -> do + reqNotEnabled requester bob + statusShouldBe "no_consent" + + legalholdWhitelistTeam tid alice >>= assertSuccess + postLegalHoldSettings tid alice (mkLegalHoldSettings lhPort) >>= assertSuccess + + statusShouldBe "disabled" + + requestLegalHoldDevice tid alice bob >>= assertStatus 201 + statusShouldBe "pending" + + -- requesting twice should be idempotent wrt the approval + -- mind that requesting twice means two "user.legalhold-request" notifications + -- for the clients of the user under legalhold (bob) + requestLegalHoldDevice tid alice bob >>= assertStatus 204 + statusShouldBe "pending" + + [bobc1, bobc2] <- replicateM 2 do + objId $ addClient bob def `bindResponse` getJSON 201 + for_ [bobc1, bobc2] \client -> + awaitNotification bob client noValue isUserLegalholdRequestNotif >>= \notif -> do + notif %. "payload.0.last_prekey" `shouldMatch` lpk + notif %. "payload.0.id" `shouldMatch` objId bob + +-- | pops a channel until it finds an event that returns a 'Just' +-- upon running the matcher function +checkChan :: HasCallStack => Chan t -> (t -> App (Maybe a)) -> App a +checkChan chan match = do + tSecs <- asks ((* 1_000_000) . timeOutSeconds) + + maybe (assertFailure "checkChan: timed out") pure =<< timeout tSecs do + let go = readChan chan >>= match >>= maybe go pure + go + +-- | like 'checkChan' but throws away the request and decodes the body +checkChanVal :: HasCallStack => Chan (t, LazyByteString) -> (Value -> MaybeT App a) -> App a +checkChanVal chan match = checkChan chan \(_, bs) -> runMaybeT do + MaybeT (pure (decode bs)) >>= match + +testLHApproveDevice :: App () +testLHApproveDevice = do + startDynamicBackends [mempty] \[dom] -> do + -- team users + -- alice (boss) and bob and charlie (member) + (alice, tid, [bob, charlie]) <- createTeam dom 3 + + -- ollie the outsider + ollie <- do + o <- randomUser dom def + connectTwoUsers o alice + pure o + + -- sandy the stranger + sandy <- randomUser dom def + + legalholdWhitelistTeam tid alice >>= assertStatus 200 + approveLegalHoldDevice tid (bob %. "qualified_id") defPassword + >>= assertLabel 412 "legalhold-not-pending" + + withMockServer lhMockApp \lhPort chan -> do + legalholdWhitelistTeam tid alice + >>= assertStatus 200 + postLegalHoldSettings tid alice (mkLegalHoldSettings lhPort) + >>= assertStatus 201 + requestLegalHoldDevice tid alice bob + >>= assertStatus 201 + + let uidsAndTidMatch val = do + actualTid <- + lookupFieldM val "team_id" + >>= lift . asString + actualUid <- + lookupFieldM val "user_id" + >>= lift . asString + bobUid <- lift $ objId bob + + -- we pass the check on equality + unless ((actualTid, actualUid) == (tid, bobUid)) do + mzero + + checkChanVal chan uidsAndTidMatch + + -- the team owner cannot approve for bob + approveLegalHoldDevice' tid alice bob defPassword + >>= assertLabel 403 "access-denied" + -- bob needs to provide a password + approveLegalHoldDevice tid bob "wrong-password" + >>= assertLabel 403 "access-denied" + -- now bob finally found his password + approveLegalHoldDevice tid bob defPassword + >>= assertStatus 200 + + let matchAuthToken val = + lookupFieldM val "refresh_token" + >>= lift . asString + + checkChanVal chan matchAuthToken + >>= renewToken bob + >>= assertStatus 200 + + lhdId <- lhDeviceIdOf bob + + legalholdUserStatus tid alice bob `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "client.id" `shouldMatch` lhdId + resp.json %. "status" `shouldMatch` "enabled" + + replicateM 2 do + objId $ addClient bob def `bindResponse` getJSON 201 + >>= traverse_ \client -> + awaitNotification bob client noValue isUserClientAddNotif >>= \notif -> do + notif %. "payload.0.client.type" `shouldMatch` "legalhold" + notif %. "payload.0.client.class" `shouldMatch` "legalhold" + + -- the other team members receive a notification about the + -- legalhold device being approved in their team + for_ [alice, charlie] \user -> do + client <- objId $ addClient user def `bindResponse` getJSON 201 + awaitNotification user client noValue isUserLegalholdEnabledNotif >>= \notif -> do + notif %. "payload.0.id" `shouldMatch` objId bob + for_ [ollie, sandy] \outsider -> do + outsiderClient <- objId $ addClient outsider def `bindResponse` getJSON 201 + assertNoNotifications outsider outsiderClient Nothing isUserLegalholdEnabledNotif + +testLHGetDeviceStatus :: App () +testLHGetDeviceStatus = + startDynamicBackends [mempty] \[dom] -> do + -- team users + -- alice (team owner) and bob (member) + (alice, tid, [bob]) <- createTeam dom 2 + for_ [alice, bob] \user -> do + legalholdUserStatus tid alice user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "status" `shouldMatch` "no_consent" + + lpk <- getLastPrekey + pks <- replicateM 3 getPrekey + + withMockServer + do lhMockAppWithPrekeys MkCreateMock {nextLastPrey = pure lpk, somePrekeys = pure pks} + \lhPort _chan -> do + legalholdWhitelistTeam tid alice + >>= assertStatus 200 + + legalholdUserStatus tid alice bob `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "status" `shouldMatch` "disabled" + lookupField resp.json "last_prekey" + >>= assertNothing + runMaybeT (lookupFieldM resp.json "client" >>= flip lookupFieldM "id") + >>= assertNothing + + -- the status messages for these have already been tested + postLegalHoldSettings tid alice (mkLegalHoldSettings lhPort) + >>= assertStatus 201 + + requestLegalHoldDevice tid alice bob + >>= assertStatus 201 + + approveLegalHoldDevice tid bob defPassword + >>= assertStatus 200 + + lhdId <- lhDeviceIdOf bob + legalholdUserStatus tid alice bob `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "status" `shouldMatch` "enabled" + resp.json %. "last_prekey" `shouldMatch` lpk + resp.json %. "client.id" `shouldMatch` lhdId + + requestLegalHoldDevice tid alice bob + >>= assertLabel 409 "legalhold-already-enabled" + +-- | this sets the timeout to a higher number; we need +-- this because the SQS queue on the brig is super slow +-- and that's why client.remove events arrive really late +-- +-- FUTUREWORK(mangoiv): improve the speed of internal +-- event queuing +setTimeoutTo :: Int -> Env -> Env +setTimeoutTo tSecs env = env {timeOutSeconds = tSecs} + +testLHDisableForUser :: App () +testLHDisableForUser = + startDynamicBackends [mempty] \[dom] -> do + -- team users + -- alice (team owner) and bob (member) + (alice, tid, [bob]) <- createTeam dom 2 + + withMockServer lhMockApp \lhPort chan -> do + setUpLHDevice tid alice bob lhPort + + bobc <- objId $ addClient bob def `bindResponse` getJSON 201 + + awaitNotification bob bobc noValue isUserClientAddNotif >>= \notif -> do + notif %. "payload.0.client.type" `shouldMatch` "legalhold" + notif %. "payload.0.client.class" `shouldMatch` "legalhold" + + -- only an admin can disable legalhold + disableLegalHold tid bob bob defPassword + >>= assertLabel 403 "operation-denied" + + disableLegalHold tid alice bob "fix ((\"the password always is \" <>) . show)" + >>= assertLabel 403 "access-denied" + + disableLegalHold tid alice bob defPassword + >>= assertStatus 200 + + checkChan chan \(req, _) -> runMaybeT do + unless + do + BS8.unpack req.requestMethod == "POST" + && req.pathInfo == (T.pack <$> ["legalhold", "remove"]) + mzero + + void $ local (setTimeoutTo 90) do + awaitNotification bob bobc noValue isUserClientRemoveNotif + *> awaitNotification bob bobc noValue isUserLegalholdDisabledNotif + + bobId <- objId bob + lhClients <- + BrigI.getClientsFull bob [bobId] `bindResponse` \resp -> do + resp.json %. bobId + & asList + >>= filterM \val -> (== "legalhold") <$> (val %. "type" & asString) + + shouldBeEmpty lhClients + +testLHEnablePerTeam :: App () +testLHEnablePerTeam = do + startDynamicBackends [mempty] \[dom] -> do + -- team users + -- alice (team owner) and bob (member) + (alice, tid, [bob]) <- createTeam dom 2 + legalholdIsEnabled tid alice `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "lockStatus" `shouldMatch` "unlocked" + resp.json %. "status" `shouldMatch` "disabled" + + withMockServer lhMockApp \lhPort _chan -> do + setUpLHDevice tid alice bob lhPort + + legalholdUserStatus tid alice bob `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "status" `shouldMatch` "enabled" + + putLegalholdStatus tid alice "disabled" + `bindResponse` assertLabel 403 "legalhold-whitelisted-only" + + -- the put doesn't have any influence on the status being "enabled" + legalholdUserStatus tid alice bob `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "status" `shouldMatch` "enabled" + +testLHGetMembersIncludesStatus :: App () +testLHGetMembersIncludesStatus = do + startDynamicBackends [mempty] \[dom] -> do + -- team users + -- alice (team owner) and bob (member) + (alice, tid, [bob]) <- createTeam dom 2 + + let statusShouldBe :: String -> App () + statusShouldBe status = do + getTeamMembers alice tid `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + [bobMember] <- + resp.json %. "members" & asList >>= filterM \u -> do + (==) <$> asString (u %. "user") <*> objId bob + bobMember %. "legalhold_status" `shouldMatch` status + + statusShouldBe "no_consent" + withMockServer lhMockApp \lhPort _chan -> do + statusShouldBe "no_consent" + + legalholdWhitelistTeam tid alice + >>= assertStatus 200 + + -- the status messages for these have already been tested + postLegalHoldSettings tid alice (mkLegalHoldSettings lhPort) + >>= assertStatus 201 + + -- legalhold has been requested but is disabled + statusShouldBe "disabled" + + requestLegalHoldDevice tid alice bob + >>= assertStatus 201 + + -- legalhold has been set to pending after requesting device + statusShouldBe "pending" + + approveLegalHoldDevice tid bob defPassword + >>= assertStatus 200 + + -- bob has accepted the legalhold device + statusShouldBe "enabled" + +type TB s = TaggedBool s + +testLHNoConsentBlockOne2OneConv :: TB "connect first" -> TB "team peer" -> TB "approve LH" -> TB "test pending connection" -> App () +testLHNoConsentBlockOne2OneConv + (MkTagged connectFirst) + (MkTagged teampeer) + (MkTagged approveLH) + (MkTagged testPendingConnection) = + startDynamicBackends [mempty] \[dom1] -> do + -- team users + -- alice (team owner) and bob (member) + (alice, tid, []) <- createTeam dom1 1 + bob <- + if teampeer + then do + (walice, _tid, []) <- createTeam dom1 1 + -- FUTUREWORK(mangoiv): creating a team on a second backend + -- causes this bug: https://wearezeta.atlassian.net/browse/WPB-6640 + pure walice + else randomUser dom1 def + + legalholdWhitelistTeam tid alice + >>= assertStatus 200 + + let doEnableLH :: HasCallStack => App (Maybe String) + doEnableLH = do + -- alice requests a legalhold device for herself + requestLegalHoldDevice tid alice alice + >>= assertStatus 201 + + when approveLH do + approveLegalHoldDevice tid alice defPassword + >>= assertStatus 200 + legalholdUserStatus tid alice alice `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "status" `shouldMatch` if approveLH then "enabled" else "pending" + if approveLH + then Just <$> lhDeviceIdOf alice + else pure Nothing + + doDisableLH :: HasCallStack => App () + doDisableLH = + disableLegalHold tid alice alice defPassword + >>= assertStatus 200 + + withMockServer lhMockApp \lhPort _chan -> do + postLegalHoldSettings tid alice (mkLegalHoldSettings lhPort) + >>= assertStatus 201 + + if not connectFirst + then do + void doEnableLH + postConnection alice bob + >>= assertLabel 403 "missing-legalhold-consent" + + postConnection bob alice + >>= assertLabel 403 "missing-legalhold-consent" + else do + alicec <- objId $ addClient alice def >>= getJSON 201 + bobc <- objId $ addClient bob def >>= getJSON 201 + + postConnection alice bob + >>= assertStatus 201 + mbConvId <- + if testPendingConnection + then pure Nothing + else + Just + <$> do + putConnection bob alice "accepted" + >>= getJSON 200 + %. "qualified_conversation" + + -- we need to take away the pending/ sent status for the connections + [lastNotifAlice, lastNotifBob] <- for [(alice, alicec), (bob, bobc)] \(user, client) -> do + -- we get two events if bob accepts alice's request + let numEvents = if testPendingConnection then 1 else 2 + last <$> awaitNotifications user client Nothing numEvents isUserConnectionNotif + + mbLHDevice <- doEnableLH + + let assertConnectionsMissingLHConsent = + for_ [(bob, alice), (alice, bob)] \(a, b) -> + getConnections a `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + conn <- assertOne =<< do resp.json %. "connections" & asList + conn %. "status" `shouldMatch` "missing-legalhold-consent" + conn %. "from" `shouldMatch` objId a + conn %. "to" `shouldMatch` objId b + + assertConnectionsMissingLHConsent + + [lastNotifAlice', lastNotifBob'] <- for [(alice, alicec, lastNotifAlice), (bob, bobc, lastNotifBob)] \(user, client, lastNotif) -> do + awaitNotification user client (Just lastNotif) isUserConnectionNotif >>= \notif -> + notif %. "payload.0.connection.status" `shouldMatch` "missing-legalhold-consent" + $> notif + + for_ [(bob, alice), (alice, bob)] \(a, b) -> + putConnection a b "accepted" + >>= assertLabel 403 "bad-conn-update" + + -- putting the connection to "accepted" with 403 doesn't change the + -- connection status + assertConnectionsMissingLHConsent + + bobc2 <- objId $ addClient bob def >>= getJSON 201 + + let -- \| we send a message from bob to alice, but only if + -- we have a conversation id and a legalhold device + -- we first create a message that goes to recipients + -- chosen by the first callback passed + -- then send the message using proteus + -- and in the end running the assertino callback to + -- verify the result + sendMessageFromBobToAlice :: + HasCallStack => + (String -> [String]) -> + -- \^ if we have the legalhold device registered, this + -- callback will be passed the lh device + (Response -> App ()) -> + -- \^ the callback to verify our response (an assertion) + App () + sendMessageFromBobToAlice recipients assertion = + for_ ((,) <$> mbConvId <*> mbLHDevice) \(convId, device) -> do + successfulMsgForOtherUsers <- + mkProteusRecipients + bob -- bob is the sender + [(alice, recipients device), (bob, [bobc])] + -- we send to clients of alice, maybe the legalhold device + -- we need to send to our other clients (bobc) + "hey alice (and eve)" -- the message + let bobaliceMessage = + Proto.defMessage @Proto.QualifiedNewOtrMessage + & #sender . Proto.client .~ (bobc2 ^?! hex) + & #recipients .~ [successfulMsgForOtherUsers] + & #reportAll .~ Proto.defMessage + -- make sure that `convId` is not just the `convId` but also + -- contains the domain because `postProteusMessage` will take the + -- comain from the `convId` json object + postProteusMessage bob convId bobaliceMessage + `bindResponse` assertion + + sendMessageFromBobToAlice (\device -> [alicec, device]) \resp -> do + resp.status `shouldMatchInt` 404 + + -- now we disable legalhold + doDisableLH + + for_ mbLHDevice \lhd -> + local (setTimeoutTo 90) $ + awaitNotification alice alicec noValue isUserClientRemoveNotif >>= \notif -> + notif %. "payload.0.client.id" `shouldMatch` lhd + + let assertStatusFor user status = + getConnections user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + conn <- assertOne =<< do resp.json %. "connections" & asList + conn %. "status" `shouldMatch` status + + if testPendingConnection + then do + assertStatusFor alice "sent" + assertStatusFor bob "pending" + else do + assertStatusFor alice "accepted" + assertStatusFor bob "accepted" + + for_ [(alice, alicec, lastNotifAlice'), (bob, bobc, lastNotifBob')] \(user, client, lastNotif) -> do + awaitNotification user client (Just lastNotif) isUserConnectionNotif >>= \notif -> + notif %. "payload.0.connection.status" `shouldMatchOneOf` ["sent", "pending", "accepted"] + + sendMessageFromBobToAlice (const [alicec]) \resp -> do + resp.status `shouldMatchInt` 201 + + sendMessageFromBobToAlice (\device -> [device]) \resp -> do + resp.status `shouldMatchInt` 412 diff --git a/integration/test/Test/MLS.hs b/integration/test/Test/MLS.hs index 049d3d8d4a7..413963c2d59 100644 --- a/integration/test/Test/MLS.hs +++ b/integration/test/Test/MLS.hs @@ -24,7 +24,7 @@ testSendMessageNoReturnToSender = do -- the message withWebSockets [alice1, alice2, bob1, bob2] $ \(wsSender : wss) -> do mp <- createApplicationMessage alice1 "hello, bob" - void . bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do + bindResponse (postMLSMessage mp.sender mp.message) $ \resp -> do resp.status `shouldMatchInt` 201 for_ wss $ \ws -> do n <- awaitMatch (\n -> nPayload n %. "type" `isEqual` "conversation.mls-message-add") ws diff --git a/integration/test/Test/MLS/One2One.hs b/integration/test/Test/MLS/One2One.hs index 271f5ee9807..1e2f10e02ee 100644 --- a/integration/test/Test/MLS/One2One.hs +++ b/integration/test/Test/MLS/One2One.hs @@ -115,11 +115,12 @@ data One2OneScenario | -- | One user is remote, conversation is remote One2OneScenarioRemoteConv -instance HasTests x => HasTests (One2OneScenario -> x) where - mkTests m n s f x = - mkTests m (n <> "[domain=own]") s f (x One2OneScenarioLocal) - <> mkTests m (n <> "[domain=other;conv=own]") s f (x One2OneScenarioLocalConv) - <> mkTests m (n <> "[domain=other;conv=other]") s f (x One2OneScenarioRemoteConv) +instance TestCases One2OneScenario where + testCases = + [ MkTestCase "[domain=own]" One2OneScenarioLocal, + MkTestCase "[domain=other;conv=own]" One2OneScenarioLocalConv, + MkTestCase "[domain=other;conv=other]" One2OneScenarioRemoteConv + ] one2OneScenarioDomain :: One2OneScenario -> Domain one2OneScenarioDomain One2OneScenarioLocal = OwnDomain diff --git a/integration/test/Test/MLS/SubConversation.hs b/integration/test/Test/MLS/SubConversation.hs index 42cdb0ec95f..d73095030da 100644 --- a/integration/test/Test/MLS/SubConversation.hs +++ b/integration/test/Test/MLS/SubConversation.hs @@ -102,15 +102,11 @@ testDeleteSubConversation otherDomain = do sub2' <- getSubConversation alice1 qcnv "conference2" >>= getJSON 200 sub2 `shouldNotMatch` sub2' -data LeaveSubConvVariant = AliceLeaves | BobLeaves +data Leaver = Alice | Bob + deriving stock (Generic) -instance HasTests x => HasTests (LeaveSubConvVariant -> x) where - mkTests m n s f x = - mkTests m (n <> "[leaver=alice]") s f (x AliceLeaves) - <> mkTests m (n <> "[leaver=bob]") s f (x BobLeaves) - -testLeaveSubConv :: HasCallStack => LeaveSubConvVariant -> App () -testLeaveSubConv variant = do +testLeaveSubConv :: HasCallStack => Leaver -> App () +testLeaveSubConv leaver = do [alice, bob, charlie] <- createAndConnectUsers [OwnDomain, OwnDomain, OtherDomain] clients@[alice1, bob1, bob2, charlie1] <- traverse (createMLSClient def) [alice, bob, bob, charlie] traverse_ uploadNewKeyPackage [bob1, bob2, charlie1] @@ -126,9 +122,9 @@ testLeaveSubConv variant = do void $ createExternalCommit charlie1 Nothing >>= sendAndConsumeCommitBundle -- a member leaves the subconversation - let (firstLeaver, idxFirstLeaver) = case variant of - BobLeaves -> (bob1, 0) - AliceLeaves -> (alice1, 1) + let (firstLeaver, idxFirstLeaver) = case leaver of + Bob -> (bob1, 0) + Alice -> (alice1, 1) let idxCharlie1 = 3 let others = filter (/= firstLeaver) clients diff --git a/integration/test/Test/Search.hs b/integration/test/Test/Search.hs index ac66155b1b6..7d93b4ff015 100644 --- a/integration/test/Test/Search.hs +++ b/integration/test/Test/Search.hs @@ -76,7 +76,7 @@ data FedUserSearchTestCase = FedUserSearchTestCase testFederatedUserSearch :: HasCallStack => App () testFederatedUserSearch = do - let testCases = + let tcs = [ -- no search FedUserSearchTestCase "no_search" AllowAll AllowAll False False, FedUserSearchTestCase "no_search" TeamAllowed TeamAllowed False False, @@ -100,7 +100,7 @@ testFederatedUserSearch = do startDynamicBackends [def, def] $ \[d1, d2] -> do void $ BrigI.createFedConn d2 (BrigI.FedConn d1 "full_search" Nothing) void $ BrigI.createFedConn d1 (BrigI.FedConn d2 "full_search" Nothing) - forM_ testCases (federatedUserSearch d1 d2) + forM_ tcs (federatedUserSearch d1 d2) federatedUserSearch :: HasCallStack => String -> String -> FedUserSearchTestCase -> App () federatedUserSearch d1 d2 test = do diff --git a/integration/test/Test/User.hs b/integration/test/Test/User.hs index 903de5a0724..2c5df564377 100644 --- a/integration/test/Test/User.hs +++ b/integration/test/Test/User.hs @@ -120,8 +120,8 @@ testUpdateHandle = do -- | For now this only tests attempts to update one's own display name, email address, or -- language in E2EId-enabled teams (ie., everything except handle). More tests can be found -- under `/services/brig/test/integration` (and should be moved here). -testUpdateSelf :: HasCallStack => TestUpdateSelfMode -> App () -testUpdateSelf mode = do +testUpdateSelf :: HasCallStack => Tagged "mode" TestUpdateSelfMode -> App () +testUpdateSelf (MkTagged mode) = do -- create team with one member, without scim, but with `mlsE2EId` enabled. (owner, team, [mem1]) <- createTeam OwnDomain 2 @@ -162,11 +162,4 @@ data TestUpdateSelfMode = TestUpdateDisplayName | TestUpdateEmailAddress | TestUpdateLocale - deriving (Eq, Show, Bounded, Enum) - -instance HasTests x => HasTests (TestUpdateSelfMode -> x) where - mkTests m n s f x = - mconcat - [ mkTests m (n <> "[mode=" <> show mode <> "]") s f (x mode) - | mode <- [minBound ..] - ] + deriving (Eq, Show, Generic) diff --git a/integration/test/Test/Version.hs b/integration/test/Test/Version.hs index e6996107fc2..31295918468 100644 --- a/integration/test/Test/Version.hs +++ b/integration/test/Test/Version.hs @@ -8,13 +8,14 @@ import Testlib.Prelude newtype Versioned' = Versioned' Versioned -- | This instance is used to generate tests for some of the versions. (Not checking all of them for time efficiency reasons) -instance HasTests x => HasTests (Versioned' -> x) where - mkTests m n s f x = - mkTests m (n <> "[version=unversioned]") s f (x (Versioned' Unversioned)) - <> mkTests m (n <> "[version=versioned]") s f (x (Versioned' Versioned)) - <> mkTests m (n <> "[version=v1]") s f (x (Versioned' (ExplicitVersion 1))) - <> mkTests m (n <> "[version=v3]") s f (x (Versioned' (ExplicitVersion 3))) - <> mkTests m (n <> "[version=v6]") s f (x (Versioned' (ExplicitVersion 6))) +instance TestCases Versioned' where + testCases = + [ MkTestCase "[version=unversioned]" (Versioned' Unversioned), + MkTestCase "[version=versioned]" (Versioned' Versioned), + MkTestCase "[version=v1]" (Versioned' (ExplicitVersion 1)), + MkTestCase "[version=v3]" (Versioned' (ExplicitVersion 3)), + MkTestCase "[version=v6]" (Versioned' (ExplicitVersion 6)) + ] testVersion :: Versioned' -> App () testVersion (Versioned' v) = withModifiedBackend diff --git a/integration/test/Testlib/App.hs b/integration/test/Testlib/App.hs index 0e85badb2f7..ee6f5e4da77 100644 --- a/integration/test/Testlib/App.hs +++ b/integration/test/Testlib/App.hs @@ -7,6 +7,7 @@ import Data.IORef import qualified Data.Text as T import qualified Data.Yaml as Yaml import GHC.Exception +import GHC.Generics (Generic) import GHC.Stack (HasCallStack) import System.FilePath import Testlib.JSON @@ -52,6 +53,7 @@ readServiceConfig' srvName = do Right value -> pure value data Domain = OwnDomain | OtherDomain + deriving stock (Eq, Show, Generic) instance MakesValue Domain where make OwnDomain = asks (String . T.pack . (.domain1)) diff --git a/integration/test/Testlib/Assertions.hs b/integration/test/Testlib/Assertions.hs index 390615730c9..2668a84b745 100644 --- a/integration/test/Testlib/Assertions.hs +++ b/integration/test/Testlib/Assertions.hs @@ -55,7 +55,7 @@ shouldMatch :: a `shouldMatch` b = do xa <- make a xb <- make b - unless (xa == xb) $ do + unless (xa == xb) do pa <- prettyJSON xa pb <- prettyJSON xb assertFailure $ "Actual:\n" <> pa <> "\nExpected:\n" <> pb diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index f143fea4828..f85f1d0934a 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -93,7 +93,7 @@ mkGlobalEnv cfgFile = do tempDir <- Codensity $ withSystemTempDirectory "test" timeOutSeconds <- liftIO $ - fromMaybe 10 . (readMaybe @Int =<<) <$> (lookupEnv "TEST_TIMEOUT_SECONDS") + fromMaybe 10 . (readMaybe @Int =<<) <$> lookupEnv "TEST_TIMEOUT_SECONDS" pure GlobalEnv { gServiceMap = sm, diff --git a/integration/test/Testlib/HTTP.hs b/integration/test/Testlib/HTTP.hs index 721700df3e2..e21b6e3c588 100644 --- a/integration/test/Testlib/HTTP.hs +++ b/integration/test/Testlib/HTTP.hs @@ -16,6 +16,7 @@ import Data.String import Data.String.Conversions (cs) import qualified Data.Text as T import qualified Data.Text.Encoding as T +import GHC.Generics import GHC.Stack import qualified Network.HTTP.Client as HTTP import Network.HTTP.Types (hLocation) @@ -85,23 +86,36 @@ contentTypeMixed = addHeader "Content-Type" "multipart/mixed" bindResponse :: HasCallStack => App Response -> (Response -> App a) -> App a bindResponse m k = m >>= \r -> withResponse r k +infixl 1 `bindResponse` + withResponse :: HasCallStack => Response -> (Response -> App a) -> App a withResponse r k = onFailureAddResponse r (k r) -- | Check response status code, then return body. getBody :: HasCallStack => Int -> Response -> App ByteString -getBody status resp = withResponse resp $ \r -> do - r.status `shouldMatch` status - pure r.body +getBody status = flip withResponse \resp -> do + resp.status `shouldMatch` status + pure resp.body -- | Check response status code, then return JSON body. getJSON :: HasCallStack => Int -> Response -> App Aeson.Value -getJSON status resp = withResponse resp $ \r -> do - r.status `shouldMatch` status - r.json +getJSON status = flip withResponse \resp -> do + resp.status `shouldMatch` status + resp.json +-- | assert a response code in the 2** range assertSuccess :: HasCallStack => Response -> App () -assertSuccess resp = withResponse resp $ \r -> r.status `shouldMatchRange` (200, 299) +assertSuccess = flip withResponse \resp -> resp.status `shouldMatchRange` (200, 299) + +-- | assert a response status code +assertStatus :: HasCallStack => Int -> Response -> App () +assertStatus status = flip withResponse \resp -> resp.status `shouldMatchInt` status + +-- | assert a failure with some failure code and label +assertLabel :: HasCallStack => Int -> String -> Response -> App () +assertLabel status label resp = do + j <- getJSON status resp + j %. "label" `shouldMatch` label onFailureAddResponse :: HasCallStack => Response -> App a -> App a onFailureAddResponse r m = App $ do @@ -110,6 +124,7 @@ onFailureAddResponse r m = App $ do E.throw (AssertionFailure stack (Just r) msg) data Versioned = Versioned | Unversioned | ExplicitVersion Int + deriving stock (Generic) -- | If you don't know what domain is for or what you should put in there, try `rawBaseRequest -- OwnDomain ...`. diff --git a/integration/test/Testlib/JSON.hs b/integration/test/Testlib/JSON.hs index 59aa2400ff4..ee21cf2f7f7 100644 --- a/integration/test/Testlib/JSON.hs +++ b/integration/test/Testlib/JSON.hs @@ -189,6 +189,15 @@ renameField old new obj = o :: Value <- maybe mzero pure =<< lift (lookupField obj old) lift (removeField old obj >>= setField new o) +-- | like 'lookupField' but wrapped in 'MaybeT' for convenience +lookupFieldM :: + (HasCallStack, MakesValue a) => + a -> + -- | A plain key, e.g. "id", or a nested key "user.profile.id" + String -> + MaybeT App Value +lookupFieldM = fmap MaybeT . lookupField + -- | Look up (nested) field of a JSON object -- -- If the field key has no dots then returns Nothing if the key is missing from the @@ -292,6 +301,10 @@ assertFailureWithJSON v msg = do printJSON :: MakesValue a => a -> App () printJSON = prettyJSON >=> liftIO . putStrLn +-- | useful for debugging, same as 'printJSON' but returns input JSON +traceJSON :: MakesValue a => a -> App a +traceJSON a = printJSON a $> a + prettyJSON :: MakesValue a => a -> App String prettyJSON x = make x <&> LC8.unpack . Aeson.encodePretty diff --git a/integration/test/Testlib/MockIntegrationService.hs b/integration/test/Testlib/MockIntegrationService.hs index 4d7c64a5150..c7c279211e4 100644 --- a/integration/test/Testlib/MockIntegrationService.hs +++ b/integration/test/Testlib/MockIntegrationService.hs @@ -1,4 +1,4 @@ -module Testlib.MockIntegrationService (withMockServer, lhMockApp, mkLegalHoldSettings) where +module Testlib.MockIntegrationService (withMockServer, lhMockAppWithPrekeys, lhMockApp, mkLegalHoldSettings, CreateMock (..)) where import Control.Monad.Catch import Control.Monad.Reader @@ -13,8 +13,8 @@ import Network.Wai as Wai import qualified Network.Wai.Handler.Warp as Warp import qualified Network.Wai.Handler.Warp.Internal as Warp import qualified Network.Wai.Handler.WarpTLS as Warp -import Testlib.Prekeys import Testlib.Prelude +import UnliftIO (MonadUnliftIO (withRunInIO)) import UnliftIO.Async import UnliftIO.Chan import UnliftIO.MVar @@ -95,10 +95,12 @@ withFreePortAnyAddr = bracket openFreePortAnyAddr (liftIO . Socket.close . snd) openFreePortAnyAddr :: MonadIO m => m (Warp.Port, Socket) openFreePortAnyAddr = liftIO $ bindRandomPortTCP (fromString "*") +type LiftedApplication = Request -> (Wai.Response -> App ResponseReceived) -> App ResponseReceived + withMockServer :: - HasCallStack => + (HasCallStack) => -- | the mock server - (Chan e -> Application) -> + (Chan e -> LiftedApplication) -> -- | the test (Warp.Port -> Chan e -> App a) -> App a @@ -107,30 +109,55 @@ withMockServer mkApp go = withFreePortAnyAddr $ \(sPort, sock) -> do let tlss = Warp.tlsSettingsMemory (cs mockServerCert) (cs mockServerPrivKey) let defs = Warp.defaultSettings {Warp.settingsPort = sPort, Warp.settingsBeforeMainLoop = putMVar serverStarted ()} buf <- newChan - srv <- async . liftIO . Warp.runTLSSocket tlss defs sock $ mkApp buf + srv <- async $ withRunInIO \inIO -> do + Warp.runTLSSocket tlss defs sock \req respond -> do + inIO $ mkApp buf req (liftIO . respond) srvMVar <- UnliftIO.Timeout.timeout 5_000_000 (takeMVar serverStarted) case srvMVar of Just () -> go sPort buf `finally` cancel srv Nothing -> error . show =<< poll srv +lhMockApp :: Chan (Wai.Request, LBS.ByteString) -> LiftedApplication +lhMockApp = lhMockAppWithPrekeys def + +data CreateMock f = MkCreateMock + { -- | how to obtain the next last prekey of a mock app + nextLastPrey :: f Value, + -- | how to obtain some prekeys of a mock app + somePrekeys :: f [Value] + } + +instance (App ~ f) => Default (CreateMock f) where + def = + MkCreateMock + { nextLastPrey = getLastPrekey, + somePrekeys = replicateM 3 getPrekey + } + -- | LegalHold service. Just fake the API, do not maintain any internal state. -lhMockApp :: Chan (Wai.Request, LBS.ByteString) -> Wai.Application -lhMockApp ch req cont = do +lhMockAppWithPrekeys :: + CreateMock App -> Chan (Wai.Request, LBS.ByteString) -> LiftedApplication +lhMockAppWithPrekeys mks ch req cont = withRunInIO \inIO -> do reqBody <- Wai.strictRequestBody req writeChan ch (req, reqBody) - case (cs <$> pathInfo req, cs $ requestMethod req, cs @_ @String <$> getRequestHeader "Authorization" req) of - (["legalhold", "status"], "GET", _) -> cont respondOk - (_, _, Nothing) -> cont missingAuth - (["legalhold", "initiate"], "POST", Just _) -> cont initiateResp - (["legalhold", "confirm"], "POST", Just _) -> cont respondOk - (["legalhold", "remove"], "POST", Just _) -> cont respondOk - _ -> cont respondBad + inIO do + (nextLastPrekey, threePrekeys) <- + (,) + <$> mks.nextLastPrey + <*> mks.somePrekeys + case (cs <$> pathInfo req, cs $ requestMethod req, cs @_ @String <$> getRequestHeader "Authorization" req) of + (["legalhold", "status"], "GET", _) -> cont respondOk + (_, _, Nothing) -> cont missingAuth + (["legalhold", "initiate"], "POST", Just _) -> cont (initiateResp nextLastPrekey threePrekeys) + (["legalhold", "confirm"], "POST", Just _) -> cont respondOk + (["legalhold", "remove"], "POST", Just _) -> cont respondOk + _ -> cont respondBad where - initiateResp :: Wai.Response - initiateResp = + initiateResp :: Value -> [Value] -> Wai.Response + initiateResp npk pks = responseLBS status200 [(hContentType, cs "application/json")] . encode . Data.Aeson.object $ - [ "prekeys" .= drop 3 somePrekeysRendered, - "last_prekey" .= (someLastPrekeysRendered !! 2) + [ "prekeys" .= pks, + "last_prekey" .= npk ] respondOk :: Wai.Response diff --git a/integration/test/Testlib/ModService.hs b/integration/test/Testlib/ModService.hs index 98084b097b9..f4390d7286f 100644 --- a/integration/test/Testlib/ModService.hs +++ b/integration/test/Testlib/ModService.hs @@ -117,7 +117,7 @@ traverseConcurrentlyCodensity f args = do pure result -startDynamicBackends :: HasCallStack => [ServiceOverrides] -> (HasCallStack => [String] -> App a) -> App a +startDynamicBackends :: [ServiceOverrides] -> ([String] -> App a) -> App a startDynamicBackends beOverrides k = runCodensity do @@ -128,7 +128,7 @@ startDynamicBackends beOverrides k = pure $ map (.berDomain) resources k -startDynamicBackend :: HasCallStack => BackendResource -> ServiceOverrides -> Codensity App () +startDynamicBackend :: BackendResource -> ServiceOverrides -> Codensity App () startDynamicBackend resource beOverrides = do let overrides = mconcat diff --git a/integration/test/Testlib/PTest.hs b/integration/test/Testlib/PTest.hs index 56d6d7be10c..0364232dad5 100644 --- a/integration/test/Testlib/PTest.hs +++ b/integration/test/Testlib/PTest.hs @@ -1,8 +1,12 @@ module Testlib.PTest where +import Data.Bifunctor (bimap) +import Data.Char (toLower) +import Data.Functor ((<&>)) +import Data.Kind import Data.Proxy +import GHC.Generics import GHC.TypeLits -import Testlib.App import Testlib.Env import Testlib.Types import Prelude @@ -15,38 +19,96 @@ class HasTests x where instance HasTests (App ()) where mkTests m n s f x = [(m, n, s, f, x)] -instance HasTests x => HasTests (Domain -> x) where +instance (HasTests x, TestCases a) => HasTests (a -> x) where mkTests m n s f x = - mkTests m (n <> "[domain=own]") s f (x OwnDomain) - <> mkTests m (n <> "[domain=other]") s f (x OtherDomain) + flip foldMap (testCases @a) \tc -> + mkTests m (n <> tc.testCaseName) s f (x tc.testCase) -instance HasTests x => HasTests (Ciphersuite -> x) where - mkTests m n s f x = - mconcat - [ mkTests m (n <> "[suite=" <> suite.code <> "]") s f (x suite) - | suite <- allCiphersuites - ] +data TestCase a = MkTestCase {testCaseName :: String, testCase :: a} + deriving stock (Eq, Ord, Show, Generic) --- | this is to resolve overlapping instances issues. -newtype WithBoundedEnumArg arg x = WithBoundedEnumArg (arg -> x) +-- | enumerate all members of a bounded enum type +-- +-- >>> testCases @Bool +-- [MkTestCase {testCaseName = "[bool=false]", testCase = False},MkTestCase {testCaseName = "[bool=true]", testCase = True}] +-- >>> testCases @Domain +-- [MkTestCase {testCaseName = "[domain=owndomain]", testCase = OwnDomain},MkTestCase {testCaseName = "[domain=otherdomain]", testCase = OtherDomain}] +-- >>> testCases @Ciphersuite +-- [MkTestCase {testCaseName = "[suite=0x0001]", testCase = Ciphersuite {code = "0x0001"}},MkTestCase {testCaseName = "[suite=0xf031]", testCase = Ciphersuite {code = "0xf031"}}] +-- >>> testCases @(Tagged "foo" Bool) +-- [MkTestCase {testCaseName = "[foo=false]", testCase = MkTagged {unTagged = False}},MkTestCase {testCaseName = "[foo=true]", testCase = MkTagged {unTagged = True}}] +class TestCases a where + testCases :: [TestCase a] -instance (HasTests x, Enum arg, Bounded arg, Show arg) => HasTests (WithBoundedEnumArg arg x) where - mkTests m n s f (WithBoundedEnumArg x) = - mconcat - [ mkTests m (n <> "[" <> show arg <> "]") s f (x arg) - | arg <- [minBound ..] - ] +type Tagged :: Symbol -> Type -> Type +newtype Tagged s a = MkTagged {unTagged :: a} + deriving stock (Eq, Ord, Show, Generic) --- | bool with a tag to prevent boolean blindness in test output. -newtype TaggedBool (tag :: Symbol) = TaggedBool {untag :: Bool} - deriving newtype (Eq, Ord, Bounded, Enum) +type TaggedBool s = Tagged s Bool -instance KnownSymbol tag => Show (TaggedBool tag) where - show (TaggedBool b) = show (symbolVal (Proxy @tag)) <> "=" <> show b +pattern TaggedBool :: Bool -> Tagged s Bool +pattern TaggedBool a = MkTagged a -instance (KnownSymbol tag, HasTests x) => HasTests (TaggedBool tag -> x) where - mkTests m n s f x = - mconcat - [ mkTests m (n <> "[" <> show arg <> "]") s f (x arg) - | arg <- [minBound ..] - ] +{-# COMPLETE TaggedBool #-} + +-- | only works for outer-most use of `Tagged` (not: `Maybe (Tagged "bla" Bool)`) +-- +-- >>> testCases @(Tagged "bla" Bool) +instance (GEnum (Rep a), KnownSymbol s, Generic a) => TestCases (Tagged s a) where + testCases = + uni @(Rep a) <&> \case + -- replace the toplevel + (Left _ : ls, tc) -> + MkTestCase + { testCaseName = foldr mkName "" (Left (symbolVal @s Proxy) : ls), + testCase = MkTagged $ to tc + } + _ -> error "tagged test cases: impossible" + +instance TestCases Ciphersuite where + testCases = do + suite <- allCiphersuites + pure $ + MkTestCase + { testCaseName = mkName (Left "suite") suite.code, + testCase = suite + } + +-- | a default instance, normally we don't do such things but this is more convenient in +-- the test suite as you don't have to derive anything +instance {-# OVERLAPPABLE #-} (Generic a, GEnum (Rep a)) => TestCases a where + testCases = + uni @(Rep a) <&> \(tcn, tc) -> + MkTestCase + { testCaseName = foldr mkName "" tcn, + testCase = to tc + } + +{-# INLINE [1] mkName #-} +mkName :: Either String String -> String -> String +mkName (Left a) = \acc -> mconcat ["[", toLower <$> a, "=" <> acc <> "]"] +mkName (Right (fmap toLower -> a)) = \case + [] -> a + acc@('[' : _) -> a <> acc + acc -> a <> "." <> acc + +class GEnum f where + uni :: [([Either String String], f x)] + +instance (GEnum k, KnownSymbol n) => GEnum (D1 (MetaData n m p b) k) where + uni = bimap (Left (symbolVal @n Proxy) :) M1 <$> uni @k + +instance (GEnum k) => GEnum (S1 md k) where + uni = fmap M1 <$> uni @k + +instance (GEnum k, KnownSymbol n) => GEnum (C1 (MetaCons n p b) k) where + uni = bimap (Right (symbolVal @n Proxy) :) M1 <$> uni @k + +instance (GEnum k1, GEnum k2) => GEnum (k1 :+: k2) where + uni = (fmap L1 <$> uni @k1) <> (fmap R1 <$> uni @k2) + +instance GEnum U1 where + uni = [([Right ""], U1)] + +instance (GEnum (Rep k), Generic k) => GEnum (K1 r k) where + uni = fmap (K1 . to) <$> uni @(Rep k) diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index ed18a345dd3..4009cd99144 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -229,7 +229,7 @@ data ClientIdentity = ClientIdentity deriving stock (Show, Eq, Ord, Generic) newtype Ciphersuite = Ciphersuite {code :: String} - deriving (Eq, Ord, Show) + deriving (Eq, Ord, Show, Generic) instance Default Ciphersuite where def = Ciphersuite "0x0001" @@ -363,6 +363,9 @@ assertJust :: HasCallStack => String -> Maybe a -> App a assertJust _ (Just x) = pure x assertJust msg Nothing = assertFailure msg +assertNothing :: (HasCallStack) => Maybe a -> App () +assertNothing = maybe (pure ()) $ const $ assertFailure "Maybe value was Just, not Nothing" + addFailureContext :: String -> App a -> App a addFailureContext msg = modifyFailureMsg (\m -> m <> "\nThis failure happened in this context:\n" <> msg) diff --git a/libs/brig-types/src/Brig/Types/User/Event.hs b/libs/brig-types/src/Brig/Types/User/Event.hs index 19bfc56315e..96aed364a76 100644 --- a/libs/brig-types/src/Brig/Types/User/Event.hs +++ b/libs/brig-types/src/Brig/Types/User/Event.hs @@ -102,8 +102,11 @@ data UserIdentityRemovedData = UserIdentityRemovedData deriving stock (Show) data LegalHoldClientRequestedData = LegalHoldClientRequestedData - { lhcTargetUser :: !UserId, + { -- | the user that is under legalhold + lhcTargetUser :: !UserId, + -- | the last prekey of the user that is under legalhold lhcLastPrekey :: !LastPrekey, + -- | the client id of the legalhold device lhcClientId :: !ClientId } deriving stock (Show) diff --git a/services/galley/test/integration/API/Teams/LegalHold.hs b/services/galley/test/integration/API/Teams/LegalHold.hs index 91315aa036d..1cd1f785a01 100644 --- a/services/galley/test/integration/API/Teams/LegalHold.hs +++ b/services/galley/test/integration/API/Teams/LegalHold.hs @@ -20,39 +20,25 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module API.Teams.LegalHold - ( tests, - ) -where +module API.Teams.LegalHold (tests) where import API.Teams.LegalHold.Util import API.Util import Bilge hiding (accept, head, timeout, trace) import Bilge.Assert -import Brig.Types.Intra (UserSet (..)) import Brig.Types.Test.Arbitrary () -import Brig.Types.User.Event qualified as Ev -import Cassandra.Exec qualified as Cql -import Control.Category ((>>>)) import Control.Concurrent.Chan import Control.Lens hiding ((#)) import Data.Id import Data.LegalHold import Data.List.NonEmpty (NonEmpty (..)) -import Data.List1 qualified as List1 -import Data.Map.Strict qualified as Map import Data.PEM import Data.Qualified (Qualified (..)) import Data.Range -import Data.Set qualified as Set import Data.Time.Clock qualified as Time -import Data.Timeout -import Galley.Cassandra.Client (lookupClients) import Galley.Cassandra.LegalHold -import Galley.Cassandra.LegalHold qualified as LegalHoldData import Galley.Env qualified as Galley import Galley.Options (featureFlags, settings) -import Galley.Types.Clients qualified as Clients import Galley.Types.Teams import Imports import Network.HTTP.Types.Status (status200, status404) @@ -66,14 +52,11 @@ import Test.Tasty.Cannon qualified as WS import Test.Tasty.HUnit import TestHelpers import TestSetup -import Wire.API.Connection (UserConnection) import Wire.API.Connection qualified as Conn import Wire.API.Conversation.Role (roleNameWireAdmin, roleNameWireMember) import Wire.API.Provider.Service import Wire.API.Routes.Internal.Brig.Connection -import Wire.API.Team.Feature qualified as Public import Wire.API.Team.LegalHold -import Wire.API.Team.LegalHold.External import Wire.API.Team.Member import Wire.API.Team.Member qualified as Team import Wire.API.Team.Permission @@ -103,20 +86,12 @@ testsPublic s = -- See also Client Tests in Brig; where behaviour around deleting/adding LH clients is tested testGroup "Teams LegalHold API (with flag whitelist-teams-and-implicit-consent)" - [ -- device handling (CRUD) - testOnlyIfLhWhitelisted s "POST /teams/{tid}/legalhold/{uid}" testRequestLegalHoldDevice, - testOnlyIfLhWhitelisted s "PUT /teams/{tid}/legalhold/approve" testApproveLegalHoldDevice, - test s "(user denies approval: nothing needs to be done in backend)" (pure ()), - testOnlyIfLhWhitelisted s "GET /teams/{tid}/legalhold/{uid}" testGetLegalHoldDeviceStatus, - testOnlyIfLhWhitelisted s "DELETE /teams/{tid}/legalhold/{uid}" testDisableLegalHoldForUser, - -- legal hold settings + [ -- legal hold settings testOnlyIfLhWhitelisted s "POST /teams/{tid}/legalhold/settings" testCreateLegalHoldTeamSettings, testOnlyIfLhWhitelisted s "GET /teams/{tid}/legalhold/settings" testGetLegalHoldTeamSettings, testOnlyIfLhWhitelisted s "Not implemented: DELETE /teams/{tid}/legalhold/settings" testRemoveLegalHoldFromTeam, - testOnlyIfLhWhitelisted s "GET [/i]?/teams/{tid}/legalhold" testEnablePerTeam, -- behavior of existing end-points testOnlyIfLhWhitelisted s "POST /clients" testCannotCreateLegalHoldDeviceOldAPI, - testOnlyIfLhWhitelisted s "GET /teams/{tid}/members" testGetTeamMembersIncludesLHStatus, testOnlyIfLhWhitelisted s "POST /register - can add team members above fanout limit when whitelisting is enabled" testAddTeamUserTooLargeWithLegalholdWhitelisted, testOnlyIfLhWhitelisted s "GET legalhold status in user profile" testGetLegalholdStatus, {- TODO: @@ -129,13 +104,6 @@ testsPublic s = [ testGroup -- FUTUREWORK: ungroup this level "teams listed" [ test s "happy flow" testInWhitelist, - testGroup "no-consent" $ do - connectFirst <- ("connectFirst",) <$> [False, True] - teamPeer <- ("teamPeer",) <$> [False, True] - approveLH <- ("approveLH",) <$> [False, True] - testPendingConnection <- ("testPendingConnection",) <$> [False, True] - let name = intercalate ", " $ map (\(n, b) -> n <> "=" <> show b) [connectFirst, teamPeer, approveLH, testPendingConnection] - pure . test s name $ testNoConsentBlockOne2OneConv (snd connectFirst) (snd teamPeer) (snd approveLH) (snd testPendingConnection), testGroup "Legalhold is activated for user A in a group conversation" [ testOnlyIfLhWhitelisted s "All admins are consenting: all non-consenters get removed from conversation" (testNoConsentRemoveFromGroupConv LegalholderIsAdmin), @@ -192,234 +160,6 @@ testWhitelistingTeams = do expectWhitelisted False tid -testRequestLegalHoldDevice :: TestM () -testRequestLegalHoldDevice = withTeam $ \owner tid -> do - member <- randomUser - addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - -- Can't request a device if team feature flag is disabled - requestLegalHoldDevice owner member tid !!! testResponse 403 (Just "legalhold-not-enabled") - cannon <- view tsCannon - -- Assert that the appropriate LegalHold Request notification is sent to the user's - -- clients - WS.bracketR2 cannon member member $ \(ws, ws') -> withDummyTestServiceForTeamNoService $ \lhPort _chan -> do - do - -- test device creation without consent - requestLegalHoldDevice member member tid !!! testResponse 403 (Just "legalhold-not-enabled") - UserLegalHoldStatusResponse userStatus _ _ <- getUserStatusTyped member tid - liftIO $ - assertEqual - "User with insufficient permissions should be unable to start flow" - UserLegalHoldNoConsent - userStatus - - do - requestLegalHoldDevice owner member tid !!! testResponse 403 (Just "legalhold-not-enabled") - UserLegalHoldStatusResponse userStatus _ _ <- getUserStatusTyped member tid - liftIO $ - assertEqual - "User with insufficient permissions should be unable to start flow" - UserLegalHoldNoConsent - userStatus - - putLHWhitelistTeam tid !!! const 200 === statusCode - newService <- newLegalHoldService lhPort - postSettings owner tid newService !!! testResponse 201 Nothing - - do - requestLegalHoldDevice member member tid !!! testResponse 403 (Just "operation-denied") - UserLegalHoldStatusResponse userStatus _ _ <- getUserStatusTyped member tid - liftIO $ - assertEqual - "User with insufficient permissions should be unable to start flow" - UserLegalHoldDisabled - userStatus - - do - requestLegalHoldDevice owner member tid !!! testResponse 201 Nothing - UserLegalHoldStatusResponse userStatus _ _ <- getUserStatusTyped member tid - liftIO $ - assertEqual - "requestLegalHoldDevice should set user status to Pending" - UserLegalHoldPending - userStatus - do - requestLegalHoldDevice owner member tid !!! testResponse 204 Nothing - UserLegalHoldStatusResponse userStatus _ _ <- getUserStatusTyped member tid - liftIO $ - assertEqual - "requestLegalHoldDevice when already pending should leave status as Pending" - UserLegalHoldPending - userStatus - - cassState <- view tsCass - liftIO $ do - storedPrekeys <- Cql.runClient cassState (LegalHoldData.selectPendingPrekeys member) - assertBool "user should have pending prekeys stored" (not . null $ storedPrekeys) - let pluck = \case - (Ev.LegalHoldClientRequested rdata) -> do - Ev.lhcTargetUser rdata @?= member - Ev.lhcLastPrekey rdata @?= head someLastPrekeys - Ev.lhcClientId rdata @?= someClientId - _ -> assertBool "Unexpected event" False - assertNotification ws pluck - -- all devices get notified. - assertNotification ws' pluck - -testApproveLegalHoldDevice :: TestM () -testApproveLegalHoldDevice = do - (owner, tid) <- createBindingTeam - member <- do - usr <- randomUser - addTeamMemberInternal tid usr (rolePermissions RoleMember) Nothing - pure usr - member2 <- do - usr <- randomUser - addTeamMemberInternal tid usr (rolePermissions RoleMember) Nothing - pure usr - outsideContact <- do - usr <- randomUser - connectUsers member (List1.singleton usr) - pure usr - stranger <- randomUser - putLHWhitelistTeam tid !!! const 200 === statusCode - approveLegalHoldDevice (Just defPassword) owner member tid - !!! testResponse 403 (Just "access-denied") - cannon <- view tsCannon - WS.bracketRN cannon [owner, member, member, member2, outsideContact, stranger] $ - \[ows, mws, mws', member2Ws, outsideContactWs, strangerWs] -> withDummyTestServiceForTeam owner tid $ \chan -> do - requestLegalHoldDevice owner member tid !!! testResponse 201 Nothing - liftIO . assertMatchJSON chan $ \(RequestNewLegalHoldClient userId' teamId') -> do - assertEqual "userId == member" userId' member - assertEqual "teamId == tid" teamId' tid - -- Only the user themself can approve adding a LH device - approveLegalHoldDevice (Just defPassword) owner member tid !!! testResponse 403 (Just "access-denied") - -- Requires password - approveLegalHoldDevice Nothing member member tid !!! const 403 === statusCode - approveLegalHoldDevice (Just defPassword) member member tid !!! testResponse 200 Nothing - -- checks if the cookie we give to the legalhold service is actually valid - assertMatchJSON chan $ \(LegalHoldServiceConfirm _clientId _uid _tid authToken) -> - renewToken authToken - cassState <- view tsCass - liftIO $ do - clients' <- Cql.runClient cassState $ lookupClients [member] - assertBool "Expect clientId to be saved on the user" $ - Clients.contains member someClientId clients' - UserLegalHoldStatusResponse userStatus _ _ <- getUserStatusTyped member tid - liftIO $ - assertEqual - "After approval user legalhold status should be Enabled" - UserLegalHoldEnabled - userStatus - let pluck = \case - Ev.ClientAdded _ eClient -> do - clientId eClient @?= someClientId - clientType eClient @?= LegalHoldClientType - clientClass eClient @?= Just LegalHoldClient - _ -> assertBool "Unexpected event" False - assertNotification mws pluck - assertNotification mws' pluck - -- Other team users should get a user.legalhold-enable event - let pluck' = \case - Ev.UserLegalHoldEnabled eUser -> eUser @?= member - _ -> assertBool "Unexpected event" False - assertNotification ows pluck' - -- We send to all members of a team. which includes the team-settings - assertNotification member2Ws pluck' - when False $ do - -- this doesn't work any more since consent (personal users cannot grant consent). - assertNotification outsideContactWs pluck' - assertNoNotification strangerWs - -testGetLegalHoldDeviceStatus :: TestM () -testGetLegalHoldDeviceStatus = do - (owner, tid) <- createBindingTeam - member <- randomUser - addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - forM_ [owner, member] $ \uid -> do - status <- getUserStatusTyped uid tid - liftIO $ - assertEqual - "unexpected status" - (UserLegalHoldStatusResponse UserLegalHoldNoConsent Nothing Nothing) - status - - putLHWhitelistTeam tid !!! const 200 === statusCode - withDummyTestServiceForTeamNoService $ \lhPort _chan -> do - do - UserLegalHoldStatusResponse userStatus lastPrekey' clientId' <- getUserStatusTyped member tid - liftIO $ - do - assertEqual "User legal hold status should start as disabled" UserLegalHoldDisabled userStatus - assertEqual "last_prekey should be Nothing when LH is disabled" Nothing lastPrekey' - assertEqual "client.id should be Nothing when LH is disabled" Nothing clientId' - - do - newService <- newLegalHoldService lhPort - postSettings owner tid newService !!! testResponse 201 Nothing - requestLegalHoldDevice owner member tid !!! testResponse 201 Nothing - assertZeroLegalHoldDevices member - UserLegalHoldStatusResponse userStatus lastPrekey' clientId' <- getUserStatusTyped member tid - liftIO $ - do - assertEqual "requestLegalHoldDevice should set user status to Pending" UserLegalHoldPending userStatus - assertEqual "last_prekey should be set when LH is pending" (Just (head someLastPrekeys)) lastPrekey' - assertEqual "client.id should be set when LH is pending" (Just someClientId) clientId' - do - requestLegalHoldDevice owner member tid !!! testResponse 204 Nothing - UserLegalHoldStatusResponse userStatus _ _ <- getUserStatusTyped member tid - liftIO $ - assertEqual - "requestLegalHoldDevice when already pending should leave status as Pending" - UserLegalHoldPending - userStatus - do - approveLegalHoldDevice (Just defPassword) member member tid !!! testResponse 200 Nothing - UserLegalHoldStatusResponse userStatus lastPrekey' clientId' <- getUserStatusTyped member tid - liftIO $ - do - assertEqual "approving should change status to Enabled" UserLegalHoldEnabled userStatus - assertEqual "last_prekey should be set when LH is pending" (Just (head someLastPrekeys)) lastPrekey' - assertEqual "client.id should be set when LH is pending" (Just someClientId) clientId' - assertExactlyOneLegalHoldDevice member - requestLegalHoldDevice owner member tid !!! testResponse 409 (Just "legalhold-already-enabled") - -testDisableLegalHoldForUser :: TestM () -testDisableLegalHoldForUser = withTeam $ \owner tid -> do - member <- randomUser - addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - cannon <- view tsCannon - putLHWhitelistTeam tid !!! const 200 === statusCode - WS.bracketR2 cannon owner member $ \(ows, mws) -> withDummyTestServiceForTeam owner tid $ \chan -> do - requestLegalHoldDevice owner member tid !!! testResponse 201 Nothing - approveLegalHoldDevice (Just defPassword) member member tid !!! testResponse 200 Nothing - assertNotification mws $ \case - Ev.ClientAdded _ client -> do - clientId client @?= someClientId - clientType client @?= LegalHoldClientType - clientClass client @?= Just LegalHoldClient - _ -> assertBool "Unexpected event" False - -- Only the admin can disable legal hold - disableLegalHoldForUser (Just defPassword) tid member member !!! testResponse 403 (Just "operation-denied") - assertExactlyOneLegalHoldDevice member - -- Require password to disable for usern - disableLegalHoldForUser Nothing tid owner member !!! const 403 === statusCode - assertExactlyOneLegalHoldDevice member - disableLegalHoldForUser (Just defPassword) tid owner member !!! testResponse 200 Nothing - liftIO . assertMatchChan chan $ \(req, _) -> do - assertEqual "method" "POST" (requestMethod req) - assertEqual "path" (pathInfo req) ["legalhold", "remove"] - assertNotification mws $ \case - Ev.ClientEvent (Ev.ClientRemoved _ clientId') -> clientId' @?= someClientId - _ -> assertBool "Unexpected event" False - assertNotification mws $ \case - Ev.UserEvent (Ev.UserLegalHoldDisabled uid) -> uid @?= member - _ -> assertBool "Unexpected event" False - -- Other users should also get the event - assertNotification ows $ \case - Ev.UserLegalHoldDisabled uid -> uid @?= member - _ -> assertBool "Unexpected event" False - assertZeroLegalHoldDevices member - data IsWorking = Working | NotWorking deriving (Eq, Show) @@ -534,34 +274,6 @@ testRemoveLegalHoldFromTeam = do -- fails if LH for team is disabled deleteSettings (Just defPassword) owner tid !!! testResponse 403 (Just "legalhold-disable-unimplemented") -testEnablePerTeam :: TestM () -testEnablePerTeam = withTeam $ \owner tid -> do - member <- randomUser - addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - do - status :: Public.WithStatusNoLock Public.LegalholdConfig <- responseJsonUnsafe <$> (getEnabled tid (getEnabled tid do - putLHWhitelistTeam tid !!! const 200 === statusCode - requestLegalHoldDevice owner member tid !!! const 201 === statusCode - approveLegalHoldDevice (Just defPassword) member member tid !!! testResponse 200 Nothing - do - UserLegalHoldStatusResponse status _ _ <- getUserStatusTyped member tid - liftIO $ assertEqual "User legal hold status should be enabled" UserLegalHoldEnabled status - do - putEnabled' id tid Public.FeatureStatusDisabled !!! testResponse 403 (Just "legalhold-whitelisted-only") - status :: Public.WithStatusNoLock Public.LegalholdConfig <- responseJsonUnsafe <$> (getEnabled tid TestM () testAddTeamUserTooLargeWithLegalholdWhitelisted = withTeam $ \owner tid -> do o <- view tsGConf @@ -598,39 +310,6 @@ testCannotCreateLegalHoldDeviceOldAPI = do post req !!! const 400 === statusCode assertZeroLegalHoldDevices uid -testGetTeamMembersIncludesLHStatus :: TestM () -testGetTeamMembersIncludesLHStatus = do - (owner, tid) <- createBindingTeam - member <- randomUser - addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - - let findMemberStatus :: [TeamMember] -> Maybe UserLegalHoldStatus - findMemberStatus ms = - ms ^? traversed . filtered (has $ Team.userId . only member) . legalHoldStatus - - let check :: HasCallStack => UserLegalHoldStatus -> String -> TestM () - check status msg = do - members' <- view teamMembers <$> getTeamMembers owner tid - liftIO $ - assertEqual - ("legal hold status should be " <> msg) - (Just status) - (findMemberStatus members') - - check UserLegalHoldNoConsent "disabled when it is disabled for the team" - withDummyTestServiceForTeamNoService $ \lhPort _chan -> do - check UserLegalHoldNoConsent "no_consent on new team members" - - putLHWhitelistTeam tid !!! const 200 === statusCode - newService <- newLegalHoldService lhPort - postSettings owner tid newService !!! testResponse 201 Nothing - - check UserLegalHoldDisabled "disabled on team members that have granted consent" - requestLegalHoldDevice owner member tid !!! testResponse 201 Nothing - check UserLegalHoldPending "pending after requesting device" - approveLegalHoldDevice (Just defPassword) member member tid !!! testResponse 200 Nothing - check UserLegalHoldEnabled "enabled after confirming device" - testInWhitelist :: TestM () testInWhitelist = do g <- viewGalley @@ -692,140 +371,6 @@ testInWhitelist = do assertEqual "last_prekey should be set when LH is pending" (Just (head someLastPrekeys)) lastPrekey' assertEqual "client.id should be set when LH is pending" (Just someClientId) clientId' --- If LH is activated for other user in 1:1 conv, 1:1 conv is blocked -testNoConsentBlockOne2OneConv :: HasCallStack => Bool -> Bool -> Bool -> Bool -> TestM () -testNoConsentBlockOne2OneConv connectFirst teamPeer approveLH testPendingConnection = do - -- FUTUREWORK: maybe regular user for legalholder? - (legalholder :: UserId, tid) <- createBindingTeam - regularClient <- randomClient legalholder (head someLastPrekeys) - - peer :: UserId <- if teamPeer then fst <$> createBindingTeam else randomUser - galley <- viewGalley - - putLHWhitelistTeam tid !!! const 200 === statusCode - - let doEnableLH :: HasCallStack => TestM (Maybe ClientId) - doEnableLH = do - -- register & (possibly) approve LH device for legalholder - withLHWhitelist tid (requestLegalHoldDevice' galley legalholder legalholder tid) !!! testResponse 201 Nothing - when approveLH $ - withLHWhitelist tid (approveLegalHoldDevice' galley (Just defPassword) legalholder legalholder tid) !!! testResponse 200 Nothing - UserLegalHoldStatusResponse userStatus _ _ <- withLHWhitelist tid (getUserStatusTyped' galley legalholder tid) - liftIO $ assertEqual "approving should change status" (if approveLH then UserLegalHoldEnabled else UserLegalHoldPending) userStatus - if approveLH - then - getInternalClientsFull (UserSet $ Set.singleton legalholder) - <&> do - userClientsFull - >>> Map.elems - >>> Set.unions - >>> Set.toList - >>> listToMaybe - >>> fmap clientId - else pure Nothing - - doDisableLH :: HasCallStack => TestM () - doDisableLH = do - -- remove (only) LH device again - withLHWhitelist tid (disableLegalHoldForUser' galley (Just defPassword) tid legalholder legalholder) - !!! testResponse 200 Nothing - - cannon <- view tsCannon - - WS.bracketR2 cannon legalholder peer $ \(legalholderWs, peerWs) -> withDummyTestServiceForTeam legalholder tid $ \_chan -> do - if not connectFirst - then do - void doEnableLH - postConnection legalholder peer !!! do testResponse 403 (Just "missing-legalhold-consent") - postConnection peer legalholder !!! do testResponse 403 (Just "missing-legalhold-consent") - else do - postConnection legalholder peer !!! const 201 === statusCode - - mbConn :: Maybe UserConnection <- - if testPendingConnection - then pure Nothing - else do - res <- putConnection peer legalholder Conn.Accepted do - assertNotification ws $ - \case - (Ev.ConnectionEvent (Ev.ConnectionUpdated (Conn.ucStatus -> rel) _prev _name)) -> do - rel @?= Conn.MissingLegalholdConsent - _ -> assertBool "wrong event type" False - - forM_ [(legalholder, peer), (peer, legalholder)] $ \(one, two) -> do - putConnection one two Conn.Accepted - !!! testResponse 403 (Just "bad-conn-update") - - assertConnections legalholder [ConnectionStatus legalholder peer Conn.MissingLegalholdConsent] - assertConnections peer [ConnectionStatus peer legalholder Conn.MissingLegalholdConsent] - - -- peer can't send message to legalhodler. the conversation appears gone. - peerClient <- randomClient peer (someLastPrekeys !! 2) - for_ ((,) <$> (mbConn >>= Conn.ucConvId) <*> mbLegalholderLHDevice) $ \(convId, legalholderLHDevice) -> do - postOtrMessage - id - peer - peerClient - (qUnqualified convId) - [ (legalholder, legalholderLHDevice, "cipher"), - (legalholder, regularClient, "cipher") - ] - !!! do - const 404 === statusCode - const (Right "no-conversation") === fmap Error.label . responseJsonEither - - do - doDisableLH - - when approveLH $ do - legalholderLHDevice <- assertJust mbLegalholderLHDevice - WS.assertMatch_ (5 # Second) legalholderWs $ - wsAssertClientRemoved legalholderLHDevice - - assertConnections - legalholder - [ ConnectionStatus legalholder peer $ - if testPendingConnection then Conn.Sent else Conn.Accepted - ] - assertConnections - peer - [ ConnectionStatus peer legalholder $ - if testPendingConnection then Conn.Pending else Conn.Accepted - ] - - forM_ [legalholderWs, peerWs] $ \ws -> do - assertNotification ws $ - \case - (Ev.ConnectionEvent (Ev.ConnectionUpdated (Conn.ucStatus -> rel) _prev _name)) -> do - assertBool "" (rel `elem` [Conn.Sent, Conn.Pending, Conn.Accepted]) - _ -> assertBool "wrong event type" False - - -- conversation reappears. peer can send message to legalholder again - for_ ((,) <$> (mbConn >>= Conn.ucConvId) <*> mbLegalholderLHDevice) $ \(convId, legalholderLHDevice) -> do - postOtrMessage - id - peer - peerClient - (qUnqualified convId) - [ (legalholder, legalholderLHDevice, "cipher"), - (legalholder, regularClient, "cipher") - ] - !!! do - const 201 === statusCode - assertMismatchWithMessage - (Just "legalholderLHDevice is deleted") - [] - [] - [(legalholder, Set.singleton legalholderLHDevice)] - data GroupConvAdmin = LegalholderIsAdmin | PeerIsAdmin From 19c3fdaf89d0542e6801536c5186da8ae9e18a0f Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Mon, 26 Feb 2024 15:56:13 +0100 Subject: [PATCH 012/117] gundeck: Fix SNS endpoint parser (#3894) This commit contains two things: 1. Add fields to the log message such that mismatches between stored and app-provided data become more obvious. 2. Fix an AWS SNS Endpoint parsing bug: We parsed the environment up to the first dash. But, environment names may contain dashes themselves, thus we must accumulate them up to the last dash. --- changelog.d/3-bug-fixes/sns-arn-parsing | 3 ++ services/gundeck/default.nix | 3 ++ services/gundeck/gundeck.cabal | 3 ++ services/gundeck/src/Gundeck/Aws/Arn.hs | 10 ++++- services/gundeck/src/Gundeck/Push.hs | 27 +++++++++--- services/gundeck/test/unit/Aws/Arn.hs | 57 +++++++++++++++++++++++++ services/gundeck/test/unit/Main.hs | 4 +- 7 files changed, 97 insertions(+), 10 deletions(-) create mode 100644 changelog.d/3-bug-fixes/sns-arn-parsing create mode 100644 services/gundeck/test/unit/Aws/Arn.hs diff --git a/changelog.d/3-bug-fixes/sns-arn-parsing b/changelog.d/3-bug-fixes/sns-arn-parsing new file mode 100644 index 00000000000..fc900f8ab82 --- /dev/null +++ b/changelog.d/3-bug-fixes/sns-arn-parsing @@ -0,0 +1,3 @@ +The AWS SNS ARN was parsed by accumulating the environment name up to the first +dash ('-') such that parts of this name spilled over into the app name. Now, we +accumulate up to the last dash. diff --git a/services/gundeck/default.nix b/services/gundeck/default.nix index 3dad13b4224..bf63e2899c8 100644 --- a/services/gundeck/default.nix +++ b/services/gundeck/default.nix @@ -24,6 +24,7 @@ , exceptions , extended , extra +, foldl , gitignoreSource , gundeck-types , hedis @@ -105,6 +106,7 @@ mkDerivation { exceptions extended extra + foldl gundeck-types hedis http-client @@ -184,6 +186,7 @@ mkDerivation { aeson aeson-pretty amazonka + amazonka-core async base bytestring-conversion diff --git a/services/gundeck/gundeck.cabal b/services/gundeck/gundeck.cabal index ce1b2c82ac4..f5c42bf0ddf 100644 --- a/services/gundeck/gundeck.cabal +++ b/services/gundeck/gundeck.cabal @@ -128,6 +128,7 @@ library , exceptions >=0.4 , extended , extra >=1.1 + , foldl , gundeck-types >=1.0 , hedis >=0.14.0 , http-client >=0.7 @@ -391,6 +392,7 @@ test-suite gundeck-tests type: exitcode-stdio-1.0 main-is: Main.hs other-modules: + Aws.Arn DelayQueue Json MockGundeck @@ -452,6 +454,7 @@ test-suite gundeck-tests aeson , aeson-pretty , amazonka + , amazonka-core , async , base , bytestring-conversion diff --git a/services/gundeck/src/Gundeck/Aws/Arn.hs b/services/gundeck/src/Gundeck/Aws/Arn.hs index c0be6380d7d..17588d08106 100644 --- a/services/gundeck/src/Gundeck/Aws/Arn.hs +++ b/services/gundeck/src/Gundeck/Aws/Arn.hs @@ -53,6 +53,7 @@ where import Amazonka (Region (..)) import Amazonka.Data +import Control.Foldl qualified as Foldl import Control.Lens import Data.Attoparsec.Text import Data.Text qualified as Text @@ -151,9 +152,14 @@ endpointTopicParser :: Parser EndpointTopic endpointTopicParser = do _ <- string "endpoint" t <- char '/' *> transportParser - e <- char '/' *> takeTill (== '-') - a <- char '-' *> takeTill (== '/') + envAndName <- char '/' *> takeTill (== '/') i <- char '/' *> takeWhile1 (not . isSpace) + let xs = Text.split (== '-') envAndName + e = Text.intercalate (Text.pack "-") (init xs) + a <- case Foldl.fold Foldl.last xs of + Just x -> pure x + Nothing -> fail ("Cannot parse appName in " ++ show xs) + pure $ mkEndpointTopic (ArnEnv e) t (AppName a) (EndpointId i) transportParser :: Parser Transport diff --git a/services/gundeck/src/Gundeck/Push.hs b/services/gundeck/src/Gundeck/Push.hs index 02c6984873b..7beb3b56076 100644 --- a/services/gundeck/src/Gundeck/Push.hs +++ b/services/gundeck/src/Gundeck/Push.hs @@ -34,7 +34,7 @@ where import Control.Arrow ((&&&)) import Control.Error import Control.Exception (ErrorCall (ErrorCall)) -import Control.Lens (view, (.~), (^.)) +import Control.Lens (to, view, (.~), (^.)) import Control.Monad.Catch import Data.Aeson as Aeson (Object) import Data.Id @@ -525,22 +525,35 @@ addToken uid cid newtok = mpaRunWithBudget 1 (Left Public.AddTokenErrorNoBudget) updateEndpoint :: UserId -> PushToken -> EndpointArn -> Aws.SNSEndpoint -> Gundeck () updateEndpoint uid t arn e = do env <- view awsEnv + requestId <- view reqId + unless (equalTransport && equalApp) $ do - Log.err $ logMessage uid arn (t ^. token) "Transport or app mismatch" + Log.err $ logMessage requestId "PushToken does not fit to user_push data: Transport or app mismatch" throwM $ mkError status500 "server-error" "Server Error" - Log.info $ logMessage uid arn (t ^. token) "Upserting push token." + + Log.info $ logMessage requestId "Upserting push token." let users = Set.insert uid (e ^. endpointUsers) Aws.execute env $ Aws.updateEndpoint users (t ^. token) arn where equalTransport = t ^. tokenTransport == arn ^. snsTopic . endpointTransport equalApp = t ^. tokenApp == arn ^. snsTopic . endpointAppName - logMessage a r tk m = + logMessage requestId m = "user" - .= UUID.toASCIIBytes (toUUID a) + .= UUID.toASCIIBytes (toUUID uid) ~~ "token" - .= Text.take 16 (tokenText tk) + .= Text.take 16 (t ^. token . to tokenText) + ~~ "tokenTransport" + .= show (t ^. tokenTransport) + ~~ "tokenApp" + .= (t ^. tokenApp . to appNameText) ~~ "arn" - .= toText r + .= toText arn + ~~ "endpointTransport" + .= show (arn ^. snsTopic . endpointTransport) + ~~ "endpointAppName" + .= (arn ^. snsTopic . endpointAppName . to appNameText) + ~~ "request" + .= unRequestId requestId ~~ msg (val m) deleteToken :: UserId -> Token -> Gundeck (Maybe ()) diff --git a/services/gundeck/test/unit/Aws/Arn.hs b/services/gundeck/test/unit/Aws/Arn.hs new file mode 100644 index 00000000000..ca661c8d0de --- /dev/null +++ b/services/gundeck/test/unit/Aws/Arn.hs @@ -0,0 +1,57 @@ +module Aws.Arn where + +import Amazonka.Data.Text +import Control.Lens +import Gundeck.Aws.Arn +import Gundeck.Types +import Imports +import Test.Tasty +import Test.Tasty.HUnit + +tests :: TestTree +tests = + testGroup + "Aws.Arn" + [ testGroup + "Parser" + [ testGroup + "EndpointArn" + [ testCaseSteps "real world round-trip" realWorldArnTest, + testCaseSteps "made-up round-trip" madeUpArnTest + ] + ] + ] + +realWorldArnTest :: HasCallStack => (String -> IO ()) -> Assertion +realWorldArnTest step = do + step "Given an ARN from a test environment" + let arnText :: Text = "arn:aws:sns:eu-central-1:091205192927:endpoint/GCM/sven-test-782078216207/ded226c7-45b8-3f6c-9e89-f253340bbb60" + arnData <- + either (\e -> assertFailure ("Arn cannot be parsed: " ++ e)) pure (fromText @EndpointArn arnText) + + step "Check that values were parsed correctly" + (arnData ^. snsRegion) @?= "eu-central-1" + (arnData ^. snsAccount . to fromAccount) @?= "091205192927" + (arnData ^. snsTopic . endpointTransport) @?= GCM + (arnData ^. snsTopic . endpointAppName) @?= "782078216207" + (arnData ^. snsTopic . endpointId . to (\(EndpointId eId) -> eId)) @?= "ded226c7-45b8-3f6c-9e89-f253340bbb60" + + step "Expect values to be de-serialized correctly" + (toText arnData) @?= arnText + +madeUpArnTest :: HasCallStack => (String -> IO ()) -> Assertion +madeUpArnTest step = do + step "Given an ARN with data to cover untested cases" + let arnText :: Text = "arn:aws:sns:us-east-2:000000000001:endpoint/APNS/nodash-000000000002/8ffd8d14-db06-4f3a-a3bb-08264b9dbfb0" + arnData <- + either (\e -> assertFailure ("Arn cannot be parsed: " ++ e)) pure (fromText @EndpointArn arnText) + + step "Check that values were parsed correctly" + (arnData ^. snsRegion) @?= "us-east-2" + (arnData ^. snsAccount . to fromAccount) @?= "000000000001" + (arnData ^. snsTopic . endpointTransport) @?= APNS + (arnData ^. snsTopic . endpointAppName) @?= "000000000002" + (arnData ^. snsTopic . endpointId . to (\(EndpointId eId) -> eId)) @?= "8ffd8d14-db06-4f3a-a3bb-08264b9dbfb0" + + step "Expect values to be de-serialized correctly" + (toText arnData) @?= arnText diff --git a/services/gundeck/test/unit/Main.hs b/services/gundeck/test/unit/Main.hs index a2d76732e63..332418beb38 100644 --- a/services/gundeck/test/unit/Main.hs +++ b/services/gundeck/test/unit/Main.hs @@ -20,6 +20,7 @@ module Main ) where +import Aws.Arn qualified import Data.Metrics.Test (pathsConsistencyCheck) import Data.Metrics.WaiRoute (treeToPaths) import DelayQueue qualified @@ -50,5 +51,6 @@ main = Native.tests, Push.tests, ThreadBudget.tests, - ParseExistsError.tests + ParseExistsError.tests, + Aws.Arn.tests ] From 21cc5535b68650ffc93bfc3a6984711cbc16a584 Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Tue, 27 Feb 2024 15:10:26 +0100 Subject: [PATCH 013/117] gundeck: set request id in request env (#3903) The request id was missing in the Env. This led to "N/A" being logged. This commit adapts the approach of adding request IDs from brig. --- changelog.d/3-bug-fixes/log-requestId-gundeck | 1 + services/gundeck/src/Gundeck/Run.hs | 28 +++++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 changelog.d/3-bug-fixes/log-requestId-gundeck diff --git a/changelog.d/3-bug-fixes/log-requestId-gundeck b/changelog.d/3-bug-fixes/log-requestId-gundeck new file mode 100644 index 00000000000..59891b17fd0 --- /dev/null +++ b/changelog.d/3-bug-fixes/log-requestId-gundeck @@ -0,0 +1 @@ +Add the request ID to the request's execution environment in gundeck, such that it can be logged. diff --git a/services/gundeck/src/Gundeck/Run.hs b/services/gundeck/src/Gundeck/Run.hs index 55487f84c98..46d6b407c33 100644 --- a/services/gundeck/src/Gundeck/Run.hs +++ b/services/gundeck/src/Gundeck/Run.hs @@ -24,14 +24,17 @@ import Cassandra (runClient, shutdown) import Cassandra.Schema (versionCheck) import Control.Error (ExceptT (ExceptT)) import Control.Exception (finally) -import Control.Lens hiding (enum) +import Control.Lens ((.~), (^.)) import Control.Monad.Extra +import Data.Id (RequestId (..)) import Data.Metrics (Metrics) import Data.Metrics.AWS (gaugeTokenRemaing) import Data.Metrics.Middleware (metrics) import Data.Metrics.Middleware.Prometheus (waiPrometheusMiddleware) import Data.Proxy (Proxy (Proxy)) import Data.Text (unpack) +import Data.UUID qualified as UUID +import Data.UUID.V4 qualified as UUID import Database.Redis qualified as Redis import Gundeck.API (sitemap) import Gundeck.API.Public (servantSitemap) @@ -46,9 +49,11 @@ import Imports hiding (head) import Network.Wai as Wai import Network.Wai.Middleware.Gunzip qualified as GZip import Network.Wai.Middleware.Gzip qualified as GZip +import Network.Wai.Utilities (lookupRequestId) import Network.Wai.Utilities.Server hiding (serverPort) import Servant (Handler (Handler), (:<|>) (..)) import Servant qualified +import System.Logger ((.=), (~~)) import System.Logger qualified as Log import UnliftIO.Async qualified as Async import Util.Options @@ -69,7 +74,9 @@ run o = do lst <- Async.async $ Aws.execute (e ^. awsEnv) (Aws.listen throttleMillis (runDirect e . onEvent)) wtbs <- forM (e ^. threadBudgetState) $ \tbs -> Async.async $ runDirect e $ watchThreadBudgetState m tbs 10 wCollectAuth <- Async.async (collectAuthMetrics m (Aws._awsEnv (Env._awsEnv e))) - runSettingsWithShutdown s (middleware e $ mkApp e) Nothing `finally` do + + let app = middleware e (\requestId -> mkApp (e & reqId .~ requestId)) + runSettingsWithShutdown s app Nothing `finally` do Log.info l $ Log.msg (Log.val "Shutting down ...") shutdown (e ^. cstate) Async.cancel lst @@ -80,13 +87,28 @@ run o = do whenJust (e ^. rstateAdditionalWrite) $ (=<<) Redis.disconnect . takeMVar Log.close (e ^. applog) where - middleware :: Env -> Wai.Middleware + middleware :: Env -> (RequestId -> Wai.Application) -> Wai.Application middleware e = versionMiddleware (foldMap expandVersionExp (o ^. settings . disabledAPIVersions)) . waiPrometheusMiddleware sitemap . GZip.gunzip . GZip.gzip GZip.def . catchErrors (e ^. applog) [Right $ e ^. monitor] + . lookupRequestIdMiddleware (e ^. applog) + + lookupRequestIdMiddleware :: Log.Logger -> (RequestId -> Wai.Application) -> Wai.Application + lookupRequestIdMiddleware logger mkapp req cont = do + case lookupRequestId req of + Just rid -> do + mkapp (RequestId rid) req cont + Nothing -> do + localRid <- RequestId . cs . UUID.toText <$> UUID.nextRandom + Log.info logger $ + "request-id" .= localRid + ~~ "method" .= Wai.requestMethod req + ~~ "path" .= Wai.rawPathInfo req + ~~ Log.msg (Log.val "generated a new request id for local request") + mkapp localRid req cont type CombinedAPI = GundeckAPI :<|> Servant.Raw From 598eb63271a384f80bc6a3e30ff5a0cc4d39f0f4 Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Tue, 27 Feb 2024 22:43:17 +0100 Subject: [PATCH 014/117] added a configuration option for IP binding for coturn, and example use (commented) in the values file --- charts/coturn/templates/configmap-coturn-conf-template.yaml | 2 +- charts/coturn/values.yaml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/charts/coturn/templates/configmap-coturn-conf-template.yaml b/charts/coturn/templates/configmap-coturn-conf-template.yaml index cc458f7db61..6bda1d81130 100644 --- a/charts/coturn/templates/configmap-coturn-conf-template.yaml +++ b/charts/coturn/templates/configmap-coturn-conf-template.yaml @@ -27,7 +27,7 @@ data: no-cli ## turn, stun. - listening-ip=__COTURN_EXT_IP__ + listening-ip={{ .Values.coturnTurnListenIP }} # was: __COTURN_EXT_IP__ listening-port={{ .Values.coturnTurnListenPort }} relay-ip=__COTURN_EXT_IP__ realm=dummy.io diff --git a/charts/coturn/values.yaml b/charts/coturn/values.yaml index 84934676739..cb7baa39cca 100644 --- a/charts/coturn/values.yaml +++ b/charts/coturn/values.yaml @@ -108,3 +108,7 @@ livenessProbe: readinessProbe: timeoutSeconds: 5 failureThreshold: 5 + +# If you need to specify which IP Coturn should bind to. +# This will typically be the IP of the kubenode. +# coturnTurnListenIP: "182.168.22.133" From 5fa42b55effc0e9d6a47e095fedbb85fa1e46155 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 28 Feb 2024 09:25:10 +0100 Subject: [PATCH 015/117] Serialisation of client capabilities (#3904) * Use Multiverb in add-client endpoint * Add versioned Client schema * Add v5 version of more client endpoints * Version client list * Update golden files * Add CHANGELOG entry --- .../0-release-notes/client-internal-api | 1 + changelog.d/1-api-changes/client-capabilities | 1 + .../src/Wire/API/Routes/Public/Brig.hs | 79 +++++++++++++++++-- .../src/Wire/API/Routes/Public/Brig/Bot.hs | 33 ++++++-- libs/wire-api/src/Wire/API/User/Client.hs | 63 ++++++++++----- .../golden/Test/Wire/API/Golden/Generated.hs | 2 + .../golden/testObject_ClientV5_user_1.json | 12 +++ .../golden/testObject_ClientV5_user_10.json | 13 +++ .../golden/testObject_ClientV5_user_11.json | 13 +++ .../golden/testObject_ClientV5_user_12.json | 12 +++ .../golden/testObject_ClientV5_user_13.json | 13 +++ .../golden/testObject_ClientV5_user_14.json | 12 +++ .../golden/testObject_ClientV5_user_15.json | 12 +++ .../golden/testObject_ClientV5_user_16.json | 13 +++ .../golden/testObject_ClientV5_user_17.json | 12 +++ .../golden/testObject_ClientV5_user_18.json | 13 +++ .../golden/testObject_ClientV5_user_19.json | 12 +++ .../golden/testObject_ClientV5_user_2.json | 10 +++ .../golden/testObject_ClientV5_user_20.json | 14 ++++ .../golden/testObject_ClientV5_user_3.json | 13 +++ .../golden/testObject_ClientV5_user_4.json | 12 +++ .../golden/testObject_ClientV5_user_5.json | 12 +++ .../golden/testObject_ClientV5_user_6.json | 13 +++ .../golden/testObject_ClientV5_user_7.json | 12 +++ .../golden/testObject_ClientV5_user_8.json | 13 +++ .../golden/testObject_ClientV5_user_9.json | 13 +++ .../test/golden/testObject_Client_user_1.json | 4 +- .../golden/testObject_Client_user_10.json | 4 +- .../golden/testObject_Client_user_11.json | 4 +- .../golden/testObject_Client_user_12.json | 4 +- .../golden/testObject_Client_user_13.json | 4 +- .../golden/testObject_Client_user_14.json | 4 +- .../golden/testObject_Client_user_15.json | 4 +- .../golden/testObject_Client_user_16.json | 4 +- .../golden/testObject_Client_user_17.json | 4 +- .../golden/testObject_Client_user_18.json | 4 +- .../golden/testObject_Client_user_19.json | 4 +- .../test/golden/testObject_Client_user_2.json | 4 +- .../golden/testObject_Client_user_20.json | 8 +- .../test/golden/testObject_Client_user_3.json | 4 +- .../test/golden/testObject_Client_user_4.json | 4 +- .../test/golden/testObject_Client_user_5.json | 4 +- .../test/golden/testObject_Client_user_6.json | 4 +- .../test/golden/testObject_Client_user_7.json | 4 +- .../test/golden/testObject_Client_user_8.json | 4 +- .../test/golden/testObject_Client_user_9.json | 4 +- services/brig/src/Brig/API/Public.hs | 15 ++-- services/brig/src/Brig/Provider/API.hs | 1 + services/brig/src/Brig/Run.hs | 2 +- 49 files changed, 428 insertions(+), 102 deletions(-) create mode 100644 changelog.d/0-release-notes/client-internal-api create mode 100644 changelog.d/1-api-changes/client-capabilities create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_1.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_10.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_11.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_12.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_13.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_14.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_15.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_16.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_17.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_18.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_19.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_2.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_20.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_3.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_4.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_5.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_6.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_7.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_8.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_9.json diff --git a/changelog.d/0-release-notes/client-internal-api b/changelog.d/0-release-notes/client-internal-api new file mode 100644 index 00000000000..d0d44c9b703 --- /dev/null +++ b/changelog.d/0-release-notes/client-internal-api @@ -0,0 +1 @@ +The "addClient" internal endpoint of galley has been changed. This can cause temporary failures during upgrades if brig attempts to use this endpoint on a different version of galley. diff --git a/changelog.d/1-api-changes/client-capabilities b/changelog.d/1-api-changes/client-capabilities new file mode 100644 index 00000000000..7bd98638b78 --- /dev/null +++ b/changelog.d/1-api-changes/client-capabilities @@ -0,0 +1 @@ +Create version 6 of client-related endpoints, fixing an oddity in the serialisation of capabilities. diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index 15b07451e10..0cd23b3c3e3 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -65,6 +65,7 @@ import Wire.API.Routes.Public.Brig.Services (ServicesAPI) import Wire.API.Routes.Public.Util import Wire.API.Routes.QualifiedCapture import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.API.SystemSettings import Wire.API.Team.Invitation import Wire.API.Team.Size @@ -126,8 +127,6 @@ type QualifiedCaptureUserId name = QualifiedCapture' '[Description "User Id"] na type CaptureClientId name = Capture' '[Description "ClientId"] name ClientId -type NewClientResponse = Headers '[Header "Location" ClientId] Client - type DeleteSelfResponses = '[ RespondEmpty 200 "Deletion is initiated.", RespondWithDeletionCodeTimeout @@ -730,15 +729,18 @@ type PrekeyAPI = :> Post '[JSON] QualifiedUserClientPrekeyMapV4 ) -type UserClientAPI = - -- User Client API ---------------------------------------------------- +-- User Client API ---------------------------------------------------- + +type ClientHeaders = '[DescHeader "Location" "Client ID" ClientId] +type UserClientAPI = -- This endpoint can lead to the following events being sent: -- - ClientAdded event to self -- - ClientRemoved event to self, if removing old clients due to max number Named - "add-client" + "add-client-v5" ( Summary "Register a new client" + :> Until 'V6 :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'TooManyClients :> CanThrow 'MissingAuth @@ -749,8 +751,38 @@ type UserClientAPI = :> ZConn :> "clients" :> ReqBody '[JSON] NewClient - :> Verb 'POST 201 '[JSON] NewClientResponse + :> MultiVerb1 + 'POST + '[JSON] + ( WithHeaders + ClientHeaders + Client + (VersionedRespond 'V5 201 "Client registered" Client) + ) ) + :<|> Named + "add-client" + ( Summary "Register a new client" + :> From 'V6 + :> MakesFederatedCall 'Brig "send-connection-action" + :> CanThrow 'TooManyClients + :> CanThrow 'MissingAuth + :> CanThrow 'MalformedPrekeys + :> CanThrow 'CodeAuthenticationFailed + :> CanThrow 'CodeAuthenticationRequired + :> ZUser + :> ZConn + :> "clients" + :> ReqBody '[JSON] NewClient + :> MultiVerb1 + 'POST + '[JSON] + ( WithHeaders + ClientHeaders + Client + (Respond 201 "Client registered" Client) + ) + ) :<|> Named "update-client" ( Summary "Update a registered client" @@ -774,16 +806,49 @@ type UserClientAPI = :> ReqBody '[JSON] RmClient :> MultiVerb 'DELETE '[JSON] '[RespondEmpty 200 "Client deleted"] () ) + :<|> Named + "list-clients-v5" + ( Summary "List the registered clients" + :> Until 'V6 + :> ZUser + :> "clients" + :> MultiVerb1 + 'GET + '[JSON] + ( VersionedRespond 'V5 200 "List of clients" [Client] + ) + ) :<|> Named "list-clients" ( Summary "List the registered clients" + :> From 'V6 + :> ZUser + :> "clients" + :> MultiVerb1 + 'GET + '[JSON] + ( Respond 200 "List of clients" [Client] + ) + ) + :<|> Named + "get-client-v5" + ( Summary "Get a registered client by ID" + :> Until 'V6 :> ZUser :> "clients" - :> Get '[JSON] [Client] + :> CaptureClientId "client" + :> MultiVerb + 'GET + '[JSON] + '[ EmptyErrorForLegacyReasons 404 "Client not found", + VersionedRespond 'V5 200 "Client found" Client + ] + (Maybe Client) ) :<|> Named "get-client" ( Summary "Get a registered client by ID" + :> From 'V6 :> ZUser :> "clients" :> CaptureClientId "client" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs index 4d544b64a53..635e6711c4a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs @@ -30,6 +30,8 @@ import Wire.API.Provider.Bot (BotUserView) import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named (Named) import Wire.API.Routes.Public +import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.API.User import Wire.API.User.Client import Wire.API.User.Client.Prekey (PrekeyId) @@ -39,11 +41,6 @@ type DeleteResponses = Respond 200 "User found" RemoveBotResponse ] -type GetClientResponses = - '[ ErrorResponse 'ClientNotFound, - Respond 200 "Client found" Client - ] - type BotAPI = Named "add-bot" @@ -116,15 +113,39 @@ type BotAPI = :> ReqBody '[JSON] UpdateBotPrekeys :> MultiVerb1 'POST '[JSON] (RespondEmpty 200 "") ) + :<|> Named + "bot-get-client-v5" + ( Summary "Get client for bot" + :> Until 'V6 + :> CanThrow 'AccessDenied + :> CanThrow 'ClientNotFound + :> ZBot + :> "bot" + :> "client" + :> MultiVerb + 'GET + '[JSON] + '[ ErrorResponse 'ClientNotFound, + VersionedRespond 'V5 200 "Client found" Client + ] + (Maybe Client) + ) :<|> Named "bot-get-client" ( Summary "Get client for bot" + :> From 'V6 :> CanThrow 'AccessDenied :> CanThrow 'ClientNotFound :> ZBot :> "bot" :> "client" - :> MultiVerb 'GET '[JSON] GetClientResponses (Maybe Client) + :> MultiVerb + 'GET + '[JSON] + '[ ErrorResponse 'ClientNotFound, + Respond 200 "Client found" Client + ] + (Maybe Client) ) :<|> Named "bot-claim-users-prekeys" diff --git a/libs/wire-api/src/Wire/API/User/Client.hs b/libs/wire-api/src/Wire/API/User/Client.hs index d900d8b830a..17f51c39961 100644 --- a/libs/wire-api/src/Wire/API/User/Client.hs +++ b/libs/wire-api/src/Wire/API/User/Client.hs @@ -85,9 +85,11 @@ import Data.Misc (Latitude (..), Longitude (..), PlainTextPassword6) import Data.OpenApi hiding (Schema, ToSchema, nullable, schema) import Data.OpenApi qualified as Swagger hiding (nullable) import Data.Qualified +import Data.SOP import Data.Schema import Data.Set qualified as Set -import Data.Text.Encoding qualified as Text.E +import Data.Text qualified as T +import Data.Text.Encoding qualified as T import Data.Time.Clock import Data.UUID (toASCIIBytes) import Deriving.Swagger @@ -98,6 +100,9 @@ import Deriving.Swagger ) import Imports import Wire.API.MLS.CipherSuite +import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.API.User.Auth import Wire.API.User.Client.Prekey as Prekey import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..), generateExample, mapOf', setOf') @@ -373,17 +378,17 @@ instance Swagger.ToSchema UserClientsFull where instance ToJSON UserClientsFull where toJSON = - toJSON . Map.foldrWithKey' fn Map.empty . userClientsFull + toJSON . Map.foldrWithKey' f Map.empty . userClientsFull where - fn u c m = - let k = Text.E.decodeLatin1 (toASCIIBytes (toUUID u)) + f u c m = + let k = T.decodeLatin1 (toASCIIBytes (toUUID u)) in Map.insert k c m instance FromJSON UserClientsFull where parseJSON = - A.withObject "UserClientsFull" (fmap UserClientsFull . foldrM fn Map.empty . KeyMap.toList) + A.withObject "UserClientsFull" (fmap UserClientsFull . foldrM f Map.empty . KeyMap.toList) where - fn (k, v) m = Map.insert <$> parseJSON (A.String $ Key.toText k) <*> parseJSON v <*> pure m + f (k, v) m = Map.insert <$> parseJSON (A.String $ Key.toText k) <*> parseJSON v <*> pure m instance Arbitrary UserClientsFull where arbitrary = UserClientsFull <$> mapOf' arbitrary (setOf' arbitrary) @@ -498,24 +503,46 @@ mlsPublicKeysSchema = mapSchema :: ValueSchema SwaggerDoc MLSPublicKeys mapSchema = map_ base64Schema +clientSchema :: Maybe Version -> ValueSchema NamedSwaggerDoc Client +clientSchema mv = + object ("Client" <> T.pack (foldMap show mv)) $ + Client + <$> clientId .= field "id" schema + <*> clientType .= field "type" schema + <*> clientTime .= field "time" schema + <*> clientClass .= maybe_ (optField "class" schema) + <*> clientLabel .= maybe_ (optField "label" schema) + <*> clientCookie .= maybe_ (optField "cookie" schema) + <*> clientModel .= maybe_ (optField "model" schema) + <*> clientCapabilities .= (fromMaybe mempty <$> caps) + <*> clientMLSPublicKeys .= mlsPublicKeysFieldSchema + <*> clientLastActive .= maybe_ (optField "last_active" utcTimeSchema) + where + caps :: ObjectSchemaP SwaggerDoc ClientCapabilityList (Maybe ClientCapabilityList) + caps = case mv of + -- broken capability serialisation for backwards compatibility + Just v | v <= V5 -> optField "capabilities" schema + _ -> fmap ClientCapabilityList <$> fromClientCapabilityList .= capabilitiesFieldSchema + instance ToSchema Client where + schema = clientSchema Nothing + +instance ToSchema (Versioned 'V5 Client) where + schema = Versioned <$> unVersioned .= clientSchema (Just V5) + +instance {-# OVERLAPPING #-} ToSchema (Versioned 'V5 [Client]) where schema = - object "Client" $ - Client - <$> clientId .= field "id" schema - <*> clientType .= field "type" schema - <*> clientTime .= field "time" schema - <*> clientClass .= maybe_ (optField "class" schema) - <*> clientLabel .= maybe_ (optField "label" schema) - <*> clientCookie .= maybe_ (optField "cookie" schema) - <*> clientModel .= maybe_ (optField "model" schema) - <*> clientCapabilities .= (fromMaybe mempty <$> optField "capabilities" schema) - <*> clientMLSPublicKeys .= mlsPublicKeysFieldSchema - <*> clientLastActive .= maybe_ (optField "last_active" utcTimeSchema) + Versioned + <$> unVersioned + .= named "ClientList" (array (clientSchema (Just V5))) mlsPublicKeysFieldSchema :: ObjectSchema SwaggerDoc MLSPublicKeys mlsPublicKeysFieldSchema = fromMaybe mempty <$> optField "mls_public_keys" mlsPublicKeysSchema +instance AsHeaders '[ClientId] Client Client where + toHeaders c = (I (clientId c) :* Nil, c) + fromHeaders = snd + -------------------------------------------------------------------------------- -- ClientList diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs index a6003b36d81..345668ebf46 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs @@ -1010,6 +1010,8 @@ tests = testObjects [(Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_1, "testObject_ClientClass_user_1.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_2, "testObject_ClientClass_user_2.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_3, "testObject_ClientClass_user_3.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_4, "testObject_ClientClass_user_4.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_5, "testObject_ClientClass_user_5.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_6, "testObject_ClientClass_user_6.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_7, "testObject_ClientClass_user_7.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_8, "testObject_ClientClass_user_8.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_9, "testObject_ClientClass_user_9.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_10, "testObject_ClientClass_user_10.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_11, "testObject_ClientClass_user_11.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_12, "testObject_ClientClass_user_12.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_13, "testObject_ClientClass_user_13.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_14, "testObject_ClientClass_user_14.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_15, "testObject_ClientClass_user_15.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_16, "testObject_ClientClass_user_16.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_17, "testObject_ClientClass_user_17.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_18, "testObject_ClientClass_user_18.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_19, "testObject_ClientClass_user_19.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_20, "testObject_ClientClass_user_20.json")], testGroup "Golden: PubClient_user" $ testObjects [(Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_1, "testObject_PubClient_user_1.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_2, "testObject_PubClient_user_2.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_3, "testObject_PubClient_user_3.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_4, "testObject_PubClient_user_4.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_5, "testObject_PubClient_user_5.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_6, "testObject_PubClient_user_6.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_7, "testObject_PubClient_user_7.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_8, "testObject_PubClient_user_8.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_9, "testObject_PubClient_user_9.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_10, "testObject_PubClient_user_10.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_11, "testObject_PubClient_user_11.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_12, "testObject_PubClient_user_12.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_13, "testObject_PubClient_user_13.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_14, "testObject_PubClient_user_14.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_15, "testObject_PubClient_user_15.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_16, "testObject_PubClient_user_16.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_17, "testObject_PubClient_user_17.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_18, "testObject_PubClient_user_18.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_19, "testObject_PubClient_user_19.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_20, "testObject_PubClient_user_20.json")], + testGroup "Golden: ClientV5_user" $ + testObjects [(Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_1, "testObject_ClientV5_user_1.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_2, "testObject_ClientV5_user_2.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_3, "testObject_ClientV5_user_3.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_4, "testObject_ClientV5_user_4.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_5, "testObject_ClientV5_user_5.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_6, "testObject_ClientV5_user_6.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_7, "testObject_ClientV5_user_7.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_8, "testObject_ClientV5_user_8.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_9, "testObject_ClientV5_user_9.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_10, "testObject_ClientV5_user_10.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_11, "testObject_ClientV5_user_11.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_12, "testObject_ClientV5_user_12.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_13, "testObject_ClientV5_user_13.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_14, "testObject_ClientV5_user_14.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_15, "testObject_ClientV5_user_15.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_16, "testObject_ClientV5_user_16.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_17, "testObject_ClientV5_user_17.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_18, "testObject_ClientV5_user_18.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_19, "testObject_ClientV5_user_19.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_20, "testObject_ClientV5_user_20.json")], testGroup "Golden: Client_user" $ testObjects [(Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_1, "testObject_Client_user_1.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_2, "testObject_Client_user_2.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_3, "testObject_Client_user_3.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_4, "testObject_Client_user_4.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_5, "testObject_Client_user_5.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_6, "testObject_Client_user_6.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_7, "testObject_Client_user_7.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_8, "testObject_Client_user_8.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_9, "testObject_Client_user_9.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_10, "testObject_Client_user_10.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_11, "testObject_Client_user_11.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_12, "testObject_Client_user_12.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_13, "testObject_Client_user_13.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_14, "testObject_Client_user_14.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_15, "testObject_Client_user_15.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_16, "testObject_Client_user_16.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_17, "testObject_Client_user_17.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_18, "testObject_Client_user_18.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_19, "testObject_Client_user_19.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_20, "testObject_Client_user_20.json")], testGroup "Golden: NewClient_user" $ diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_1.json b/libs/wire-api/test/golden/testObject_ClientV5_user_1.json new file mode 100644 index 00000000000..9fc8b644e4a --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_1.json @@ -0,0 +1,12 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "desktop", + "id": "2", + "label": "%*", + "mls_public_keys": {}, + "model": "󳇚;􇻫", + "time": "1864-05-06T19:39:12.770Z", + "type": "permanent" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_10.json b/libs/wire-api/test/golden/testObject_ClientV5_user_10.json new file mode 100644 index 00000000000..1d08a33cfd0 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_10.json @@ -0,0 +1,13 @@ +{ + "capabilities": { + "capabilities": [] + }, + "cookie": "L", + "id": "0", + "mls_public_keys": { + "ed25519": "Wm1GclpTQndkV0pzYVdNZ2EyVjU=" + }, + "model": "\u0018", + "time": "1864-05-10T18:42:04.137Z", + "type": "permanent" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_11.json b/libs/wire-api/test/golden/testObject_ClientV5_user_11.json new file mode 100644 index 00000000000..6e4c38b8dc9 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_11.json @@ -0,0 +1,13 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "legalhold", + "cookie": "5", + "id": "3", + "label": "\u001fb", + "mls_public_keys": {}, + "model": "ML", + "time": "1864-05-08T11:57:08.087Z", + "type": "temporary" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_12.json b/libs/wire-api/test/golden/testObject_ClientV5_user_12.json new file mode 100644 index 00000000000..644db85ecbf --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_12.json @@ -0,0 +1,12 @@ +{ + "capabilities": { + "capabilities": [] + }, + "cookie": "0", + "id": "2", + "label": "", + "mls_public_keys": {}, + "model": "", + "time": "1864-05-08T18:44:00.378Z", + "type": "permanent" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_13.json b/libs/wire-api/test/golden/testObject_ClientV5_user_13.json new file mode 100644 index 00000000000..9034bcbc4ab --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_13.json @@ -0,0 +1,13 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "phone", + "cookie": "\u000c^󷋏", + "id": "2", + "label": "􃱽", + "mls_public_keys": {}, + "model": "\u0017𐲤", + "time": "1864-05-07T01:09:04.597Z", + "type": "permanent" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_14.json b/libs/wire-api/test/golden/testObject_ClientV5_user_14.json new file mode 100644 index 00000000000..a4d61fe168c --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_14.json @@ -0,0 +1,12 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "tablet", + "id": "2", + "label": "x\u000e", + "mls_public_keys": {}, + "model": "􀸏\r󠁨", + "time": "1864-05-12T11:00:10.449Z", + "type": "temporary" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_15.json b/libs/wire-api/test/golden/testObject_ClientV5_user_15.json new file mode 100644 index 00000000000..626f76201cd --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_15.json @@ -0,0 +1,12 @@ +{ + "capabilities": { + "capabilities": [] + }, + "cookie": "􌨷N", + "id": "3", + "label": "\u0004G", + "mls_public_keys": {}, + "model": "zAI", + "time": "1864-05-08T11:28:27.778Z", + "type": "temporary" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_16.json b/libs/wire-api/test/golden/testObject_ClientV5_user_16.json new file mode 100644 index 00000000000..7216da58868 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_16.json @@ -0,0 +1,13 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "legalhold", + "cookie": "U", + "id": "2", + "label": "=E", + "mls_public_keys": {}, + "model": "", + "time": "1864-05-12T11:31:10.072Z", + "type": "temporary" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_17.json b/libs/wire-api/test/golden/testObject_ClientV5_user_17.json new file mode 100644 index 00000000000..9f0f36f96a3 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_17.json @@ -0,0 +1,12 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "desktop", + "cookie": "", + "id": "4", + "mls_public_keys": {}, + "model": "", + "time": "1864-05-12T02:25:34.770Z", + "type": "temporary" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_18.json b/libs/wire-api/test/golden/testObject_ClientV5_user_18.json new file mode 100644 index 00000000000..80dad343c4e --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_18.json @@ -0,0 +1,13 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "legalhold", + "cookie": "PG:", + "id": "1", + "label": "󳔺", + "mls_public_keys": {}, + "model": "􅩹", + "time": "1864-05-07T17:21:05.930Z", + "type": "temporary" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_19.json b/libs/wire-api/test/golden/testObject_ClientV5_user_19.json new file mode 100644 index 00000000000..db061827756 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_19.json @@ -0,0 +1,12 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "desktop", + "id": "2", + "label": "􌇰l", + "mls_public_keys": {}, + "model": "", + "time": "1864-05-12T07:49:27.999Z", + "type": "permanent" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_2.json b/libs/wire-api/test/golden/testObject_ClientV5_user_2.json new file mode 100644 index 00000000000..08dd2786531 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_2.json @@ -0,0 +1,10 @@ +{ + "capabilities": { + "capabilities": [] + }, + "cookie": "􏬺c􄂩", + "id": "1", + "mls_public_keys": {}, + "time": "1864-05-07T08:48:22.537Z", + "type": "legalhold" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_20.json b/libs/wire-api/test/golden/testObject_ClientV5_user_20.json new file mode 100644 index 00000000000..253cd8c3952 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_20.json @@ -0,0 +1,14 @@ +{ + "capabilities": { + "capabilities": [ + "legalhold-implicit-consent" + ] + }, + "class": "phone", + "cookie": "", + "id": "1", + "label": "-󼊣v", + "mls_public_keys": {}, + "time": "1864-05-06T18:43:52.483Z", + "type": "legalhold" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_3.json b/libs/wire-api/test/golden/testObject_ClientV5_user_3.json new file mode 100644 index 00000000000..8c5026d2cb7 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_3.json @@ -0,0 +1,13 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "legalhold", + "cookie": "", + "id": "1", + "label": "pi", + "last_active": "2023-07-04T09:35:32Z", + "mls_public_keys": {}, + "time": "1864-05-07T00:38:22.384Z", + "type": "temporary" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_4.json b/libs/wire-api/test/golden/testObject_ClientV5_user_4.json new file mode 100644 index 00000000000..25e8c8860bd --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_4.json @@ -0,0 +1,12 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "legalhold", + "cookie": "j", + "id": "3", + "mls_public_keys": {}, + "model": "", + "time": "1864-05-06T09:13:45.902Z", + "type": "permanent" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_5.json b/libs/wire-api/test/golden/testObject_ClientV5_user_5.json new file mode 100644 index 00000000000..0af93523dc2 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_5.json @@ -0,0 +1,12 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "desktop", + "cookie": "", + "id": "0", + "mls_public_keys": {}, + "model": "⌷o", + "time": "1864-05-07T09:07:14.559Z", + "type": "temporary" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_6.json b/libs/wire-api/test/golden/testObject_ClientV5_user_6.json new file mode 100644 index 00000000000..90a2b0ea16e --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_6.json @@ -0,0 +1,13 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "tablet", + "cookie": "l\u0002", + "id": "4", + "last_active": "2021-09-15T22:00:21Z", + "mls_public_keys": {}, + "model": "", + "time": "1864-05-08T22:37:53.030Z", + "type": "permanent" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_7.json b/libs/wire-api/test/golden/testObject_ClientV5_user_7.json new file mode 100644 index 00000000000..41253b1fb0a --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_7.json @@ -0,0 +1,12 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "phone", + "id": "4", + "label": "", + "mls_public_keys": {}, + "model": "", + "time": "1864-05-07T04:35:34.201Z", + "type": "permanent" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_8.json b/libs/wire-api/test/golden/testObject_ClientV5_user_8.json new file mode 100644 index 00000000000..fafbbc7e6e5 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_8.json @@ -0,0 +1,13 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "phone", + "cookie": "\u0015p`", + "id": "4", + "label": "", + "mls_public_keys": {}, + "model": "􏽉", + "time": "1864-05-11T06:32:01.921Z", + "type": "legalhold" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_9.json b/libs/wire-api/test/golden/testObject_ClientV5_user_9.json new file mode 100644 index 00000000000..ed4e67747ca --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_9.json @@ -0,0 +1,13 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "legalhold", + "cookie": "G", + "id": "1", + "label": "v", + "mls_public_keys": {}, + "model": "㌀m", + "time": "1864-05-08T03:54:56.526Z", + "type": "legalhold" +} diff --git a/libs/wire-api/test/golden/testObject_Client_user_1.json b/libs/wire-api/test/golden/testObject_Client_user_1.json index 9fc8b644e4a..3ae58f75402 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_1.json +++ b/libs/wire-api/test/golden/testObject_Client_user_1.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "desktop", "id": "2", "label": "%*", diff --git a/libs/wire-api/test/golden/testObject_Client_user_10.json b/libs/wire-api/test/golden/testObject_Client_user_10.json index 1d08a33cfd0..35ad363f074 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_10.json +++ b/libs/wire-api/test/golden/testObject_Client_user_10.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "cookie": "L", "id": "0", "mls_public_keys": { diff --git a/libs/wire-api/test/golden/testObject_Client_user_11.json b/libs/wire-api/test/golden/testObject_Client_user_11.json index 6e4c38b8dc9..8d6af47dc49 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_11.json +++ b/libs/wire-api/test/golden/testObject_Client_user_11.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "legalhold", "cookie": "5", "id": "3", diff --git a/libs/wire-api/test/golden/testObject_Client_user_12.json b/libs/wire-api/test/golden/testObject_Client_user_12.json index 644db85ecbf..63ca4553dee 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_12.json +++ b/libs/wire-api/test/golden/testObject_Client_user_12.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "cookie": "0", "id": "2", "label": "", diff --git a/libs/wire-api/test/golden/testObject_Client_user_13.json b/libs/wire-api/test/golden/testObject_Client_user_13.json index 9034bcbc4ab..9b2552d9086 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_13.json +++ b/libs/wire-api/test/golden/testObject_Client_user_13.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "phone", "cookie": "\u000c^󷋏", "id": "2", diff --git a/libs/wire-api/test/golden/testObject_Client_user_14.json b/libs/wire-api/test/golden/testObject_Client_user_14.json index a4d61fe168c..c95b927805a 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_14.json +++ b/libs/wire-api/test/golden/testObject_Client_user_14.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "tablet", "id": "2", "label": "x\u000e", diff --git a/libs/wire-api/test/golden/testObject_Client_user_15.json b/libs/wire-api/test/golden/testObject_Client_user_15.json index 626f76201cd..7050d356278 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_15.json +++ b/libs/wire-api/test/golden/testObject_Client_user_15.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "cookie": "􌨷N", "id": "3", "label": "\u0004G", diff --git a/libs/wire-api/test/golden/testObject_Client_user_16.json b/libs/wire-api/test/golden/testObject_Client_user_16.json index 7216da58868..e70257998b5 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_16.json +++ b/libs/wire-api/test/golden/testObject_Client_user_16.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "legalhold", "cookie": "U", "id": "2", diff --git a/libs/wire-api/test/golden/testObject_Client_user_17.json b/libs/wire-api/test/golden/testObject_Client_user_17.json index 9f0f36f96a3..485f822a3d2 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_17.json +++ b/libs/wire-api/test/golden/testObject_Client_user_17.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "desktop", "cookie": "", "id": "4", diff --git a/libs/wire-api/test/golden/testObject_Client_user_18.json b/libs/wire-api/test/golden/testObject_Client_user_18.json index 80dad343c4e..5f1ba1bf5b8 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_18.json +++ b/libs/wire-api/test/golden/testObject_Client_user_18.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "legalhold", "cookie": "PG:", "id": "1", diff --git a/libs/wire-api/test/golden/testObject_Client_user_19.json b/libs/wire-api/test/golden/testObject_Client_user_19.json index db061827756..f6263f00203 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_19.json +++ b/libs/wire-api/test/golden/testObject_Client_user_19.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "desktop", "id": "2", "label": "􌇰l", diff --git a/libs/wire-api/test/golden/testObject_Client_user_2.json b/libs/wire-api/test/golden/testObject_Client_user_2.json index 08dd2786531..802de9bd21f 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_2.json +++ b/libs/wire-api/test/golden/testObject_Client_user_2.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "cookie": "􏬺c􄂩", "id": "1", "mls_public_keys": {}, diff --git a/libs/wire-api/test/golden/testObject_Client_user_20.json b/libs/wire-api/test/golden/testObject_Client_user_20.json index 253cd8c3952..c9f3ae4459b 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_20.json +++ b/libs/wire-api/test/golden/testObject_Client_user_20.json @@ -1,9 +1,7 @@ { - "capabilities": { - "capabilities": [ - "legalhold-implicit-consent" - ] - }, + "capabilities": [ + "legalhold-implicit-consent" + ], "class": "phone", "cookie": "", "id": "1", diff --git a/libs/wire-api/test/golden/testObject_Client_user_3.json b/libs/wire-api/test/golden/testObject_Client_user_3.json index 8c5026d2cb7..b6cb51e0fbf 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_3.json +++ b/libs/wire-api/test/golden/testObject_Client_user_3.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "legalhold", "cookie": "", "id": "1", diff --git a/libs/wire-api/test/golden/testObject_Client_user_4.json b/libs/wire-api/test/golden/testObject_Client_user_4.json index 25e8c8860bd..4a8398a2e9b 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_4.json +++ b/libs/wire-api/test/golden/testObject_Client_user_4.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "legalhold", "cookie": "j", "id": "3", diff --git a/libs/wire-api/test/golden/testObject_Client_user_5.json b/libs/wire-api/test/golden/testObject_Client_user_5.json index 0af93523dc2..e1967bb1bcf 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_5.json +++ b/libs/wire-api/test/golden/testObject_Client_user_5.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "desktop", "cookie": "", "id": "0", diff --git a/libs/wire-api/test/golden/testObject_Client_user_6.json b/libs/wire-api/test/golden/testObject_Client_user_6.json index 90a2b0ea16e..929f3132496 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_6.json +++ b/libs/wire-api/test/golden/testObject_Client_user_6.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "tablet", "cookie": "l\u0002", "id": "4", diff --git a/libs/wire-api/test/golden/testObject_Client_user_7.json b/libs/wire-api/test/golden/testObject_Client_user_7.json index 41253b1fb0a..8ca4dc49b6a 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_7.json +++ b/libs/wire-api/test/golden/testObject_Client_user_7.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "phone", "id": "4", "label": "", diff --git a/libs/wire-api/test/golden/testObject_Client_user_8.json b/libs/wire-api/test/golden/testObject_Client_user_8.json index fafbbc7e6e5..35f568dd53c 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_8.json +++ b/libs/wire-api/test/golden/testObject_Client_user_8.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "phone", "cookie": "\u0015p`", "id": "4", diff --git a/libs/wire-api/test/golden/testObject_Client_user_9.json b/libs/wire-api/test/golden/testObject_Client_user_9.json index ed4e67747ca..cfda4f2768a 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_9.json +++ b/libs/wire-api/test/golden/testObject_Client_user_9.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "legalhold", "cookie": "G", "id": "1", diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 0630ee43602..8776b0545c7 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -369,10 +369,13 @@ servantSitemap = userClientAPI :: ServerT UserClientAPI (Handler r) userClientAPI = - Named @"add-client" (callsFed (exposeAnnotations addClient)) + Named @"add-client-v5" (callsFed (exposeAnnotations addClient)) + :<|> Named @"add-client" (callsFed (exposeAnnotations addClient)) :<|> Named @"update-client" updateClient :<|> Named @"delete-client" deleteClient + :<|> Named @"list-clients-v5" listClients :<|> Named @"list-clients" listClients + :<|> Named @"get-client-v5" getClient :<|> Named @"get-client" getClient :<|> Named @"get-client-capabilities" getClientCapabilities :<|> Named @"get-client-prekeys" getClientPrekeys @@ -578,17 +581,13 @@ addClient :: UserId -> ConnId -> Public.NewClient -> - (Handler r) NewClientResponse + Handler r Public.Client addClient usr con new = do -- Users can't add legal hold clients when (Public.newClientType new == Public.LegalHoldClientType) $ throwE (clientError ClientLegalHoldCannotBeAdded) - clientResponse - <$> API.addClient usr (Just con) new - !>> clientError - where - clientResponse :: Public.Client -> NewClientResponse - clientResponse client = Servant.addHeader (Public.clientId client) client + API.addClient usr (Just con) new + !>> clientError deleteClient :: UserId -> ConnId -> ClientId -> Public.RmClient -> (Handler r) () deleteClient usr con clt body = diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 0bc3963beeb..0bdc32745e1 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -142,6 +142,7 @@ botAPI = :<|> Named @"bot-delete-self" botDeleteSelf :<|> Named @"bot-list-prekeys" botListPrekeys :<|> Named @"bot-update-prekeys" botUpdatePrekeys + :<|> Named @"bot-get-client-v5" botGetClient :<|> Named @"bot-get-client" botGetClient :<|> Named @"bot-claim-users-prekeys" botClaimUsersPrekeys :<|> Named @"bot-list-users" botListUserProfiles diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index 553a773366f..b74e58081c2 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -26,7 +26,7 @@ import Brig.API (sitemap) import Brig.API.Federation import Brig.API.Handler import Brig.API.Internal qualified as IAPI -import Brig.API.Public (DocsAPI, docsAPI, servantSitemap) +import Brig.API.Public import Brig.API.User qualified as API import Brig.AWS (amazonkaEnv, sesQueue) import Brig.AWS qualified as AWS From 6fa86afcf8ff6749e9efc68f3a9a08f1970a76f6 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Wed, 28 Feb 2024 11:31:34 +0100 Subject: [PATCH 016/117] Revert "Serialisation of client capabilities (#3904)" This reverts commit 5fa42b55effc0e9d6a47e095fedbb85fa1e46155. --- .../0-release-notes/client-internal-api | 1 - changelog.d/1-api-changes/client-capabilities | 1 - .../src/Wire/API/Routes/Public/Brig.hs | 79 ++----------------- .../src/Wire/API/Routes/Public/Brig/Bot.hs | 33 ++------ libs/wire-api/src/Wire/API/User/Client.hs | 63 +++++---------- .../golden/Test/Wire/API/Golden/Generated.hs | 2 - .../golden/testObject_ClientV5_user_1.json | 12 --- .../golden/testObject_ClientV5_user_10.json | 13 --- .../golden/testObject_ClientV5_user_11.json | 13 --- .../golden/testObject_ClientV5_user_12.json | 12 --- .../golden/testObject_ClientV5_user_13.json | 13 --- .../golden/testObject_ClientV5_user_14.json | 12 --- .../golden/testObject_ClientV5_user_15.json | 12 --- .../golden/testObject_ClientV5_user_16.json | 13 --- .../golden/testObject_ClientV5_user_17.json | 12 --- .../golden/testObject_ClientV5_user_18.json | 13 --- .../golden/testObject_ClientV5_user_19.json | 12 --- .../golden/testObject_ClientV5_user_2.json | 10 --- .../golden/testObject_ClientV5_user_20.json | 14 ---- .../golden/testObject_ClientV5_user_3.json | 13 --- .../golden/testObject_ClientV5_user_4.json | 12 --- .../golden/testObject_ClientV5_user_5.json | 12 --- .../golden/testObject_ClientV5_user_6.json | 13 --- .../golden/testObject_ClientV5_user_7.json | 12 --- .../golden/testObject_ClientV5_user_8.json | 13 --- .../golden/testObject_ClientV5_user_9.json | 13 --- .../test/golden/testObject_Client_user_1.json | 4 +- .../golden/testObject_Client_user_10.json | 4 +- .../golden/testObject_Client_user_11.json | 4 +- .../golden/testObject_Client_user_12.json | 4 +- .../golden/testObject_Client_user_13.json | 4 +- .../golden/testObject_Client_user_14.json | 4 +- .../golden/testObject_Client_user_15.json | 4 +- .../golden/testObject_Client_user_16.json | 4 +- .../golden/testObject_Client_user_17.json | 4 +- .../golden/testObject_Client_user_18.json | 4 +- .../golden/testObject_Client_user_19.json | 4 +- .../test/golden/testObject_Client_user_2.json | 4 +- .../golden/testObject_Client_user_20.json | 8 +- .../test/golden/testObject_Client_user_3.json | 4 +- .../test/golden/testObject_Client_user_4.json | 4 +- .../test/golden/testObject_Client_user_5.json | 4 +- .../test/golden/testObject_Client_user_6.json | 4 +- .../test/golden/testObject_Client_user_7.json | 4 +- .../test/golden/testObject_Client_user_8.json | 4 +- .../test/golden/testObject_Client_user_9.json | 4 +- services/brig/src/Brig/API/Public.hs | 15 ++-- services/brig/src/Brig/Provider/API.hs | 1 - services/brig/src/Brig/Run.hs | 2 +- 49 files changed, 102 insertions(+), 428 deletions(-) delete mode 100644 changelog.d/0-release-notes/client-internal-api delete mode 100644 changelog.d/1-api-changes/client-capabilities delete mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_1.json delete mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_10.json delete mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_11.json delete mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_12.json delete mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_13.json delete mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_14.json delete mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_15.json delete mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_16.json delete mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_17.json delete mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_18.json delete mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_19.json delete mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_2.json delete mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_20.json delete mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_3.json delete mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_4.json delete mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_5.json delete mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_6.json delete mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_7.json delete mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_8.json delete mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_9.json diff --git a/changelog.d/0-release-notes/client-internal-api b/changelog.d/0-release-notes/client-internal-api deleted file mode 100644 index d0d44c9b703..00000000000 --- a/changelog.d/0-release-notes/client-internal-api +++ /dev/null @@ -1 +0,0 @@ -The "addClient" internal endpoint of galley has been changed. This can cause temporary failures during upgrades if brig attempts to use this endpoint on a different version of galley. diff --git a/changelog.d/1-api-changes/client-capabilities b/changelog.d/1-api-changes/client-capabilities deleted file mode 100644 index 7bd98638b78..00000000000 --- a/changelog.d/1-api-changes/client-capabilities +++ /dev/null @@ -1 +0,0 @@ -Create version 6 of client-related endpoints, fixing an oddity in the serialisation of capabilities. diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index 0cd23b3c3e3..15b07451e10 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -65,7 +65,6 @@ import Wire.API.Routes.Public.Brig.Services (ServicesAPI) import Wire.API.Routes.Public.Util import Wire.API.Routes.QualifiedCapture import Wire.API.Routes.Version -import Wire.API.Routes.Versioned import Wire.API.SystemSettings import Wire.API.Team.Invitation import Wire.API.Team.Size @@ -127,6 +126,8 @@ type QualifiedCaptureUserId name = QualifiedCapture' '[Description "User Id"] na type CaptureClientId name = Capture' '[Description "ClientId"] name ClientId +type NewClientResponse = Headers '[Header "Location" ClientId] Client + type DeleteSelfResponses = '[ RespondEmpty 200 "Deletion is initiated.", RespondWithDeletionCodeTimeout @@ -729,18 +730,15 @@ type PrekeyAPI = :> Post '[JSON] QualifiedUserClientPrekeyMapV4 ) --- User Client API ---------------------------------------------------- - -type ClientHeaders = '[DescHeader "Location" "Client ID" ClientId] - type UserClientAPI = + -- User Client API ---------------------------------------------------- + -- This endpoint can lead to the following events being sent: -- - ClientAdded event to self -- - ClientRemoved event to self, if removing old clients due to max number Named - "add-client-v5" + "add-client" ( Summary "Register a new client" - :> Until 'V6 :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'TooManyClients :> CanThrow 'MissingAuth @@ -751,38 +749,8 @@ type UserClientAPI = :> ZConn :> "clients" :> ReqBody '[JSON] NewClient - :> MultiVerb1 - 'POST - '[JSON] - ( WithHeaders - ClientHeaders - Client - (VersionedRespond 'V5 201 "Client registered" Client) - ) + :> Verb 'POST 201 '[JSON] NewClientResponse ) - :<|> Named - "add-client" - ( Summary "Register a new client" - :> From 'V6 - :> MakesFederatedCall 'Brig "send-connection-action" - :> CanThrow 'TooManyClients - :> CanThrow 'MissingAuth - :> CanThrow 'MalformedPrekeys - :> CanThrow 'CodeAuthenticationFailed - :> CanThrow 'CodeAuthenticationRequired - :> ZUser - :> ZConn - :> "clients" - :> ReqBody '[JSON] NewClient - :> MultiVerb1 - 'POST - '[JSON] - ( WithHeaders - ClientHeaders - Client - (Respond 201 "Client registered" Client) - ) - ) :<|> Named "update-client" ( Summary "Update a registered client" @@ -806,49 +774,16 @@ type UserClientAPI = :> ReqBody '[JSON] RmClient :> MultiVerb 'DELETE '[JSON] '[RespondEmpty 200 "Client deleted"] () ) - :<|> Named - "list-clients-v5" - ( Summary "List the registered clients" - :> Until 'V6 - :> ZUser - :> "clients" - :> MultiVerb1 - 'GET - '[JSON] - ( VersionedRespond 'V5 200 "List of clients" [Client] - ) - ) :<|> Named "list-clients" ( Summary "List the registered clients" - :> From 'V6 - :> ZUser - :> "clients" - :> MultiVerb1 - 'GET - '[JSON] - ( Respond 200 "List of clients" [Client] - ) - ) - :<|> Named - "get-client-v5" - ( Summary "Get a registered client by ID" - :> Until 'V6 :> ZUser :> "clients" - :> CaptureClientId "client" - :> MultiVerb - 'GET - '[JSON] - '[ EmptyErrorForLegacyReasons 404 "Client not found", - VersionedRespond 'V5 200 "Client found" Client - ] - (Maybe Client) + :> Get '[JSON] [Client] ) :<|> Named "get-client" ( Summary "Get a registered client by ID" - :> From 'V6 :> ZUser :> "clients" :> CaptureClientId "client" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs index 635e6711c4a..4d544b64a53 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs @@ -30,8 +30,6 @@ import Wire.API.Provider.Bot (BotUserView) import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named (Named) import Wire.API.Routes.Public -import Wire.API.Routes.Version -import Wire.API.Routes.Versioned import Wire.API.User import Wire.API.User.Client import Wire.API.User.Client.Prekey (PrekeyId) @@ -41,6 +39,11 @@ type DeleteResponses = Respond 200 "User found" RemoveBotResponse ] +type GetClientResponses = + '[ ErrorResponse 'ClientNotFound, + Respond 200 "Client found" Client + ] + type BotAPI = Named "add-bot" @@ -113,39 +116,15 @@ type BotAPI = :> ReqBody '[JSON] UpdateBotPrekeys :> MultiVerb1 'POST '[JSON] (RespondEmpty 200 "") ) - :<|> Named - "bot-get-client-v5" - ( Summary "Get client for bot" - :> Until 'V6 - :> CanThrow 'AccessDenied - :> CanThrow 'ClientNotFound - :> ZBot - :> "bot" - :> "client" - :> MultiVerb - 'GET - '[JSON] - '[ ErrorResponse 'ClientNotFound, - VersionedRespond 'V5 200 "Client found" Client - ] - (Maybe Client) - ) :<|> Named "bot-get-client" ( Summary "Get client for bot" - :> From 'V6 :> CanThrow 'AccessDenied :> CanThrow 'ClientNotFound :> ZBot :> "bot" :> "client" - :> MultiVerb - 'GET - '[JSON] - '[ ErrorResponse 'ClientNotFound, - Respond 200 "Client found" Client - ] - (Maybe Client) + :> MultiVerb 'GET '[JSON] GetClientResponses (Maybe Client) ) :<|> Named "bot-claim-users-prekeys" diff --git a/libs/wire-api/src/Wire/API/User/Client.hs b/libs/wire-api/src/Wire/API/User/Client.hs index 17f51c39961..d900d8b830a 100644 --- a/libs/wire-api/src/Wire/API/User/Client.hs +++ b/libs/wire-api/src/Wire/API/User/Client.hs @@ -85,11 +85,9 @@ import Data.Misc (Latitude (..), Longitude (..), PlainTextPassword6) import Data.OpenApi hiding (Schema, ToSchema, nullable, schema) import Data.OpenApi qualified as Swagger hiding (nullable) import Data.Qualified -import Data.SOP import Data.Schema import Data.Set qualified as Set -import Data.Text qualified as T -import Data.Text.Encoding qualified as T +import Data.Text.Encoding qualified as Text.E import Data.Time.Clock import Data.UUID (toASCIIBytes) import Deriving.Swagger @@ -100,9 +98,6 @@ import Deriving.Swagger ) import Imports import Wire.API.MLS.CipherSuite -import Wire.API.Routes.MultiVerb -import Wire.API.Routes.Version -import Wire.API.Routes.Versioned import Wire.API.User.Auth import Wire.API.User.Client.Prekey as Prekey import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..), generateExample, mapOf', setOf') @@ -378,17 +373,17 @@ instance Swagger.ToSchema UserClientsFull where instance ToJSON UserClientsFull where toJSON = - toJSON . Map.foldrWithKey' f Map.empty . userClientsFull + toJSON . Map.foldrWithKey' fn Map.empty . userClientsFull where - f u c m = - let k = T.decodeLatin1 (toASCIIBytes (toUUID u)) + fn u c m = + let k = Text.E.decodeLatin1 (toASCIIBytes (toUUID u)) in Map.insert k c m instance FromJSON UserClientsFull where parseJSON = - A.withObject "UserClientsFull" (fmap UserClientsFull . foldrM f Map.empty . KeyMap.toList) + A.withObject "UserClientsFull" (fmap UserClientsFull . foldrM fn Map.empty . KeyMap.toList) where - f (k, v) m = Map.insert <$> parseJSON (A.String $ Key.toText k) <*> parseJSON v <*> pure m + fn (k, v) m = Map.insert <$> parseJSON (A.String $ Key.toText k) <*> parseJSON v <*> pure m instance Arbitrary UserClientsFull where arbitrary = UserClientsFull <$> mapOf' arbitrary (setOf' arbitrary) @@ -503,46 +498,24 @@ mlsPublicKeysSchema = mapSchema :: ValueSchema SwaggerDoc MLSPublicKeys mapSchema = map_ base64Schema -clientSchema :: Maybe Version -> ValueSchema NamedSwaggerDoc Client -clientSchema mv = - object ("Client" <> T.pack (foldMap show mv)) $ - Client - <$> clientId .= field "id" schema - <*> clientType .= field "type" schema - <*> clientTime .= field "time" schema - <*> clientClass .= maybe_ (optField "class" schema) - <*> clientLabel .= maybe_ (optField "label" schema) - <*> clientCookie .= maybe_ (optField "cookie" schema) - <*> clientModel .= maybe_ (optField "model" schema) - <*> clientCapabilities .= (fromMaybe mempty <$> caps) - <*> clientMLSPublicKeys .= mlsPublicKeysFieldSchema - <*> clientLastActive .= maybe_ (optField "last_active" utcTimeSchema) - where - caps :: ObjectSchemaP SwaggerDoc ClientCapabilityList (Maybe ClientCapabilityList) - caps = case mv of - -- broken capability serialisation for backwards compatibility - Just v | v <= V5 -> optField "capabilities" schema - _ -> fmap ClientCapabilityList <$> fromClientCapabilityList .= capabilitiesFieldSchema - instance ToSchema Client where - schema = clientSchema Nothing - -instance ToSchema (Versioned 'V5 Client) where - schema = Versioned <$> unVersioned .= clientSchema (Just V5) - -instance {-# OVERLAPPING #-} ToSchema (Versioned 'V5 [Client]) where schema = - Versioned - <$> unVersioned - .= named "ClientList" (array (clientSchema (Just V5))) + object "Client" $ + Client + <$> clientId .= field "id" schema + <*> clientType .= field "type" schema + <*> clientTime .= field "time" schema + <*> clientClass .= maybe_ (optField "class" schema) + <*> clientLabel .= maybe_ (optField "label" schema) + <*> clientCookie .= maybe_ (optField "cookie" schema) + <*> clientModel .= maybe_ (optField "model" schema) + <*> clientCapabilities .= (fromMaybe mempty <$> optField "capabilities" schema) + <*> clientMLSPublicKeys .= mlsPublicKeysFieldSchema + <*> clientLastActive .= maybe_ (optField "last_active" utcTimeSchema) mlsPublicKeysFieldSchema :: ObjectSchema SwaggerDoc MLSPublicKeys mlsPublicKeysFieldSchema = fromMaybe mempty <$> optField "mls_public_keys" mlsPublicKeysSchema -instance AsHeaders '[ClientId] Client Client where - toHeaders c = (I (clientId c) :* Nil, c) - fromHeaders = snd - -------------------------------------------------------------------------------- -- ClientList diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs index 345668ebf46..a6003b36d81 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs @@ -1010,8 +1010,6 @@ tests = testObjects [(Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_1, "testObject_ClientClass_user_1.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_2, "testObject_ClientClass_user_2.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_3, "testObject_ClientClass_user_3.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_4, "testObject_ClientClass_user_4.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_5, "testObject_ClientClass_user_5.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_6, "testObject_ClientClass_user_6.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_7, "testObject_ClientClass_user_7.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_8, "testObject_ClientClass_user_8.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_9, "testObject_ClientClass_user_9.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_10, "testObject_ClientClass_user_10.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_11, "testObject_ClientClass_user_11.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_12, "testObject_ClientClass_user_12.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_13, "testObject_ClientClass_user_13.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_14, "testObject_ClientClass_user_14.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_15, "testObject_ClientClass_user_15.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_16, "testObject_ClientClass_user_16.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_17, "testObject_ClientClass_user_17.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_18, "testObject_ClientClass_user_18.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_19, "testObject_ClientClass_user_19.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_20, "testObject_ClientClass_user_20.json")], testGroup "Golden: PubClient_user" $ testObjects [(Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_1, "testObject_PubClient_user_1.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_2, "testObject_PubClient_user_2.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_3, "testObject_PubClient_user_3.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_4, "testObject_PubClient_user_4.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_5, "testObject_PubClient_user_5.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_6, "testObject_PubClient_user_6.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_7, "testObject_PubClient_user_7.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_8, "testObject_PubClient_user_8.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_9, "testObject_PubClient_user_9.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_10, "testObject_PubClient_user_10.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_11, "testObject_PubClient_user_11.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_12, "testObject_PubClient_user_12.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_13, "testObject_PubClient_user_13.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_14, "testObject_PubClient_user_14.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_15, "testObject_PubClient_user_15.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_16, "testObject_PubClient_user_16.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_17, "testObject_PubClient_user_17.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_18, "testObject_PubClient_user_18.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_19, "testObject_PubClient_user_19.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_20, "testObject_PubClient_user_20.json")], - testGroup "Golden: ClientV5_user" $ - testObjects [(Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_1, "testObject_ClientV5_user_1.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_2, "testObject_ClientV5_user_2.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_3, "testObject_ClientV5_user_3.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_4, "testObject_ClientV5_user_4.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_5, "testObject_ClientV5_user_5.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_6, "testObject_ClientV5_user_6.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_7, "testObject_ClientV5_user_7.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_8, "testObject_ClientV5_user_8.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_9, "testObject_ClientV5_user_9.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_10, "testObject_ClientV5_user_10.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_11, "testObject_ClientV5_user_11.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_12, "testObject_ClientV5_user_12.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_13, "testObject_ClientV5_user_13.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_14, "testObject_ClientV5_user_14.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_15, "testObject_ClientV5_user_15.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_16, "testObject_ClientV5_user_16.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_17, "testObject_ClientV5_user_17.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_18, "testObject_ClientV5_user_18.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_19, "testObject_ClientV5_user_19.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_20, "testObject_ClientV5_user_20.json")], testGroup "Golden: Client_user" $ testObjects [(Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_1, "testObject_Client_user_1.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_2, "testObject_Client_user_2.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_3, "testObject_Client_user_3.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_4, "testObject_Client_user_4.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_5, "testObject_Client_user_5.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_6, "testObject_Client_user_6.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_7, "testObject_Client_user_7.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_8, "testObject_Client_user_8.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_9, "testObject_Client_user_9.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_10, "testObject_Client_user_10.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_11, "testObject_Client_user_11.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_12, "testObject_Client_user_12.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_13, "testObject_Client_user_13.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_14, "testObject_Client_user_14.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_15, "testObject_Client_user_15.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_16, "testObject_Client_user_16.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_17, "testObject_Client_user_17.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_18, "testObject_Client_user_18.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_19, "testObject_Client_user_19.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_20, "testObject_Client_user_20.json")], testGroup "Golden: NewClient_user" $ diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_1.json b/libs/wire-api/test/golden/testObject_ClientV5_user_1.json deleted file mode 100644 index 9fc8b644e4a..00000000000 --- a/libs/wire-api/test/golden/testObject_ClientV5_user_1.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "capabilities": { - "capabilities": [] - }, - "class": "desktop", - "id": "2", - "label": "%*", - "mls_public_keys": {}, - "model": "󳇚;􇻫", - "time": "1864-05-06T19:39:12.770Z", - "type": "permanent" -} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_10.json b/libs/wire-api/test/golden/testObject_ClientV5_user_10.json deleted file mode 100644 index 1d08a33cfd0..00000000000 --- a/libs/wire-api/test/golden/testObject_ClientV5_user_10.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "capabilities": { - "capabilities": [] - }, - "cookie": "L", - "id": "0", - "mls_public_keys": { - "ed25519": "Wm1GclpTQndkV0pzYVdNZ2EyVjU=" - }, - "model": "\u0018", - "time": "1864-05-10T18:42:04.137Z", - "type": "permanent" -} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_11.json b/libs/wire-api/test/golden/testObject_ClientV5_user_11.json deleted file mode 100644 index 6e4c38b8dc9..00000000000 --- a/libs/wire-api/test/golden/testObject_ClientV5_user_11.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "capabilities": { - "capabilities": [] - }, - "class": "legalhold", - "cookie": "5", - "id": "3", - "label": "\u001fb", - "mls_public_keys": {}, - "model": "ML", - "time": "1864-05-08T11:57:08.087Z", - "type": "temporary" -} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_12.json b/libs/wire-api/test/golden/testObject_ClientV5_user_12.json deleted file mode 100644 index 644db85ecbf..00000000000 --- a/libs/wire-api/test/golden/testObject_ClientV5_user_12.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "capabilities": { - "capabilities": [] - }, - "cookie": "0", - "id": "2", - "label": "", - "mls_public_keys": {}, - "model": "", - "time": "1864-05-08T18:44:00.378Z", - "type": "permanent" -} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_13.json b/libs/wire-api/test/golden/testObject_ClientV5_user_13.json deleted file mode 100644 index 9034bcbc4ab..00000000000 --- a/libs/wire-api/test/golden/testObject_ClientV5_user_13.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "capabilities": { - "capabilities": [] - }, - "class": "phone", - "cookie": "\u000c^󷋏", - "id": "2", - "label": "􃱽", - "mls_public_keys": {}, - "model": "\u0017𐲤", - "time": "1864-05-07T01:09:04.597Z", - "type": "permanent" -} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_14.json b/libs/wire-api/test/golden/testObject_ClientV5_user_14.json deleted file mode 100644 index a4d61fe168c..00000000000 --- a/libs/wire-api/test/golden/testObject_ClientV5_user_14.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "capabilities": { - "capabilities": [] - }, - "class": "tablet", - "id": "2", - "label": "x\u000e", - "mls_public_keys": {}, - "model": "􀸏\r󠁨", - "time": "1864-05-12T11:00:10.449Z", - "type": "temporary" -} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_15.json b/libs/wire-api/test/golden/testObject_ClientV5_user_15.json deleted file mode 100644 index 626f76201cd..00000000000 --- a/libs/wire-api/test/golden/testObject_ClientV5_user_15.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "capabilities": { - "capabilities": [] - }, - "cookie": "􌨷N", - "id": "3", - "label": "\u0004G", - "mls_public_keys": {}, - "model": "zAI", - "time": "1864-05-08T11:28:27.778Z", - "type": "temporary" -} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_16.json b/libs/wire-api/test/golden/testObject_ClientV5_user_16.json deleted file mode 100644 index 7216da58868..00000000000 --- a/libs/wire-api/test/golden/testObject_ClientV5_user_16.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "capabilities": { - "capabilities": [] - }, - "class": "legalhold", - "cookie": "U", - "id": "2", - "label": "=E", - "mls_public_keys": {}, - "model": "", - "time": "1864-05-12T11:31:10.072Z", - "type": "temporary" -} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_17.json b/libs/wire-api/test/golden/testObject_ClientV5_user_17.json deleted file mode 100644 index 9f0f36f96a3..00000000000 --- a/libs/wire-api/test/golden/testObject_ClientV5_user_17.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "capabilities": { - "capabilities": [] - }, - "class": "desktop", - "cookie": "", - "id": "4", - "mls_public_keys": {}, - "model": "", - "time": "1864-05-12T02:25:34.770Z", - "type": "temporary" -} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_18.json b/libs/wire-api/test/golden/testObject_ClientV5_user_18.json deleted file mode 100644 index 80dad343c4e..00000000000 --- a/libs/wire-api/test/golden/testObject_ClientV5_user_18.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "capabilities": { - "capabilities": [] - }, - "class": "legalhold", - "cookie": "PG:", - "id": "1", - "label": "󳔺", - "mls_public_keys": {}, - "model": "􅩹", - "time": "1864-05-07T17:21:05.930Z", - "type": "temporary" -} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_19.json b/libs/wire-api/test/golden/testObject_ClientV5_user_19.json deleted file mode 100644 index db061827756..00000000000 --- a/libs/wire-api/test/golden/testObject_ClientV5_user_19.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "capabilities": { - "capabilities": [] - }, - "class": "desktop", - "id": "2", - "label": "􌇰l", - "mls_public_keys": {}, - "model": "", - "time": "1864-05-12T07:49:27.999Z", - "type": "permanent" -} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_2.json b/libs/wire-api/test/golden/testObject_ClientV5_user_2.json deleted file mode 100644 index 08dd2786531..00000000000 --- a/libs/wire-api/test/golden/testObject_ClientV5_user_2.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "capabilities": { - "capabilities": [] - }, - "cookie": "􏬺c􄂩", - "id": "1", - "mls_public_keys": {}, - "time": "1864-05-07T08:48:22.537Z", - "type": "legalhold" -} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_20.json b/libs/wire-api/test/golden/testObject_ClientV5_user_20.json deleted file mode 100644 index 253cd8c3952..00000000000 --- a/libs/wire-api/test/golden/testObject_ClientV5_user_20.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "capabilities": { - "capabilities": [ - "legalhold-implicit-consent" - ] - }, - "class": "phone", - "cookie": "", - "id": "1", - "label": "-󼊣v", - "mls_public_keys": {}, - "time": "1864-05-06T18:43:52.483Z", - "type": "legalhold" -} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_3.json b/libs/wire-api/test/golden/testObject_ClientV5_user_3.json deleted file mode 100644 index 8c5026d2cb7..00000000000 --- a/libs/wire-api/test/golden/testObject_ClientV5_user_3.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "capabilities": { - "capabilities": [] - }, - "class": "legalhold", - "cookie": "", - "id": "1", - "label": "pi", - "last_active": "2023-07-04T09:35:32Z", - "mls_public_keys": {}, - "time": "1864-05-07T00:38:22.384Z", - "type": "temporary" -} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_4.json b/libs/wire-api/test/golden/testObject_ClientV5_user_4.json deleted file mode 100644 index 25e8c8860bd..00000000000 --- a/libs/wire-api/test/golden/testObject_ClientV5_user_4.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "capabilities": { - "capabilities": [] - }, - "class": "legalhold", - "cookie": "j", - "id": "3", - "mls_public_keys": {}, - "model": "", - "time": "1864-05-06T09:13:45.902Z", - "type": "permanent" -} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_5.json b/libs/wire-api/test/golden/testObject_ClientV5_user_5.json deleted file mode 100644 index 0af93523dc2..00000000000 --- a/libs/wire-api/test/golden/testObject_ClientV5_user_5.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "capabilities": { - "capabilities": [] - }, - "class": "desktop", - "cookie": "", - "id": "0", - "mls_public_keys": {}, - "model": "⌷o", - "time": "1864-05-07T09:07:14.559Z", - "type": "temporary" -} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_6.json b/libs/wire-api/test/golden/testObject_ClientV5_user_6.json deleted file mode 100644 index 90a2b0ea16e..00000000000 --- a/libs/wire-api/test/golden/testObject_ClientV5_user_6.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "capabilities": { - "capabilities": [] - }, - "class": "tablet", - "cookie": "l\u0002", - "id": "4", - "last_active": "2021-09-15T22:00:21Z", - "mls_public_keys": {}, - "model": "", - "time": "1864-05-08T22:37:53.030Z", - "type": "permanent" -} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_7.json b/libs/wire-api/test/golden/testObject_ClientV5_user_7.json deleted file mode 100644 index 41253b1fb0a..00000000000 --- a/libs/wire-api/test/golden/testObject_ClientV5_user_7.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "capabilities": { - "capabilities": [] - }, - "class": "phone", - "id": "4", - "label": "", - "mls_public_keys": {}, - "model": "", - "time": "1864-05-07T04:35:34.201Z", - "type": "permanent" -} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_8.json b/libs/wire-api/test/golden/testObject_ClientV5_user_8.json deleted file mode 100644 index fafbbc7e6e5..00000000000 --- a/libs/wire-api/test/golden/testObject_ClientV5_user_8.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "capabilities": { - "capabilities": [] - }, - "class": "phone", - "cookie": "\u0015p`", - "id": "4", - "label": "", - "mls_public_keys": {}, - "model": "􏽉", - "time": "1864-05-11T06:32:01.921Z", - "type": "legalhold" -} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_9.json b/libs/wire-api/test/golden/testObject_ClientV5_user_9.json deleted file mode 100644 index ed4e67747ca..00000000000 --- a/libs/wire-api/test/golden/testObject_ClientV5_user_9.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "capabilities": { - "capabilities": [] - }, - "class": "legalhold", - "cookie": "G", - "id": "1", - "label": "v", - "mls_public_keys": {}, - "model": "㌀m", - "time": "1864-05-08T03:54:56.526Z", - "type": "legalhold" -} diff --git a/libs/wire-api/test/golden/testObject_Client_user_1.json b/libs/wire-api/test/golden/testObject_Client_user_1.json index 3ae58f75402..9fc8b644e4a 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_1.json +++ b/libs/wire-api/test/golden/testObject_Client_user_1.json @@ -1,5 +1,7 @@ { - "capabilities": [], + "capabilities": { + "capabilities": [] + }, "class": "desktop", "id": "2", "label": "%*", diff --git a/libs/wire-api/test/golden/testObject_Client_user_10.json b/libs/wire-api/test/golden/testObject_Client_user_10.json index 35ad363f074..1d08a33cfd0 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_10.json +++ b/libs/wire-api/test/golden/testObject_Client_user_10.json @@ -1,5 +1,7 @@ { - "capabilities": [], + "capabilities": { + "capabilities": [] + }, "cookie": "L", "id": "0", "mls_public_keys": { diff --git a/libs/wire-api/test/golden/testObject_Client_user_11.json b/libs/wire-api/test/golden/testObject_Client_user_11.json index 8d6af47dc49..6e4c38b8dc9 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_11.json +++ b/libs/wire-api/test/golden/testObject_Client_user_11.json @@ -1,5 +1,7 @@ { - "capabilities": [], + "capabilities": { + "capabilities": [] + }, "class": "legalhold", "cookie": "5", "id": "3", diff --git a/libs/wire-api/test/golden/testObject_Client_user_12.json b/libs/wire-api/test/golden/testObject_Client_user_12.json index 63ca4553dee..644db85ecbf 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_12.json +++ b/libs/wire-api/test/golden/testObject_Client_user_12.json @@ -1,5 +1,7 @@ { - "capabilities": [], + "capabilities": { + "capabilities": [] + }, "cookie": "0", "id": "2", "label": "", diff --git a/libs/wire-api/test/golden/testObject_Client_user_13.json b/libs/wire-api/test/golden/testObject_Client_user_13.json index 9b2552d9086..9034bcbc4ab 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_13.json +++ b/libs/wire-api/test/golden/testObject_Client_user_13.json @@ -1,5 +1,7 @@ { - "capabilities": [], + "capabilities": { + "capabilities": [] + }, "class": "phone", "cookie": "\u000c^󷋏", "id": "2", diff --git a/libs/wire-api/test/golden/testObject_Client_user_14.json b/libs/wire-api/test/golden/testObject_Client_user_14.json index c95b927805a..a4d61fe168c 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_14.json +++ b/libs/wire-api/test/golden/testObject_Client_user_14.json @@ -1,5 +1,7 @@ { - "capabilities": [], + "capabilities": { + "capabilities": [] + }, "class": "tablet", "id": "2", "label": "x\u000e", diff --git a/libs/wire-api/test/golden/testObject_Client_user_15.json b/libs/wire-api/test/golden/testObject_Client_user_15.json index 7050d356278..626f76201cd 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_15.json +++ b/libs/wire-api/test/golden/testObject_Client_user_15.json @@ -1,5 +1,7 @@ { - "capabilities": [], + "capabilities": { + "capabilities": [] + }, "cookie": "􌨷N", "id": "3", "label": "\u0004G", diff --git a/libs/wire-api/test/golden/testObject_Client_user_16.json b/libs/wire-api/test/golden/testObject_Client_user_16.json index e70257998b5..7216da58868 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_16.json +++ b/libs/wire-api/test/golden/testObject_Client_user_16.json @@ -1,5 +1,7 @@ { - "capabilities": [], + "capabilities": { + "capabilities": [] + }, "class": "legalhold", "cookie": "U", "id": "2", diff --git a/libs/wire-api/test/golden/testObject_Client_user_17.json b/libs/wire-api/test/golden/testObject_Client_user_17.json index 485f822a3d2..9f0f36f96a3 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_17.json +++ b/libs/wire-api/test/golden/testObject_Client_user_17.json @@ -1,5 +1,7 @@ { - "capabilities": [], + "capabilities": { + "capabilities": [] + }, "class": "desktop", "cookie": "", "id": "4", diff --git a/libs/wire-api/test/golden/testObject_Client_user_18.json b/libs/wire-api/test/golden/testObject_Client_user_18.json index 5f1ba1bf5b8..80dad343c4e 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_18.json +++ b/libs/wire-api/test/golden/testObject_Client_user_18.json @@ -1,5 +1,7 @@ { - "capabilities": [], + "capabilities": { + "capabilities": [] + }, "class": "legalhold", "cookie": "PG:", "id": "1", diff --git a/libs/wire-api/test/golden/testObject_Client_user_19.json b/libs/wire-api/test/golden/testObject_Client_user_19.json index f6263f00203..db061827756 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_19.json +++ b/libs/wire-api/test/golden/testObject_Client_user_19.json @@ -1,5 +1,7 @@ { - "capabilities": [], + "capabilities": { + "capabilities": [] + }, "class": "desktop", "id": "2", "label": "􌇰l", diff --git a/libs/wire-api/test/golden/testObject_Client_user_2.json b/libs/wire-api/test/golden/testObject_Client_user_2.json index 802de9bd21f..08dd2786531 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_2.json +++ b/libs/wire-api/test/golden/testObject_Client_user_2.json @@ -1,5 +1,7 @@ { - "capabilities": [], + "capabilities": { + "capabilities": [] + }, "cookie": "􏬺c􄂩", "id": "1", "mls_public_keys": {}, diff --git a/libs/wire-api/test/golden/testObject_Client_user_20.json b/libs/wire-api/test/golden/testObject_Client_user_20.json index c9f3ae4459b..253cd8c3952 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_20.json +++ b/libs/wire-api/test/golden/testObject_Client_user_20.json @@ -1,7 +1,9 @@ { - "capabilities": [ - "legalhold-implicit-consent" - ], + "capabilities": { + "capabilities": [ + "legalhold-implicit-consent" + ] + }, "class": "phone", "cookie": "", "id": "1", diff --git a/libs/wire-api/test/golden/testObject_Client_user_3.json b/libs/wire-api/test/golden/testObject_Client_user_3.json index b6cb51e0fbf..8c5026d2cb7 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_3.json +++ b/libs/wire-api/test/golden/testObject_Client_user_3.json @@ -1,5 +1,7 @@ { - "capabilities": [], + "capabilities": { + "capabilities": [] + }, "class": "legalhold", "cookie": "", "id": "1", diff --git a/libs/wire-api/test/golden/testObject_Client_user_4.json b/libs/wire-api/test/golden/testObject_Client_user_4.json index 4a8398a2e9b..25e8c8860bd 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_4.json +++ b/libs/wire-api/test/golden/testObject_Client_user_4.json @@ -1,5 +1,7 @@ { - "capabilities": [], + "capabilities": { + "capabilities": [] + }, "class": "legalhold", "cookie": "j", "id": "3", diff --git a/libs/wire-api/test/golden/testObject_Client_user_5.json b/libs/wire-api/test/golden/testObject_Client_user_5.json index e1967bb1bcf..0af93523dc2 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_5.json +++ b/libs/wire-api/test/golden/testObject_Client_user_5.json @@ -1,5 +1,7 @@ { - "capabilities": [], + "capabilities": { + "capabilities": [] + }, "class": "desktop", "cookie": "", "id": "0", diff --git a/libs/wire-api/test/golden/testObject_Client_user_6.json b/libs/wire-api/test/golden/testObject_Client_user_6.json index 929f3132496..90a2b0ea16e 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_6.json +++ b/libs/wire-api/test/golden/testObject_Client_user_6.json @@ -1,5 +1,7 @@ { - "capabilities": [], + "capabilities": { + "capabilities": [] + }, "class": "tablet", "cookie": "l\u0002", "id": "4", diff --git a/libs/wire-api/test/golden/testObject_Client_user_7.json b/libs/wire-api/test/golden/testObject_Client_user_7.json index 8ca4dc49b6a..41253b1fb0a 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_7.json +++ b/libs/wire-api/test/golden/testObject_Client_user_7.json @@ -1,5 +1,7 @@ { - "capabilities": [], + "capabilities": { + "capabilities": [] + }, "class": "phone", "id": "4", "label": "", diff --git a/libs/wire-api/test/golden/testObject_Client_user_8.json b/libs/wire-api/test/golden/testObject_Client_user_8.json index 35f568dd53c..fafbbc7e6e5 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_8.json +++ b/libs/wire-api/test/golden/testObject_Client_user_8.json @@ -1,5 +1,7 @@ { - "capabilities": [], + "capabilities": { + "capabilities": [] + }, "class": "phone", "cookie": "\u0015p`", "id": "4", diff --git a/libs/wire-api/test/golden/testObject_Client_user_9.json b/libs/wire-api/test/golden/testObject_Client_user_9.json index cfda4f2768a..ed4e67747ca 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_9.json +++ b/libs/wire-api/test/golden/testObject_Client_user_9.json @@ -1,5 +1,7 @@ { - "capabilities": [], + "capabilities": { + "capabilities": [] + }, "class": "legalhold", "cookie": "G", "id": "1", diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 8776b0545c7..0630ee43602 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -369,13 +369,10 @@ servantSitemap = userClientAPI :: ServerT UserClientAPI (Handler r) userClientAPI = - Named @"add-client-v5" (callsFed (exposeAnnotations addClient)) - :<|> Named @"add-client" (callsFed (exposeAnnotations addClient)) + Named @"add-client" (callsFed (exposeAnnotations addClient)) :<|> Named @"update-client" updateClient :<|> Named @"delete-client" deleteClient - :<|> Named @"list-clients-v5" listClients :<|> Named @"list-clients" listClients - :<|> Named @"get-client-v5" getClient :<|> Named @"get-client" getClient :<|> Named @"get-client-capabilities" getClientCapabilities :<|> Named @"get-client-prekeys" getClientPrekeys @@ -581,13 +578,17 @@ addClient :: UserId -> ConnId -> Public.NewClient -> - Handler r Public.Client + (Handler r) NewClientResponse addClient usr con new = do -- Users can't add legal hold clients when (Public.newClientType new == Public.LegalHoldClientType) $ throwE (clientError ClientLegalHoldCannotBeAdded) - API.addClient usr (Just con) new - !>> clientError + clientResponse + <$> API.addClient usr (Just con) new + !>> clientError + where + clientResponse :: Public.Client -> NewClientResponse + clientResponse client = Servant.addHeader (Public.clientId client) client deleteClient :: UserId -> ConnId -> ClientId -> Public.RmClient -> (Handler r) () deleteClient usr con clt body = diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 0bdc32745e1..0bc3963beeb 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -142,7 +142,6 @@ botAPI = :<|> Named @"bot-delete-self" botDeleteSelf :<|> Named @"bot-list-prekeys" botListPrekeys :<|> Named @"bot-update-prekeys" botUpdatePrekeys - :<|> Named @"bot-get-client-v5" botGetClient :<|> Named @"bot-get-client" botGetClient :<|> Named @"bot-claim-users-prekeys" botClaimUsersPrekeys :<|> Named @"bot-list-users" botListUserProfiles diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index b74e58081c2..553a773366f 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -26,7 +26,7 @@ import Brig.API (sitemap) import Brig.API.Federation import Brig.API.Handler import Brig.API.Internal qualified as IAPI -import Brig.API.Public +import Brig.API.Public (DocsAPI, docsAPI, servantSitemap) import Brig.API.User qualified as API import Brig.AWS (amazonkaEnv, sesQueue) import Brig.AWS qualified as AWS From 93d61d7c5adf47b4ce591ca547eab8fb83bb48b7 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Wed, 28 Feb 2024 15:42:02 +0100 Subject: [PATCH 017/117] Deploy the released mandarin version in kind dev environment (#3898) * Upgrade kind to latest version (unpin kind) * Deploy cert-manager and cert for federation in kind * Deploy mandarin version to wire-federation-v0 namespace in kind * Less confusing encoding-decoding of federation ca certificates in test setup --------- Co-authored-by: Akshay Mankar --- Makefile | 1 + changelog.d/5-internal/v0-integration-setup | 2 +- hack/bin/integration-setup-federation.sh | 2 +- hack/helm_vars/common.yaml.gotmpl | 2 +- .../nginx-ingress-services/values.yaml.gotmpl | 2 +- .../wire-federation-v0/values.yaml.gotmpl | 306 ++++++++++++++++++ hack/helm_vars/wire-server/values.yaml.gotmpl | 2 +- hack/helmfile-federation-v0.yaml | 110 +++++++ nix/overlay.nix | 13 - 9 files changed, 422 insertions(+), 18 deletions(-) create mode 100644 hack/helm_vars/wire-federation-v0/values.yaml.gotmpl create mode 100644 hack/helmfile-federation-v0.yaml diff --git a/Makefile b/Makefile index 9656c7e9d17..17e11e3aec0 100644 --- a/Makefile +++ b/Makefile @@ -512,6 +512,7 @@ guard-inotify: .PHONY: kind-integration-setup kind-integration-setup: guard-inotify .local/kind-kubeconfig + KUBECONFIG=$(CURDIR)/.local/kind-kubeconfig helmfile sync -f $(CURDIR)/hack/helmfile-federation-v0.yaml HELMFILE_ENV="kind" KUBECONFIG=$(CURDIR)/.local/kind-kubeconfig make kube-integration-setup .PHONY: kind-integration-test diff --git a/changelog.d/5-internal/v0-integration-setup b/changelog.d/5-internal/v0-integration-setup index a25f4d3a6c0..b38bc03fe24 100644 --- a/changelog.d/5-internal/v0-integration-setup +++ b/changelog.d/5-internal/v0-integration-setup @@ -1,3 +1,3 @@ Setup federation-v0 environment for use in integration tests: - add federation-v0 domain to test environment - - provision integration certificates with cert-manager + - provision integration certificates with cert-manager (#3849, #3898) diff --git a/hack/bin/integration-setup-federation.sh b/hack/bin/integration-setup-federation.sh index 95261e8cccc..b0abffc8184 100755 --- a/hack/bin/integration-setup-federation.sh +++ b/hack/bin/integration-setup-federation.sh @@ -44,7 +44,7 @@ export FEDERATION_DOMAIN_BASE_2="$NAMESPACE_2.svc.cluster.local" export FEDERATION_DOMAIN_2="federation-test-helper.$FEDERATION_DOMAIN_BASE_2" echo "Fetch federation-ca secret from cert-manager namespace" -FEDERATION_CA_CERTIFICATE=$(kubectl -n cert-manager get secrets federation-ca -o json -o jsonpath="{.data['tls\.crt']}") +FEDERATION_CA_CERTIFICATE=$(kubectl -n cert-manager get secrets federation-ca -o json -o jsonpath="{.data['tls\.crt']}" | base64 -d) export FEDERATION_CA_CERTIFICATE echo "Installing charts..." diff --git a/hack/helm_vars/common.yaml.gotmpl b/hack/helm_vars/common.yaml.gotmpl index 1e4b9b4d06d..4b296fb8fc4 100644 --- a/hack/helm_vars/common.yaml.gotmpl +++ b/hack/helm_vars/common.yaml.gotmpl @@ -4,7 +4,7 @@ federationDomainBase1: {{ requiredEnv "FEDERATION_DOMAIN_BASE_1" }} namespace2: {{ requiredEnv "NAMESPACE_2" }} federationDomain2: {{ requiredEnv "FEDERATION_DOMAIN_2" }} federationDomainBase2: {{ requiredEnv "FEDERATION_DOMAIN_BASE_2" }} -federationCACertificate: {{ requiredEnv "FEDERATION_CA_CERTIFICATE" }} +federationCACertificate: {{ requiredEnv "FEDERATION_CA_CERTIFICATE" | quote }} ingressChart: {{ requiredEnv "INGRESS_CHART" }} rabbitmqUsername: guest rabbitmqPassword: guest diff --git a/hack/helm_vars/nginx-ingress-services/values.yaml.gotmpl b/hack/helm_vars/nginx-ingress-services/values.yaml.gotmpl index 10ca09507ef..9cc214d779b 100644 --- a/hack/helm_vars/nginx-ingress-services/values.yaml.gotmpl +++ b/hack/helm_vars/nginx-ingress-services/values.yaml.gotmpl @@ -26,4 +26,4 @@ config: # certificateDomain: dynamically set by hack/helmfile.yaml secrets: - tlsClientCA: {{ .Values.federationCACertificate }} + tlsClientCA: {{ .Values.federationCACertificate | quote }} diff --git a/hack/helm_vars/wire-federation-v0/values.yaml.gotmpl b/hack/helm_vars/wire-federation-v0/values.yaml.gotmpl new file mode 100644 index 00000000000..c012a3b19f1 --- /dev/null +++ b/hack/helm_vars/wire-federation-v0/values.yaml.gotmpl @@ -0,0 +1,306 @@ +tags: + nginz: true + brig: true + galley: true + gundeck: true + cannon: true + cargohold: true + spar: true + federation: true # also see galley.config.enableFederation and brig.config.enableFederation + backoffice: true + proxy: false + webapp: false + team-settings: false + account-pages: false + legalhold: false + sftd: false + +cassandra-migrations: + cassandra: + host: cassandra-ephemeral + replicationFactor: 1 +elasticsearch-index: + elasticsearch: + host: elasticsearch-ephemeral + index: directory_test + cassandra: + host: cassandra-ephemeral + +brig: + replicaCount: 1 + resources: + requests: {} + limits: + memory: 512Mi + config: + externalUrls: + nginz: https://kube-staging-nginz-https.zinfra.io + teamCreatorWelcome: https://teams.wire.com/login + teamMemberWelcome: https://wire.com/download + cassandra: + host: cassandra-ephemeral + replicaCount: 1 + elasticsearch: + host: elasticsearch-ephemeral + index: directory_test + authSettings: + userTokenTimeout: 120 + sessionTokenTimeout: 20 + accessTokenTimeout: 30 + providerTokenTimeout: 60 + enableFederation: true # keep in sync with galley.config.enableFederation, cargohold.config.enableFederation and tags.federator! + optSettings: + setActivationTimeout: 10 + setVerificationTimeout: 10 + # keep this in sync with brigSettingsTeamInvitationTimeout in spar/templates/tests/configmap.yaml + setTeamInvitationTimeout: 10 + setExpiredUserCleanupTimeout: 1 + setUserMaxConnections: 16 + setCookieInsecure: true + setUserCookieRenewAge: 2 + setUserCookieLimit: 5 + setUserCookieThrottle: + stdDev: 5 + retryAfter: 5 + setLimitFailedLogins: + timeout: 5 # seconds. if you reach the limit, how long do you have to wait to try again. + retryLimit: 5 # how many times can you have a failed login in that timeframe. + setSuspendInactiveUsers: + suspendTimeout: 10 + setDefaultTemplateLocale: en + setDefaultUserLocale: en + setMaxConvAndTeamSize: 16 + setMaxTeamSize: 32 + setMaxConvSize: 16 + setFederationDomain: federation-test-helper.wire-federation-v0.svc.cluster.local + setFederationStrategy: allowAll + setFederationDomainConfigsUpdateFreq: 10 + set2FACodeGenerationDelaySecs: 5 + setNonceTtlSecs: 300 + setDpopMaxSkewSecs: 1 + setDpopTokenExpirationTimeSecs: 300 + setEnableMLS: true + setOAuthAuthCodeExpirationTimeSecs: 3 # 3 secs + setOAuthAccessTokenExpirationTimeSecs: 3 # 3 secs + setOAuthEnabled: true + setOAuthRefreshTokenExpirationTimeSecs: 14515200 # 24 weeks + setOAuthMaxActiveRefreshTokens: 10 + aws: + sesEndpoint: http://fake-aws-ses:4569 + sqsEndpoint: http://fake-aws-sqs:4568 + dynamoDBEndpoint: http://fake-aws-dynamodb:4567 + sesQueue: integration-brig-events + internalQueue: integration-brig-events-internal + prekeyTable: integration-brig-prekeys + emailSMS: + general: + emailSender: backend-integrationk8s@wire.com + smsSender: dummy + secrets: + # these secrets are only used during integration tests and should therefore be safe to include unencrypted in git. + # Normally these would live in a separately-encrypted secrets.yaml file and incorporated using the helm secrets plugin (wrapper around mozilla sops) + zAuth: + privateKeys: 7owt9MgvLd3D1nQ5s5Zm-5kOiUZcJ_iqASOYdzLUpjHRRbfyx7XJ6hzltU0S9_kvKsdYZmTK9wZNWKUraB4Z1Q== + publicKeys: 0UW38se1yeoc5bVNEvf5LyrHWGZkyvcGTVilK2geGdU= + turn: + secret: rPrUbws7PQZlfN2GG8Ggi7g5iOYPk7BiCoKHl3VoFZ + awsKeyId: dummykey + awsSecretKey: dummysecret + setTwilio: | + sid: "dummy" + token: "dummy" + setNexmo: |- + key: "dummy" + secret: "dummy" + smtpPassword: dummy-smtp-password + dpopSigKeyBundle: | + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIFANnxZLNE4p+GDzWzR3wm/v8x/0bxZYkCyke1aTRucX + -----END PRIVATE KEY----- + -----BEGIN PUBLIC KEY----- + MCowBQYDK2VwAyEACPvhIdimF20tOPjbb+fXJrwS2RKDp7686T90AZ0+Th8= + -----END PUBLIC KEY----- + oauthJwkKeyPair: | + { + "kty": "OKP", + "crv": "Ed25519", + "x": "mhP-NgFw3ifIXGZqJVB0kemt9L3BtD5P8q4Gah4Iklc", + "d": "R8-pV2-sPN7dykV8HFJ73S64F3kMHTNnJiSN8UdWk_o" + } + rabbitmq: + username: {{ .Values.rabbitmqUsername }} + password: {{ .Values.rabbitmqPassword }} + tests: + enableFederationTests: true +cannon: + replicaCount: 2 + resources: + requests: {} + limits: + memory: 512Mi + drainTimeout: 0 +cargohold: + replicaCount: 1 + resources: + requests: {} + limits: + memory: 512Mi + config: + aws: + s3Bucket: dummy-bucket + s3Endpoint: http://fake-aws-s3:9000 + enableFederation: true # keep in sync with brig.config.enableFederation, galley.config.enableFederation and tags.federator! + settings: + federationDomain: federation-test-helper.wire-federation-v0.svc.cluster.local + secrets: + awsKeyId: dummykey + awsSecretKey: dummysecret +galley: + replicaCount: 1 + config: + cassandra: + host: cassandra-ephemeral + replicaCount: 1 + enableFederation: true # keep in sync with brig.config.enableFederation, cargohold.config.enableFederation and tags.federator! + settings: + maxConvAndTeamSize: 16 + maxTeamSize: 32 + maxFanoutSize: 18 + maxConvSize: 16 + conversationCodeURI: https://kube-staging-nginz-https.zinfra.io/conversation-join/ + # See helmfile for the real value + federationDomain: federation-test-helper.wire-federation-v0.svc.cluster.local + featureFlags: + sso: disabled-by-default # this needs to be the default; tests can enable it when needed. + legalhold: whitelist-teams-and-implicit-consent + teamSearchVisibility: disabled-by-default + classifiedDomains: + status: enabled + config: + domains: ["example.com"] + journal: + endpoint: http://fake-aws-sqs:4568 + queueName: integration-team-events.fifo + secrets: + awsKeyId: dummykey + awsSecretKey: dummysecret + mlsPrivateKeys: + removal: + ed25519: | + -----BEGIN PRIVATE KEY----- + MC4CAQAwBQYDK2VwBCIEIAocCDXsKIAjb65gOUn5vEF0RIKnVJkKR4ebQzuZ709c + -----END PRIVATE KEY----- + rabbitmq: + username: {{ .Values.rabbitmqUsername }} + password: {{ .Values.rabbitmqPassword }} + +gundeck: + replicaCount: 1 + resources: + requests: {} + limits: + memory: 1024Mi + config: + cassandra: + host: cassandra-ephemeral + replicaCount: 1 + redis: + host: redis-ephemeral-master + connectionMode: master + aws: + account: "123456789012" + region: eu-west-1 + arnEnv: integration + queueName: integration-gundeck-events + sqsEndpoint: http://fake-aws-sqs:4568 + snsEndpoint: http://fake-aws-sns:4575 + bulkPush: true + setMaxConcurrentNativePushes: + hard: 30 + soft: 10 + secrets: + awsKeyId: dummykey + awsSecretKey: dummysecret +nginz: + replicaCount: 1 + nginx_conf: + env: staging + external_env_domain: zinfra.io + # NOTE: Web apps are disabled by default + allowlisted_origins: [] + randomport_allowlisted_origins: [] # default is empty by intention + rate_limit_reqs_per_user: "10r/s" + rate_limit_reqs_per_addr: "100r/s" + secrets: + basicAuth: "whatever" + zAuth: + # this must match the key in brig! + publicKeys: 0UW38se1yeoc5bVNEvf5LyrHWGZkyvcGTVilK2geGdU= + oAuth: + publicKeys: | + { + "kty": "OKP", + "crv": "Ed25519", + "x": "mhP-NgFw3ifIXGZqJVB0kemt9L3BtD5P8q4Gah4Iklc" + } +proxy: + replicaCount: 1 + secrets: + proxy_config: |- + secrets { + youtube = "..." + googlemaps = "..." + soundcloud = "..." + giphy = "..." + spotify = "Basic ..." + } +spar: + replicaCount: 1 + resources: + requests: {} + limits: + memory: 1024Mi + config: + tlsDisableCertValidation: true + cassandra: + host: cassandra-ephemeral + logLevel: Debug + domain: zinfra.io + appUri: http://spar:8080/ + ssoUri: http://spar:8080/sso + maxttlAuthreq: 5 + maxttlAuthresp: 7200 + maxScimTokens: 2 + contacts: + - type: ContactSupport + company: Example Company + email: email:backend+spar@wire.com + +federator: + replicaCount: 1 + resources: + requests: {} + config: + optSettings: + useSystemCAStore: false + remoteCAContents: {{ .Values.federationCACertificate | quote }} + tls: + useCertManager: true + useSharedFederatorSecret: true + +background-worker: + replicaCount: 1 + resources: + requests: {} + config: + backendNotificationPusher: + pushBackoffMinWait: 1000 # 1ms + pushBackoffMaxWait: 500000 # 0.5s + secrets: + rabbitmq: + username: {{ .Values.rabbitmqUsername }} + password: {{ .Values.rabbitmqPassword }} + +integration: + ingress: + class: "nginx-{{ .Release.Namespace }}" diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 0d1ffba6b87..437159b7263 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -393,7 +393,7 @@ federator: resources: requests: {} imagePullPolicy: {{ .Values.imagePullPolicy }} - remoteCAContents: {{ .Values.federationCACertificate | b64dec | quote }} + remoteCAContents: {{ .Values.federationCACertificate | quote }} tls: useCertManager: true useSharedFederatorSecret: true diff --git a/hack/helmfile-federation-v0.yaml b/hack/helmfile-federation-v0.yaml new file mode 100644 index 00000000000..5400307d84b --- /dev/null +++ b/hack/helmfile-federation-v0.yaml @@ -0,0 +1,110 @@ +--- +helmDefaults: + wait: true + timeout: 600 + devel: true + createNamespace: true + +environments: + default: + values: + - federationCACertificate: {{ readFile "../services/nginz/integration-test/conf/nginz/integration-ca.pem" | quote }} + - rabbitmqUsername: guest + - rabbitmqPassword: guest +--- +repositories: + - name: jetstack + url: 'https://charts.jetstack.io' + + - name: bedag + url: 'https://bedag.github.io/helm-charts/' + + - name: wire + url: 'https://s3-eu-west-1.amazonaws.com/public.wire.com/charts-develop' + +releases: + - name: 'cert-manager' + namespace: cert-manager + chart: jetstack/cert-manager + set: + - name: installCRDs + value: true + + - name: 'federation-certs' + namespace: cert-manager + chart: bedag/raw + values: + - resources: + - apiVersion: v1 + kind: Secret + metadata: + name: federation-ca + namespace: cert-manager + data: + tls.crt: {{ readFile "../services/nginz/integration-test/conf/nginz/integration-ca.pem" | b64enc | quote }} + tls.key: {{ readFile "../services/nginz/integration-test/conf/nginz/integration-ca-key.pem" | b64enc | quote }} + - apiVersion: cert-manager.io/v1 + kind: ClusterIssuer + metadata: + name: federation + spec: + ca: + secretName: federation-ca + needs: + - 'cert-manager/cert-manager' + + - name: 'fake-aws' + namespace: wire-federation-v0 + chart: wire/fake-aws + version: 4.38.0-mandarin.14 + values: + - './helm_vars/fake-aws/values.yaml' + + - name: 'databases-ephemeral' + namespace: wire-federation-v0 + chart: 'wire/databases-ephemeral' + version: 4.38.0-mandarin.14 + + - name: 'rabbitmq' + namespace: wire-federation-v0 + chart: 'wire/rabbitmq' + version: 4.38.0-mandarin.14 + values: + - './helm_vars/rabbitmq/values.yaml.gotmpl' + + - name: 'ingress' + namespace: wire-federation-v0 + chart: 'wire/ingress-nginx-controller' + version: 4.38.0-mandarin.14 + values: + - './helm_vars/ingress-nginx-controller/values.yaml.gotmpl' + + - name: 'ingress-svc' + namespace: wire-federation-v0 + chart: 'wire/nginx-ingress-services' + version: 4.38.0-mandarin.14 + values: + - './helm_vars/nginx-ingress-services/values.yaml.gotmpl' + set: + # Federation domain is also the SRV record created by the + # federation-test-helper service. Maybe we can find a way to make these + # differ, so we don't make any silly assumptions in the code. + - name: config.dns.federator + value: wire-federation-v0.svc.cluster.local + - name: config.dns.certificateDomain + value: '*.wire-federation-v0.svc.cluster.local' + needs: + - 'ingress' + - 'cert-manager/cert-manager' + - 'cert-manager/federation-certs' + + - name: wire-server + namespace: wire-federation-v0 + chart: wire/wire-server + version: 4.38.0-mandarin.14 + values: + - './helm_vars/wire-federation-v0/values.yaml.gotmpl' + needs: + - 'cert-manager/cert-manager' + - 'cert-manager/federation-certs' + diff --git a/nix/overlay.nix b/nix/overlay.nix index 20aa6eb7995..a6390ab1d38 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -102,18 +102,5 @@ self: super: { inherit (super) stdenv fetchurl; }; - kind = staticBinary { - pname = "kind"; - version = "0.11.0"; - - darwinAmd64Url = "https://github.com/kubernetes-sigs/kind/releases/download/v0.11.1/kind-darwin-amd64"; - darwinAmd64Sha256 = "432bef555a70e9360b44661c759658265b9eaaf7f75f1beec4c4d1e6bbf97ce3"; - - linuxAmd64Url = "https://github.com/kubernetes-sigs/kind/releases/download/v0.11.1/kind-linux-amd64"; - linuxAmd64Sha256 = "949f81b3c30ca03a3d4effdecda04f100fa3edc07a28b19400f72ede7c5f0491"; - - inherit (super) stdenv fetchurl; - }; - rabbitmqadmin = super.callPackage ./pkgs/rabbitmqadmin { }; } From 9c31d457afa7c1f1ee7a4b0c99ccadfd12d406fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Thu, 29 Feb 2024 10:02:05 +0100 Subject: [PATCH 018/117] [WPB-6144] Don't remove MLS clients from a 1-1 conversation (#3906) * Don't remove MLS clients from a 1-1 conversation * Update the changelog --- changelog.d/3-bug-fixes/wpb-6144-messaging-blocked-user | 2 +- services/galley/src/Galley/API/One2One.hs | 6 ------ .../galley/src/Galley/Cassandra/Conversation/Members.hs | 6 ------ services/galley/src/Galley/Cassandra/Queries.hs | 3 --- services/galley/src/Galley/Effects/MemberStore.hs | 2 -- 5 files changed, 1 insertion(+), 18 deletions(-) diff --git a/changelog.d/3-bug-fixes/wpb-6144-messaging-blocked-user b/changelog.d/3-bug-fixes/wpb-6144-messaging-blocked-user index 44b986f57ed..70d315f9e42 100644 --- a/changelog.d/3-bug-fixes/wpb-6144-messaging-blocked-user +++ b/changelog.d/3-bug-fixes/wpb-6144-messaging-blocked-user @@ -1 +1 @@ -Do not deliver MLS one-to-one conversation messages to a user that blocked the sender +Do not deliver MLS one-to-one conversation messages to a user that blocked the sender (#3889, #3906) diff --git a/services/galley/src/Galley/API/One2One.hs b/services/galley/src/Galley/API/One2One.hs index 031a4dd81d3..05b1d16b9c4 100644 --- a/services/galley/src/Galley/API/One2One.hs +++ b/services/galley/src/Galley/API/One2One.hs @@ -35,7 +35,6 @@ import Galley.Types.UserList import Imports import Polysemy import Wire.API.Conversation hiding (Member) -import Wire.API.Conversation.Protocol import Wire.API.Routes.Internal.Galley.ConversationsIntra import Wire.API.User @@ -86,11 +85,6 @@ iUpsertOne2OneConversation UpsertOne2OneConversationRequest {..} = do deleteMembers (tUnqualified lconvId) (UserList [tUnqualified uooLocalUser] []) - let mGroupId = case convProtocol conv of - ProtocolProteus -> Nothing - ProtocolMLS meta -> Just . cnvmlsGroupId $ meta - ProtocolMixed meta -> Just . cnvmlsGroupId $ meta - for_ mGroupId $ flip removeAllMLSClientsOfUser (tUntagged uooLocalUser) (RemoteActor, Included) -> do void $ createMembers (tUnqualified lconvId) (UserList [] [uooRemoteUser]) unless (null (convLocalMembers conv)) $ diff --git a/services/galley/src/Galley/Cassandra/Conversation/Members.hs b/services/galley/src/Galley/Cassandra/Conversation/Members.hs index f4a043cd11e..abd3a0139e6 100644 --- a/services/galley/src/Galley/Cassandra/Conversation/Members.hs +++ b/services/galley/src/Galley/Cassandra/Conversation/Members.hs @@ -384,11 +384,6 @@ removeMLSClients groupId (Qualified usr domain) cs = retry x5 . batch $ do for_ cs $ \c -> addPrepQuery Cql.removeMLSClient (groupId, domain, usr, c) -removeAllMLSClientsOfUser :: GroupId -> Qualified UserId -> Client () -removeAllMLSClientsOfUser groupId (Qualified usr domain) = - retry x5 $ - write Cql.removeAllMLSClientsOfUser (params LocalQuorum (groupId, domain, usr)) - removeAllMLSClients :: GroupId -> Client () removeAllMLSClients groupId = do retry x5 $ write Cql.removeAllMLSClients (params LocalQuorum (Identity groupId)) @@ -421,7 +416,6 @@ interpretMemberStoreToCassandra = interpret $ \case AddMLSClients lcnv quid cs -> embedClient $ addMLSClients lcnv quid cs PlanClientRemoval lcnv cids -> embedClient $ planMLSClientRemoval lcnv cids RemoveMLSClients lcnv quid cs -> embedClient $ removeMLSClients lcnv quid cs - RemoveAllMLSClientsOfUser lcnv quid -> embedClient $ removeAllMLSClientsOfUser lcnv quid RemoveAllMLSClients gid -> embedClient $ removeAllMLSClients gid LookupMLSClients lcnv -> embedClient $ lookupMLSClients lcnv LookupMLSClientLeafIndices lcnv -> embedClient $ lookupMLSClientLeafIndices lcnv diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index df52a52571b..560d8d9a19f 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -493,9 +493,6 @@ planMLSClientRemoval = "update mls_group_member_client set removal_pending = tru removeMLSClient :: PrepQuery W (GroupId, Domain, UserId, ClientId) () removeMLSClient = "delete from mls_group_member_client where group_id = ? and user_domain = ? and user = ? and client = ?" -removeAllMLSClientsOfUser :: PrepQuery W (GroupId, Domain, UserId) () -removeAllMLSClientsOfUser = "delete from mls_group_member_client where group_id = ? and user_domain = ? and user = ?" - removeAllMLSClients :: PrepQuery W (Identity GroupId) () removeAllMLSClients = "DELETE FROM mls_group_member_client WHERE group_id = ?" diff --git a/services/galley/src/Galley/Effects/MemberStore.hs b/services/galley/src/Galley/Effects/MemberStore.hs index 56cd4fe9740..0513cc6570e 100644 --- a/services/galley/src/Galley/Effects/MemberStore.hs +++ b/services/galley/src/Galley/Effects/MemberStore.hs @@ -44,7 +44,6 @@ module Galley.Effects.MemberStore addMLSClients, planClientRemoval, removeMLSClients, - removeAllMLSClientsOfUser, removeAllMLSClients, lookupMLSClients, lookupMLSClientLeafIndices, @@ -89,7 +88,6 @@ data MemberStore m a where AddMLSClients :: GroupId -> Qualified UserId -> Set (ClientId, LeafIndex) -> MemberStore m () PlanClientRemoval :: Foldable f => GroupId -> f ClientIdentity -> MemberStore m () RemoveMLSClients :: GroupId -> Qualified UserId -> Set ClientId -> MemberStore m () - RemoveAllMLSClientsOfUser :: GroupId -> Qualified UserId -> MemberStore m () RemoveAllMLSClients :: GroupId -> MemberStore m () LookupMLSClients :: GroupId -> MemberStore m ClientMap LookupMLSClientLeafIndices :: GroupId -> MemberStore m (ClientMap, IndexMap) From d0b4321be59d3d37066a38fb36cd6f6e874a48c8 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Thu, 29 Feb 2024 11:50:42 +0100 Subject: [PATCH 019/117] hack/ingress-nginx-controller/values: Set ingressClassResource.controllerValue (#3910) This is required for the controller to only watch this class --- hack/helm_vars/ingress-nginx-controller/values.yaml.gotmpl | 1 + 1 file changed, 1 insertion(+) diff --git a/hack/helm_vars/ingress-nginx-controller/values.yaml.gotmpl b/hack/helm_vars/ingress-nginx-controller/values.yaml.gotmpl index dce7f5d0ab0..c137f045884 100644 --- a/hack/helm_vars/ingress-nginx-controller/values.yaml.gotmpl +++ b/hack/helm_vars/ingress-nginx-controller/values.yaml.gotmpl @@ -5,6 +5,7 @@ ingress-nginx: name: "nginx-{{ .Release.Namespace }}" # -- Is this ingressClass enabled or not enabled: true + controllerValue: "k8s.io/{{ .Release.Namespace }}-nginx-ingress" ingressClass: "nginx-{{ .Release.Namespace }}" kind: Deployment replicaCount: 1 From aebaab3f5a861f9a1eed9758ba2a485dd7d3c0a7 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Date: Mon, 26 Feb 2024 15:15:29 +0100 Subject: [PATCH 020/117] Rethrow async exceptions --- integration/test/Testlib/Run.hs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index de574293eec..6446ec6d52d 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -39,7 +39,12 @@ runTest ge action = lowerCodensity $ do env <- mkEnv ge liftIO $ (Right <$> runAppWithEnv env action) - `E.catches` [ E.Handler -- AssertionFailure + `E.catches` [ E.Handler $ \(e :: SomeAsyncException) -> do + -- AsyncExceptions need rethrowing + -- to prevend the last handler from handling async exceptions. + -- This ensures things like UserInterrupt are properly handled. + E.throw e, + E.Handler -- AssertionFailure (fmap Left . printFailureDetails), E.Handler (fmap Left . printExceptionDetails) From 30deace127535edce5c28e1bdcd5f0c98bb4c339 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Date: Mon, 26 Feb 2024 15:52:48 +0100 Subject: [PATCH 021/117] Addresses actual issue --- integration/test/Testlib/Run.hs | 43 ++++++++++++++------------------- 1 file changed, 18 insertions(+), 25 deletions(-) diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index 6446ec6d52d..36e4b0a61b0 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -16,7 +16,6 @@ import Data.Functor import Data.List import Data.PEM import Data.Time.Clock -import Data.Traversable (for) import RunAllTests import System.Directory import System.Environment @@ -39,12 +38,7 @@ runTest ge action = lowerCodensity $ do env <- mkEnv ge liftIO $ (Right <$> runAppWithEnv env action) - `E.catches` [ E.Handler $ \(e :: SomeAsyncException) -> do - -- AsyncExceptions need rethrowing - -- to prevend the last handler from handling async exceptions. - -- This ensures things like UserInterrupt are properly handled. - E.throw e, - E.Handler -- AssertionFailure + `E.catches` [ E.Handler -- AssertionFailure (fmap Left . printFailureDetails), E.Handler (fmap Left . printExceptionDetails) @@ -147,24 +141,23 @@ runTests tests mXMLOutput cfg = do runCodensity (createGlobalEnv cfg) $ \genv -> withAsync displayOutput $ \displayThread -> do - report <- fmap mconcat $ for tests $ \(qname, _, _, action) -> do - do - (mErr, tm) <- withTime (runTest genv action) - case mErr of - Left err -> do - writeOutput $ - "----- " - <> qname - <> colored red " FAIL" - <> " (" - <> printTime tm - <> ") -----\n" - <> err - <> "\n" - pure (TestSuiteReport [TestCaseReport qname (TestFailure err) tm]) - Right _ -> do - writeOutput $ qname <> colored green " OK" <> " (" <> printTime tm <> ")" <> "\n" - pure (TestSuiteReport [TestCaseReport qname TestSuccess tm]) + report <- fmap mconcat $ forConcurrently tests $ \(qname, _, _, action) -> do + (mErr, tm) <- withTime (runTest genv action) + case mErr of + Left err -> do + writeOutput $ + "----- " + <> qname + <> colored red " FAIL" + <> " (" + <> printTime tm + <> ") -----\n" + <> err + <> "\n" + pure (TestSuiteReport [TestCaseReport qname (TestFailure err) tm]) + Right _ -> do + writeOutput $ qname <> colored green " OK" <> " (" <> printTime tm <> ")" <> "\n" + pure (TestSuiteReport [TestCaseReport qname TestSuccess tm]) writeChan output Nothing wait displayThread printReport report From 20c4b3e299f8e5b0b650b070a68b19eb434149c8 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Date: Thu, 29 Feb 2024 11:03:16 +0100 Subject: [PATCH 022/117] Limit concurrency to the proc capabilities --- integration/test/Testlib/Run.hs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index 36e4b0a61b0..c940958c0a2 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -38,7 +38,12 @@ runTest ge action = lowerCodensity $ do env <- mkEnv ge liftIO $ (Right <$> runAppWithEnv env action) - `E.catches` [ E.Handler -- AssertionFailure + `E.catches` [ E.Handler $ \(e :: SomeAsyncException) -> do + -- AsyncExceptions need rethrowing + -- to prevend the last handler from handling async exceptions. + -- This ensures things like UserInterrupt are properly handled. + E.throw e, + E.Handler -- AssertionFailure (fmap Left . printFailureDetails), E.Handler (fmap Left . printExceptionDetails) @@ -141,7 +146,7 @@ runTests tests mXMLOutput cfg = do runCodensity (createGlobalEnv cfg) $ \genv -> withAsync displayOutput $ \displayThread -> do - report <- fmap mconcat $ forConcurrently tests $ \(qname, _, _, action) -> do + report <- fmap mconcat $ pooledForConcurrently tests $ \(qname, _, _, action) -> do (mErr, tm) <- withTime (runTest genv action) case mErr of Left err -> do From 33e21f4cb63f987168bca325d276c2f7b6e12800 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Date: Thu, 29 Feb 2024 11:23:51 +0100 Subject: [PATCH 023/117] Fix typo --- integration/test/Testlib/Run.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index c940958c0a2..4fccd814099 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -40,7 +40,7 @@ runTest ge action = lowerCodensity $ do (Right <$> runAppWithEnv env action) `E.catches` [ E.Handler $ \(e :: SomeAsyncException) -> do -- AsyncExceptions need rethrowing - -- to prevend the last handler from handling async exceptions. + -- to prevent the last handler from handling async exceptions. -- This ensures things like UserInterrupt are properly handled. E.throw e, E.Handler -- AssertionFailure From 817f36996c720b2aba4d40ceff92b2bc99236616 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Date: Thu, 29 Feb 2024 13:47:07 +0100 Subject: [PATCH 024/117] Increased timeout for starting a service in integration tests. --- integration/test/Testlib/ModService.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integration/test/Testlib/ModService.hs b/integration/test/Testlib/ModService.hs index f4390d7286f..3620278f51b 100644 --- a/integration/test/Testlib/ModService.hs +++ b/integration/test/Testlib/ModService.hs @@ -386,7 +386,7 @@ retryRequestUntil :: HasCallStack => App Bool -> String -> App () retryRequestUntil reqAction err = do isUp <- retrying - (limitRetriesByCumulativeDelay (4 * 1000 * 1000) (fibonacciBackoff (200 * 1000))) + (limitRetriesByCumulativeDelay (7 * 1000 * 1000) (fibonacciBackoff (300 * 1000))) (\_ isUp -> pure (not isUp)) (const reqAction) unless isUp $ From c0a770d567c61cfb554f8427437aa301ebe51aa8 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Date: Thu, 29 Feb 2024 15:01:36 +0100 Subject: [PATCH 025/117] print cap --- integration/test/Testlib/Run.hs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index 4fccd814099..78ca8028acf 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -146,6 +146,8 @@ runTests tests mXMLOutput cfg = do runCodensity (createGlobalEnv cfg) $ \genv -> withAsync displayOutput $ \displayThread -> do + cap <- getNumCapabilities + print $ "Cap: " <> show cap report <- fmap mconcat $ pooledForConcurrently tests $ \(qname, _, _, action) -> do (mErr, tm) <- withTime (runTest genv action) case mErr of From 12b5e6f3668094cafc0286b4eddf8d7f7a69eefa Mon Sep 17 00:00:00 2001 From: Igor Ranieri Date: Thu, 29 Feb 2024 15:45:08 +0100 Subject: [PATCH 026/117] Reduce pool --- integration/test/Testlib/Run.hs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index 78ca8028acf..ea191cd94b3 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -147,8 +147,7 @@ runTests tests mXMLOutput cfg = do runCodensity (createGlobalEnv cfg) $ \genv -> withAsync displayOutput $ \displayThread -> do cap <- getNumCapabilities - print $ "Cap: " <> show cap - report <- fmap mconcat $ pooledForConcurrently tests $ \(qname, _, _, action) -> do + report <- fmap mconcat $ pooledForConcurrentlyN (min 6 cap) tests $ \(qname, _, _, action) -> do (mErr, tm) <- withTime (runTest genv action) case mErr of Left err -> do From 430dfb25a2aa34729d6840c9cc6a382fd38316cb Mon Sep 17 00:00:00 2001 From: Igor Ranieri Date: Thu, 29 Feb 2024 16:51:47 +0100 Subject: [PATCH 027/117] Serial testing test --- integration/test/Testlib/ModService.hs | 2 +- integration/test/Testlib/Run.hs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/integration/test/Testlib/ModService.hs b/integration/test/Testlib/ModService.hs index 3620278f51b..f4390d7286f 100644 --- a/integration/test/Testlib/ModService.hs +++ b/integration/test/Testlib/ModService.hs @@ -386,7 +386,7 @@ retryRequestUntil :: HasCallStack => App Bool -> String -> App () retryRequestUntil reqAction err = do isUp <- retrying - (limitRetriesByCumulativeDelay (7 * 1000 * 1000) (fibonacciBackoff (300 * 1000))) + (limitRetriesByCumulativeDelay (4 * 1000 * 1000) (fibonacciBackoff (200 * 1000))) (\_ isUp -> pure (not isUp)) (const reqAction) unless isUp $ diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index ea191cd94b3..b3275b0f7fd 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -16,6 +16,7 @@ import Data.Functor import Data.List import Data.PEM import Data.Time.Clock +import Data.Traversable (for) import RunAllTests import System.Directory import System.Environment @@ -146,8 +147,7 @@ runTests tests mXMLOutput cfg = do runCodensity (createGlobalEnv cfg) $ \genv -> withAsync displayOutput $ \displayThread -> do - cap <- getNumCapabilities - report <- fmap mconcat $ pooledForConcurrentlyN (min 6 cap) tests $ \(qname, _, _, action) -> do + report <- fmap mconcat $ for tests $ \(qname, _, _, action) -> do (mErr, tm) <- withTime (runTest genv action) case mErr of Left err -> do From 13b3bdbae4c736bfd13bab1089e53981d1ac84b0 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Mon, 4 Mar 2024 10:41:28 +0100 Subject: [PATCH 028/117] charts/background-worker: Fix name of the service monitor (#3913) * charts/background-worker: Fix name of the service monitor * changelog --- changelog.d/3-bug-fixes/bw-service-monitor | 1 + charts/background-worker/templates/servicemonitor.yaml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 changelog.d/3-bug-fixes/bw-service-monitor diff --git a/changelog.d/3-bug-fixes/bw-service-monitor b/changelog.d/3-bug-fixes/bw-service-monitor new file mode 100644 index 00000000000..95cd30bab17 --- /dev/null +++ b/changelog.d/3-bug-fixes/bw-service-monitor @@ -0,0 +1 @@ +charts/background-worker: Fix name of the service monitor \ No newline at end of file diff --git a/charts/background-worker/templates/servicemonitor.yaml b/charts/background-worker/templates/servicemonitor.yaml index dc9e0636107..14dd65b488e 100644 --- a/charts/background-worker/templates/servicemonitor.yaml +++ b/charts/background-worker/templates/servicemonitor.yaml @@ -2,9 +2,9 @@ apiVersion: monitoring.coreos.com/v1 kind: ServiceMonitor metadata: - name: brig + name: background-worker labels: - app: brig + app: background-worker chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} release: {{ .Release.Name }} heritage: {{ .Release.Service }} From b93a24fb92701c3ff3c50a8aeeaf73b0de3a4ba2 Mon Sep 17 00:00:00 2001 From: Mathias Staab <71255223+mastaab@users.noreply.github.com> Date: Mon, 4 Mar 2024 11:21:06 +0100 Subject: [PATCH 029/117] coturn cert-reloader sidecar config: process name should not contain the path (helm chart) (#3916) the process name should not contain the path --- changelog.d/3-bug-fixes/WPB-6567 | 1 + charts/coturn/templates/statefulset.yaml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/3-bug-fixes/WPB-6567 diff --git a/changelog.d/3-bug-fixes/WPB-6567 b/changelog.d/3-bug-fixes/WPB-6567 new file mode 100644 index 00000000000..abcc7935654 --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-6567 @@ -0,0 +1 @@ +coturn cert-reloader sidecar config: process name should not contain the path (helm chart) diff --git a/charts/coturn/templates/statefulset.yaml b/charts/coturn/templates/statefulset.yaml index 8fa0d5f0ede..d2b9c7ef9b7 100644 --- a/charts/coturn/templates/statefulset.yaml +++ b/charts/coturn/templates/statefulset.yaml @@ -186,7 +186,7 @@ spec: - name: CONFIG_DIR value: /secrets-tls - name: PROCESS_NAME - value: /usr/bin/turnserver + value: turnserver - name: RELOAD_SIGNAL value: SIGUSR2 volumeMounts: From 2435f4efc3c2b2b1cdd94f417dd75116fa58d922 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Date: Mon, 4 Mar 2024 11:02:19 +0100 Subject: [PATCH 030/117] Restore pooling --- integration/test/Testlib/Run.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index b3275b0f7fd..a9bf804513c 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -16,7 +16,6 @@ import Data.Functor import Data.List import Data.PEM import Data.Time.Clock -import Data.Traversable (for) import RunAllTests import System.Directory import System.Environment @@ -147,7 +146,8 @@ runTests tests mXMLOutput cfg = do runCodensity (createGlobalEnv cfg) $ \genv -> withAsync displayOutput $ \displayThread -> do - report <- fmap mconcat $ for tests $ \(qname, _, _, action) -> do + cap <- getNumCapabilities + report <- fmap mconcat $ pooledForConcurrentlyN (min 4 cap) tests $ \(qname, _, _, action) -> do (mErr, tm) <- withTime (runTest genv action) case mErr of Left err -> do From fae2d9f80b3642114628aa6f5f9dada67d21ca46 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Date: Mon, 4 Mar 2024 13:59:20 +0100 Subject: [PATCH 031/117] hi ci From 9e0cefb4dc56a26dd32f2a5e95b81bb5459a400e Mon Sep 17 00:00:00 2001 From: Igor Ranieri Date: Mon, 4 Mar 2024 15:30:45 +0100 Subject: [PATCH 032/117] restore serialised test run --- integration/test/Testlib/Run.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index a9bf804513c..b3275b0f7fd 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -16,6 +16,7 @@ import Data.Functor import Data.List import Data.PEM import Data.Time.Clock +import Data.Traversable (for) import RunAllTests import System.Directory import System.Environment @@ -146,8 +147,7 @@ runTests tests mXMLOutput cfg = do runCodensity (createGlobalEnv cfg) $ \genv -> withAsync displayOutput $ \displayThread -> do - cap <- getNumCapabilities - report <- fmap mconcat $ pooledForConcurrentlyN (min 4 cap) tests $ \(qname, _, _, action) -> do + report <- fmap mconcat $ for tests $ \(qname, _, _, action) -> do (mErr, tm) <- withTime (runTest genv action) case mErr of Left err -> do From e263004f5b016c1347bb995acbb5955619594c38 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Date: Mon, 4 Mar 2024 16:07:56 +0100 Subject: [PATCH 033/117] hi ci From e3f190cd68a35d87d47fc2c584630fc3db8d89bc Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Mon, 4 Mar 2024 23:41:47 +0100 Subject: [PATCH 034/117] Update ports table according to WPB-2043 --- docs/src/how-to/install/sft.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/src/how-to/install/sft.md b/docs/src/how-to/install/sft.md index 9074bd93a21..dec1f3bf113 100644 --- a/docs/src/how-to/install/sft.md +++ b/docs/src/how-to/install/sft.md @@ -123,6 +123,7 @@ An SFT instance does **not** communicate with other SFT instances, TURN does tal Recapitulation table: ```{eval-rst} + +----------------------------+-------------+-------------+-----------+----------+-----------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Name | Origin | Destination | Direction | Protocol | Ports | Action (Policy) | Description | +============================+=============+=============+===========+==========+=============================================================================+======================================+===============================================================================================================================================================================================+ @@ -136,8 +137,19 @@ Recapitulation table: +----------------------------+-------------+-------------+-----------+----------+-----------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Allowing SFT media ingress | Any | Here | Incoming | UDP | 32768-61000 | Allow | Allow ports in the "Ephemeral range" (https://en.wikipedia.org/wiki/Ephemeral_port), defined by the Linux Kernel ass the range from ports 32768 to 61000, used for UDP transmission of media. | +----------------------------+-------------+-------------+-----------+----------+-----------------------------------------------------------------------------+--------------------------------------+ | -| Allowing SFT media egress | Here | Anny | Outgoing | UDP | 32768-61000 | Allow | | +| Allowing SFT media egress | Here | Any | Outgoing | UDP | 32768-61000 | Allow | | ++----------------------------+-------------+-------------+-----------+----------+-----------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Federation traffic in | Any | Here | Incoming | UDP/DTLS | 9191 | Allow | The TURN-servers communicate via this port. Either encrypted or unencrypted. | ++----------------------------+-------------+-------------+-----------+----------+-----------------------------------------------------------------------------+--------------------------------------+ | +| Federation traffic out | Here | Any | Outgoing | UDP/DTLS | 9191 | Allow | | ++----------------------------+-------------+-------------+-----------+----------+-----------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Coturn control in | Any | Here | Incoming | TCP | 3478 | Allow | (STUN and TURN (TCP), helm setting: `coturn:coturnTurnListenPort`) | +----------------------------+-------------+-------------+-----------+----------+-----------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Coturn control in (TLS) | Any | Here | Incoming | TCP/TLS | 3478 | Allow | (STUN and TURN (TLS via TCP), helm setting: `coturn:coturnTurnTlsListenPort`) | ++----------------------------+-------------+-------------+-----------+----------+-----------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| Coturn control in (UDP) | Any | Here | Incoming | UDP | 3478 | Allow | (STUN and TURN (UDP), helm setting: `coturn:coturnTurnListenPort`) | ++----------------------------+-------------+-------------+-----------+----------+-----------------------------------------------------------------------------+--------------------------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ + ``` *For more information, please refer to the source code of the Ansible role:* [sft-server](https://github.com/wireapp/ansible-sft/blob/develop/roles/sft-server/tasks/traffic.yml). From 6d160a73d42a7816661858a7f3760c5b7a5c8885 Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Mon, 4 Mar 2024 23:55:03 +0100 Subject: [PATCH 035/117] made modifications according to julia and sebastian --- .../install/img/architecture-server-ha.drawio | 329 +++++++++++++++++- .../install/img/architecture-server-ha.png | Bin 170040 -> 216366 bytes 2 files changed, 328 insertions(+), 1 deletion(-) diff --git a/docs/src/how-to/install/img/architecture-server-ha.drawio b/docs/src/how-to/install/img/architecture-server-ha.drawio index c7caf837bf1..bf43f299408 100644 --- a/docs/src/how-to/install/img/architecture-server-ha.drawio +++ b/docs/src/how-to/install/img/architecture-server-ha.drawio @@ -1 +1,328 @@ -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 \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docs/src/how-to/install/img/architecture-server-ha.png b/docs/src/how-to/install/img/architecture-server-ha.png index 115674626de7ac0362ca974785c4f5180fc4abb4..a7113b2d95c2240205af1ae08f7f644b141dbdc1 100644 GIT binary patch literal 216366 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ßgSFpf9ve)E7ww|h#NH^(lI`JR$->vzo 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 yMB4`ZJO6VI From bbb37ca87c85c03253a106fb79ede98a793e3bf9 Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Tue, 5 Mar 2024 00:27:18 +0100 Subject: [PATCH 036/117] Add a "default" value going to the ENV variable if the value file variable isn't set. --- charts/coturn/templates/configmap-coturn-conf-template.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/coturn/templates/configmap-coturn-conf-template.yaml b/charts/coturn/templates/configmap-coturn-conf-template.yaml index 6bda1d81130..b020ee5080d 100644 --- a/charts/coturn/templates/configmap-coturn-conf-template.yaml +++ b/charts/coturn/templates/configmap-coturn-conf-template.yaml @@ -27,7 +27,7 @@ data: no-cli ## turn, stun. - listening-ip={{ .Values.coturnTurnListenIP }} # was: __COTURN_EXT_IP__ + listening-ip={{ default "__COTURN_EXT_IP__" .Values.coturnTurnListenIP }} listening-port={{ .Values.coturnTurnListenPort }} relay-ip=__COTURN_EXT_IP__ realm=dummy.io From 97d2ef1c1554d99e19770fc12af72a9294cae79d Mon Sep 17 00:00:00 2001 From: Igor Ranieri Elland <54423+elland@users.noreply.github.com> Date: Wed, 6 Mar 2024 09:50:54 +0100 Subject: [PATCH 037/117] Pin hsaml2 to a diff branch. (#3855) --- nix/haskell-pins.nix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 88be5c9094c..1b0505fa7c7 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -111,8 +111,8 @@ let hsaml2 = { src = fetchgit { url = "https://github.com/wireapp/hsaml2"; - rev = "723b377fcd759c8be9ad4b2e159a6a06df0d17c9"; - sha256 = "sha256-rPfztTu+NR/5FuoYWGMCfJFhrMn4o09bMcEKoerNX4A="; + rev = "c11ad42e6bd6ef6a1eb298413ab131234b171224"; + sha256 = "sha256-OYqIxe+9M8YKUpqJPgOeqOTmez7JdOd351J5NgaHrMY="; }; }; From 08b25405badaef97f5841277dc43e647391073b4 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Wed, 6 Mar 2024 15:34:50 +0100 Subject: [PATCH 038/117] Make federator error logs more informative. (#3919) --- .../wpb6998-improve-federator-error-logs | 1 + libs/wire-api-federation/default.nix | 2 + .../src/Wire/API/Federation/Error.hs | 69 ++++++++++++------- .../wire-api-federation.cabal | 1 + services/federator/src/Federator/Remote.hs | 8 +-- 5 files changed, 53 insertions(+), 28 deletions(-) create mode 100644 changelog.d/5-internal/wpb6998-improve-federator-error-logs diff --git a/changelog.d/5-internal/wpb6998-improve-federator-error-logs b/changelog.d/5-internal/wpb6998-improve-federator-error-logs new file mode 100644 index 00000000000..9c8240b4bdb --- /dev/null +++ b/changelog.d/5-internal/wpb6998-improve-federator-error-logs @@ -0,0 +1 @@ +Include remote domain in federator error logs diff --git a/libs/wire-api-federation/default.nix b/libs/wire-api-federation/default.nix index 9775b18ddb1..b287e8fdc21 100644 --- a/libs/wire-api-federation/default.nix +++ b/libs/wire-api-federation/default.nix @@ -11,6 +11,7 @@ , bytestring , bytestring-conversion , containers +, dns-util , exceptions , gitignoreSource , HsOpenSSL @@ -58,6 +59,7 @@ mkDerivation { bytestring bytestring-conversion containers + dns-util exceptions HsOpenSSL http-media diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Error.hs b/libs/wire-api-federation/src/Wire/API/Federation/Error.hs index fcf3c9adce1..2303d744abb 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Error.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Error.hs @@ -84,6 +84,7 @@ module Wire.API.Federation.Error where import Data.Aeson qualified as Aeson +import Data.Domain import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.Text.Encoding.Error qualified as T @@ -97,6 +98,7 @@ import Network.Wai.Utilities.Error qualified as Wai import OpenSSL.Session (SomeSSLException) import Servant.Client import Wire.API.Error +import Wire.Network.DNS.SRV -- | Transport-layer errors in federator client. data FederatorClientHTTP2Error @@ -209,27 +211,43 @@ federationClientErrorToWai FederatorClientVersionMismatch = "internal-error" "Endpoint version mismatch in federation client" -federationRemoteHTTP2Error :: FederatorClientHTTP2Error -> Wai.Error -federationRemoteHTTP2Error FederatorClientNoStatusCode = - Wai.mkError - unexpectedFederationResponseStatus - "federation-http2-error" - "No status code in HTTP2 response" -federationRemoteHTTP2Error (FederatorClientHTTP2Exception e) = - Wai.mkError - unexpectedFederationResponseStatus - "federation-http2-error" - (LT.pack (displayException e)) -federationRemoteHTTP2Error (FederatorClientTLSException e) = - Wai.mkError - (HTTP.mkStatus 525 "SSL Handshake Failure") - "federation-tls-error" - (LT.pack (displayException e)) -federationRemoteHTTP2Error (FederatorClientConnectionError e) = - Wai.mkError - federatorConnectionRefusedStatus - "federation-connection-refused" - (LT.pack (displayException e)) +federationRemoteHTTP2Error :: SrvTarget -> Text -> FederatorClientHTTP2Error -> Wai.Error +federationRemoteHTTP2Error target path = \case + FederatorClientNoStatusCode -> + ( Wai.mkError + unexpectedFederationResponseStatus + "federation-http2-error" + "No status code in HTTP2 response" + ) + & addErrData + (FederatorClientHTTP2Exception e) -> + ( Wai.mkError + unexpectedFederationResponseStatus + "federation-http2-error" + (LT.pack (displayException e)) + ) + & addErrData + (FederatorClientTLSException e) -> + ( Wai.mkError + (HTTP.mkStatus 525 "SSL Handshake Failure") + "federation-tls-error" + (LT.pack (displayException e)) + ) + & addErrData + (FederatorClientConnectionError e) -> + ( Wai.mkError + federatorConnectionRefusedStatus + "federation-connection-refused" + (LT.pack (displayException e)) + ) + & addErrData + where + addErrData err = + err + { Wai.errorData = + ((mkDomain . cs . srvTargetDomain $ target) :: Either String Domain) + & either (const Nothing) (\dom -> Just (Wai.FederationErrorData dom path)) + } federationClientHTTP2Error :: FederatorClientHTTP2Error -> Wai.Error federationClientHTTP2Error (FederatorClientConnectionError e) = @@ -243,8 +261,8 @@ federationClientHTTP2Error e = "federation-local-error" (LT.pack (displayException e)) -federationRemoteResponseError :: HTTP.Status -> LByteString -> Wai.Error -federationRemoteResponseError status body = +federationRemoteResponseError :: SrvTarget -> Text -> HTTP.Status -> LByteString -> Wai.Error +federationRemoteResponseError target path status body = ( Wai.mkError unexpectedFederationResponseStatus "federation-remote-error" @@ -252,7 +270,10 @@ federationRemoteResponseError status body = <> LT.pack (show (HTTP.statusCode status)) ) ) - { Wai.innerError = + { Wai.errorData = + ((mkDomain . cs . srvTargetDomain $ target) :: Either String Domain) + & either (const Nothing) (\dom -> Just (Wai.FederationErrorData dom path)), + Wai.innerError = Just $ fromMaybe ( Wai.mkError diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index d92f39c0792..3cee60745bb 100644 --- a/libs/wire-api-federation/wire-api-federation.cabal +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -90,6 +90,7 @@ library , bytestring , bytestring-conversion , containers + , dns-util , exceptions , HsOpenSSL , http-media diff --git a/services/federator/src/Federator/Remote.hs b/services/federator/src/Federator/Remote.hs index 48b75cdd0db..2bc3ae9a05b 100644 --- a/services/federator/src/Federator/Remote.hs +++ b/services/federator/src/Federator/Remote.hs @@ -63,10 +63,10 @@ data RemoteError deriving (Show) instance AsWai RemoteError where - toWai (RemoteError _ _ e) = - federationRemoteHTTP2Error e - toWai (RemoteErrorResponse _ _ status body) = - federationRemoteResponseError status body + toWai (RemoteError target msg err) = + federationRemoteHTTP2Error target msg err + toWai (RemoteErrorResponse target msg status body) = + federationRemoteResponseError target msg status body data Remote m a where DiscoverAndCall :: From 163eb29e7c8110e8bb2a6b296f3ab5c062e67868 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Thu, 7 Mar 2024 14:30:37 +0100 Subject: [PATCH 039/117] [WPB-183] Version federation API queue notifications (#3831) * Define the version range type This commit does not make any application logic use of it just yet * Pass the version range to a BackendNotification * Utility: compute a version in common * Parameterise version negotiation Instead of directly looking into 'supportedVersions', parameterise the function by taking as input a set of versions. This makes it usable for negotiation based on an endpoint's version range. * Define the PayloadBundle type This gets enqueued instead of individual 'BackendNotification's * Convert a call to use fedQueueClientBundle The fedQueueClientBundle function is to be renamed to fedQueueClient once aligning is done * Move fedNotifToBackendNotif next to BackendNotification * Parse a queue message as a PayloadBundle * Remove targetDomain from PayloadBundle * Remove originDomain from PayloadBundle * instance Semigroup PayloadBundle * TODO note: refactoring * More focused signature of toBundle * Make all fedQueue endpoints use the new client * Test: dequeueing a payload bundle * Add a futurework note on dropping support for parsing backend notifications in RabbitMQ queue * Pull out mostRecentNotif into a standalone function * Fix Nix dependencies * A TODO and a FUTUREWORK note * Unit tests for mostRecentNotif. * Refactor VersionRange * Slight refactoring of mostRecentNotif * Move mostRecentTuple * Disable a deprecation warning I don't know how to fix the warning so turning it off is probably not the nicest thing to do * Inline reqOrigin * Remove redundant maxBound Co-authored-by: fisx * Introduce change in notification API * Clean up backend notification effect * Refactor fedQueueClient API * Make ConversationUpdate request versioned * Expose v0 notification endpoint * Fatal logs for version mismatch in pusher * Add TODOs * Add notification mods and generate version ranges * Use existential to lift components to type level * Skip negotiation when fetching remote versions * Move notification change to V2 * Test conversation update on V0 * Parse remote versions liberally We cannot parse the supported versions returned by a remote federator using our own `Version` type, because this breaks forward compatibility. Instead, use integers and convert later, ignoring any version that doesn't exist locally. * Support broken fed API version negotiation Old backends are not able to parse version lists containing newer versions. This commit changes the JSON format of the response of the `api-version` federation endpoint, and leaves a hardcoded value for the legacy field that old backends are able to parse. This means that version negotiation running within an old backend will return a bogus result, but since those old backends were not actually making use of federation API versioning, that is not a problem. * Turn on-conversation-updated uses into notifs * Refactor notification version negotiation * Add CHANGELOG entries * Lint * Fix ConversationUpdate golden tests * Remove federation version V2 It was introduced to accommodate older backends that were already claiming to support V1. However, now that the version negotiation system is bypassing the old one, there is no need for the extra version bump. * Remove obsolete tests from galley integration * Remove commented out code Co-authored-by: Akshay Mankar * Refactor mock federator arguments * Unit-test pusher version negotiation * Ignore version mismatches when pushing This prevents the background worker from getting stuck when a remote backend is running an incompatible or broken instance. * Rename test case --------- Co-authored-by: Matthias Fischmann Co-authored-by: Paolo Capriotti Co-authored-by: Akshay Mankar --- .../on-conversation-updated-async | 3 + .../6-federation/wpb-183-versioned-async-b2b | 2 + deploy/dockerephemeral/federation-v0.yaml | 2 + integration/test/Test/Conversation.hs | 14 + integration/test/Testlib/Env.hs | 4 + .../src/Wire/API/Federation/API.hs | 46 ++- .../Federation/API/Galley/Notifications.hs | 57 ++- .../src/Wire/API/Federation/API/Util.hs | 29 ++ .../API/Federation/BackendNotifications.hs | 134 +++++-- .../src/Wire/API/Federation/Client.hs | 41 ++- .../src/Wire/API/Federation/Component.hs | 9 + .../src/Wire/API/Federation/Endpoint.hs | 38 +- .../API/Federation/HasNotificationEndpoint.hs | 89 +++-- .../src/Wire/API/Federation/Version.hs | 130 ++++++- .../Federation/Golden/ConversationUpdate.hs | 46 ++- .../Wire/API/Federation/Golden/GoldenSpec.hs | 4 + .../testObject_ConversationUpdate1.json | 12 +- .../testObject_ConversationUpdate1V0.json | 26 ++ .../testObject_ConversationUpdate2.json | 12 +- .../testObject_ConversationUpdate2V0.json | 17 + .../wire-api-federation.cabal | 1 + .../background-worker/background-worker.cabal | 3 + services/background-worker/default.nix | 4 + .../src/Wire/BackendNotificationPusher.hs | 137 ++++++-- .../Wire/BackendNotificationPusherSpec.hs | 94 ++++- services/brig/src/Brig/Federation/Client.hs | 3 +- .../brig/test/integration/API/Federation.hs | 2 +- .../brig/test/integration/Federation/Util.hs | 4 +- services/brig/test/integration/Util.hs | 4 +- services/cargohold/cargohold.cabal | 1 + services/cargohold/default.nix | 2 + .../cargohold/test/integration/API/Util.hs | 3 +- services/federator/default.nix | 1 + services/federator/federator.cabal | 1 + .../federator/src/Federator/MockServer.hs | 44 ++- .../test/unit/Test/Federator/Client.hs | 26 +- services/galley/src/Galley/API/Action.hs | 47 ++- services/galley/src/Galley/API/Clients.hs | 41 ++- services/galley/src/Galley/API/Create.hs | 25 +- services/galley/src/Galley/API/Federation.hs | 22 +- services/galley/src/Galley/API/Internal.hs | 40 +-- .../Galley/API/MLS/Commit/ExternalCommit.hs | 4 +- services/galley/src/Galley/API/MLS/Message.hs | 5 - .../galley/src/Galley/API/MLS/Propagate.hs | 36 +- .../galley/src/Galley/API/MLS/Proposal.hs | 2 + services/galley/src/Galley/API/MLS/Removal.hs | 14 +- .../src/Galley/API/MLS/SubConversation.hs | 1 + services/galley/src/Galley/API/Message.hs | 42 +-- services/galley/src/Galley/API/Teams.hs | 11 +- services/galley/src/Galley/API/Update.hs | 34 +- services/galley/src/Galley/API/Util.hs | 31 +- .../Effects/BackendNotificationQueueAccess.hs | 51 ++- .../src/Galley/Effects/FederatorAccess.hs | 10 +- .../Galley/Intra/BackendNotificationQueue.hs | 22 +- services/galley/src/Galley/Intra/Federator.hs | 11 +- services/galley/test/integration/API.hs | 328 +++--------------- .../galley/test/integration/API/Federation.hs | 102 +++--- services/galley/test/integration/API/MLS.hs | 10 +- .../galley/test/integration/API/MLS/Util.hs | 10 +- services/galley/test/integration/API/Util.hs | 10 +- services/galley/test/integration/TestSetup.hs | 3 +- 61 files changed, 1224 insertions(+), 733 deletions(-) create mode 100644 changelog.d/6-federation/on-conversation-updated-async create mode 100644 changelog.d/6-federation/wpb-183-versioned-async-b2b create mode 100644 libs/wire-api-federation/src/Wire/API/Federation/API/Util.hs create mode 100644 libs/wire-api-federation/test/golden/testObject_ConversationUpdate1V0.json create mode 100644 libs/wire-api-federation/test/golden/testObject_ConversationUpdate2V0.json diff --git a/changelog.d/6-federation/on-conversation-updated-async b/changelog.d/6-federation/on-conversation-updated-async new file mode 100644 index 00000000000..24885094cf6 --- /dev/null +++ b/changelog.d/6-federation/on-conversation-updated-async @@ -0,0 +1,3 @@ +The on-conversation-updated notification is now queued instead of being sent directly. A new version of the notification has been introduced with a different JSON format for the body, mostly for testing purposes of the versioning system. + +Since the notification is now sent asynchronously, some error conditions in case of unreachable backends cannot be triggered anymore. diff --git a/changelog.d/6-federation/wpb-183-versioned-async-b2b b/changelog.d/6-federation/wpb-183-versioned-async-b2b new file mode 100644 index 00000000000..c33245c5ccd --- /dev/null +++ b/changelog.d/6-federation/wpb-183-versioned-async-b2b @@ -0,0 +1,2 @@ +Versioning of backend to backend notifications. Notifications are now stored in "bundles" containing a serialised payload for each supported version. The background worker then dynamically selects the best version to use and sends only the notification corresponding to that version. + diff --git a/deploy/dockerephemeral/federation-v0.yaml b/deploy/dockerephemeral/federation-v0.yaml index 1342056cac5..8ed1179b048 100644 --- a/deploy/dockerephemeral/federation-v0.yaml +++ b/deploy/dockerephemeral/federation-v0.yaml @@ -182,6 +182,8 @@ services: networks: - demo_wire - coredns + extra_hosts: + - "host.docker.internal.:host-gateway" ports: - '127.0.0.1:21097:8080' - '127.0.0.1:21098:8081' diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index 9ff4641bbcc..1d5587f40ae 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -850,3 +850,17 @@ testGuestLinksExpired = do liftIO $ threadDelay (1_100_000) bindResponse (getJoinCodeConv tm k v) $ \resp -> do resp.status `shouldMatchInt` 404 + +testConversationWithFedV0 :: HasCallStack => App () +testConversationWithFedV0 = do + alice <- randomUser OwnDomain def + bob <- randomUser FedV0Domain def + withAPIVersion 4 $ connectTwoUsers alice bob + + conv <- + postConversation alice (defProteus {qualifiedUsers = [bob]}) + >>= getJSON 201 + + withWebSocket bob $ \ws -> do + void $ changeConversationName alice conv "foobar" >>= getJSON 200 + void $ awaitMatch isConvNameChangeNotif ws diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index f85f1d0934a..4becf8eb9a3 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -4,6 +4,7 @@ module Testlib.Env where import Control.Monad.Codensity import Control.Monad.IO.Class +import Control.Monad.Reader import Data.Default import Data.Function ((&)) import Data.Functor @@ -184,3 +185,6 @@ mkMLSState = Codensity $ \k -> ciphersuite = def, protocol = MLSProtocolMLS } + +withAPIVersion :: Int -> App a -> App a +withAPIVersion v = local $ \e -> e {defaultAPIVersion = v} diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API.hs b/libs/wire-api-federation/src/Wire/API/Federation/API.hs index 242991d2c41..053275577e3 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API.hs @@ -24,12 +24,14 @@ module Wire.API.Federation.API HasUnsafeFedEndpoint, fedClient, fedQueueClient, + sendBundle, fedClientIn, unsafeFedClientIn, module Wire.API.MakesFederatedCall, -- * Re-exports Component (..), + makeConversationUpdateBundle, ) where @@ -45,6 +47,7 @@ import Servant.Client.Core import Wire.API.Federation.API.Brig import Wire.API.Federation.API.Cargohold import Wire.API.Federation.API.Galley +import Wire.API.Federation.API.Util import Wire.API.Federation.BackendNotifications import Wire.API.Federation.Client import Wire.API.Federation.Component @@ -88,21 +91,21 @@ fedClient :: Client m api fedClient = clientIn (Proxy @api) (Proxy @m) -fedQueueClient :: - forall {k} (tag :: k). - ( HasNotificationEndpoint tag, - KnownSymbol (NotificationPath tag), - KnownComponent (NotificationComponent k), - ToJSON (Payload tag) - ) => - Payload tag -> - FedQueueClient (NotificationComponent k) () -fedQueueClient payload = do +fedClientIn :: + forall (comp :: Component) (name :: Symbol) m api. + (HasFedEndpoint comp api name, HasClient m api) => + Client m api +fedClientIn = clientIn (Proxy @api) (Proxy @m) + +sendBundle :: + KnownComponent c => + PayloadBundle c -> + FedQueueClient c () +sendBundle bundle = do env <- ask - let notif = fedNotifToBackendNotif @tag env.requestId env.originDomain payload - msg = + let msg = newMsg - { msgBody = encode notif, + { msgBody = encode bundle, msgDeliveryMode = Just (env.deliveryMode), msgContentType = Just "application/json" } @@ -112,11 +115,18 @@ fedQueueClient payload = do ensureQueue env.channel env.targetDomain._domainText void $ publishMsg env.channel exchange (routingKey env.targetDomain._domainText) msg -fedClientIn :: - forall (comp :: Component) (name :: Symbol) m api. - (HasFedEndpoint comp api name, HasClient m api) => - Client m api -fedClientIn = clientIn (Proxy @api) (Proxy @m) +fedQueueClient :: + forall {k} (tag :: k) c. + ( HasNotificationEndpoint tag, + HasVersionRange tag, + HasFedPath tag, + KnownComponent (NotificationComponent k), + ToJSON (Payload tag), + c ~ NotificationComponent k + ) => + Payload tag -> + FedQueueClient c () +fedQueueClient payload = sendBundle =<< makeBundle @tag payload -- | Like 'fedClientIn', but doesn't propagate a 'CallsFed' constraint. Intended -- to be used in test situations only. diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley/Notifications.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley/Notifications.hs index 9f9e1ee589e..0318e84d666 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/API/Galley/Notifications.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Galley/Notifications.hs @@ -34,8 +34,10 @@ import Wire.API.Conversation.Action import Wire.API.Federation.Component import Wire.API.Federation.Endpoint import Wire.API.Federation.HasNotificationEndpoint +import Wire.API.Federation.Version import Wire.API.MLS.SubConversation import Wire.API.Message +import Wire.API.Routes.Version (From, Until) import Wire.API.Util.Aeson import Wire.Arbitrary @@ -43,6 +45,7 @@ data GalleyNotificationTag = OnClientRemovedTag | OnMessageSentTag | OnMLSMessageSentTag + | OnConversationUpdatedTagV0 | OnConversationUpdatedTag | OnUserDeletedConversationsTag deriving (Show, Eq, Generic, Bounded, Enum) @@ -66,9 +69,16 @@ instance HasNotificationEndpoint 'OnMLSMessageSentTag where -- used by the backend that owns a conversation to inform this backend of -- changes to the conversation +instance HasNotificationEndpoint 'OnConversationUpdatedTagV0 where + type Payload 'OnConversationUpdatedTagV0 = ConversationUpdateV0 + type NotificationPath 'OnConversationUpdatedTagV0 = "on-conversation-updated" + type NotificationVersionTag 'OnConversationUpdatedTagV0 = 'Just 'V0 + type NotificationMods 'OnConversationUpdatedTagV0 = '[Until 'V1] + instance HasNotificationEndpoint 'OnConversationUpdatedTag where type Payload 'OnConversationUpdatedTag = ConversationUpdate type NotificationPath 'OnConversationUpdatedTag = "on-conversation-updated" + type NotificationMods 'OnConversationUpdatedTag = '[From 'V1] instance HasNotificationEndpoint 'OnUserDeletedConversationsTag where type Payload 'OnUserDeletedConversationsTag = UserDeletedConversationsNotification @@ -79,6 +89,7 @@ type GalleyNotificationAPI = NotificationFedEndpoint 'OnClientRemovedTag :<|> NotificationFedEndpoint 'OnMessageSentTag :<|> NotificationFedEndpoint 'OnMLSMessageSentTag + :<|> NotificationFedEndpoint 'OnConversationUpdatedTagV0 :<|> NotificationFedEndpoint 'OnConversationUpdatedTag :<|> NotificationFedEndpoint 'OnUserDeletedConversationsTag @@ -129,7 +140,7 @@ data RemoteMLSMessage = RemoteMLSMessage instance ToSchema RemoteMLSMessage -data ConversationUpdate = ConversationUpdate +data ConversationUpdateV0 = ConversationUpdateV0 { cuTime :: UTCTime, cuOrigUserId :: Qualified UserId, -- | The unqualified ID of the conversation where the update is happening. @@ -147,12 +158,56 @@ data ConversationUpdate = ConversationUpdate } deriving (Eq, Show, Generic) +instance ToJSON ConversationUpdateV0 + +instance FromJSON ConversationUpdateV0 + +instance ToSchema ConversationUpdateV0 + +data ConversationUpdate = ConversationUpdate + { time :: UTCTime, + origUserId :: Qualified UserId, + -- | The unqualified ID of the conversation where the update is happening. + -- The ID is local to the sender to prevent putting arbitrary domain that + -- is different than that of the backend making a conversation membership + -- update request. + convId :: ConvId, + -- | A list of users from the receiving backend that need to be sent + -- notifications about this change. This is required as we do not expect a + -- non-conversation owning backend to have an indexed mapping of + -- conversation to users. + alreadyPresentUsers :: [UserId], + -- | Information on the specific action that caused the update. + action :: SomeConversationAction + } + deriving (Eq, Show, Generic) + instance ToJSON ConversationUpdate instance FromJSON ConversationUpdate instance ToSchema ConversationUpdate +conversationUpdateToV0 :: ConversationUpdate -> ConversationUpdateV0 +conversationUpdateToV0 cu = + ConversationUpdateV0 + { cuTime = cu.time, + cuOrigUserId = cu.origUserId, + cuConvId = cu.convId, + cuAlreadyPresentUsers = cu.alreadyPresentUsers, + cuAction = cu.action + } + +conversationUpdateFromV0 :: ConversationUpdateV0 -> ConversationUpdate +conversationUpdateFromV0 cu = + ConversationUpdate + { time = cu.cuTime, + origUserId = cu.cuOrigUserId, + convId = cu.cuConvId, + alreadyPresentUsers = cu.cuAlreadyPresentUsers, + action = cu.cuAction + } + type UserDeletedNotificationMaxConvs = 1000 data UserDeletedConversationsNotification = UserDeletedConversationsNotification diff --git a/libs/wire-api-federation/src/Wire/API/Federation/API/Util.hs b/libs/wire-api-federation/src/Wire/API/Federation/API/Util.hs new file mode 100644 index 00000000000..d855c2abb01 --- /dev/null +++ b/libs/wire-api-federation/src/Wire/API/Federation/API/Util.hs @@ -0,0 +1,29 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2023 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.API.Federation.API.Util where + +import Imports +import Wire.API.Federation.API.Galley.Notifications +import Wire.API.Federation.BackendNotifications +import Wire.API.Federation.Component + +makeConversationUpdateBundle :: + ConversationUpdate -> + FedQueueClient 'Galley (PayloadBundle 'Galley) +makeConversationUpdateBundle update = + (<>) <$> makeBundle update <*> makeBundle (conversationUpdateToV0 update) diff --git a/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs b/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs index b3cb2546ab4..43849c716da 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/BackendNotifications.hs @@ -4,11 +4,14 @@ module Wire.API.Federation.BackendNotifications where import Control.Exception +import Control.Monad.Codensity import Control.Monad.Except -import Data.Aeson +import Data.Aeson qualified as A import Data.Domain import Data.Id (RequestId) +import Data.List.NonEmpty qualified as NE import Data.Map qualified as Map +import Data.Schema import Data.Text qualified as Text import Data.Text.Lazy.Encoding qualified as TL import Imports @@ -20,6 +23,8 @@ import Wire.API.Federation.API.Common import Wire.API.Federation.Client import Wire.API.Federation.Component import Wire.API.Federation.Error +import Wire.API.Federation.HasNotificationEndpoint +import Wire.API.Federation.Version import Wire.API.RawJson -- | NOTE: Stored in RabbitMQ, any changes to serialization of this object could cause @@ -33,46 +38,115 @@ data BackendNotification = BackendNotification -- pusher. This also makes development less clunky as we don't have to -- create a sum type here for all types of notifications that could exist. body :: RawJson, + -- | The federation API versions that the 'body' corresponds to. The field + -- is optional so that messages already in the queue are not lost. + bodyVersions :: Maybe VersionRange, requestId :: Maybe RequestId } deriving (Show, Eq) - -instance ToJSON BackendNotification where - toJSON notif = - object - [ "ownDomain" .= notif.ownDomain, - "targetComponent" .= notif.targetComponent, - "path" .= notif.path, - "body" .= TL.decodeUtf8 notif.body.rawJsonBytes, - "requestId" .= notif.requestId - ] - -instance FromJSON BackendNotification where - parseJSON = withObject "BackendNotification" $ \o -> - BackendNotification - <$> o .: "ownDomain" - <*> o .: "targetComponent" - <*> o .: "path" - <*> (RawJson . TL.encodeUtf8 <$> o .: "body") - <*> o .:? "requestId" + deriving (A.ToJSON, A.FromJSON) via (Schema BackendNotification) + +instance ToSchema BackendNotification where + schema = + object "BackendNotification" $ + BackendNotification + <$> ownDomain .= field "ownDomain" schema + <*> targetComponent .= field "targetComponent" schema + <*> path .= field "path" schema + <*> (TL.decodeUtf8 . rawJsonBytes . body) + .= field "body" (RawJson . TL.encodeUtf8 <$> schema) + <*> bodyVersions .= maybe_ (optField "bodyVersions" schema) + <*> (.requestId) .= maybe_ (optField "requestId" schema) + +-- | Convert a federation endpoint to a backend notification to be enqueued to a +-- RabbitMQ queue. +fedNotifToBackendNotif :: + forall {k} (tag :: k). + ( HasFedPath tag, + HasVersionRange tag, + KnownComponent (NotificationComponent k), + A.ToJSON (Payload tag) + ) => + RequestId -> + Domain -> + Payload tag -> + BackendNotification +fedNotifToBackendNotif rid ownDomain payload = + let p = Text.pack $ fedPath @tag + b = RawJson . A.encode $ payload + in toNotif p b + where + toNotif :: Text -> RawJson -> BackendNotification + toNotif path body = + BackendNotification + { ownDomain = ownDomain, + targetComponent = componentVal @(NotificationComponent k), + path = path, + body = body, + bodyVersions = Just $ versionRange @tag, + requestId = Just rid + } + +newtype PayloadBundle (c :: Component) = PayloadBundle + { notifications :: NE.NonEmpty BackendNotification + } + deriving (A.ToJSON, A.FromJSON) via (Schema (PayloadBundle c)) + deriving newtype (Semigroup) + +instance ToSchema (PayloadBundle c) where + schema = + object "PayloadBundle" $ + PayloadBundle + <$> notifications .= field "notifications" (nonEmptyArray schema) + +toBundle :: + forall {k} (tag :: k). + ( HasFedPath tag, + HasVersionRange tag, + KnownComponent (NotificationComponent k), + A.ToJSON (Payload tag) + ) => + RequestId -> + -- | The origin domain + Domain -> + Payload tag -> + PayloadBundle (NotificationComponent k) +toBundle reqId originDomain payload = + let notif = fedNotifToBackendNotif @tag reqId originDomain payload + in PayloadBundle . pure $ notif + +makeBundle :: + forall {k} (tag :: k) c. + ( HasFedPath tag, + HasVersionRange tag, + KnownComponent (NotificationComponent k), + A.ToJSON (Payload tag), + c ~ NotificationComponent k + ) => + Payload tag -> + FedQueueClient c (PayloadBundle c) +makeBundle payload = do + reqId <- asks (.requestId) + origin <- asks (.originDomain) + pure $ toBundle @tag reqId origin payload type BackendNotificationAPI = Capture "name" Text :> ReqBody '[JSON] RawJson :> Post '[JSON] EmptyResponse -sendNotification :: FederatorClientEnv -> Component -> Text -> RawJson -> IO (Either FederatorClientError ()) -sendNotification env component path body = - case component of - Brig -> go @'Brig - Galley -> go @'Galley - Cargohold -> go @'Cargohold +sendNotification :: FederatorClientVersionedEnv -> Component -> Text -> RawJson -> IO (Either FederatorClientError ()) +sendNotification env component path body = case someComponent component of + SomeComponent p -> go p where withoutFirstSlash :: Text -> Text withoutFirstSlash (Text.stripPrefix "/" -> Just t) = t withoutFirstSlash t = t - go :: forall c. (KnownComponent c) => IO (Either FederatorClientError ()) - go = - runFederatorClient env . void $ - clientIn (Proxy @BackendNotificationAPI) (Proxy @(FederatorClient c)) (withoutFirstSlash path) body + go :: forall c. KnownComponent c => Proxy c -> IO (Either FederatorClientError ()) + go _ = + lowerCodensity + . runExceptT + . runVersionedFederatorClientToCodensity env + . void + $ clientIn (Proxy @BackendNotificationAPI) (Proxy @(FederatorClient c)) (withoutFirstSlash path) body enqueue :: Q.Channel -> RequestId -> Domain -> Domain -> Q.DeliveryMode -> FedQueueClient c a -> IO a enqueue channel requestId originDomain targetDomain deliveryMode (FedQueueClient action) = diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Client.hs b/libs/wire-api-federation/src/Wire/API/Federation/Client.hs index 648a4ee3ec6..37444a6a49e 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Client.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Client.hs @@ -21,8 +21,10 @@ module Wire.API.Federation.Client ( FederatorClientEnv (..), FederatorClientVersionedEnv (..), + unversionedEnv, FederatorClient, runFederatorClient, + runVersionedFederatorClient, runFederatorClientToCodensity, runVersionedFederatorClientToCodensity, performHTTP2Request, @@ -85,6 +87,9 @@ data FederatorClientVersionedEnv = FederatorClientVersionedEnv cveVersion :: Maybe Version } +unversionedEnv :: FederatorClientEnv -> FederatorClientVersionedEnv +unversionedEnv env = FederatorClientVersionedEnv env Nothing + -- | A request to a remote backend. The API version of the remote backend is in -- the environment. The 'MaybeT' layer is used to match endpoint versions (via -- the 'Alternative' and 'VersionedMonad' instances). @@ -171,7 +176,16 @@ instance KnownComponent c => RunClient (FederatorClient c) where expectedStatuses v <- asks cveVersion - let vreq = req {requestHeaders = (versionHeader, toByteString' (versionInt (fromMaybe V0 v))) :<| requestHeaders req} + let vreq = + req + { requestHeaders = + ( versionHeader, + toByteString' + ( versionInt (fromMaybe V0 v) + ) + ) + :<| requestHeaders req + } withHTTP2StreamingRequest successfulStatus vreq $ \resp -> do bdy <- @@ -297,6 +311,15 @@ runFederatorClient env = lowerCodensity . runFederatorClientToCodensity env +runVersionedFederatorClient :: + FederatorClientVersionedEnv -> + FederatorClient c a -> + IO (Either FederatorClientError a) +runVersionedFederatorClient venv = + lowerCodensity + . runExceptT + . runVersionedFederatorClientToCodensity venv + runFederatorClientToCodensity :: forall c a. FederatorClientEnv -> @@ -306,7 +329,7 @@ runFederatorClientToCodensity env action = runExceptT $ do v <- runVersionedFederatorClientToCodensity (FederatorClientVersionedEnv env Nothing) - versionNegotiation + (versionNegotiation supportedVersions) runVersionedFederatorClientToCodensity @c (FederatorClientVersionedEnv env (Just v)) action @@ -323,8 +346,8 @@ runVersionedFederatorClientToCodensity env = where unmaybe = (maybe (E.throw FederatorClientVersionMismatch) pure =<<) -versionNegotiation :: FederatorClient 'Brig Version -versionNegotiation = +versionNegotiation :: Set Version -> FederatorClient 'Brig Version +versionNegotiation localVersions = let req = defaultRequest { requestPath = "/api-version", @@ -334,13 +357,15 @@ versionNegotiation = } in withHTTP2StreamingRequest @'Brig HTTP.statusIsSuccessful req $ \resp -> do body <- toLazyByteString <$> streamingResponseStrictBody resp - remoteVersions <- case Aeson.decode body of + allRemoteVersions <- case Aeson.decode body of Nothing -> E.throw (FederatorClientVersionNegotiationError InvalidVersionInfo) - Just info -> pure (Set.fromList (vinfoSupported info)) - case Set.lookupMax (Set.intersection remoteVersions supportedVersions) of + Just info -> pure (vinfoSupported info) + -- ignore versions that don't even exist locally + let remoteVersions = Set.fromList $ Imports.mapMaybe intToVersion allRemoteVersions + case Set.lookupMax (Set.intersection remoteVersions localVersions) of Just v -> pure v Nothing -> E.throw . FederatorClientVersionNegotiationError $ - if Set.lookupMax supportedVersions > Set.lookupMax remoteVersions + if Set.lookupMax localVersions > Set.lookupMax remoteVersions then RemoteTooOld else RemoteTooNew diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Component.hs b/libs/wire-api-federation/src/Wire/API/Federation/Component.hs index 73595904f7c..1a5b91e6bd3 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Component.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Component.hs @@ -21,6 +21,7 @@ module Wire.API.Federation.Component ) where +import Data.Proxy import Imports import Wire.API.MakesFederatedCall (Component (..)) @@ -46,3 +47,11 @@ instance KnownComponent 'Galley where instance KnownComponent 'Cargohold where componentVal = Cargohold + +data SomeComponent where + SomeComponent :: KnownComponent c => Proxy c -> SomeComponent + +someComponent :: Component -> SomeComponent +someComponent Brig = SomeComponent (Proxy @'Brig) +someComponent Galley = SomeComponent (Proxy @'Galley) +someComponent Cargohold = SomeComponent (Proxy @'Cargohold) diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs b/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs index e656a3eda2f..f24085139cb 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Endpoint.hs @@ -23,6 +23,7 @@ where import Data.Kind import GHC.TypeLits +import Imports import Servant.API import Wire.API.ApplyMods import Wire.API.Federation.API.Common @@ -41,21 +42,29 @@ type instance FedPath (name :: Symbol) = name type instance FedPath (Versioned v name) = name +type UnnamedFedEndpointWithMods (mods :: [Type]) path input output = + ( ApplyMods + mods + (path :> OriginDomainHeader :> ReqBody '[JSON] input :> Post '[JSON] output) + ) + type FedEndpointWithMods (mods :: [Type]) name input output = Named name - ( ApplyMods - mods - (FedPath name :> OriginDomainHeader :> ReqBody '[JSON] input :> Post '[JSON] output) + ( UnnamedFedEndpointWithMods mods (FedPath name) input output ) -type NotificationFedEndpointWithMods (mods :: [Type]) name input = - FedEndpointWithMods mods name input EmptyResponse - type FedEndpoint name input output = FedEndpointWithMods '[] name input output +type NotificationFedEndpointWithMods (mods :: [Type]) name path input = + Named name (UnnamedFedEndpointWithMods mods path input EmptyResponse) + type NotificationFedEndpoint tag = - FedEndpoint (NotificationPath tag) (Payload tag) EmptyResponse + MkNotificationFedEndpoint + (NotificationMods tag) + (NotificationPath tag) + (NotificationVersionTag tag) + (Payload tag) type StreamingFedEndpoint name input output = Named @@ -65,3 +74,18 @@ type StreamingFedEndpoint name input output = :> ReqBody '[JSON] input :> StreamPost NoFraming OctetStream output ) + +type family + MkNotificationFedEndpoint + (m :: [Type]) + (s :: Symbol) + (v :: Maybe k) + (p :: Type) + +type instance + MkNotificationFedEndpoint m s 'Nothing p = + NotificationFedEndpointWithMods m s s p + +type instance + MkNotificationFedEndpoint m s ('Just v) p = + NotificationFedEndpointWithMods m (Versioned v s) s p diff --git a/libs/wire-api-federation/src/Wire/API/Federation/HasNotificationEndpoint.hs b/libs/wire-api-federation/src/Wire/API/Federation/HasNotificationEndpoint.hs index c2f5772a255..7fba640ee90 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/HasNotificationEndpoint.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/HasNotificationEndpoint.hs @@ -15,53 +15,78 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Wire.API.Federation.HasNotificationEndpoint where +module Wire.API.Federation.HasNotificationEndpoint + ( IsNotificationTag (..), + HasNotificationEndpoint (..), + HasFedPath, + HasVersionRange, + fedPath, + versionRange, + ) +where -import Data.Aeson -import Data.Domain -import Data.Id import Data.Kind import Data.Proxy -import Data.Text qualified as T +import Data.Singletons import GHC.TypeLits import Imports -import Wire.API.Federation.BackendNotifications import Wire.API.Federation.Component -import Wire.API.RawJson +import Wire.API.Federation.Version +import Wire.API.Routes.Version (From, Until) class IsNotificationTag k where type NotificationComponent k = (c :: Component) | c -> k class HasNotificationEndpoint t where -- | The type of the payload for this endpoint - type Payload t :: Type + type Payload t = (p :: Type) | p -> t -- | The central path component of a notification endpoint, e.g., -- "on-conversation-updated". type NotificationPath t :: Symbol --- | Convert a federation endpoint to a backend notification to be enqueued to a --- RabbitMQ queue. -fedNotifToBackendNotif :: - forall {k} (tag :: k). - KnownSymbol (NotificationPath tag) => - KnownComponent (NotificationComponent k) => - ToJSON (Payload tag) => - RequestId -> - Domain -> - Payload tag -> - BackendNotification -fedNotifToBackendNotif rid ownDomain payload = - let p = T.pack . symbolVal $ Proxy @(NotificationPath tag) - b = RawJson . encode $ payload - in toNotif p b + -- | An optional version tag to distinguish different versions of the same + -- endpoint. + type NotificationVersionTag t :: Maybe Version + + type NotificationVersionTag t = 'Nothing + + type NotificationMods t :: [Type] + + type NotificationMods t = '[] + +type HasFedPath t = KnownSymbol (NotificationPath t) + +type HasVersionRange t = MkVersionRange (NotificationMods t) + +fedPath :: forall t. HasFedPath t => String +fedPath = symbolVal (Proxy @(NotificationPath t)) + +-- | Build a version range using any 'Until' and 'From' combinators present in +-- the endpoint modifiers. +class MkVersionRange mods where + mkVersionRange :: VersionRange + +instance MkVersionRange '[] where + mkVersionRange = allVersions + +instance + {-# OVERLAPPING #-} + (MkVersionRange mods, SingI v) => + MkVersionRange (From (v :: Version) ': mods) + where + mkVersionRange = mkVersionRange @mods <> rangeFromVersion (demote @v) + +instance + {-# OVERLAPPING #-} + (MkVersionRange mods, SingI v) => + MkVersionRange (Until (v :: Version) ': mods) where - toNotif :: Text -> RawJson -> BackendNotification - toNotif path body = - BackendNotification - { ownDomain = ownDomain, - targetComponent = componentVal @(NotificationComponent k), - path = path, - body = body, - requestId = Just rid - } + mkVersionRange = mkVersionRange @mods <> rangeUntilVersion (demote @v) + +instance {-# OVERLAPPABLE #-} MkVersionRange mods => MkVersionRange (m ': mods) where + mkVersionRange = mkVersionRange @mods + +-- | The federation API version range this endpoint is supported in. +versionRange :: forall t. HasVersionRange t => VersionRange +versionRange = mkVersionRange @(NotificationMods t) diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Version.hs b/libs/wire-api-federation/src/Wire/API/Federation/Version.hs index b1e29cf520e..a9055c7384b 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Version.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Version.hs @@ -17,16 +17,37 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Wire.API.Federation.Version where +module Wire.API.Federation.Version + ( -- * Version, VersionInfo + Version (..), + V0Sym0, + V1Sym0, + intToVersion, + versionInt, + supportedVersions, + VersionInfo (..), + versionInfo, -import Control.Lens ((?~)) + -- * VersionRange + VersionUpperBound (..), + VersionRange (..), + fromVersion, + toVersionExcl, + allVersions, + latestCommonVersion, + rangeFromVersion, + rangeUntilVersion, + enumVersionRange, + ) +where + +import Control.Lens (makeLenses, (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.OpenApi qualified as S import Data.Schema import Data.Set qualified as Set import Data.Singletons.Base.TH import Imports -import Wire.API.VersionInfo data Version = V0 | V1 deriving stock (Eq, Ord, Bounded, Enum, Show, Generic) @@ -36,6 +57,9 @@ versionInt :: Version -> Int versionInt V0 = 0 versionInt V1 = 1 +intToVersion :: Int -> Maybe Version +intToVersion intV = find (\v -> versionInt v == intV) [minBound ..] + instance ToSchema Version where schema = enum @Integer "Version" . mconcat $ @@ -47,7 +71,7 @@ supportedVersions :: Set Version supportedVersions = Set.fromList [minBound .. maxBound] data VersionInfo = VersionInfo - { vinfoSupported :: [Version] + { vinfoSupported :: [Int] } deriving (FromJSON, ToJSON, S.ToSchema) via (Schema VersionInfo) @@ -55,16 +79,108 @@ instance ToSchema VersionInfo where schema = objectWithDocModifier "VersionInfo" (S.schema . S.example ?~ toJSON example) $ VersionInfo - <$> vinfoSupported .= vinfoObjectSchema schema + -- if the supported_versions field does not exist, assume an old backend + -- that only supports V0 + <$> vinfoSupported + .= fmap + (fromMaybe [0]) + (optField "supported_versions" (array schema)) + -- legacy field to support older versions of the backend with broken + -- version negotiation + <* const [0 :: Int, 1] .= field "supported" (array schema) where example :: VersionInfo example = VersionInfo - { vinfoSupported = toList supportedVersions + { vinfoSupported = map versionInt (toList supportedVersions) } versionInfo :: VersionInfo -versionInfo = VersionInfo (toList supportedVersions) +versionInfo = VersionInfo (map versionInt (toList supportedVersions)) + +---------------------------------------------------------------------- + +-- | The upper bound of a version range. +-- +-- The order of constructors here makes the 'Unbounded' value maximum in the +-- generated lexicographic ordering. +data VersionUpperBound = VersionUpperBound Version | Unbounded + deriving (Eq, Ord, Show) + +versionFromUpperBound :: VersionUpperBound -> Maybe Version +versionFromUpperBound (VersionUpperBound v) = Just v +versionFromUpperBound Unbounded = Nothing + +versionToUpperBound :: Maybe Version -> VersionUpperBound +versionToUpperBound (Just v) = VersionUpperBound v +versionToUpperBound Nothing = Unbounded + +data VersionRange = VersionRange + { _fromVersion :: Version, + _toVersionExcl :: VersionUpperBound + } + +deriving instance Eq VersionRange + +deriving instance Show VersionRange + +deriving instance Ord VersionRange + +makeLenses ''VersionRange + +instance ToSchema VersionRange where + schema = + object "VersionRange" $ + VersionRange + <$> _fromVersion .= field "from" schema + <*> (versionFromUpperBound . _toVersionExcl) + .= maybe_ (versionToUpperBound <$> optFieldWithDocModifier "until_excl" desc schema) + where + desc = description ?~ "exlusive upper version bound" + +deriving via Schema VersionRange instance ToJSON VersionRange + +deriving via Schema VersionRange instance FromJSON VersionRange + +allVersions :: VersionRange +allVersions = VersionRange minBound Unbounded + +-- | The semigroup instance of VersionRange is intersection. +instance Semigroup VersionRange where + VersionRange from1 to1 <> VersionRange from2 to2 = + VersionRange (max from1 from2) (min to1 to2) + +inVersionRange :: VersionRange -> Version -> Bool +inVersionRange (VersionRange a b) v = + v >= a && VersionUpperBound v < b + +rangeFromVersion :: Version -> VersionRange +rangeFromVersion v = VersionRange v Unbounded + +rangeUntilVersion :: Version -> VersionRange +rangeUntilVersion v = VersionRange minBound (VersionUpperBound v) + +enumVersionRange :: VersionRange -> Set Version +enumVersionRange = + Set.fromList . \case + VersionRange l Unbounded -> [l ..] + VersionRange l (VersionUpperBound u) -> init [l .. u] + +-- | For a version range of a local backend and for a set of versions that a +-- remote backend supports, compute the newest version supported by both. The +-- remote versions are given as integers as the range of versions supported by +-- the remote backend can include a version unknown to the local backend. If +-- there is no version in common, the return value is 'Nothing'. +latestCommonVersion :: Foldable f => VersionRange -> f Int -> Maybe Version +latestCommonVersion localVersions = + safeMaximum + . filter (inVersionRange localVersions) + . mapMaybe intToVersion + . toList + +safeMaximum :: Ord a => [a] -> Maybe a +safeMaximum [] = Nothing +safeMaximum as = Just (maximum as) $(genSingletons [''Version]) diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/ConversationUpdate.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/ConversationUpdate.hs index 3e8635f9851..568e6533b67 100644 --- a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/ConversationUpdate.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/ConversationUpdate.hs @@ -16,7 +16,9 @@ -- with this program. If not, see . module Test.Wire.API.Federation.Golden.ConversationUpdate - ( testObject_ConversationUpdate1, + ( testObject_ConversationUpdate1V0, + testObject_ConversationUpdate2V0, + testObject_ConversationUpdate1, testObject_ConversationUpdate2, ) where @@ -31,7 +33,7 @@ import Imports import Wire.API.Conversation import Wire.API.Conversation.Action import Wire.API.Conversation.Role (roleNameWireAdmin) -import Wire.API.Federation.API.Galley (ConversationUpdate (..)) +import Wire.API.Federation.API.Galley qAlice, qBob :: Qualified UserId qAlice = @@ -47,9 +49,9 @@ chad, dee :: UserId chad = Id (fromJust (UUID.fromString "00000fff-0000-0000-0000-000100005007")) dee = Id (fromJust (UUID.fromString "00000fff-0000-aaaa-0000-000100005007")) -testObject_ConversationUpdate1 :: ConversationUpdate -testObject_ConversationUpdate1 = - ConversationUpdate +testObject_ConversationUpdate1V0 :: ConversationUpdateV0 +testObject_ConversationUpdate1V0 = + ConversationUpdateV0 { cuTime = read "1864-04-12 12:22:43.673 UTC", cuOrigUserId = Qualified @@ -61,9 +63,9 @@ testObject_ConversationUpdate1 = cuAction = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (qAlice :| [qBob]) roleNameWireAdmin) } -testObject_ConversationUpdate2 :: ConversationUpdate -testObject_ConversationUpdate2 = - ConversationUpdate +testObject_ConversationUpdate2V0 :: ConversationUpdateV0 +testObject_ConversationUpdate2V0 = + ConversationUpdateV0 { cuTime = read "1864-04-12 12:22:43.673 UTC", cuOrigUserId = Qualified @@ -74,3 +76,31 @@ testObject_ConversationUpdate2 = cuAlreadyPresentUsers = [chad, dee], cuAction = SomeConversationAction (sing @'ConversationLeaveTag) () } + +testObject_ConversationUpdate1 :: ConversationUpdate +testObject_ConversationUpdate1 = + ConversationUpdate + { time = read "1864-04-12 12:22:43.673 UTC", + origUserId = + Qualified + (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000007"))) + (Domain "golden.example.com"), + convId = + Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000006")), + alreadyPresentUsers = [], + action = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (qAlice :| [qBob]) roleNameWireAdmin) + } + +testObject_ConversationUpdate2 :: ConversationUpdate +testObject_ConversationUpdate2 = + ConversationUpdate + { time = read "1864-04-12 12:22:43.673 UTC", + origUserId = + Qualified + (Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000007"))) + (Domain "golden.example.com"), + convId = + Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000006")), + alreadyPresentUsers = [chad, dee], + action = SomeConversationAction (sing @'ConversationLeaveTag) () + } diff --git a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GoldenSpec.hs b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GoldenSpec.hs index b436775494e..b691cd8e962 100644 --- a/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GoldenSpec.hs +++ b/libs/wire-api-federation/test/Test/Wire/API/Federation/Golden/GoldenSpec.hs @@ -46,6 +46,10 @@ spec = (MLSMessageSendingStatus.testObject_MLSMessageSendingStatus3, "testObject_MLSMessageSendingStatus3.json") ] testObjects [(LeaveConversationRequest.testObject_LeaveConversationRequest1, "testObject_LeaveConversationRequest1.json")] + testObjects + [ (ConversationUpdate.testObject_ConversationUpdate1V0, "testObject_ConversationUpdate1V0.json"), + (ConversationUpdate.testObject_ConversationUpdate2V0, "testObject_ConversationUpdate2V0.json") + ] testObjects [ (ConversationUpdate.testObject_ConversationUpdate1, "testObject_ConversationUpdate1.json"), (ConversationUpdate.testObject_ConversationUpdate2, "testObject_ConversationUpdate2.json") diff --git a/libs/wire-api-federation/test/golden/testObject_ConversationUpdate1.json b/libs/wire-api-federation/test/golden/testObject_ConversationUpdate1.json index 0c5ff9a27f2..a559d4197e5 100644 --- a/libs/wire-api-federation/test/golden/testObject_ConversationUpdate1.json +++ b/libs/wire-api-federation/test/golden/testObject_ConversationUpdate1.json @@ -1,5 +1,5 @@ { - "cuAction": { + "action": { "action": { "role": "wire_admin", "users": [ @@ -15,11 +15,11 @@ }, "tag": "ConversationJoinTag" }, - "cuAlreadyPresentUsers": [], - "cuConvId": "00000000-0000-0000-0000-000100000006", - "cuOrigUserId": { + "alreadyPresentUsers": [], + "convId": "00000000-0000-0000-0000-000100000006", + "origUserId": { "domain": "golden.example.com", "id": "00000000-0000-0000-0000-000100000007" }, - "cuTime": "1864-04-12T12:22:43.673Z" -} \ No newline at end of file + "time": "1864-04-12T12:22:43.673Z" +} diff --git a/libs/wire-api-federation/test/golden/testObject_ConversationUpdate1V0.json b/libs/wire-api-federation/test/golden/testObject_ConversationUpdate1V0.json new file mode 100644 index 00000000000..89e99c41c09 --- /dev/null +++ b/libs/wire-api-federation/test/golden/testObject_ConversationUpdate1V0.json @@ -0,0 +1,26 @@ +{ + "cuAction": { + "action": { + "role": "wire_admin", + "users": [ + { + "domain": "golden.example.com", + "id": "00000000-0000-0000-0000-000100004007" + }, + { + "domain": "golden2.example.com", + "id": "00000000-0000-0000-0000-000100005007" + } + ] + }, + "tag": "ConversationJoinTag" + }, + "cuAlreadyPresentUsers": [], + "cuConvId": "00000000-0000-0000-0000-000100000006", + "cuOrigUserId": { + "domain": "golden.example.com", + "id": "00000000-0000-0000-0000-000100000007" + }, + "cuTime": "1864-04-12T12:22:43.673Z" +} + diff --git a/libs/wire-api-federation/test/golden/testObject_ConversationUpdate2.json b/libs/wire-api-federation/test/golden/testObject_ConversationUpdate2.json index 8b443934beb..fea5fc43ecb 100644 --- a/libs/wire-api-federation/test/golden/testObject_ConversationUpdate2.json +++ b/libs/wire-api-federation/test/golden/testObject_ConversationUpdate2.json @@ -1,16 +1,16 @@ { - "cuAction": { + "action": { "action": {}, "tag": "ConversationLeaveTag" }, - "cuAlreadyPresentUsers": [ + "alreadyPresentUsers": [ "00000fff-0000-0000-0000-000100005007", "00000fff-0000-aaaa-0000-000100005007" ], - "cuConvId": "00000000-0000-0000-0000-000100000006", - "cuOrigUserId": { + "convId": "00000000-0000-0000-0000-000100000006", + "origUserId": { "domain": "golden.example.com", "id": "00000000-0000-0000-0000-000100000007" }, - "cuTime": "1864-04-12T12:22:43.673Z" -} \ No newline at end of file + "time": "1864-04-12T12:22:43.673Z" +} diff --git a/libs/wire-api-federation/test/golden/testObject_ConversationUpdate2V0.json b/libs/wire-api-federation/test/golden/testObject_ConversationUpdate2V0.json new file mode 100644 index 00000000000..df533d7bad9 --- /dev/null +++ b/libs/wire-api-federation/test/golden/testObject_ConversationUpdate2V0.json @@ -0,0 +1,17 @@ +{ + "cuAction": { + "action": {}, + "tag": "ConversationLeaveTag" + }, + "cuAlreadyPresentUsers": [ + "00000fff-0000-0000-0000-000100005007", + "00000fff-0000-aaaa-0000-000100005007" + ], + "cuConvId": "00000000-0000-0000-0000-000100000006", + "cuOrigUserId": { + "domain": "golden.example.com", + "id": "00000000-0000-0000-0000-000100000007" + }, + "cuTime": "1864-04-12T12:22:43.673Z" +} + diff --git a/libs/wire-api-federation/wire-api-federation.cabal b/libs/wire-api-federation/wire-api-federation.cabal index 3cee60745bb..e2490419ca0 100644 --- a/libs/wire-api-federation/wire-api-federation.cabal +++ b/libs/wire-api-federation/wire-api-federation.cabal @@ -23,6 +23,7 @@ library Wire.API.Federation.API.Common Wire.API.Federation.API.Galley Wire.API.Federation.API.Galley.Notifications + Wire.API.Federation.API.Util Wire.API.Federation.BackendNotifications Wire.API.Federation.Client Wire.API.Federation.Component diff --git a/services/background-worker/background-worker.cabal b/services/background-worker/background-worker.cabal index 1eb4df1229d..36e566299da 100644 --- a/services/background-worker/background-worker.cabal +++ b/services/background-worker/background-worker.cabal @@ -29,6 +29,7 @@ library build-depends: aeson , amqp + , base , containers , exceptions , extended @@ -49,6 +50,7 @@ library , types-common , unliftio , wai-utilities + , wire-api , wire-api-federation default-extensions: @@ -176,6 +178,7 @@ test-suite background-worker-test , base , bytestring , containers + , data-default , extended , federator , hspec diff --git a/services/background-worker/default.nix b/services/background-worker/default.nix index 910b9a396dd..31ce1fae0eb 100644 --- a/services/background-worker/default.nix +++ b/services/background-worker/default.nix @@ -8,6 +8,7 @@ , base , bytestring , containers +, data-default , exceptions , extended , federator @@ -50,6 +51,7 @@ mkDerivation { libraryHaskellDepends = [ aeson amqp + base containers exceptions extended @@ -70,6 +72,7 @@ mkDerivation { types-common unliftio wai-utilities + wire-api wire-api-federation ]; executableHaskellDepends = [ HsOpenSSL imports types-common ]; @@ -79,6 +82,7 @@ mkDerivation { base bytestring containers + data-default extended federator hspec diff --git a/services/background-worker/src/Wire/BackendNotificationPusher.hs b/services/background-worker/src/Wire/BackendNotificationPusher.hs index 1fb6721eb58..7dfad1390f1 100644 --- a/services/background-worker/src/Wire/BackendNotificationPusher.hs +++ b/services/background-worker/src/Wire/BackendNotificationPusher.hs @@ -3,11 +3,13 @@ module Wire.BackendNotificationPusher where +import Control.Arrow import Control.Monad.Catch import Control.Retry import Data.Aeson qualified as A import Data.Domain import Data.Id +import Data.List.NonEmpty qualified as NE import Data.Map.Strict qualified as Map import Data.Set qualified as Set import Data.Text qualified as Text @@ -19,8 +21,12 @@ import Network.RabbitMqAdmin import Prometheus import System.Logger.Class qualified as Log import UnliftIO +import Wire.API.Federation.API import Wire.API.Federation.BackendNotifications import Wire.API.Federation.Client +import Wire.API.Federation.Error +import Wire.API.Federation.Version +import Wire.API.RawJson import Wire.BackgroundWorker.Env import Wire.BackgroundWorker.Options import Wire.BackgroundWorker.Util @@ -78,32 +84,115 @@ pushNotification runningFlag targetDomain (msg, envelope) = do UnliftIO.bracket_ (takeMVar runningFlag) (putMVar runningFlag ()) go where go :: AppT IO () - go = case A.eitherDecode @BackendNotification (Q.msgBody msg) of + go = case A.eitherDecode @(PayloadBundle _) (Q.msgBody msg) of Left e -> do - Log.err $ - Log.msg (Log.val "Failed to parse notification, the notification will be ignored") - . Log.field "domain" (domainText targetDomain) - . Log.field "error" e + case A.eitherDecode @BackendNotification (Q.msgBody msg) of + Left eBN -> do + Log.err $ + Log.msg + ( Log.val "Cannot parse a queued message as s notification " + <> "nor as a bundle; the message will be ignored" + ) + . Log.field "domain" (domainText targetDomain) + . Log.field "error-notification" eBN + . Log.field + "error-bundle" + e + -- FUTUREWORK: This rejects the message without any requeueing. This is + -- dangerous as it could happen that a new type of notification is + -- introduced and an old instance of this worker is running, in which case + -- the notification will just get dropped. On the other hand not dropping + -- this message blocks the whole queue. Perhaps there is a better way to + -- deal with this. + lift $ reject envelope False + Right notif -> do + -- FUTUREWORK: Drop support for parsing it as a + -- single notification as soon as we can guarantee + -- that the message queue does not contain any + -- 'BackendNotification's anymore. + ceFederator <- asks (.federatorInternal) + ceHttp2Manager <- asks http2Manager + let ceOriginDomain = notif.ownDomain + ceTargetDomain = targetDomain + ceOriginRequestId = fromMaybe (RequestId "N/A") notif.requestId + cveEnv = FederatorClientEnv {..} + cveVersion = Just V0 -- V0 is assumed for non-versioned queue messages + fcEnv = FederatorClientVersionedEnv {..} + sendNotificationIgnoringVersionMismatch fcEnv notif.targetComponent notif.path notif.body + lift $ ack envelope + metrics <- asks backendNotificationMetrics + withLabel metrics.pushedCounter (domainText targetDomain) incCounter + withLabel metrics.stuckQueuesGauge (domainText targetDomain) (flip setGauge 0) + Right bundle -> do + federator <- asks (.federatorInternal) + manager <- asks http2Manager + let env = + FederatorClientEnv + { ceOriginDomain = ownDomain . NE.head $ bundle.notifications, + ceTargetDomain = targetDomain, + ceFederator = federator, + ceHttp2Manager = manager, + ceOriginRequestId = + fromMaybe (RequestId "N/A") . (.requestId) . NE.head $ bundle.notifications + } + remoteVersions :: Set Int <- + liftIO + -- use versioned client with no version set: since we are manually + -- performing version negotiation, we don't want the client to + -- negotiate a version for us + ( runVersionedFederatorClient @'Brig (unversionedEnv env) $ + fedClientIn @'Brig @"api-version" () + ) + >>= \case + Left e -> do + Log.err $ + Log.msg (Log.val "Failed to get supported API versions") + . Log.field "domain" (domainText targetDomain) + . Log.field "error" (displayException e) + throwM e + Right vi -> pure . Set.fromList . vinfoSupported $ vi - -- FUTUREWORK: This rejects the message without any requeueing. This is - -- dangerous as it could happen that a new type of notification is - -- introduced and an old instance of this worker is running, in which case - -- the notification will just get dropped. On the other hand not dropping - -- this message blocks the whole queue. Perhaps there is a better way to - -- deal with this. - lift $ reject envelope False - Right notif -> do - ceFederator <- asks (.federatorInternal) - ceHttp2Manager <- asks http2Manager - let ceOriginDomain = notif.ownDomain - ceTargetDomain = targetDomain - ceOriginRequestId = fromMaybe (RequestId "N/A") notif.requestId - fcEnv = FederatorClientEnv {..} - liftIO $ either throwM pure =<< sendNotification fcEnv notif.targetComponent notif.path notif.body - lift $ ack envelope - metrics <- asks backendNotificationMetrics - withLabel metrics.pushedCounter (domainText targetDomain) incCounter - withLabel metrics.stuckQueuesGauge (domainText targetDomain) (flip setGauge 0) + -- compute the best usable version in a notification + let bestVersion = bodyVersions >=> flip latestCommonVersion remoteVersions + case pairedMaximumOn bestVersion (toList (notifications bundle)) of + (_, Nothing) -> + Log.fatal $ + Log.msg (Log.val "No federation API version in common, the notification will be ignored") + . Log.field "domain" (domainText targetDomain) + (notif, cveVersion) -> do + ceFederator <- asks (.federatorInternal) + ceHttp2Manager <- asks http2Manager + let ceOriginDomain = notif.ownDomain + ceTargetDomain = targetDomain + ceOriginRequestId = fromMaybe (RequestId "N/A") notif.requestId + cveEnv = FederatorClientEnv {..} + fcEnv = FederatorClientVersionedEnv {..} + sendNotificationIgnoringVersionMismatch fcEnv notif.targetComponent notif.path notif.body + lift $ ack envelope + metrics <- asks backendNotificationMetrics + withLabel metrics.pushedCounter (domainText targetDomain) incCounter + withLabel metrics.stuckQueuesGauge (domainText targetDomain) (flip setGauge 0) + +sendNotificationIgnoringVersionMismatch :: + FederatorClientVersionedEnv -> + Component -> + Text -> + RawJson -> + AppT IO () +sendNotificationIgnoringVersionMismatch env comp path body = + liftIO (sendNotification env comp path body) >>= \case + Left (FederatorClientVersionNegotiationError v) -> do + Log.fatal $ + Log.msg (Log.val "Federator version negotiation error") + . Log.field "domain" (domainText env.cveEnv.ceTargetDomain) + . Log.field "error" (show v) + pure () + Left e -> throwM e + Right () -> pure () + +-- | Find the pair that maximises b. +pairedMaximumOn :: Ord b => (a -> b) -> [a] -> (a, b) +pairedMaximumOn f = maximumBy (compare `on` snd) . map (id &&& f) -- FUTUREWORK: Recosider using 1 channel for many consumers. It shouldn't matter -- for a handful of remote domains. diff --git a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs index 2a458b6990e..472f02d1f2e 100644 --- a/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs +++ b/services/background-worker/test/Test/Wire/BackendNotificationPusherSpec.hs @@ -9,6 +9,7 @@ import Control.Monad.Trans.Except import Data.Aeson qualified as Aeson import Data.ByteString.Builder qualified as Builder import Data.ByteString.Lazy qualified as LBS +import Data.Default import Data.Domain import Data.Id import Data.Range @@ -37,9 +38,11 @@ import Test.QuickCheck import Test.Wire.Util import UnliftIO.Async import Util.Options +import Wire.API.Conversation.Action import Wire.API.Federation.API import Wire.API.Federation.API.Brig import Wire.API.Federation.API.Common +import Wire.API.Federation.API.Galley import Wire.API.Federation.BackendNotifications import Wire.API.RawJson import Wire.BackendNotificationPusher @@ -51,7 +54,6 @@ spec :: Spec spec = do describe "pushNotification" $ do it "should push notifications" $ do - let returnSuccess _ = pure ("application/json", Aeson.encode EmptyResponse) let origDomain = Domain "origin.example.com" targetDomain = Domain "target.example.com" -- Just using 'arbitrary' could generate a very big list, making tests very @@ -64,6 +66,7 @@ spec = do ownDomain = origDomain, path = "/on-user-deleted-connections", body = RawJson $ Aeson.encode notifContent, + bodyVersions = Nothing, requestId = Just $ RequestId "N/A" } envelope <- newMockEnvelope @@ -74,7 +77,7 @@ spec = do } runningFlag <- newMVar () (env, fedReqs) <- - withTempMockFederator [] returnSuccess . runTestAppT $ do + withTempMockFederator def . runTestAppT $ do wait =<< pushNotification runningFlag targetDomain (msg, envelope) ask @@ -92,8 +95,88 @@ spec = do getVectorWith env.backendNotificationMetrics.pushedCounter getCounter `shouldReturn` [(domainText targetDomain, 1)] + it "should push notification bundles" $ do + let origDomain = Domain "origin.example.com" + targetDomain = Domain "target.example.com" + -- Just using 'arbitrary' could generate a very big list, making tests very + -- slow. Make me wonder if notification pusher should even try to parse the + -- actual content, seems like wasted compute power. + notifContent <- + generate $ + ClientRemovedRequest <$> arbitrary <*> arbitrary <*> arbitrary + let bundle = toBundle @'OnClientRemovedTag (RequestId "N/A") origDomain notifContent + envelope <- newMockEnvelope + let msg = + Q.newMsg + { Q.msgBody = Aeson.encode bundle, + Q.msgContentType = Just "application/json" + } + runningFlag <- newMVar () + (env, fedReqs) <- + withTempMockFederator def . runTestAppT $ do + wait =<< pushNotification runningFlag targetDomain (msg, envelope) + ask + + readIORef envelope.acks `shouldReturn` 1 + readIORef envelope.rejections `shouldReturn` [] + fedReqs + `shouldBe` [ FederatedRequest + { frTargetDomain = targetDomain, + frOriginDomain = origDomain, + frComponent = Galley, + frRPC = "on-client-removed", + frBody = Aeson.encode notifContent + } + ] + getVectorWith env.backendNotificationMetrics.pushedCounter getCounter + `shouldReturn` [(domainText targetDomain, 1)] + + it "should negotiate the best version" $ do + let origDomain = Domain "origin.example.com" + targetDomain = Domain "target.example.com" + update <- generate $ do + now <- arbitrary + user <- arbitrary + convId <- arbitrary + pure + ConversationUpdate + { time = now, + origUserId = user, + convId = convId, + alreadyPresentUsers = [], + action = SomeConversationAction SConversationLeaveTag () + } + let update0 = conversationUpdateToV0 update + let bundle = + toBundle (RequestId "N/A") origDomain update + <> toBundle (RequestId "N/A") origDomain update0 + envelope <- newMockEnvelope + let msg = + Q.newMsg + { Q.msgBody = Aeson.encode bundle, + Q.msgContentType = Just "application/json" + } + runningFlag <- newMVar () + (env, fedReqs) <- + withTempMockFederator def {versions = [0, 2]} . runTestAppT $ do + wait =<< pushNotification runningFlag targetDomain (msg, envelope) + ask + + readIORef envelope.acks `shouldReturn` 1 + readIORef envelope.rejections `shouldReturn` [] + fedReqs + `shouldBe` [ FederatedRequest + { frTargetDomain = targetDomain, + frOriginDomain = origDomain, + frComponent = Galley, + frRPC = "on-conversation-updated", + frBody = Aeson.encode update0 + } + ] + getVectorWith env.backendNotificationMetrics.pushedCounter getCounter + `shouldReturn` [(domainText targetDomain, 1)] + it "should reject invalid notifications" $ do - let returnSuccess _ = pure ("application/json", Aeson.encode EmptyResponse) envelope <- newMockEnvelope let msg = Q.newMsg @@ -102,7 +185,7 @@ spec = do } runningFlag <- newMVar () (env, fedReqs) <- - withTempMockFederator [] returnSuccess . runTestAppT $ do + withTempMockFederator def . runTestAppT $ do wait =<< pushNotification runningFlag (Domain "target.example.com") (msg, envelope) ask @@ -131,6 +214,7 @@ spec = do ownDomain = origDomain, path = "/on-user-deleted-connections", body = RawJson $ Aeson.encode notifContent, + bodyVersions = Nothing, requestId = Just $ RequestId "N/A" } envelope <- newMockEnvelope @@ -142,7 +226,7 @@ spec = do runningFlag <- newMVar () env <- testEnv pushThread <- - async $ withTempMockFederator [] mockRemote . runTestAppTWithEnv env $ do + async $ withTempMockFederator def {handler = mockRemote} . runTestAppTWithEnv env $ do wait =<< pushNotification runningFlag targetDomain (msg, envelope) -- Wait for two calls, so we can be sure that the metric about stuck diff --git a/services/brig/src/Brig/Federation/Client.hs b/services/brig/src/Brig/Federation/Client.hs index ad697843719..d86f706169f 100644 --- a/services/brig/src/Brig/Federation/Client.hs +++ b/services/brig/src/Brig/Federation/Client.hs @@ -157,8 +157,7 @@ notifyUserDeleted self remotes = do view rabbitmqChannel >>= \case Just chanVar -> do enqueueNotification (tDomain self) remoteDomain Q.Persistent chanVar $ - void $ - fedQueueClient @'OnUserDeletedConnectionsTag notif + fedQueueClient @'OnUserDeletedConnectionsTag notif Nothing -> Log.err $ Log.msg ("Federation error while notifying remote backends of a user deletion." :: ByteString) diff --git a/services/brig/test/integration/API/Federation.hs b/services/brig/test/integration/API/Federation.hs index 2f0def2baef..3b5829f1c5a 100644 --- a/services/brig/test/integration/API/Federation.hs +++ b/services/brig/test/integration/API/Federation.hs @@ -373,4 +373,4 @@ testGetUserClientsNotFound fedBrigClient = do testAPIVersion :: Brig -> FedClient 'Brig -> Http () testAPIVersion _brig fedBrigClient = do vinfo <- runFedClient @"api-version" fedBrigClient (Domain "far-away.example.com") () - liftIO $ vinfoSupported vinfo @?= toList supportedVersions + liftIO $ vinfoSupported vinfo @?= map versionInt (toList supportedVersions) diff --git a/services/brig/test/integration/Federation/Util.hs b/services/brig/test/integration/Federation/Util.hs index cb0eb3e35bb..4a1376e8686 100644 --- a/services/brig/test/integration/Federation/Util.hs +++ b/services/brig/test/integration/Federation/Util.hs @@ -35,6 +35,7 @@ import Data.Aeson (FromJSON, Value, decode, (.=)) import Data.Aeson qualified as Aeson import Data.ByteString qualified as BS import Data.ByteString.Conversion (toByteString') +import Data.Default import Data.Domain (Domain (Domain)) import Data.Handle (fromHandle) import Data.Id @@ -79,8 +80,7 @@ import Wire.API.User.Client.Prekey withTempMockFederator :: Opt.Opts -> LByteString -> Session a -> IO (a, [Mock.FederatedRequest]) withTempMockFederator opts resp action = Mock.withTempMockFederator - [("Content-Type", "application/json")] - (const (pure ("application" // "json", resp))) + def {Mock.handler = const (pure ("application" // "json", resp))} $ \mockPort -> do let opts' = opts diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index 91cd6b674b6..46b8aa917c3 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -50,6 +50,7 @@ import Data.ByteString.Builder (toLazyByteString) import Data.ByteString.Char8 (pack) import Data.ByteString.Char8 qualified as B8 import Data.ByteString.Conversion +import Data.Default import Data.Domain (Domain (..), domainText, mkDomain) import Data.Handle (Handle (..)) import Data.Id @@ -1231,8 +1232,7 @@ withMockedFederatorAndGalley opts _domain fedResp galleyHandler action = do result <- assertRight <=< runExceptT $ withTempMockedService initState galleyHandler $ \galleyMockState -> Mock.withTempMockFederator - [("Content-Type", "application/json")] - ((\r -> pure ("application" // "json", r)) <=< fedResp) + def {Mock.handler = (\r -> pure ("application" // "json", r)) <=< fedResp} $ \fedMockPort -> do let opts' = opts diff --git a/services/cargohold/cargohold.cabal b/services/cargohold/cargohold.cabal index 7723b5814f9..f3c5ad95c44 100644 --- a/services/cargohold/cargohold.cabal +++ b/services/cargohold/cargohold.cabal @@ -269,6 +269,7 @@ executable cargohold-integration , cargohold , cargohold-types , containers + , data-default , federator , http-api-data , http-client >=0.7 diff --git a/services/cargohold/default.nix b/services/cargohold/default.nix index 9c9c13d493b..58b2e770a30 100644 --- a/services/cargohold/default.nix +++ b/services/cargohold/default.nix @@ -19,6 +19,7 @@ , conduit-extra , containers , crypton +, data-default , errors , exceptions , extended @@ -138,6 +139,7 @@ mkDerivation { bytestring-conversion cargohold-types containers + data-default federator HsOpenSSL http-api-data diff --git a/services/cargohold/test/integration/API/Util.hs b/services/cargohold/test/integration/API/Util.hs index 1c9e1057248..2c51dc9b29f 100644 --- a/services/cargohold/test/integration/API/Util.hs +++ b/services/cargohold/test/integration/API/Util.hs @@ -42,6 +42,7 @@ import Data.ByteString.Builder import qualified Data.ByteString.Char8 as C import Data.ByteString.Conversion import qualified Data.ByteString.Lazy as Lazy +import Data.Default import Data.Id import Data.Qualified import Data.Text.Encoding (decodeLatin1, encodeUtf8) @@ -223,7 +224,7 @@ withMockFederator :: TestM a -> TestM (a, [FederatedRequest]) withMockFederator respond action = do - withTempMockFederator [] respond $ \p -> + withTempMockFederator def {handler = respond} $ \p -> withSettingsOverrides (federator . _Just %~ setLocalEndpoint (fromIntegral p)) action diff --git a/services/federator/default.nix b/services/federator/default.nix index 0871b678a85..af4aa3d502b 100644 --- a/services/federator/default.nix +++ b/services/federator/default.nix @@ -88,6 +88,7 @@ mkDerivation { containers crypton-x509 crypton-x509-validation + data-default dns dns-util exceptions diff --git a/services/federator/federator.cabal b/services/federator/federator.cabal index fe6fa823004..dec1a6d01e1 100644 --- a/services/federator/federator.cabal +++ b/services/federator/federator.cabal @@ -115,6 +115,7 @@ library , containers , crypton-x509 , crypton-x509-validation + , data-default , dns , dns-util , exceptions diff --git a/services/federator/src/Federator/MockServer.hs b/services/federator/src/Federator/MockServer.hs index 81b657d9760..463967531ba 100644 --- a/services/federator/src/Federator/MockServer.hs +++ b/services/federator/src/Federator/MockServer.hs @@ -19,6 +19,7 @@ module Federator.MockServer ( -- * Federator mock server + MockFederator (..), MockException (..), withTempMockFederator, FederatedRequest (..), @@ -44,6 +45,7 @@ import Control.Monad.Catch hiding (fromException) import Control.Monad.Trans.Except import Control.Monad.Trans.Maybe import Data.Aeson qualified as Aeson +import Data.Default import Data.Domain import Data.Text qualified as Text import Data.Text.Lazy qualified as LText @@ -66,6 +68,7 @@ import Servant.API import Servant.Server (Tagged (..)) import Servant.Server.Generic import Wire.API.Federation.API (Component) +import Wire.API.Federation.API.Common import Wire.API.Federation.Domain import Wire.API.Federation.Version import Wire.Sem.Logger.TinyLog @@ -104,16 +107,15 @@ mockServer :: Member (Error ValidationError) r ) => IORef [FederatedRequest] -> - [HTTP.Header] -> - (FederatedRequest -> IO (HTTP.MediaType, LByteString)) -> + MockFederator -> (Sem r Wai.Response -> IO Wai.Response) -> API AsServer -mockServer remoteCalls headers resp interpreter = +mockServer remoteCalls mock interpreter = Federator.InternalServer.API { status = const $ pure NoContent, internalRequest = \_mReqId targetDomain component rpc -> Tagged $ \req respond -> - respond =<< interpreter (mockInternalRequest remoteCalls headers resp targetDomain component rpc req) + respond =<< interpreter (mockInternalRequest remoteCalls mock targetDomain component rpc req) } mockInternalRequest :: @@ -123,14 +125,13 @@ mockInternalRequest :: Member (Error ValidationError) r ) => IORef [FederatedRequest] -> - [HTTP.Header] -> - (FederatedRequest -> IO (HTTP.MediaType, LByteString)) -> + MockFederator -> Domain -> Component -> RPC -> Wai.Request -> Sem r Wai.Response -mockInternalRequest remoteCalls headers resp targetDomain component (RPC path) req = do +mockInternalRequest remoteCalls mock targetDomain component (RPC path) req = do domainTxt <- note NoOriginDomain $ lookup originDomainHeaderName (Wai.requestHeaders req) originDomain <- parseDomain domainTxt reqBody <- embed $ Wai.lazyRequestBody req @@ -145,20 +146,34 @@ mockInternalRequest remoteCalls headers resp targetDomain component (RPC path) r ) (ct, resBody) <- if path == "api-version" - then pure ("application/json", Aeson.encode versionInfo) + then pure ("application/json", Aeson.encode (VersionInfo mock.versions)) else do modifyIORef remoteCalls (<> [fedRequest]) fromException @MockException . handle (throw . handleException) - $ resp fedRequest - let headers' = ("Content-Type", HTTP.renderHeader ct) : headers - pure $ Wai.responseLBS HTTP.status200 headers' resBody + $ mock.handler fedRequest + let headers = ("Content-Type", HTTP.renderHeader ct) : mock.headers + pure $ Wai.responseLBS HTTP.status200 headers resBody where handleException :: SomeException -> MockException handleException e = case Exception.fromException e of Just mockE -> mockE Nothing -> MockErrorResponse HTTP.status500 (LText.pack (displayException e)) +data MockFederator = MockFederator + { headers :: [HTTP.Header], + handler :: FederatedRequest -> IO (HTTP.MediaType, LByteString), + versions :: [Int] + } + +instance Default MockFederator where + def = + MockFederator + { headers = [], + handler = \_ -> pure ("application/json", Aeson.encode EmptyResponse), + versions = map versionInt (toList supportedVersions) + } + -- | Spawn a mock federator on a random port and run an action while it is running. -- -- A mock federator is a web application that parses requests of the same form @@ -166,11 +181,10 @@ mockInternalRequest remoteCalls headers resp targetDomain component (RPC path) r -- forwarding them to a remote federator. withTempMockFederator :: (MonadIO m, MonadMask m) => - [HTTP.Header] -> - (FederatedRequest -> IO (HTTP.MediaType, LByteString)) -> + MockFederator -> (Warp.Port -> m a) -> m (a, [FederatedRequest]) -withTempMockFederator headers resp action = do +withTempMockFederator mock action = do remoteCalls <- newIORef [] let interpreter = runM @@ -180,7 +194,7 @@ withTempMockFederator headers resp action = do ServerError, MockException ] - app = genericServe (mockServer remoteCalls headers resp interpreter) + app = genericServe (mockServer remoteCalls mock interpreter) result <- bracket (liftIO (startMockServer Nothing app)) diff --git a/services/federator/test/unit/Test/Federator/Client.hs b/services/federator/test/unit/Test/Federator/Client.hs index 2b79f8ab2a1..6100252dbbe 100644 --- a/services/federator/test/unit/Test/Federator/Client.hs +++ b/services/federator/test/unit/Test/Federator/Client.hs @@ -26,6 +26,7 @@ import Data.Bifunctor (first) import Data.ByteString qualified as BS import Data.ByteString.Builder (Builder, byteString, toLazyByteString) import Data.ByteString.Lazy qualified as LBS +import Data.Default import Data.Domain import Data.Id import Data.Proxy @@ -60,9 +61,6 @@ targetDomain = Domain "target.example.com" originDomain :: Domain originDomain = Domain "origin.example.com" -defaultHeaders :: [HTTP.Header] -defaultHeaders = [("Content-Type", "application/json")] - tests :: TestTree tests = testGroup @@ -87,11 +85,10 @@ newtype ResponseFailure = ResponseFailure Wai.Error deriving (Show) withMockFederatorClient :: - [HTTP.Header] -> - (FederatedRequest -> IO (MediaType, LByteString)) -> + MockFederator -> FederatorClient c a -> IO (Either ResponseFailure a, [FederatedRequest]) -withMockFederatorClient headers resp action = withTempMockFederator headers resp $ \port -> do +withMockFederatorClient mock action = withTempMockFederator mock $ \port -> do mgr <- defaultHttp2Manager let env = FederatorClientEnv @@ -114,8 +111,7 @@ testClientSuccess = do (actualResponse, sentRequests) <- withMockFederatorClient - defaultHeaders - (const (pure ("application/json", Aeson.encode (Just expectedResponse)))) + def {handler = const (pure ("application/json", Aeson.encode (Just expectedResponse)))} $ fedClient @'Brig @"get-user-by-handle" handle sentRequests @@ -157,8 +153,7 @@ testClientFailure = do (actualResponse, _) <- withMockFederatorClient - defaultHeaders - (const (throw (MockErrorResponse HTTP.status422 "wrong domain"))) + def {handler = const (throw (MockErrorResponse HTTP.status422 "wrong domain"))} $ do fedClient @'Brig @"get-user-by-handle" handle @@ -174,8 +169,7 @@ testFederatorFailure = do (actualResponse, _) <- withMockFederatorClient - defaultHeaders - (const (throw (MockErrorResponse HTTP.status403 "invalid path"))) + def {handler = const (throw (MockErrorResponse HTTP.status403 "invalid path"))} $ do fedClient @'Brig @"get-user-by-handle" handle @@ -190,7 +184,7 @@ testClientExceptions = do handle <- generate arbitrary (response, _) <- - withMockFederatorClient defaultHeaders (const (evaluate (error "unhandled exception"))) $ + withMockFederatorClient def {handler = const (evaluate (error "unhandled exception"))} $ fedClient @'Brig @"get-user-by-handle" handle case response of @@ -218,8 +212,10 @@ testClientConnectionError = do testResponseHeaders :: IO () testResponseHeaders = do (r, _) <- withTempMockFederator - [("X-Foo", "bar")] - (const $ pure ("application" // "json", mempty)) + def + { headers = [("X-Foo", "bar")], + handler = const $ pure ("application" // "json", mempty) + } $ \port -> do let req = HTTP2.requestBuilder diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 0b373dc9574..063b985fd52 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -720,7 +720,6 @@ updateLocalConversation :: Member ExternalAccess r, Member NotificationSubsystem r, Member (Input UTCTime) r, - Member (Logger (Log.Msg -> Log.Msg)) r, HasConversationActionEffects tag r, SingI tag ) => @@ -760,7 +759,6 @@ updateLocalConversationUnchecked :: Member ExternalAccess r, Member NotificationSubsystem r, Member (Input UTCTime) r, - Member (Logger (Log.Msg -> Log.Msg)) r, HasConversationActionEffects tag r ) => Local Conversation -> @@ -861,9 +859,9 @@ notifyConversationAction :: forall tag r. ( Member BackendNotificationQueueAccess r, Member ExternalAccess r, + Member (Error FederationError) r, Member NotificationSubsystem r, - Member (Input UTCTime) r, - Member (Logger (Log.Msg -> Log.Msg)) r + Member (Input UTCTime) r ) => Sing tag -> Qualified UserId -> @@ -884,22 +882,19 @@ notifyConversationAction tag quid notifyOrigDomain con lconv targets action = do (tUnqualified lcnv) uids (SomeConversationAction tag action) - handleError :: FederationError -> Sem r (Maybe ConversationUpdate) - handleError fedErr = - logRemoteNotificationError @"on-conversation-updated" fedErr $> Nothing - update <- - fmap (fromMaybe (mkUpdate [])) - . (either handleError (pure . asum . map tUnqualified)) - <=< enqueueNotificationsConcurrently Q.Persistent (toList (bmRemotes targets)) - $ \ruids -> do - let update = mkUpdate (tUnqualified ruids) - -- if notifyOrigDomain is false, filter out user from quid's domain, - -- because quid's backend will update local state and notify its users - -- itself using the ConversationUpdate returned by this function - if notifyOrigDomain || tDomain ruids /= qDomain quid - then fedQueueClient @'OnConversationUpdatedTag update $> Nothing - else pure (Just update) + fmap (fromMaybe (mkUpdate []) . asum . map tUnqualified) $ + enqueueNotificationsConcurrently Q.Persistent (toList (bmRemotes targets)) $ + \ruids -> do + let update = mkUpdate (tUnqualified ruids) + -- if notifyOrigDomain is false, filter out user from quid's domain, + -- because quid's backend will update local state and notify its users + -- itself using the ConversationUpdate returned by this function + if notifyOrigDomain || tDomain ruids /= qDomain quid + then do + makeConversationUpdateBundle update >>= sendBundle + pure Nothing + else pure (Just update) -- notify local participants and bots pushConversationEvent con e (qualifyAs lcnv (bmLocals targets)) (bmBots targets) @@ -924,14 +919,14 @@ updateLocalStateOfRemoteConv :: updateLocalStateOfRemoteConv rcu con = do loc <- qualifyLocal () let cu = tUnqualified rcu - rconvId = fmap F.cuConvId rcu + rconvId = fmap (.convId) rcu qconvId = tUntagged rconvId -- Note: we generally do not send notifications to users that are not part of -- the conversation (from our point of view), to prevent spam from the remote -- backend. See also the comment below. (presentUsers, allUsersArePresent) <- - E.selectRemoteMembers (F.cuAlreadyPresentUsers cu) rconvId + E.selectRemoteMembers cu.alreadyPresentUsers rconvId -- Perform action, and determine extra notification targets. -- @@ -942,12 +937,12 @@ updateLocalStateOfRemoteConv rcu con = do -- updated, we do **not** add them to the list of targets, because we have no -- way to make sure that they are actually supposed to receive that notification. - (mActualAction, extraTargets) <- case F.cuAction cu of + (mActualAction, extraTargets) <- case cu.action of sca@(SomeConversationAction singTag action) -> case singTag of SConversationJoinTag -> do let ConversationJoin toAdd role = action let (localUsers, remoteUsers) = partitionQualified loc toAdd - addedLocalUsers <- Set.toList <$> addLocalUsersToRemoteConv rconvId (F.cuOrigUserId cu) localUsers + addedLocalUsers <- Set.toList <$> addLocalUsersToRemoteConv rconvId cu.origUserId localUsers let allAddedUsers = map (tUntagged . qualifyAs loc) addedLocalUsers <> map tUntagged remoteUsers pure $ ( fmap @@ -956,7 +951,7 @@ updateLocalStateOfRemoteConv rcu con = do addedLocalUsers ) SConversationLeaveTag -> do - let users = foldQualified loc (pure . tUnqualified) (const []) (F.cuOrigUserId cu) + let users = foldQualified loc (pure . tUnqualified) (const []) cu.origUserId E.deleteMembersInRemoteConversation rconvId users pure (Just sca, []) SConversationRemoveMembersTag -> do @@ -976,7 +971,7 @@ updateLocalStateOfRemoteConv rcu con = do unless allUsersArePresent $ P.warn $ - Log.field "conversation" (toByteString' (F.cuConvId cu)) + Log.field "conversation" (toByteString' cu.convId) . Log.field "domain" (toByteString' (tDomain rcu)) . Log.msg ( "Attempt to send notification about conversation update \ @@ -986,7 +981,7 @@ updateLocalStateOfRemoteConv rcu con = do -- Send notifications for mActualAction $ \(SomeConversationAction tag action) -> do - let event = conversationActionToEvent tag (F.cuTime cu) (F.cuOrigUserId cu) qconvId Nothing action + let event = conversationActionToEvent tag cu.time cu.origUserId qconvId Nothing action targets = nubOrd $ presentUsers <> extraTargets -- FUTUREWORK: support bots? pushConversationEvent con event (qualifyAs loc targets) [] $> event diff --git a/services/galley/src/Galley/API/Clients.hs b/services/galley/src/Galley/API/Clients.hs index 9ae38817dc8..cfb18cd320a 100644 --- a/services/galley/src/Galley/API/Clients.hs +++ b/services/galley/src/Galley/API/Clients.hs @@ -50,6 +50,7 @@ import Polysemy.TinyLog qualified as P import Wire.API.Conversation hiding (Member) import Wire.API.Federation.API import Wire.API.Federation.API.Galley +import Wire.API.Federation.Error import Wire.API.Routes.MultiTablePaging import Wire.NotificationSubsystem import Wire.Sem.Paging.Cassandra (CassandraPaging) @@ -91,23 +92,22 @@ addClientH (usr ::: clt) = do rmClientH :: forall p1 r. ( p1 ~ CassandraPaging, - ( Member ClientStore r, - Member ConversationStore r, - Member ExternalAccess r, - Member BackendNotificationQueueAccess r, - Member FederatorAccess r, - Member NotificationSubsystem r, - Member (Input Env) r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ListItems p1 ConvId) r, - Member (ListItems p1 (Remote ConvId)) r, - Member MemberStore r, - Member (Error InternalError) r, - Member ProposalStore r, - Member SubConversationStore r, - Member P.TinyLog r - ) + Member ClientStore r, + Member ConversationStore r, + Member (Error FederationError) r, + Member ExternalAccess r, + Member BackendNotificationQueueAccess r, + Member NotificationSubsystem r, + Member (Input Env) r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member (ListItems p1 ConvId) r, + Member (ListItems p1 (Remote ConvId)) r, + Member MemberStore r, + Member (Error InternalError) r, + Member ProposalStore r, + Member SubConversationStore r, + Member P.TinyLog r ) => UserId ::: ClientId -> Sem r Response @@ -138,5 +138,8 @@ rmClientH (usr ::: cid) = do removeRemoteMLSClients :: Range 1 1000 [Remote ConvId] -> Sem r () removeRemoteMLSClients convIds = do for_ (bucketRemote (fromRange convIds)) $ \remoteConvs -> - let rpc = void $ fedQueueClient @'OnClientRemovedTag (ClientRemovedRequest usr cid (tUnqualified remoteConvs)) - in enqueueNotification remoteConvs Q.Persistent rpc + let rpc = + fedQueueClient + @'OnClientRemovedTag + (ClientRemovedRequest usr cid (tUnqualified remoteConvs)) + in enqueueNotification Q.Persistent remoteConvs rpc diff --git a/services/galley/src/Galley/API/Create.hs b/services/galley/src/Galley/API/Create.hs index f255c0d9658..837a79dfd7e 100644 --- a/services/galley/src/Galley/API/Create.hs +++ b/services/galley/src/Galley/API/Create.hs @@ -91,7 +91,8 @@ import Wire.NotificationSubsystem -- | The public-facing endpoint for creating group conversations in the client -- API up to and including version 3. createGroupConversationUpToV3 :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (ErrorS 'ConvAccessDenied) r, Member (Error FederationError) r, @@ -129,7 +130,8 @@ createGroupConversationUpToV3 lusr conn newConv = mapError UnreachableBackendsLe -- | The public-facing endpoint for creating group conversations in the client -- API in version 4 and above. createGroupConversation :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (ErrorS 'ConvAccessDenied) r, Member (Error FederationError) r, @@ -169,7 +171,8 @@ createGroupConversation lusr conn newConv = do CreateGroupConversation conv mempty createGroupConversationGeneric :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (ErrorS 'ConvAccessDenied) r, Member (Error FederationError) r, @@ -309,7 +312,8 @@ createProteusSelfConversation lusr = do conversationCreated lusr c createOne2OneConversation :: - ( Member BrigAccess r, + ( Member BackendNotificationQueueAccess r, + Member BrigAccess r, Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -386,7 +390,8 @@ createOne2OneConversation lusr zcon j = Nothing -> throwS @'TeamNotFound createLegacyOne2OneConversationUnchecked :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, Member (Error InvalidInput) r, @@ -428,7 +433,8 @@ createLegacyOne2OneConversationUnchecked self zcon name mtid other = do Right () -> conversationCreated self c createOne2OneConversationUnchecked :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, Member (Error UnreachableBackends) r, @@ -452,7 +458,8 @@ createOne2OneConversationUnchecked self zcon name mtid other = do create (one2OneConvId BaseProtocolProteusTag (tUntagged self) other) self zcon name mtid other createOne2OneConversationLocally :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (Error FederationError) r, Member (Error InternalError) r, Member (Error UnreachableBackends) r, @@ -502,7 +509,8 @@ createOne2OneConversationRemotely _ _ _ _ _ _ = throw FederationNotImplemented createConnectConversation :: - ( Member ConversationStore r, + ( Member BackendNotificationQueueAccess r, + Member ConversationStore r, Member (ErrorS 'ConvNotFound) r, Member (Error FederationError) r, Member (Error InternalError) r, @@ -654,6 +662,7 @@ notifyCreatedConversation :: Member (Error UnreachableBackends) r, Member FederatorAccess r, Member NotificationSubsystem r, + Member BackendNotificationQueueAccess r, Member (Input UTCTime) r, Member P.TinyLog r ) => diff --git a/services/galley/src/Galley/API/Federation.hs b/services/galley/src/Galley/API/Federation.hs index 7e292c55aab..6bee0e21f14 100644 --- a/services/galley/src/Galley/API/Federation.hs +++ b/services/galley/src/Galley/API/Federation.hs @@ -84,7 +84,9 @@ import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Common (EmptyResponse (..)) import Wire.API.Federation.API.Galley +import Wire.API.Federation.Endpoint import Wire.API.Federation.Error +import Wire.API.Federation.Version import Wire.API.MLS.Credential import Wire.API.MLS.GroupInfo import Wire.API.MLS.Serialisation @@ -119,6 +121,7 @@ federationSitemap = :<|> Named @"on-client-removed" onClientRemoved :<|> Named @"on-message-sent" onMessageSent :<|> Named @"on-mls-message-sent" onMLSMessageSent + :<|> Named @(Versioned 'V0 "on-conversation-updated") onConversationUpdatedV0 :<|> Named @"on-conversation-updated" onConversationUpdated :<|> Named @"on-user-deleted-conversations" onUserDeleted @@ -126,6 +129,7 @@ onClientRemoved :: ( Member BackendNotificationQueueAccess r, Member ConversationStore r, Member ExternalAccess r, + Member (Error FederationError) r, Member NotificationSubsystem r, Member (Input Env) r, Member (Input (Local ())) r, @@ -225,6 +229,20 @@ onConversationUpdated requestingDomain cu = do void $ updateLocalStateOfRemoteConv rcu Nothing pure EmptyResponse +onConversationUpdatedV0 :: + ( Member BrigAccess r, + Member NotificationSubsystem r, + Member ExternalAccess r, + Member (Input (Local ())) r, + Member MemberStore r, + Member P.TinyLog r + ) => + Domain -> + ConversationUpdateV0 -> + Sem r EmptyResponse +onConversationUpdatedV0 domain cu = + onConversationUpdated domain (conversationUpdateFromV0 cu) + -- as of now this will not generate the necessary events on the leaver's domain leaveConversation :: ( Member BackendNotificationQueueAccess r, @@ -378,6 +396,7 @@ onUserDeleted :: ( Member BackendNotificationQueueAccess r, Member ConversationStore r, Member FireAndForget r, + Member (Error FederationError) r, Member ExternalAccess r, Member NotificationSubsystem r, Member (Input (Local ())) r, @@ -464,7 +483,7 @@ updateConversation origDomain updateRequest = do let rusr = toRemoteUnsafe origDomain updateRequest.user lcnv = qualifyAs loc updateRequest.convId - mkResponse $ case action updateRequest of + mkResponse $ case updateRequest.action of SomeConversationAction tag action -> case tag of SConversationJoinTag -> mapToGalleyError @(HasConversationActionGalleyErrors 'ConversationJoinTag) @@ -662,6 +681,7 @@ getSubConversationForRemoteUser domain GetSubConversationsRequest {..} = leaveSubConversation :: ( HasLeaveSubConversationEffects r, + Member (Error FederationError) r, Member (Input (Local ())) r, Member Resource r ) => diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 2e4d7435980..15d0107d009 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -56,7 +56,6 @@ import Galley.Effects import Galley.Effects.BackendNotificationQueueAccess import Galley.Effects.ClientStore import Galley.Effects.ConversationStore -import Galley.Effects.FederatorAccess import Galley.Effects.LegalHoldStore as LegalHoldStore import Galley.Effects.MemberStore qualified as E import Galley.Effects.TeamStore @@ -303,9 +302,9 @@ rmUser :: Member ClientStore r, Member ConversationStore r, Member (Error DynError) r, + Member (Error FederationError) r, Member (Error InternalError) r, Member ExternalAccess r, - Member FederatorAccess r, Member NotificationSubsystem r, Member (Input Env) r, Member (Input Opts) r, @@ -411,39 +410,22 @@ rmUser lusr conn = do notifyRemoteMembers now qUser cid remotes = do let convUpdate = ConversationUpdate - { cuTime = now, - cuOrigUserId = qUser, - cuConvId = cid, - cuAlreadyPresentUsers = tUnqualified remotes, - cuAction = SomeConversationAction (sing @'ConversationLeaveTag) () + { time = now, + origUserId = qUser, + convId = cid, + alreadyPresentUsers = tUnqualified remotes, + action = SomeConversationAction (sing @'ConversationLeaveTag) () } - let rpc = fedClient @'Galley @"on-conversation-updated" convUpdate - runFederatedEither remotes rpc - >>= logAndIgnoreError "Error in onConversationUpdated call" (qUnqualified qUser) + enqueueNotification Q.Persistent remotes $ do + makeConversationUpdateBundle convUpdate + >>= sendBundle leaveRemoteConversations :: Range 1 UserDeletedNotificationMaxConvs [Remote ConvId] -> Sem r () leaveRemoteConversations cids = for_ (bucketRemote (fromRange cids)) $ \remoteConvs -> do let userDelete = UserDeletedConversationsNotification (tUnqualified lusr) (unsafeRange (tUnqualified remoteConvs)) - let rpc = void $ fedQueueClient @'OnUserDeletedConversationsTag userDelete - enqueueNotification remoteConvs Q.Persistent rpc - - -- FUTUREWORK: Add a retry mechanism if there are federation errrors. - -- See https://wearezeta.atlassian.net/browse/SQCORE-1091 - logAndIgnoreError :: Text -> UserId -> Either FederationError a -> Sem r () - logAndIgnoreError message usr res = do - case res of - Left federationError -> - P.err - ( Log.msg - ( "Federation error while notifying remote backends of a user deletion (Galley). " - <> message - <> " " - <> (cs . show $ federationError) - ) - . Log.field "user" (show usr) - ) - Right _ -> pure () + let rpc = fedQueueClient @'OnUserDeletedConversationsTag userDelete + enqueueNotification Q.Persistent remoteConvs rpc deleteLoop :: App () deleteLoop = do diff --git a/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs index 907e9ecb36d..484f5812332 100644 --- a/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs @@ -41,6 +41,7 @@ import Polysemy.State import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Error.Galley +import Wire.API.Federation.Error import Wire.API.MLS.Commit import Wire.API.MLS.Credential import Wire.API.MLS.LeafNode @@ -121,7 +122,8 @@ getExternalCommitData senderIdentity lConvOrSub epoch commit = do processExternalCommit :: forall r. - ( Member (ErrorS 'MLSStaleMessage) r, + ( Member (Error FederationError) r, + Member (ErrorS 'MLSStaleMessage) r, Member (ErrorS 'MLSSubConvClientNotInParent) r, Member Resource r, HasProposalActionEffects r diff --git a/services/galley/src/Galley/API/MLS/Message.hs b/services/galley/src/Galley/API/MLS/Message.hs index 3afffb4d0a3..e49bd404de0 100644 --- a/services/galley/src/Galley/API/MLS/Message.hs +++ b/services/galley/src/Galley/API/MLS/Message.hs @@ -115,7 +115,6 @@ type MLSBundleStaticErrors = postMLSMessageFromLocalUser :: ( HasProposalEffects r, - Member (Error FederationError) r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'ConvMemberNotFound) r, Member (ErrorS 'ConvNotFound) r, @@ -149,7 +148,6 @@ postMLSMessageFromLocalUser lusr c conn smsg = do postMLSCommitBundle :: ( HasProposalEffects r, Members MLSBundleStaticErrors r, - Member (Error FederationError) r, Member Resource r, Member SubConversationStore r ) => @@ -171,7 +169,6 @@ postMLSCommitBundle loc qusr c ctype qConvOrSub conn bundle = postMLSCommitBundleFromLocalUser :: ( HasProposalEffects r, Members MLSBundleStaticErrors r, - Member (Error FederationError) r, Member Resource r, Member SubConversationStore r ) => @@ -318,7 +315,6 @@ postMLSCommitBundleToRemoteConv loc qusr c con bundle ctype rConvOrSubId = do postMLSMessage :: ( HasProposalEffects r, - Member (Error FederationError) r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'ConvMemberNotFound) r, Member (ErrorS 'ConvNotFound) r, @@ -417,7 +413,6 @@ postMLSMessageToLocalConv qusr c con msg ctype convOrSubId = do postMLSMessageToRemoteConv :: ( Members MLSMessageStaticErrors r, - Member (Error FederationError) r, HasProposalEffects r ) => Local x -> diff --git a/services/galley/src/Galley/API/MLS/Propagate.hs b/services/galley/src/Galley/API/MLS/Propagate.hs index 53efadec2dc..b0fe16e6c8c 100644 --- a/services/galley/src/Galley/API/MLS/Propagate.hs +++ b/services/galley/src/Galley/API/MLS/Propagate.hs @@ -27,7 +27,6 @@ import Data.Qualified import Data.Time import Galley.API.MLS.Types import Galley.API.Push -import Galley.API.Util import Galley.Data.Services import Galley.Effects import Galley.Effects.BackendNotificationQueueAccess @@ -36,11 +35,13 @@ import Gundeck.Types.Push.V2 (RecipientClients (..)) import Imports import Network.AMQP qualified as Q import Polysemy +import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog hiding (trace) import Wire.API.Event.Conversation import Wire.API.Federation.API import Wire.API.Federation.API.Galley +import Wire.API.Federation.Error import Wire.API.MLS.Credential import Wire.API.MLS.Message import Wire.API.MLS.Serialisation @@ -53,6 +54,7 @@ import Wire.NotificationSubsystem -- a requirement from Core Crypto and the clients. propagateMessage :: ( Member BackendNotificationQueueAccess r, + Member (Error FederationError) r, Member ExternalAccess r, Member (Input UTCTime) r, Member TinyLog r, @@ -87,21 +89,23 @@ propagateMessage qusr mSenderClient lConvOrSub con msg cm = do newMessagePush botMap con mm (lmems >>= toList . localMemberRecipient mlsConv) e -- send to remotes - (either (logRemoteNotificationError @"on-mls-message-sent") (const (pure ())) <=< enqueueNotificationsConcurrently Q.Persistent (map remoteMemberQualify rmems)) $ - \rs -> - fedQueueClient @'OnMLSMessageSentTag $ - RemoteMLSMessage - { time = now, - sender = qusr, - metadata = mm, - conversation = qUnqualified qcnv, - subConversation = sconv, - recipients = - Map.fromList $ - tUnqualified rs - >>= toList . remoteMemberMLSClients, - message = Base64ByteString msg.raw - } + void $ + enqueueNotificationsConcurrently Q.Persistent (map remoteMemberQualify rmems) $ + \rs -> + fedQueueClient + @'OnMLSMessageSentTag + RemoteMLSMessage + { time = now, + sender = qusr, + metadata = mm, + conversation = qUnqualified qcnv, + subConversation = sconv, + recipients = + Map.fromList $ + tUnqualified rs + >>= toList . remoteMemberMLSClients, + message = Base64ByteString msg.raw + } where cmWithoutSender = maybe cm (flip cmRemoveClient cm . mkClientIdentity qusr) mSenderClient diff --git a/services/galley/src/Galley/API/MLS/Proposal.hs b/services/galley/src/Galley/API/MLS/Proposal.hs index 39d56406b4c..9047db2a946 100644 --- a/services/galley/src/Galley/API/MLS/Proposal.hs +++ b/services/galley/src/Galley/API/MLS/Proposal.hs @@ -58,6 +58,7 @@ import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Protocol import Wire.API.Error import Wire.API.Error.Galley +import Wire.API.Federation.Error import Wire.API.MLS.AuthenticatedContent import Wire.API.MLS.Credential import Wire.API.MLS.KeyPackage @@ -116,6 +117,7 @@ type HasProposalEffects r = Member ConversationStore r, Member NotificationSubsystem r, Member (Error InternalError) r, + Member (Error FederationError) r, Member (Error MLSProposalFailure) r, Member (Error MLSProtocolError) r, Member (ErrorS 'MLSClientMismatch) r, diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs index f48631e7d23..f8491cdba47 100644 --- a/services/galley/src/Galley/API/MLS/Removal.hs +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -44,10 +44,12 @@ import Galley.Env import Galley.Types.Conversations.Members import Imports hiding (cs) import Polysemy +import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog import System.Logger qualified as Log import Wire.API.Conversation.Protocol +import Wire.API.Federation.Error import Wire.API.MLS.AuthenticatedContent import Wire.API.MLS.Credential import Wire.API.MLS.LeafNode @@ -59,7 +61,8 @@ import Wire.NotificationSubsystem -- | Send remove proposals for a set of clients to clients in the ClientMap. createAndSendRemoveProposals :: - ( Member (Input UTCTime) r, + ( Member (Error FederationError) r, + Member (Input UTCTime) r, Member TinyLog r, Member BackendNotificationQueueAccess r, Member ExternalAccess r, @@ -106,7 +109,8 @@ createAndSendRemoveProposals lConvOrSubConv indices qusr cm = do propagateMessage qusr Nothing lConvOrSubConv Nothing msg cm removeClientsWithClientMapRecursively :: - ( Member (Input UTCTime) r, + ( Member (Error FederationError) r, + Member (Input UTCTime) r, Member TinyLog r, Member BackendNotificationQueueAccess r, Member ExternalAccess r, @@ -138,7 +142,8 @@ removeClientsWithClientMapRecursively lMlsConv getClients qusr = do removeClientsFromSubConvs lMlsConv getClients qusr removeClientsFromSubConvs :: - ( Member (Input UTCTime) r, + ( Member (Error FederationError) r, + Member (Input UTCTime) r, Member TinyLog r, Member BackendNotificationQueueAccess r, Member ExternalAccess r, @@ -177,6 +182,7 @@ removeClientsFromSubConvs lMlsConv getClients qusr = do -- | Send remove proposals for a single client of a user to the local conversation. removeClient :: ( Member BackendNotificationQueueAccess r, + Member (Error FederationError) r, Member ExternalAccess r, Member NotificationSubsystem r, Member (Input Env) r, @@ -212,6 +218,7 @@ data RemoveUserIncludeMain -- | Send remove proposals for all clients of the user to the local conversation. removeUser :: ( Member BackendNotificationQueueAccess r, + Member (Error FederationError) r, Member ExternalAccess r, Member NotificationSubsystem r, Member (Input Env) r, @@ -257,6 +264,7 @@ listSubConversations' cid = do -- | Send remove proposals for clients of users that are not part of a conversation removeExtraneousClients :: ( Member BackendNotificationQueueAccess r, + Member (Error FederationError) r, Member ExternalAccess r, Member NotificationSubsystem r, Member (Input Env) r, diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index 7841a718396..ba13a24757b 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -377,6 +377,7 @@ leaveLocalSubConversation :: Member (Error MLSProtocolError) r, Member (ErrorS 'MLSStaleMessage) r, Member (ErrorS 'MLSNotEnabled) r, + Member (Error FederationError) r, Member Resource r, Members LeaveSubConversationStaticErrors r ) => diff --git a/services/galley/src/Galley/API/Message.hs b/services/galley/src/Galley/API/Message.hs index 355fccd7942..b436ea62250 100644 --- a/services/galley/src/Galley/API/Message.hs +++ b/services/galley/src/Galley/API/Message.hs @@ -680,26 +680,28 @@ sendRemoteMessages :: MessageMetadata -> Map (UserId, ClientId) Text -> Sem r (Set (UserId, ClientId)) -sendRemoteMessages domain now sender senderClient lcnv metadata messages = (handle =<<) $ do - let rcpts = - foldr - (\((u, c), t) -> Map.insertWith (<>) u (Map.singleton c t)) - mempty - (Map.assocs messages) - rm = - RemoteMessage - { time = now, - _data = mmData metadata, - sender = sender, - senderClient = senderClient, - conversation = tUnqualified lcnv, - priority = mmNativePriority metadata, - push = mmNativePush metadata, - transient = mmTransient metadata, - recipients = UserClientMap rcpts - } - let rpc = void $ fedQueueClient @'OnMessageSentTag rm - enqueueNotification domain Q.Persistent rpc +sendRemoteMessages domain now sender senderClient lcnv metadata messages = + -- FUTUREWORK: a FederationError here just means that queueing did not work. + -- It should not result in clients ending up in failedToSend. + (handle <=< runError) $ do + let rcpts = + foldr + (\((u, c), t) -> Map.insertWith (<>) u (Map.singleton c t)) + mempty + (Map.assocs messages) + rm = + RemoteMessage + { time = now, + _data = mmData metadata, + sender = sender, + senderClient = senderClient, + conversation = tUnqualified lcnv, + priority = mmNativePriority metadata, + push = mmNativePush metadata, + transient = mmTransient metadata, + recipients = UserClientMap rcpts + } + enqueueNotification Q.Persistent domain (fedQueueClient @'OnMessageSentTag rm) where handle :: Either FederationError a -> Sem r (Set (UserId, ClientId)) handle (Right _) = pure mempty diff --git a/services/galley/src/Galley/API/Teams.hs b/services/galley/src/Galley/API/Teams.hs index 95272e6a832..f8a44b29b0a 100644 --- a/services/galley/src/Galley/API/Teams.hs +++ b/services/galley/src/Galley/API/Teams.hs @@ -122,7 +122,6 @@ import Polysemy.Input import Polysemy.Output import Polysemy.TinyLog qualified as P import SAML2.WebSSO qualified as SAML -import System.Logger (Msg) import System.Logger qualified as Log import Wire.API.Conversation (ConversationRemoveMembers (..)) import Wire.API.Conversation.Role (wireConvRoles) @@ -885,6 +884,7 @@ deleteTeamMember :: Member BrigAccess r, Member ConversationStore r, Member (Error AuthenticationError) r, + Member (Error FederationError) r, Member (Error InvalidInput) r, Member (ErrorS 'AccessDenied) r, Member (ErrorS 'TeamMemberNotFound) r, @@ -913,6 +913,7 @@ deleteNonBindingTeamMember :: Member BrigAccess r, Member ConversationStore r, Member (Error AuthenticationError) r, + Member (Error FederationError) r, Member (Error InvalidInput) r, Member (ErrorS 'AccessDenied) r, Member (ErrorS 'TeamMemberNotFound) r, @@ -942,6 +943,7 @@ deleteTeamMember' :: Member ConversationStore r, Member (Error AuthenticationError) r, Member (Error InvalidInput) r, + Member (Error FederationError) r, Member (ErrorS 'AccessDenied) r, Member (ErrorS 'TeamMemberNotFound) r, Member (ErrorS 'TeamNotFound) r, @@ -1009,9 +1011,9 @@ uncheckedDeleteTeamMember :: ( Member BackendNotificationQueueAccess r, Member ConversationStore r, Member NotificationSubsystem r, + Member (Error FederationError) r, Member ExternalAccess r, Member (Input UTCTime) r, - Member (P.Logger (Log.Msg -> Log.Msg)) r, Member MemberStore r, Member TeamStore r ) => @@ -1059,10 +1061,10 @@ removeFromConvsAndPushConvLeaveEvent :: forall r. ( Member BackendNotificationQueueAccess r, Member ConversationStore r, + Member (Error FederationError) r, Member ExternalAccess r, Member NotificationSubsystem r, Member (Input UTCTime) r, - Member (P.Logger (Log.Msg -> Log.Msg)) r, Member MemberStore r, Member TeamStore r ) => @@ -1149,8 +1151,7 @@ deleteTeamConversation :: Member NotificationSubsystem r, Member (Input UTCTime) r, Member SubConversationStore r, - Member TeamStore r, - Member (P.Logger (Msg -> Msg)) r + Member TeamStore r ) => Local UserId -> ConnId -> diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index ddc89b92164..809f0376188 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -120,7 +120,6 @@ import Polysemy import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog -import System.Logger (Msg) import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Action import Wire.API.Conversation.Code @@ -400,8 +399,7 @@ updateConversationMessageTimer :: Member (Error FederationError) r, Member ExternalAccess r, Member NotificationSubsystem r, - Member (Input UTCTime) r, - Member (Logger (Msg -> Msg)) r + Member (Input UTCTime) r ) => Local UserId -> ConnId -> @@ -433,8 +431,7 @@ updateConversationMessageTimerUnqualified :: Member (Error FederationError) r, Member ExternalAccess r, Member NotificationSubsystem r, - Member (Input UTCTime) r, - Member (Logger (Msg -> Msg)) r + Member (Input UTCTime) r ) => Local UserId -> ConnId -> @@ -460,8 +457,7 @@ deleteLocalConversation :: Member MemberStore r, Member ProposalStore r, Member (Input UTCTime) r, - Member TeamStore r, - Member (Logger (Msg -> Msg)) r + Member TeamStore r ) => Local UserId -> ConnId -> @@ -723,6 +719,7 @@ joinConversationByReusableCode :: Member BrigAccess r, Member CodeStore r, Member ConversationStore r, + Member (Error FederationError) r, Member (ErrorS 'CodeNotFound) r, Member (ErrorS 'InvalidConversationPassword) r, Member (ErrorS 'ConvAccessDenied) r, @@ -737,8 +734,7 @@ joinConversationByReusableCode :: Member (Input UTCTime) r, Member MemberStore r, Member TeamStore r, - Member TeamFeatureStore r, - Member (Logger (Msg -> Msg)) r + Member TeamFeatureStore r ) => Local UserId -> ConnId -> @@ -755,6 +751,7 @@ joinConversationById :: ( Member BackendNotificationQueueAccess r, Member BrigAccess r, Member ConversationStore r, + Member (Error FederationError) r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'ConvNotFound) r, Member (ErrorS 'InvalidOperation) r, @@ -765,8 +762,7 @@ joinConversationById :: Member (Input Opts) r, Member (Input UTCTime) r, Member MemberStore r, - Member TeamStore r, - Member (Logger (Msg -> Msg)) r + Member TeamStore r ) => Local UserId -> ConnId -> @@ -780,6 +776,7 @@ joinConversation :: forall r. ( Member BackendNotificationQueueAccess r, Member BrigAccess r, + Member (Error FederationError) r, Member (ErrorS 'ConvAccessDenied) r, Member (ErrorS 'InvalidOperation) r, Member (ErrorS 'NotATeamMember) r, @@ -789,8 +786,7 @@ joinConversation :: Member (Input Opts) r, Member (Input UTCTime) r, Member MemberStore r, - Member TeamStore r, - Member (Logger (Msg -> Msg)) r + Member TeamStore r ) => Local UserId -> ConnId -> @@ -1017,8 +1013,7 @@ updateOtherMemberLocalConv :: Member ExternalAccess r, Member NotificationSubsystem r, Member (Input UTCTime) r, - Member MemberStore r, - Member (Logger (Msg -> Msg)) r + Member MemberStore r ) => Local ConvId -> Local UserId -> @@ -1044,8 +1039,7 @@ updateOtherMemberUnqualified :: Member ExternalAccess r, Member NotificationSubsystem r, Member (Input UTCTime) r, - Member MemberStore r, - Member (Logger (Msg -> Msg)) r + Member MemberStore r ) => Local UserId -> ConnId -> @@ -1070,8 +1064,7 @@ updateOtherMember :: Member ExternalAccess r, Member NotificationSubsystem r, Member (Input UTCTime) r, - Member MemberStore r, - Member (Logger (Msg -> Msg)) r + Member MemberStore r ) => Local UserId -> ConnId -> @@ -1402,7 +1395,6 @@ updateConversationName :: Member ExternalAccess r, Member NotificationSubsystem r, Member (Input UTCTime) r, - Member (Logger (Msg -> Msg)) r, Member TeamStore r ) => Local UserId -> @@ -1429,7 +1421,6 @@ updateUnqualifiedConversationName :: Member ExternalAccess r, Member NotificationSubsystem r, Member (Input UTCTime) r, - Member (Logger (Msg -> Msg)) r, Member TeamStore r ) => Local UserId -> @@ -1452,7 +1443,6 @@ updateLocalConversationName :: Member ExternalAccess r, Member NotificationSubsystem r, Member (Input UTCTime) r, - Member (Logger (Msg -> Msg)) r, Member TeamStore r ) => Local UserId -> diff --git a/services/galley/src/Galley/API/Util.hs b/services/galley/src/Galley/API/Util.hs index b4759ef59e2..0c8d4df22fb 100644 --- a/services/galley/src/Galley/API/Util.hs +++ b/services/galley/src/Galley/API/Util.hs @@ -45,6 +45,7 @@ import Galley.Data.Conversation qualified as Data import Galley.Data.Services (BotMember, newBotMember) import Galley.Data.Types qualified as DataTypes import Galley.Effects +import Galley.Effects.BackendNotificationQueueAccess import Galley.Effects.BrigAccess import Galley.Effects.CodeStore import Galley.Effects.ConversationStore @@ -60,6 +61,7 @@ import Galley.Types.Teams import Galley.Types.UserList import Gundeck.Types.Push.V2 qualified as PushV2 import Imports hiding (forkIO) +import Network.AMQP qualified as Q import Network.HTTP.Types import Network.Wai import Network.Wai.Predicate hiding (Error, fromEither) @@ -823,12 +825,12 @@ ensureNoUnreachableBackends results = do throw (UnreachableBackends (map (tDomain . fst) errors)) pure values --- | Notify remote users of being added to a new conversation. In case a remote --- domain is unreachable, an exception is thrown, the conversation deleted and --- the client gets an error response. +-- | Notify remote users of being added to a new conversation. registerRemoteConversationMemberships :: ( Member ConversationStore r, Member (Error UnreachableBackends) r, + Member (Error FederationError) r, + Member BackendNotificationQueueAccess r, Member FederatorAccess r ) => -- | The time stamp when the conversation was created @@ -861,6 +863,7 @@ registerRemoteConversationMemberships now lusr lc = deleteOnUnreachable $ do -- reachable members in buckets per remote domain let joined :: [Remote [RemoteMember]] = allRemoteBuckets + joinedCoupled :: [Remote ([RemoteMember], NonEmpty (Remote UserId))] joinedCoupled = foldMap ( \ruids -> @@ -869,14 +872,12 @@ registerRemoteConversationMemberships now lusr lc = deleteOnUnreachable $ do filter (\r -> tDomain r /= tDomain ruids) joined in case NE.nonEmpty nj of Nothing -> [] - Just v -> [(ruids, v)] + Just v -> [fmap (,v) ruids] ) joined - void . (ensureNoUnreachableBackends =<<) $ - -- Send an update to remotes about the final list of participants - runFederatedConcurrentlyBucketsEither joinedCoupled $ - fedClient @'Galley @"on-conversation-updated" . convUpdateJoin + void $ enqueueNotificationsConcurrentlyBuckets Q.Persistent joinedCoupled $ \z -> + makeConversationUpdateBundle (convUpdateJoin z) >>= sendBundle where creator :: Maybe UserId creator = cnvmCreator . DataTypes.convMetadata . tUnqualified $ lc @@ -893,14 +894,14 @@ registerRemoteConversationMemberships now lusr lc = deleteOnUnreachable $ do toMembers :: [RemoteMember] -> Set OtherMember toMembers rs = Set.fromList $ localNonCreators <> fmap remoteMemberToOther rs - convUpdateJoin :: (QualifiedWithTag t [RemoteMember], NonEmpty (QualifiedWithTag t' UserId)) -> ConversationUpdate - convUpdateJoin (toNotify, newMembers) = + convUpdateJoin :: Remote ([RemoteMember], NonEmpty (Remote UserId)) -> ConversationUpdate + convUpdateJoin (tUnqualified -> (toNotify, newMembers)) = ConversationUpdate - { cuTime = now, - cuOrigUserId = tUntagged lusr, - cuConvId = DataTypes.convId (tUnqualified lc), - cuAlreadyPresentUsers = fmap (tUnqualified . rmId) . tUnqualified $ toNotify, - cuAction = + { time = now, + origUserId = tUntagged lusr, + convId = DataTypes.convId (tUnqualified lc), + alreadyPresentUsers = fmap (tUnqualified . rmId) toNotify, + action = SomeConversationAction (sing @'ConversationJoinTag) -- FUTUREWORK(md): replace the member role with whatever is provided in diff --git a/services/galley/src/Galley/Effects/BackendNotificationQueueAccess.hs b/services/galley/src/Galley/Effects/BackendNotificationQueueAccess.hs index bdefa146314..9c2fe5d4004 100644 --- a/services/galley/src/Galley/Effects/BackendNotificationQueueAccess.hs +++ b/services/galley/src/Galley/Effects/BackendNotificationQueueAccess.hs @@ -1,11 +1,10 @@ -{-# LANGUAGE TemplateHaskell #-} - module Galley.Effects.BackendNotificationQueueAccess where import Data.Qualified import Imports import Network.AMQP qualified as Q import Polysemy +import Polysemy.Error import Wire.API.Federation.BackendNotifications import Wire.API.Federation.Component import Wire.API.Federation.Error @@ -13,8 +12,8 @@ import Wire.API.Federation.Error data BackendNotificationQueueAccess m a where EnqueueNotification :: KnownComponent c => - Remote x -> Q.DeliveryMode -> + Remote x -> FedQueueClient c a -> BackendNotificationQueueAccess m (Either FederationError a) EnqueueNotificationsConcurrently :: @@ -23,5 +22,49 @@ data BackendNotificationQueueAccess m a where f (Remote x) -> (Remote [x] -> FedQueueClient c a) -> BackendNotificationQueueAccess m (Either FederationError [Remote a]) + EnqueueNotificationsConcurrentlyBuckets :: + (KnownComponent c, Foldable f, Functor f) => + Q.DeliveryMode -> + f (Remote x) -> + (Remote x -> FedQueueClient c a) -> + BackendNotificationQueueAccess m (Either FederationError [Remote a]) + +enqueueNotification :: + ( KnownComponent c, + Member (Error FederationError) r, + Member BackendNotificationQueueAccess r + ) => + Q.DeliveryMode -> + Remote x -> + FedQueueClient c a -> + Sem r a +enqueueNotification m r q = send (EnqueueNotification m r q) >>= either throw pure + +enqueueNotificationsConcurrently :: + ( KnownComponent c, + Foldable f, + Functor f, + Member (Error FederationError) r, + Member BackendNotificationQueueAccess r + ) => + Q.DeliveryMode -> + f (Remote x) -> + (Remote [x] -> FedQueueClient c a) -> + Sem r [Remote a] +enqueueNotificationsConcurrently m r q = + send (EnqueueNotificationsConcurrently m r q) + >>= either throw pure -makeSem ''BackendNotificationQueueAccess +enqueueNotificationsConcurrentlyBuckets :: + ( KnownComponent c, + Foldable f, + Functor f, + Member (Error FederationError) r, + Member BackendNotificationQueueAccess r + ) => + Q.DeliveryMode -> + f (Remote x) -> + (Remote x -> FedQueueClient c a) -> + Sem r [Remote a] +enqueueNotificationsConcurrentlyBuckets m r q = + send (EnqueueNotificationsConcurrentlyBuckets m r q) >>= either throw pure diff --git a/services/galley/src/Galley/Effects/FederatorAccess.hs b/services/galley/src/Galley/Effects/FederatorAccess.hs index 8afd28cb842..cfa3b508c76 100644 --- a/services/galley/src/Galley/Effects/FederatorAccess.hs +++ b/services/galley/src/Galley/Effects/FederatorAccess.hs @@ -63,11 +63,11 @@ data FederatorAccess m a where -- already in buckets. The buckets are paired with arbitrary data that affect -- the payload of the request for each remote backend. RunFederatedConcurrentlyBucketsEither :: - forall (c :: Component) a m x y. - (KnownComponent c) => - [(Remote [x], y)] -> - ((Remote [x], y) -> FederatorClient c a) -> - FederatorAccess m [Either (Remote [x], FederationError) (Remote a)] + forall (c :: Component) f a m x. + (KnownComponent c, Foldable f) => + f (Remote x) -> + (Remote x -> FederatorClient c a) -> + FederatorAccess m [Either (Remote x, FederationError) (Remote a)] IsFederationConfigured :: FederatorAccess m Bool makeSem ''FederatorAccess diff --git a/services/galley/src/Galley/Intra/BackendNotificationQueue.hs b/services/galley/src/Galley/Intra/BackendNotificationQueue.hs index 316a94dcce7..cefe3cdc1e4 100644 --- a/services/galley/src/Galley/Intra/BackendNotificationQueue.hs +++ b/services/galley/src/Galley/Intra/BackendNotificationQueue.hs @@ -28,10 +28,12 @@ interpretBackendNotificationQueueAccess :: Sem (BackendNotificationQueueAccess ': r) a -> Sem r a interpretBackendNotificationQueueAccess = interpret $ \case - EnqueueNotification remote deliveryMode action -> do - embedApp . runExceptT $ enqueueNotification (tDomain remote) deliveryMode action + EnqueueNotification deliveryMode remote action -> do + embedApp . runExceptT $ enqueueNotification deliveryMode (tDomain remote) action EnqueueNotificationsConcurrently m xs rpc -> do embedApp . runExceptT $ enqueueNotificationsConcurrently m xs rpc + EnqueueNotificationsConcurrentlyBuckets m xs rpc -> do + embedApp . runExceptT $ enqueueNotificationsConcurrentlyBuckets m xs rpc getChannel :: ExceptT FederationError App (MVar Q.Channel) getChannel = view rabbitmqChannel >>= maybe (throwE FederationNotConfigured) pure @@ -61,8 +63,8 @@ enqueueSingleNotification remoteDomain deliveryMode chanVar action = do Just chan -> do liftIO $ enqueue chan rid ownDomain remoteDomain deliveryMode action -enqueueNotification :: Domain -> Q.DeliveryMode -> FedQueueClient c a -> ExceptT FederationError App a -enqueueNotification remoteDomain deliveryMode action = do +enqueueNotification :: Q.DeliveryMode -> Domain -> FedQueueClient c a -> ExceptT FederationError App a +enqueueNotification deliveryMode remoteDomain action = do chanVar <- getChannel lift $ enqueueSingleNotification remoteDomain deliveryMode chanVar action @@ -78,6 +80,18 @@ enqueueNotificationsConcurrently m xs f = do qualifyAs r <$> enqueueSingleNotification (tDomain r) m chanVar (f r) +enqueueNotificationsConcurrentlyBuckets :: + (Foldable f) => + Q.DeliveryMode -> + f (Remote x) -> + (Remote x -> FedQueueClient c a) -> + ExceptT FederationError App [Remote a] +enqueueNotificationsConcurrentlyBuckets m xs f = do + chanVar <- getChannel + lift $ pooledForConcurrentlyN 8 (toList xs) $ \r -> + qualifyAs r + <$> enqueueSingleNotification (tDomain r) m chanVar (f r) + data NoRabbitMqChannel = NoRabbitMqChannel deriving (Show) diff --git a/services/galley/src/Galley/Intra/Federator.hs b/services/galley/src/Galley/Intra/Federator.hs index 6e09422c98a..c1dd13bae16 100644 --- a/services/galley/src/Galley/Intra/Federator.hs +++ b/services/galley/src/Galley/Intra/Federator.hs @@ -102,9 +102,10 @@ runFederatedConcurrentlyEither xs rpc = bimap (r,) (qualifyAs r) <$> runFederatedEither r (rpc r) runFederatedConcurrentlyBucketsEither :: - [(Remote [a], y)] -> - ((Remote [a], y) -> FederatorClient c b) -> - App [Either (Remote [a], FederationError) (Remote b)] + Foldable f => + f (Remote x) -> + (Remote x -> FederatorClient c b) -> + App [Either (Remote x, FederationError) (Remote b)] runFederatedConcurrentlyBucketsEither xs rpc = - pooledForConcurrentlyN 8 xs $ \(r, v) -> - bimap (r,) (qualifyAs r) <$> runFederatedEither r (rpc (r, v)) + pooledForConcurrentlyN 8 (toList xs) $ \r -> + bimap (r,) (qualifyAs r) <$> runFederatedEither r (rpc r) diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index 2ac9f185e71..ccff779599f 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -132,7 +132,6 @@ tests s = test s "metrics" metrics, test s "fetch conversation by qualified ID (v2)" testGetConvQualifiedV2, test s "create Proteus conversation" postProteusConvOk, - test s "create conversation with remote users all reachable" (postConvWithRemoteUsersOk $ Set.fromList [rb1, rb2]), test s "create conversation with remote users some unreachable" (postConvWithUnreachableRemoteUsers $ Set.fromList [rb1, rb2, rb3, rb4]), test s "get empty conversations" getConvsOk, test s "get conversations by ids" getConvsOk2, @@ -242,7 +241,6 @@ tests s = test s "existing has password, requested has password - 409" postCodeWithPasswordExistsWithPasswordRequested ], test s "remove user with only local convs" removeUserNoFederation, - test s "remove user with local and remote convs" removeUser, test s "iUpsertOne2OneConversation" testAllOne2OneConversationRequests, test s "post message - reject if missing client" postMessageRejectIfMissingClients, test s "post message - client that is not in group doesn't receive message" postMessageClientNotInGroupDoesNotReceiveMsg, @@ -412,121 +410,6 @@ postConvWithUnreachableRemoteUsers rbs = do groupConvs WS.assertNoEvent (3 # Second) [wsAlice, wsAlex] -postConvWithRemoteUsersOk :: Set (Remote Backend) -> TestM () -postConvWithRemoteUsersOk rbs = do - c <- view tsCannon - (alice, qAlice) <- randomUserTuple - (alex, qAlex) <- randomUserTuple - (amy, qAmy) <- randomUserTuple - connectUsers alice (list1 alex [amy]) - (allRemotes, participatingRemotes) <- do - v <- forM (toList rbs) $ \rb -> do - users <- connectBackend alice rb - pure (users, participating rb users) - pure $ foldr (\(a, p) acc -> bimap ((<>) a) ((<>) p) acc) ([], []) v - liftIO $ - assertBool "Not every backend is reachable in the test" (allRemotes == participatingRemotes) - - let convName = "some chat" - otherLocals = [qAlex, qAmy] - WS.bracketR3 c alice alex amy $ \(wsAlice, wsAlex, wsAmy) -> do - let joiners = allRemotes <> otherLocals - unreachableBackends = - Set.fromList $ - foldMap - ( \rb -> - guard (rbReachable rb == BackendUnreachable) - $> tDomain rb - ) - rbs - (rsp, federatedRequests) <- - withTempMockFederator' - ( asum - [ getNotFullyConnectedBackendsMock, - mockUnreachableFor unreachableBackends, - "on-conversation-created" ~> EmptyResponse, - "on-conversation-updated" ~> EmptyResponse - ] - ) - $ postConvQualified - alice - Nothing - defNewProteusConv - { newConvName = checked convName, - newConvQualifiedUsers = joiners - } - minimalShouldBePresent) - qcid <- - assertConv - rsp - RegularConv - (Just alice) - qAlice - (otherLocals <> participatingRemotes) - (Just convName) - Nothing - let cid = qUnqualified qcid - cvs <- mapM (convView qcid) [alice, alex, amy] - liftIO $ - mapM_ WS.assertSuccess - =<< Async.mapConcurrently (checkWs qAlice) (zip cvs [wsAlice, wsAlex, wsAmy]) - - liftIO $ do - let expectedReqs = - Set.fromList $ - [ "on-conversation-created", - "on-conversation-updated" - ] - in assertBool "Some federated calls are missing" $ - expectedReqs `Set.isSubsetOf` Set.fromList (frRPC <$> federatedRequests) - - -- assertions on the conversation.create event triggering federation request - let fedReqsCreated = filter (\r -> frRPC r == "on-conversation-created") federatedRequests - fedReqCreatedBodies <- for fedReqsCreated $ assertRight . parseFedRequest - forM_ fedReqCreatedBodies $ \(fedReqCreatedBody :: ConversationCreated ConvId) -> liftIO $ do - fedReqCreatedBody.origUserId @?= alice - fedReqCreatedBody.cnvId @?= cid - fedReqCreatedBody.cnvType @?= RegularConv - fedReqCreatedBody.cnvAccess @?= [InviteAccess] - fedReqCreatedBody.cnvAccessRoles - @?= Set.fromList [TeamMemberAccessRole, NonTeamMemberAccessRole, ServiceAccessRole] - fedReqCreatedBody.cnvName @?= Just convName - assertBool "Notifying an incorrect set of conversation members" $ - minimalShouldBePresentSet `Set.isSubsetOf` fedReqCreatedBody.nonCreatorMembers - fedReqCreatedBody.messageTimer @?= Nothing - fedReqCreatedBody.receiptMode @?= Nothing - - -- assertions on the conversation.member-join event triggering federation request - let fedReqsAdd = filter (\r -> frRPC r == "on-conversation-updated") federatedRequests - fedReqAddBodies <- for fedReqsAdd $ assertRight . parseFedRequest - forM_ fedReqAddBodies $ \(fedReqAddBody :: ConversationUpdate) -> liftIO $ do - fedReqAddBody.cuOrigUserId @?= qAlice - fedReqAddBody.cuConvId @?= cid - -- This remote backend must already have their users in the conversation, - -- otherwise they should not be receiving the conversation update message - assertBool "The list of already present users should be non-empty" - . not - . null - $ fedReqAddBody.cuAlreadyPresentUsers - case fedReqAddBody.cuAction of - SomeConversationAction SConversationJoinTag _action -> pure () - _ -> assertFailure @() "Unexpected update action" - where - toOtherMember qid = OtherMember qid Nothing roleNameWireAdmin - convView cnv usr = - responseJsonError =<< getConvQualified usr cnv do - ntfTransient n @?= False - let e = List1.head (WS.unpackPayload n) - evtConv e @?= cnvQualifiedId cnv - evtType e @?= ConvCreate - evtFrom e @?= qalice - case evtData e of - EdConversation c' -> assertConvEquals cnv c' - _ -> assertFailure "Unexpected event data" - -- @SF.Separation @TSFI.RESTfulAPI @S2 -- This test verifies whether a message actually gets sent all the way to -- cannon. @@ -1867,11 +1750,11 @@ paginateConvListIds = do conv <- randomId let cu = ConversationUpdate - { cuTime = now, - cuOrigUserId = qChad, - cuConvId = conv, - cuAlreadyPresentUsers = [], - cuAction = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qAlice) roleNameWireMember) + { time = now, + origUserId = qChad, + convId = conv, + alreadyPresentUsers = [], + action = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qAlice) roleNameWireMember) } void $ runFedClient @"on-conversation-updated" fedGalleyClient chadDomain cu @@ -1883,11 +1766,11 @@ paginateConvListIds = do conv <- randomId let cu = ConversationUpdate - { cuTime = now, - cuOrigUserId = qDee, - cuConvId = conv, - cuAlreadyPresentUsers = [], - cuAction = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qAlice) roleNameWireMember) + { time = now, + origUserId = qDee, + convId = conv, + alreadyPresentUsers = [], + action = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qAlice) roleNameWireMember) } void $ runFedClient @"on-conversation-updated" fedGalleyClient deeDomain cu @@ -1928,11 +1811,11 @@ paginateConvListIdsPageEndingAtLocalsAndDomain = do conv <- randomId let cu = ConversationUpdate - { cuTime = now, - cuOrigUserId = qChad, - cuConvId = conv, - cuAlreadyPresentUsers = [], - cuAction = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qAlice) roleNameWireMember) + { time = now, + origUserId = qChad, + convId = conv, + alreadyPresentUsers = [], + action = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qAlice) roleNameWireMember) } void $ runFedClient @"on-conversation-updated" fedGalleyClient chadDomain cu @@ -1946,11 +1829,11 @@ paginateConvListIdsPageEndingAtLocalsAndDomain = do conv <- randomId let cu = ConversationUpdate - { cuTime = now, - cuOrigUserId = qDee, - cuConvId = conv, - cuAlreadyPresentUsers = [], - cuAction = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qAlice) roleNameWireMember) + { time = now, + origUserId = qDee, + convId = conv, + alreadyPresentUsers = [], + action = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qAlice) roleNameWireMember) } void $ runFedClient @"on-conversation-updated" fedGalleyClient deeDomain cu @@ -3204,11 +3087,11 @@ putRemoteConvMemberOk update = do now <- liftIO getCurrentTime let cu = ConversationUpdate - { cuTime = now, - cuOrigUserId = qbob, - cuConvId = qUnqualified qconv, - cuAlreadyPresentUsers = [], - cuAction = + { time = now, + origUserId = qbob, + convId = qUnqualified qconv, + alreadyPresentUsers = [], + action = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qalice) roleNameWireMember) } void $ runFedClient @"on-conversation-updated" fedGalleyClient remoteDomain cu @@ -3349,11 +3232,11 @@ putRemoteReceiptModeOk = do now <- liftIO getCurrentTime let cuAddAlice = ConversationUpdate - { cuTime = now, - cuOrigUserId = qbob, - cuConvId = qUnqualified qconv, - cuAlreadyPresentUsers = [], - cuAction = + { time = now, + origUserId = qbob, + convId = qUnqualified qconv, + alreadyPresentUsers = [], + action = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qalice) roleNameWireAdmin) } void $ runFedClient @"on-conversation-updated" fedGalleyClient remoteDomain cuAddAlice @@ -3364,11 +3247,11 @@ putRemoteReceiptModeOk = do connectWithRemoteUser adam qbob let cuAddAdam = ConversationUpdate - { cuTime = now, - cuOrigUserId = qbob, - cuConvId = qUnqualified qconv, - cuAlreadyPresentUsers = [], - cuAction = + { time = now, + origUserId = qbob, + convId = qUnqualified qconv, + alreadyPresentUsers = [], + action = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qadam) roleNameWireMember) } void $ runFedClient @"on-conversation-updated" fedGalleyClient remoteDomain cuAddAdam @@ -3377,11 +3260,11 @@ putRemoteReceiptModeOk = do let action = ConversationReceiptModeUpdate newReceiptMode let responseConvUpdate = ConversationUpdate - { cuTime = now, - cuOrigUserId = qalice, - cuConvId = qUnqualified qconv, - cuAlreadyPresentUsers = [adam], - cuAction = + { time = now, + origUserId = qalice, + convId = qUnqualified qconv, + alreadyPresentUsers = [adam], + action = SomeConversationAction (sing @'ConversationReceiptModeUpdateTag) action } let mockResponse = mockReply (ConversationUpdateResponseUpdate responseConvUpdate) @@ -3553,137 +3436,6 @@ removeUserNoFederation = do (mems3 >>= other bob) @?= Nothing (mems3 >>= other carl) @?= Just (OtherMember carl Nothing roleNameWireAdmin) -removeUser :: TestM () -removeUser = do - c <- view tsCannon - [alice, alexDel, amy] <- replicateM 3 randomQualifiedUser - let [alice', alexDel', amy'] = qUnqualified <$> [alice, alexDel, amy] - - let bDomain = Domain "b.example.com" - bart <- randomQualifiedId bDomain - berta <- randomQualifiedId bDomain - - let cDomain = Domain "c.example.com" - carl <- randomQualifiedId cDomain - - let dDomain = Domain "d.example.com" - dwight <- randomQualifiedId dDomain - dory <- randomQualifiedId dDomain - - connectUsers alice' (list1 alexDel' [amy']) - connectWithRemoteUser alice' bart - connectWithRemoteUser alice' berta - connectWithRemoteUser alexDel' bart - connectWithRemoteUser alice' carl - connectWithRemoteUser alexDel' carl - connectWithRemoteUser alice' dwight - connectWithRemoteUser alexDel' dory - - qconvA1 <- decodeQualifiedConvId <$> postConv alice' [alexDel'] (Just "gossip") [] Nothing Nothing - qconvA2 <- decodeQualifiedConvId <$> postConvWithRemoteUsers alice' Nothing defNewProteusConv {newConvQualifiedUsers = [alexDel, amy, berta, dwight]} - qconvA3 <- decodeQualifiedConvId <$> postConv alice' [amy'] (Just "gossip3") [] Nothing Nothing - qconvA4 <- decodeQualifiedConvId <$> postConvWithRemoteUsers alice' Nothing defNewProteusConv {newConvQualifiedUsers = [alexDel, bart, carl]} - convB1 <- randomId -- a remote conversation at 'bDomain' that Alice, AlexDel and Bart will be in - convB2 <- randomId -- a remote conversation at 'bDomain' that AlexDel and Bart will be in - convC1 <- randomId -- a remote conversation at 'cDomain' that AlexDel and Carl will be in - convD1 <- randomId -- a remote conversation at 'cDomain' that AlexDel and Dory will be in - now <- liftIO getCurrentTime - fedGalleyClient <- view tsFedGalleyClient - let nc cid creator quids = - ConversationCreated - { time = now, - origUserId = qUnqualified creator, - cnvId = cid, - cnvType = RegularConv, - cnvAccess = [], - cnvAccessRoles = Set.fromList [], - cnvName = Just "gossip4", - nonCreatorMembers = Set.fromList $ createOtherMember <$> quids, - messageTimer = Nothing, - receiptMode = Nothing, - protocol = ProtocolProteus - } - void $ runFedClient @"on-conversation-created" fedGalleyClient bDomain $ nc convB1 bart [alice, alexDel] - void $ runFedClient @"on-conversation-created" fedGalleyClient bDomain $ nc convB2 bart [alexDel] - void $ runFedClient @"on-conversation-created" fedGalleyClient cDomain $ nc convC1 carl [alexDel] - void $ runFedClient @"on-conversation-created" fedGalleyClient dDomain $ nc convD1 dory [alexDel] - - WS.bracketR3 c alice' alexDel' amy' $ \(wsAlice, wsAlexDel, wsAmy) -> do - let handler = do - d <- frTargetDomain <$> getRequest - asum - [ do - guard (d == dDomain) - throw (DiscoveryFailureSrvNotAvailable "dDomain"), - do - guard (d `elem` [bDomain, cDomain]) - "leave-conversation" ~> LeaveConversationResponse (Right mempty) - ] - (_, fedRequests) <- - withTempMockFederator' handler $ - deleteUser alexDel' !!! const 200 === statusCode - - liftIO $ do - assertEqual ("expect exactly 4 federated requests in : " <> show fedRequests) 4 (length fedRequests) - - liftIO $ do - WS.assertMatchN_ (5 # Second) [wsAlice, wsAlexDel] $ - wsAssertMembersLeave qconvA1 alexDel [alexDel] - WS.assertMatchN_ (5 # Second) [wsAlice, wsAlexDel, wsAmy] $ - wsAssertMembersLeave qconvA2 alexDel [alexDel] - - liftIO $ do - let bConvUpdateRPCs = filter (matchFedRequest bDomain "on-conversation-updated") fedRequests - bConvUpdates <- mapM (assertRight . eitherDecode . frBody) bConvUpdateRPCs - - bConvUpdatesA2 <- assertOne $ filter (\cu -> cuConvId cu == qUnqualified qconvA2) bConvUpdates - cuOrigUserId bConvUpdatesA2 @?= alexDel - cuAction bConvUpdatesA2 @?= SomeConversationAction (sing @'ConversationLeaveTag) () - cuAlreadyPresentUsers bConvUpdatesA2 @?= [qUnqualified berta] - - bConvUpdatesA4 <- assertOne $ filter (\cu -> cuConvId cu == qUnqualified qconvA4) bConvUpdates - cuOrigUserId bConvUpdatesA4 @?= alexDel - cuAction bConvUpdatesA4 @?= SomeConversationAction (sing @'ConversationLeaveTag) () - cuAlreadyPresentUsers bConvUpdatesA4 @?= [qUnqualified bart] - - liftIO $ do - cConvUpdateRPC <- assertOne $ filter (matchFedRequest cDomain "on-conversation-updated") fedRequests - Right convUpdate <- pure . eitherDecode . frBody $ cConvUpdateRPC - cuConvId convUpdate @?= qUnqualified qconvA4 - cuOrigUserId convUpdate @?= alexDel - cuAction convUpdate @?= SomeConversationAction (sing @'ConversationLeaveTag) () - cuAlreadyPresentUsers convUpdate @?= [qUnqualified carl] - - liftIO $ do - dConvUpdateRPC <- assertOne $ filter (matchFedRequest dDomain "on-conversation-updated") fedRequests - Right convUpdate <- pure . eitherDecode . frBody $ dConvUpdateRPC - cuConvId convUpdate @?= qUnqualified qconvA2 - cuOrigUserId convUpdate @?= alexDel - cuAction convUpdate @?= SomeConversationAction (sing @'ConversationLeaveTag) () - cuAlreadyPresentUsers convUpdate @?= [qUnqualified dwight] - - -- Check memberships - mems1 <- fmap cnvMembers . responseJsonError =<< getConvQualified alice' qconvA1 - mems2 <- fmap cnvMembers . responseJsonError =<< getConvQualified alice' qconvA2 - mems3 <- fmap cnvMembers . responseJsonError =<< getConvQualified alice' qconvA3 - mems4 <- fmap cnvMembers . responseJsonError =<< getConvQualified alice' qconvA4 - let findOther u = find ((== u) . omQualifiedId) . cmOthers - liftIO $ do - findOther alexDel mems1 @?= Nothing - findOther alexDel mems2 @?= Nothing - findOther amy mems2 @?= Just (OtherMember amy Nothing roleNameWireAdmin) - findOther alexDel mems3 @?= Nothing - findOther amy mems3 @?= Just (OtherMember amy Nothing roleNameWireAdmin) - findOther alexDel mems4 @?= Nothing - where - createOtherMember :: Qualified UserId -> OtherMember - createOtherMember quid = - OtherMember - { omQualifiedId = quid, - omService = Nothing, - omConvRoleName = roleNameWireAdmin - } - testAllOne2OneConversationRequests :: TestM () testAllOne2OneConversationRequests = do for_ [LocalActor, RemoteActor] $ \actor -> diff --git a/services/galley/test/integration/API/Federation.hs b/services/galley/test/integration/API/Federation.hs index 070010d0867..e6bd8eea883 100644 --- a/services/galley/test/integration/API/Federation.hs +++ b/services/galley/test/integration/API/Federation.hs @@ -241,11 +241,11 @@ addLocalUser = do now <- liftIO getCurrentTime let cu = FedGalley.ConversationUpdate - { FedGalley.cuTime = now, - FedGalley.cuOrigUserId = qbob, - FedGalley.cuConvId = conv, - FedGalley.cuAlreadyPresentUsers = [charlie], - FedGalley.cuAction = + { FedGalley.time = now, + FedGalley.origUserId = qbob, + FedGalley.convId = conv, + FedGalley.alreadyPresentUsers = [charlie], + FedGalley.action = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (qalice :| [qdee]) roleNameWireMember) } WS.bracketRN c [alice, charlie, dee] $ \[wsA, wsC, wsD] -> do @@ -295,15 +295,15 @@ addUnconnectedUsersOnly = do -- Bob attempts to add unconnected Charlie (possible abuse) let cu = FedGalley.ConversationUpdate - { FedGalley.cuTime = now, - FedGalley.cuOrigUserId = qBob, - FedGalley.cuConvId = conv, - FedGalley.cuAlreadyPresentUsers = [alice], - FedGalley.cuAction = + { FedGalley.time = now, + FedGalley.origUserId = qBob, + FedGalley.convId = conv, + FedGalley.alreadyPresentUsers = [alice], + FedGalley.action = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (qCharlie :| []) roleNameWireMember) } -- Alice receives no notifications from this - void $ runFedClient @"on-conversation-updated" fedGalleyClient remoteDomain cu + void $ runFedClient @("on-conversation-updated") fedGalleyClient remoteDomain cu WS.assertNoEvent (5 # Second) [wsA] -- | This test invokes the federation endpoint: @@ -329,20 +329,20 @@ removeLocalUser = do now <- liftIO getCurrentTime let cuAdd = FedGalley.ConversationUpdate - { FedGalley.cuTime = now, - FedGalley.cuOrigUserId = qBob, - FedGalley.cuConvId = conv, - FedGalley.cuAlreadyPresentUsers = [], - FedGalley.cuAction = + { FedGalley.time = now, + FedGalley.origUserId = qBob, + FedGalley.convId = conv, + FedGalley.alreadyPresentUsers = [], + FedGalley.action = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qAlice) roleNameWireMember) } cuRemove = FedGalley.ConversationUpdate - { FedGalley.cuTime = addUTCTime (secondsToNominalDiffTime 5) now, - FedGalley.cuOrigUserId = qAlice, - FedGalley.cuConvId = conv, - FedGalley.cuAlreadyPresentUsers = [alice], - FedGalley.cuAction = + { FedGalley.time = addUTCTime (secondsToNominalDiffTime 5) now, + FedGalley.origUserId = qAlice, + FedGalley.convId = conv, + FedGalley.alreadyPresentUsers = [alice], + FedGalley.action = SomeConversationAction (sing @'ConversationLeaveTag) () } @@ -402,11 +402,11 @@ removeRemoteUser = do let cuRemove user = FedGalley.ConversationUpdate - { FedGalley.cuTime = addUTCTime (secondsToNominalDiffTime 5) now, - FedGalley.cuOrigUserId = qBob, - FedGalley.cuConvId = conv, - FedGalley.cuAlreadyPresentUsers = [alice, charlie, dee], - FedGalley.cuAction = + { FedGalley.time = addUTCTime (secondsToNominalDiffTime 5) now, + FedGalley.origUserId = qBob, + FedGalley.convId = conv, + FedGalley.alreadyPresentUsers = [alice, charlie, dee], + FedGalley.action = SomeConversationAction (sing @'ConversationRemoveMembersTag) (ConversationRemoveMembers (pure user) EdReasonRemoved) @@ -457,11 +457,11 @@ notifyUpdate extras action etype edata = do now <- liftIO getCurrentTime let cu = FedGalley.ConversationUpdate - { FedGalley.cuTime = now, - FedGalley.cuOrigUserId = qbob, - FedGalley.cuConvId = conv, - FedGalley.cuAlreadyPresentUsers = [alice, charlie], - FedGalley.cuAction = action + { FedGalley.time = now, + FedGalley.origUserId = qbob, + FedGalley.convId = conv, + FedGalley.alreadyPresentUsers = [alice, charlie], + FedGalley.action = action } WS.bracketR2 c alice charlie $ \(wsA, wsC) -> do void $ runFedClient @"on-conversation-updated" fedGalleyClient bdom cu @@ -499,11 +499,11 @@ notifyUpdateUnavailable extras action etype edata = do now <- liftIO getCurrentTime let cu = FedGalley.ConversationUpdate - { FedGalley.cuTime = now, - FedGalley.cuOrigUserId = qbob, - FedGalley.cuConvId = conv, - FedGalley.cuAlreadyPresentUsers = [alice, charlie], - FedGalley.cuAction = action + { FedGalley.time = now, + FedGalley.origUserId = qbob, + FedGalley.convId = conv, + FedGalley.alreadyPresentUsers = [alice, charlie], + FedGalley.action = action } WS.bracketR2 c alice charlie $ \(wsA, wsC) -> do ((), _fedRequests) <- @@ -635,11 +635,11 @@ notifyDeletedConversation = do now <- liftIO getCurrentTime let cu = FedGalley.ConversationUpdate - { FedGalley.cuTime = now, - FedGalley.cuOrigUserId = qbob, - FedGalley.cuConvId = qUnqualified qconv, - FedGalley.cuAlreadyPresentUsers = [alice], - FedGalley.cuAction = SomeConversationAction (sing @'ConversationDeleteTag) () + { FedGalley.time = now, + FedGalley.origUserId = qbob, + FedGalley.convId = qUnqualified qconv, + FedGalley.alreadyPresentUsers = [alice], + FedGalley.action = SomeConversationAction (sing @'ConversationDeleteTag) () } void $ runFedClient @"on-conversation-updated" fedGalleyClient bobDomain cu @@ -691,11 +691,11 @@ addRemoteUser = do -- The conversation owning let cu = FedGalley.ConversationUpdate - { FedGalley.cuTime = now, - FedGalley.cuOrigUserId = qbob, - FedGalley.cuConvId = qUnqualified qconv, - FedGalley.cuAlreadyPresentUsers = map qUnqualified [qalice, qcharlie], - FedGalley.cuAction = + { FedGalley.time = now, + FedGalley.origUserId = qbob, + FedGalley.convId = qUnqualified qconv, + FedGalley.alreadyPresentUsers = map qUnqualified [qalice, qcharlie], + FedGalley.action = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (qdee :| [qeve, qflo]) roleNameWireMember) } WS.bracketRN c (map qUnqualified [qalice, qcharlie, qdee, qflo]) $ \[wsA, wsC, wsD, wsF] -> do @@ -774,11 +774,11 @@ onMessageSent = do connectWithRemoteUser alice qbob let cu = FedGalley.ConversationUpdate - { FedGalley.cuTime = now, - FedGalley.cuOrigUserId = qbob, - FedGalley.cuConvId = conv, - FedGalley.cuAlreadyPresentUsers = [], - FedGalley.cuAction = + { FedGalley.time = now, + FedGalley.origUserId = qbob, + FedGalley.convId = conv, + FedGalley.alreadyPresentUsers = [], + FedGalley.action = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qalice) roleNameWireMember) } void $ runFedClient @"on-conversation-updated" fedGalleyClient bdom cu diff --git a/services/galley/test/integration/API/MLS.hs b/services/galley/test/integration/API/MLS.hs index 3429d03a1bc..d998d891fc7 100644 --- a/services/galley/test/integration/API/MLS.hs +++ b/services/galley/test/integration/API/MLS.hs @@ -880,11 +880,11 @@ testRemoteToRemoteInSub = do connectWithRemoteUser alice qbob let cu = ConversationUpdate - { cuTime = now, - cuOrigUserId = qbob, - cuConvId = conv, - cuAlreadyPresentUsers = [], - cuAction = + { time = now, + origUserId = qbob, + convId = conv, + alreadyPresentUsers = [], + action = SomeConversationAction (sing @'ConversationJoinTag) (ConversationJoin (pure qalice) roleNameWireMember) } void $ runFedClient @"on-conversation-updated" fedGalleyClient bdom cu diff --git a/services/galley/test/integration/API/MLS/Util.hs b/services/galley/test/integration/API/MLS/Util.hs index 8cc9b51c601..d979556e596 100644 --- a/services/galley/test/integration/API/MLS/Util.hs +++ b/services/galley/test/integration/API/MLS/Util.hs @@ -965,11 +965,11 @@ receiveOnConvUpdated conv origUser joiner = do now <- liftIO getCurrentTime let cu = ConversationUpdate - { cuTime = now, - cuOrigUserId = origUser, - cuConvId = qUnqualified conv, - cuAlreadyPresentUsers = [qUnqualified joiner], - cuAction = + { time = now, + origUserId = origUser, + convId = qUnqualified conv, + alreadyPresentUsers = [qUnqualified joiner], + action = SomeConversationAction SConversationJoinTag ConversationJoin diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index b6227da8967..4de8b00e8df 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -2676,11 +2676,13 @@ withTempMockFederator' :: m b -> m (b, [FederatedRequest]) withTempMockFederator' resp action = do - let mock = runMock (assertFailure . Text.unpack) $ do - r <- resp - pure ("application" // "json", r) + let mock = + def + { handler = runMock (assertFailure . Text.unpack) $ do + r <- resp + pure ("application" // "json", r) + } Mock.withTempMockFederator - [("Content-Type", "application/json")] mock $ \mockPort -> do withSettingsOverrides (\opts -> opts & Opts.federator ?~ Endpoint "127.0.0.1" (fromIntegral mockPort)) action diff --git a/services/galley/test/integration/TestSetup.hs b/services/galley/test/integration/TestSetup.hs index 2cdb594af24..d4d8c7151b0 100644 --- a/services/galley/test/integration/TestSetup.hs +++ b/services/galley/test/integration/TestSetup.hs @@ -55,7 +55,6 @@ import Data.ByteString.Conversion import Data.Domain import Data.Proxy import Data.Text qualified as Text -import GHC.TypeLits import Galley.Aws qualified as Aws import Galley.Options (Opts) import Imports @@ -141,7 +140,7 @@ instance VersionedMonad v ClientM where guardVersion _ = pure () runFedClient :: - forall (name :: Symbol) comp m api. + forall name comp m api. ( HasUnsafeFedEndpoint comp api name, Servant.HasClient Servant.ClientM api, MonadIO m, From 3577ea31e96527492a2062bd8e336c1ba5b9825b Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Fri, 8 Mar 2024 08:42:27 +0100 Subject: [PATCH 040/117] WPB-6577 Fix user creation conflict in SCIM (#3914) --- cassandra-schema.cql | 1 + changelog.d/3-bug-fixes/WPB-6577 | 1 + integration/integration.cabal | 1 + integration/test/API/Common.hs | 7 +- integration/test/API/Spar.hs | 13 +++ integration/test/SetupHelpers.hs | 12 +++ integration/test/Test/Spar.hs | 28 +++++++ libs/wire-api/src/Wire/API/User.hs | 6 ++ libs/wire-api/src/Wire/API/User/Scim.hs | 19 ++++- services/brig/src/Brig/API/User.hs | 3 +- services/brig/src/Brig/Team/API.hs | 16 ++-- services/spar/spar.cabal | 1 + services/spar/src/Spar/Data/Instances.hs | 13 +++ services/spar/src/Spar/Intra/Brig.hs | 5 +- services/spar/src/Spar/Schema/Run.hs | 4 +- services/spar/src/Spar/Schema/V18.hs | 33 ++++++++ services/spar/src/Spar/Scim/Types.hs | 8 ++ services/spar/src/Spar/Scim/User.hs | 83 +++++++++++++------ services/spar/src/Spar/Sem/BrigAccess.hs | 2 +- services/spar/src/Spar/Sem/BrigAccess/Http.hs | 2 +- .../spar/src/Spar/Sem/ScimExternalIdStore.hs | 7 ++ .../Spar/Sem/ScimExternalIdStore/Cassandra.hs | 22 +++++ .../src/Spar/Sem/ScimExternalIdStore/Mem.hs | 14 ++-- .../src/Spar/Sem/ScimExternalIdStore/Spec.hs | 7 +- services/spar/test/Arbitrary.hs | 3 + .../spar/test/Test/Spar/Intra/BrigSpec.hs | 10 --- 26 files changed, 259 insertions(+), 62 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-6577 create mode 100644 integration/test/Test/Spar.hs create mode 100644 services/spar/src/Spar/Schema/V18.hs diff --git a/cassandra-schema.cql b/cassandra-schema.cql index efcf3424035..a35870fedfd 100644 --- a/cassandra-schema.cql +++ b/cassandra-schema.cql @@ -2165,6 +2165,7 @@ CREATE TABLE spar_test.scim_user_times ( CREATE TABLE spar_test.scim_external ( team uuid, external_id text, + creation_status int, user uuid, PRIMARY KEY (team, external_id) ) WITH CLUSTERING ORDER BY (external_id ASC) diff --git a/changelog.d/3-bug-fixes/WPB-6577 b/changelog.d/3-bug-fixes/WPB-6577 new file mode 100644 index 00000000000..c637c5de29f --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-6577 @@ -0,0 +1 @@ +Prevent conflict on subsequent tries to provision a SCIM user diff --git a/integration/integration.cabal b/integration/integration.cabal index fdbcd8a5230..5ffb61552cb 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -136,6 +136,7 @@ library Test.Roles Test.Search Test.Services + Test.Spar Test.Swagger Test.TeamSettings Test.User diff --git a/integration/test/API/Common.hs b/integration/test/API/Common.hs index 42e33973294..623a2c56579 100644 --- a/integration/test/API/Common.hs +++ b/integration/test/API/Common.hs @@ -31,8 +31,11 @@ randomName = liftIO $ do pick = (chars !) <$> randomRIO (Array.bounds chars) randomHandle :: App String -randomHandle = liftIO $ do - n <- randomRIO (50, 256) +randomHandle = randomHandleWithRange 50 256 + +randomHandleWithRange :: Int -> Int -> App String +randomHandleWithRange min' max' = liftIO $ do + n <- randomRIO (min', max') replicateM n pick where chars = mkArray $ ['a' .. 'z'] <> ['0' .. '9'] <> "_-." diff --git a/integration/test/API/Spar.hs b/integration/test/API/Spar.hs index aff62ca6d5a..e8d1e7cc2f3 100644 --- a/integration/test/API/Spar.hs +++ b/integration/test/API/Spar.hs @@ -1,5 +1,6 @@ module API.Spar where +import API.Common (defPassword) import GHC.Stack import Testlib.Prelude @@ -8,3 +9,15 @@ getScimTokens :: (HasCallStack, MakesValue caller) => caller -> App Response getScimTokens caller = do req <- baseRequest caller Spar Versioned "/scim/auth-tokens" submit "GET" req + +-- https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/post_scim_auth_tokens +createScimToken :: (HasCallStack, MakesValue caller) => caller -> App Response +createScimToken caller = do + req <- baseRequest caller Spar Versioned "/scim/auth-tokens" + submit "POST" $ req & addJSONObject ["password" .= defPassword, "description" .= "integration test"] + +createScimUser :: (HasCallStack, MakesValue domain, MakesValue scimUser) => domain -> String -> scimUser -> App Response +createScimUser domain token scimUser = do + req <- baseRequest domain Spar Versioned "/scim/v2/Users" + body <- make scimUser + submit "POST" $ req & addJSON body . addHeader "Authorization" ("Bearer " <> token) diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 7a9eab93257..0c263d969e3 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -314,3 +314,15 @@ lhDeviceIdOf bob = do >>= assertOne >>= (%. "id") >>= asString + +randomScimUser :: App Value +randomScimUser = do + email <- randomEmail + handle <- randomHandleWithRange 12 128 + pure $ + object + [ "schemas" .= ["urn:ietf:params:scim:schemas:core:2.0:User"], + "externalId" .= email, + "userName" .= handle, + "displayName" .= handle + ] diff --git a/integration/test/Test/Spar.hs b/integration/test/Test/Spar.hs new file mode 100644 index 00000000000..d1e14e85984 --- /dev/null +++ b/integration/test/Test/Spar.hs @@ -0,0 +1,28 @@ +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} + +module Test.Spar where + +import API.Spar +import Control.Concurrent (threadDelay) +import SetupHelpers +import Testlib.Prelude + +testSparUserCreationInvitationTimeout :: HasCallStack => App () +testSparUserCreationInvitationTimeout = do + (owner, _tid, _) <- createTeam OwnDomain 1 + tok <- createScimToken owner >>= \resp -> resp.json %. "token" >>= asString + scimUser <- randomScimUser + bindResponse (createScimUser OwnDomain tok scimUser) $ \res -> do + res.status `shouldMatchInt` 201 + + -- Trying to create the same user again right away should fail + bindResponse (createScimUser OwnDomain tok scimUser) $ \res -> do + res.status `shouldMatchInt` 409 + + -- However, if we wait until the invitation timeout has passed + -- (assuming it is configured to 10s locally and in CI)... + liftIO $ threadDelay (11_000_000) + + -- ...we should be able to create the user again + retryT $ bindResponse (createScimUser OwnDomain tok scimUser) $ \res -> do + res.status `shouldMatchInt` 201 diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index c0095855963..d7f9da9ae09 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -59,6 +59,7 @@ module Wire.API.User CreateUserSparInternalResponses, newUserFromSpar, urefToExternalId, + urefToExternalIdUnsafe, urefToEmail, ExpiresIn, newUserInvitationCode, @@ -958,6 +959,9 @@ urefToEmail uref = case uref ^. SAML.uidSubject . SAML.nameID of SAML.UNameIDEmail email -> parseEmail . SAMLEmail.render . CI.original $ email _ -> Nothing +urefToExternalIdUnsafe :: SAML.UserRef -> Text +urefToExternalIdUnsafe = CI.original . SAML.unsafeShowNameID . view SAML.uidSubject + data CreateUserSparError = CreateUserSparHandleError ChangeHandleError | CreateUserSparRegistrationError RegisterError @@ -1904,6 +1908,7 @@ instance Schema.ToSchema UserAccount where data NewUserScimInvitation = NewUserScimInvitation -- FIXME: the TID should be captured in the route as usual { newUserScimInvTeamId :: TeamId, + newUserScimInvUserId :: UserId, newUserScimInvLocale :: Maybe Locale, newUserScimInvName :: Name, newUserScimInvEmail :: Email, @@ -1918,6 +1923,7 @@ instance Schema.ToSchema NewUserScimInvitation where Schema.object "NewUserScimInvitation" $ NewUserScimInvitation <$> newUserScimInvTeamId Schema..= Schema.field "team_id" Schema.schema + <*> newUserScimInvUserId Schema..= Schema.field "user_id" Schema.schema <*> newUserScimInvLocale Schema..= maybe_ (optField "locale" Schema.schema) <*> newUserScimInvName Schema..= Schema.field "name" Schema.schema <*> newUserScimInvEmail Schema..= Schema.field "email" Schema.schema diff --git a/libs/wire-api/src/Wire/API/User/Scim.hs b/libs/wire-api/src/Wire/API/User/Scim.hs index 752c608bd85..991acf717f5 100644 --- a/libs/wire-api/src/Wire/API/User/Scim.hs +++ b/libs/wire-api/src/Wire/API/User/Scim.hs @@ -42,7 +42,7 @@ -- * Request and response types for SCIM-related endpoints. module Wire.API.User.Scim where -import Control.Lens (Prism', makeLenses, mapped, prism', (.~), (?~)) +import Control.Lens (Prism', makeLenses, mapped, prism', (.~), (?~), (^.)) import Control.Monad.Except (throwError) import Crypto.Hash (hash) import Crypto.Hash.Algorithms (SHA512) @@ -83,7 +83,8 @@ import Web.Scim.Schema.Schema qualified as Scim import Web.Scim.Schema.User qualified as Scim import Web.Scim.Schema.User qualified as Scim.User import Wire.API.Team.Role (Role) -import Wire.API.User.Identity (Email) +import Wire.API.User (emailFromSAMLNameID, urefToExternalIdUnsafe) +import Wire.API.User.Identity (Email, fromEmail) import Wire.API.User.Profile as BT import Wire.API.User.RichInfo qualified as RI import Wire.API.User.Saml () @@ -338,6 +339,15 @@ data ValidExternalId | EmailOnly Email deriving (Eq, Show, Generic) +instance Arbitrary ValidExternalId where + arbitrary = do + muref <- QC.arbitrary + case muref of + Just uref -> case emailFromSAMLNameID $ uref ^. SAML.uidSubject of + Just e -> pure $ EmailAndUref e uref + Nothing -> pure $ UrefOnly uref + Nothing -> EmailOnly <$> QC.arbitrary + -- | Take apart a 'ValidExternalId', using 'SAML.UserRef' if available, otherwise 'Email'. runValidExternalIdEither :: (SAML.UserRef -> a) -> (Email -> a) -> ValidExternalId -> a runValidExternalIdEither doUref doEmail = \case @@ -353,6 +363,11 @@ runValidExternalIdBoth merge doUref doEmail = \case UrefOnly uref -> doUref uref EmailOnly em -> doEmail em +-- | Returns either the extracted `UnqualifiedNameID` if present and not qualified, or the email address. +-- This throws an exception if there are any qualifiers. +runValidExternalIdUnsafe :: ValidExternalId -> Text +runValidExternalIdUnsafe = runValidExternalIdEither urefToExternalIdUnsafe fromEmail + veidUref :: Prism' ValidExternalId SAML.UserRef veidUref = prism' UrefOnly $ \case diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index d3d7e096ef2..8b70410265d 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -557,10 +557,9 @@ createUserInviteViaScim :: Member (UserPendingActivationStore p) r, Member TinyLog r ) => - UserId -> NewUserScimInvitation -> ExceptT Error.Error (AppT r) UserAccount -createUserInviteViaScim uid (NewUserScimInvitation tid loc name rawEmail _) = do +createUserInviteViaScim (NewUserScimInvitation tid uid loc name rawEmail _) = do email <- either (const . throwE . Error.StdError $ errorToWai @'E.InvalidEmail) pure (validateEmail rawEmail) let emKey = userEmailKey email verifyUniquenessAndCheckBlacklist emKey !>> identityErrorToBrigError diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index a163240fd13..a98512e25d7 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -161,7 +161,7 @@ createInvitationPublic uid tid body = do fst <$> logInvitationRequest context - (createInvitation' tid inviteeRole (Just (inviterUid inviter)) (inviterEmail inviter) body) + (createInvitation' tid Nothing inviteeRole (Just (inviterUid inviter)) (inviterEmail inviter) body) createInvitationViaScim :: ( Member BlacklistStore r, @@ -172,7 +172,7 @@ createInvitationViaScim :: TeamId -> NewUserScimInvitation -> (Handler r) UserAccount -createInvitationViaScim tid newUser@(NewUserScimInvitation _tid loc name email role) = do +createInvitationViaScim tid newUser@(NewUserScimInvitation _tid uid loc name email role) = do env <- ask let inviteeRole = role fromEmail = env ^. emailSender @@ -190,12 +190,11 @@ createInvitationViaScim tid newUser@(NewUserScimInvitation _tid loc name email r . logTeam tid . logEmail email - (inv, _) <- + void $ logInvitationRequest context $ - createInvitation' tid inviteeRole Nothing fromEmail invreq - let uid = Id (toUUID (inInvitation inv)) + createInvitation' tid (Just uid) inviteeRole Nothing fromEmail invreq - createUserInviteViaScim uid newUser + createUserInviteViaScim newUser logInvitationRequest :: (Msg -> Msg) -> (Handler r) (Invitation, InvitationCode) -> (Handler r) (Invitation, InvitationCode) logInvitationRequest context action = @@ -214,12 +213,13 @@ createInvitation' :: Member GalleyProvider r ) => TeamId -> + Maybe UserId -> Public.Role -> Maybe UserId -> Email -> Public.InvitationRequest -> Handler r (Public.Invitation, Public.InvitationCode) -createInvitation' tid inviteeRole mbInviterUid fromEmail body = do +createInvitation' tid mUid inviteeRole mbInviterUid fromEmail body = do -- FUTUREWORK: These validations are nearly copy+paste from accountCreation and -- sendActivationCode. Refactor this to a single place @@ -254,7 +254,7 @@ createInvitation' tid inviteeRole mbInviterUid fromEmail body = do showInvitationUrl <- lift $ liftSem $ GalleyProvider.getExposeInvitationURLsToTeamAdmin tid lift $ do - iid <- liftIO DB.mkInvitationId + iid <- maybe (liftIO DB.mkInvitationId) (pure . Id . toUUID) mUid now <- liftIO =<< view currentTime timeout <- setTeamInvitationTimeout <$> view settings (newInv, code) <- diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index 548c1f2ceff..3b15efd6505 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -39,6 +39,7 @@ library Spar.Schema.V15 Spar.Schema.V16 Spar.Schema.V17 + Spar.Schema.V18 Spar.Schema.V2 Spar.Schema.V3 Spar.Schema.V4 diff --git a/services/spar/src/Spar/Data/Instances.hs b/services/spar/src/Spar/Data/Instances.hs index f0b21768d22..3e395953898 100644 --- a/services/spar/src/Spar/Data/Instances.hs +++ b/services/spar/src/Spar/Data/Instances.hs @@ -37,6 +37,7 @@ import Data.X509 (SignedCertificate) import Imports import SAML2.Util (parseURI') import qualified SAML2.WebSSO as SAML +import Spar.Scim.Types (ScimUserCreationStatus (..)) import Text.XML.DSig (parseKeyInfo, renderKeyInfo) import URI.ByteString import Wire.API.User.Saml @@ -117,3 +118,15 @@ instance Cql ScimTokenLookupKey where fromCql s@(CqlText _) = ScimTokenLookupKeyHashed <$> fromCql s <|> ScimTokenLookupKeyPlaintext <$> fromCql s fromCql _ = Left "ScimTokenLookupKey: expected CqlText" + +instance Cql ScimUserCreationStatus where + ctype = Tagged IntColumn + + toCql ScimUserCreated = CqlInt 0 + toCql ScimUserCreating = CqlInt 1 + + fromCql (CqlInt i) = case i of + 0 -> pure ScimUserCreated + 1 -> pure ScimUserCreating + n -> Left $ "unexpected ScimUserCreationStatus: " ++ show n + fromCql _ = Left "int expected" diff --git a/services/spar/src/Spar/Intra/Brig.hs b/services/spar/src/Spar/Intra/Brig.hs index db6ea684d2d..d2a97b56dcc 100644 --- a/services/spar/src/Spar/Intra/Brig.hs +++ b/services/spar/src/Spar/Intra/Brig.hs @@ -130,14 +130,15 @@ createBrigUserSAML uref (Id buid) teamid name managedBy handle richInfo mLocale createBrigUserNoSAML :: (HasCallStack, MonadSparToBrig m) => Email -> + UserId -> TeamId -> -- | User name Name -> Maybe Locale -> Role -> m UserId -createBrigUserNoSAML email teamid uname locale role = do - let newUser = NewUserScimInvitation teamid locale uname email role +createBrigUserNoSAML email uid teamid uname locale role = do + let newUser = NewUserScimInvitation teamid uid locale uname email role resp :: ResponseLBS <- call $ method POST diff --git a/services/spar/src/Spar/Schema/Run.hs b/services/spar/src/Spar/Schema/Run.hs index a853c1c13c3..ac273fb83c4 100644 --- a/services/spar/src/Spar/Schema/Run.hs +++ b/services/spar/src/Spar/Schema/Run.hs @@ -31,6 +31,7 @@ import qualified Spar.Schema.V14 as V14 import qualified Spar.Schema.V15 as V15 import qualified Spar.Schema.V16 as V16 import qualified Spar.Schema.V17 as V17 +import qualified Spar.Schema.V18 as V18 import qualified Spar.Schema.V2 as V2 import qualified Spar.Schema.V3 as V3 import qualified Spar.Schema.V4 as V4 @@ -76,7 +77,8 @@ migrations = V14.migration, V15.migration, V16.migration, - V17.migration + V17.migration, + V18.migration -- TODO: Add a migration that removes unused fields -- (we don't want to risk running a migration which would -- effectively break the currently deployed spar service) diff --git a/services/spar/src/Spar/Schema/V18.hs b/services/spar/src/Spar/Schema/V18.hs new file mode 100644 index 00000000000..89f3bde2137 --- /dev/null +++ b/services/spar/src/Spar/Schema/V18.hs @@ -0,0 +1,33 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Spar.Schema.V18 + ( migration, + ) +where + +import Cassandra.Schema +import Imports +import Text.RawString.QQ + +migration :: Migration +migration = Migration 18 "A status field to manage user creation" $ do + void $ + schema' + [r| + ALTER TABLE scim_external ADD creation_status int; + |] diff --git a/services/spar/src/Spar/Scim/Types.hs b/services/spar/src/Spar/Scim/Types.hs index abda3fb9a81..5877b7884a2 100644 --- a/services/spar/src/Spar/Scim/Types.hs +++ b/services/spar/src/Spar/Scim/Types.hs @@ -30,8 +30,10 @@ -- * Request and response types for SCIM-related endpoints. module Spar.Scim.Types where +import Brig.Types.Test.Arbitrary (Arbitrary (..)) import Control.Lens (view) import Imports +import Test.QuickCheck.Gen (elements) import qualified Web.Scim.Schema.Common as Scim import qualified Web.Scim.Schema.User as Scim.User import Wire.API.User (AccountStatus (..)) @@ -87,3 +89,9 @@ normalizeLikeStored usr = tweakActive :: Maybe Scim.ScimBool -> Maybe Scim.ScimBool tweakActive = Just . Scim.ScimBool . maybe True Scim.unScimBool + +data ScimUserCreationStatus = ScimUserCreating | ScimUserCreated + deriving (Eq, Show, Generic) + +instance Arbitrary ScimUserCreationStatus where + arbitrary = elements [ScimUserCreating, ScimUserCreated] diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index 46bb2fcce41..c8792a6e977 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -45,7 +45,7 @@ where import qualified Control.Applicative as Applicative (empty) import Control.Lens hiding (op) import Control.Monad.Error.Class (MonadError) -import Control.Monad.Except (throwError) +import Control.Monad.Except (throwError, withExceptT) import Control.Monad.Trans.Except (mapExceptT) import Control.Monad.Trans.Maybe (MaybeT (MaybeT), runMaybeT) import Crypto.Hash (Digest, SHA256, hashlazy) @@ -66,12 +66,14 @@ import Polysemy.Input import qualified SAML2.WebSSO as SAML import Spar.App (getUserByUrefUnsafe, getUserByUrefViaOldIssuerUnsafe, getUserIdByScimExternalId) import qualified Spar.App +import Spar.Intra.BrigApp as Intra import qualified Spar.Intra.BrigApp as Brig import Spar.Options import Spar.Scim.Auth () -import Spar.Scim.Types (normalizeLikeStored) +import Spar.Scim.Types import qualified Spar.Scim.Types as ST -import Spar.Sem.BrigAccess as BrigAccess +import Spar.Sem.BrigAccess (BrigAccess) +import qualified Spar.Sem.BrigAccess as BrigAccess import Spar.Sem.GalleyAccess as GalleyAccess import Spar.Sem.IdPConfigStore (IdPConfigStore) import qualified Spar.Sem.IdPConfigStore as IdPConfigStore @@ -457,7 +459,8 @@ createValidScimUser :: Member BrigAccess r, Member ScimExternalIdStore r, Member ScimUserTimesStore r, - Member SAMLUserStore r + Member SAMLUserStore r, + Member IdPConfigStore r ) => ScimTokenInfo -> ST.ValidScimUser -> @@ -470,37 +473,50 @@ createValidScimUser tokeninfo@ScimTokenInfo {stiTeam} vsu@(ST.ValidScimUser veid ) logScimUserId $ do + lift (ScimExternalIdStore.lookupStatus stiTeam veid) >>= \case + Just (buid, ScimUserCreated) -> + -- If the user has been created, but can't be found in brig anymore, + -- the invitation has timed out and the user has been deleted on brig's side. + -- If this is the case we can safely create the user again. + -- Otherwise we return a conflict error. + lift (BrigAccess.getStatusMaybe buid) >>= \case + Just Active -> throwError externalIdTakenError + Just Suspended -> throwError externalIdTakenError + Just Ephemeral -> throwError externalIdTakenError + Just PendingInvitation -> throwError externalIdTakenError + Just Deleted -> pure () + Nothing -> pure () + Just (buid, ScimUserCreating) -> + incompleteUserCreationCleanUp buid externalIdTakenError + Nothing -> pure () + -- ensure uniqueness constraints of all affected identifiers. -- {if we crash now, retry POST will just work} assertExternalIdUnused stiTeam veid assertHandleUnused handl -- {if we crash now, retry POST will just work, or user gets told the handle -- is already in use and stops POSTing} + buid <- lift $ Id <$> Random.uuid - -- Generate a UserId will be used both for scim user in spar and for brig. - buid <- - lift $ do - buid <- - ST.runValidExternalIdEither - ( \uref -> - do - -- FUTUREWORK: outsource this and some other fragments from - -- `createValidScimUser` into a function `createValidScimUserBrig` similar - -- to `createValidScimUserSpar`? - uid <- Id <$> Random.uuid - BrigAccess.createSAML uref uid stiTeam name ManagedByScim (Just handl) (Just richInfo) language (fromMaybe defaultRole role) - ) - ( \email -> do - buid <- BrigAccess.createNoSAML email stiTeam name language (fromMaybe defaultRole role) - BrigAccess.setHandle buid handl -- FUTUREWORK: possibly do the same one req as we do for saml? - pure buid - ) - veid + lift $ ScimExternalIdStore.insertStatus stiTeam veid buid ScimUserCreating - Logger.debug ("createValidScimUser: brig says " <> show buid) + -- Generate a UserId will be used both for scim user in spar and for brig. + lift $ do + ST.runValidExternalIdEither + ( \uref -> + -- FUTUREWORK: outsource this and some other fragments from + -- `createValidScimUser` into a function `createValidScimUserBrig` similar + -- to `createValidScimUserSpar`? + void $ BrigAccess.createSAML uref buid stiTeam name ManagedByScim (Just handl) (Just richInfo) language (fromMaybe defaultRole role) + ) + ( \email -> do + void $ BrigAccess.createNoSAML email buid stiTeam name language (fromMaybe defaultRole role) + BrigAccess.setHandle buid handl -- FUTUREWORK: possibly do the same one req as we do for saml? + ) + veid + Logger.debug ("createValidScimUser: brig says " <> show buid) - BrigAccess.setRichInfo buid richInfo - pure buid + BrigAccess.setRichInfo buid richInfo -- {If we crash now, a POST retry will fail with 409 user already exists. -- Azure at some point will retry with GET /Users?filter=userName eq handle @@ -530,7 +546,22 @@ createValidScimUser tokeninfo@ScimTokenInfo {stiTeam} vsu@(ST.ValidScimUser veid let new = ST.scimActiveFlagToAccountStatus old (Scim.unScimBool <$> active) active = Scim.active . Scim.value . Scim.thing $ storedUser when (new /= old) $ BrigAccess.setStatus buid new + + lift $ ScimExternalIdStore.insertStatus stiTeam veid buid ScimUserCreated pure storedUser + where + incompleteUserCreationCleanUp :: UserId -> Scim.ScimError -> Scim.ScimHandler (Sem r) () + incompleteUserCreationCleanUp buid e = do + -- something went wrong while storing the user in brig + -- we can try clean up now, but if brig is down, we can't do much + -- maybe retrying the user creation in brig is also an option? + -- after clean up we rethrow the error so the handler returns the correct failure + lift $ Logger.warn $ Log.msg @Text "An earlier attempt of creating a user with this external ID has failed and left some inconsistent data. Attempting to clean up." + withExceptT (const e) $ deleteScimUser tokeninfo buid + lift $ Logger.info $ Log.msg @Text "Clean up successful." + + externalIdTakenError :: Scim.ScimError + externalIdTakenError = Scim.conflict {Scim.detail = Just "ExternalId is already taken"} -- | Store scim timestamps, saml credentials, scim externalId locally in spar. Table -- `spar.scim_external` gets an entry iff there is no `UserRef`: if there is, we don't do a diff --git a/services/spar/src/Spar/Sem/BrigAccess.hs b/services/spar/src/Spar/Sem/BrigAccess.hs index 8fd12220ee8..450edf7564e 100644 --- a/services/spar/src/Spar/Sem/BrigAccess.hs +++ b/services/spar/src/Spar/Sem/BrigAccess.hs @@ -62,7 +62,7 @@ import Wire.API.User.Scim (ValidExternalId (..)) data BrigAccess m a where CreateSAML :: SAML.UserRef -> UserId -> TeamId -> Name -> ManagedBy -> Maybe Handle -> Maybe RichInfo -> Maybe Locale -> Role -> BrigAccess m UserId - CreateNoSAML :: Email -> TeamId -> Name -> Maybe Locale -> Role -> BrigAccess m UserId + CreateNoSAML :: Email -> UserId -> TeamId -> Name -> Maybe Locale -> Role -> BrigAccess m UserId UpdateEmail :: UserId -> Email -> BrigAccess m () GetAccount :: HavePendingInvitations -> UserId -> BrigAccess m (Maybe UserAccount) GetByHandle :: Handle -> BrigAccess m (Maybe UserAccount) diff --git a/services/spar/src/Spar/Sem/BrigAccess/Http.hs b/services/spar/src/Spar/Sem/BrigAccess/Http.hs index 0331cca84d1..a1e5f8d04be 100644 --- a/services/spar/src/Spar/Sem/BrigAccess/Http.hs +++ b/services/spar/src/Spar/Sem/BrigAccess/Http.hs @@ -44,7 +44,7 @@ brigAccessToHttp mgr req = interpret $ viaRunHttp (RunHttpEnv mgr req) . \case CreateSAML u itlu itlt n m h ri ml r -> Intra.createBrigUserSAML u itlu itlt n m h ri ml r - CreateNoSAML e itlt n ml r -> Intra.createBrigUserNoSAML e itlt n ml r + CreateNoSAML e uid itlt n ml r -> Intra.createBrigUserNoSAML e uid itlt n ml r UpdateEmail itlu e -> Intra.updateEmail itlu e GetAccount h itlu -> Intra.getBrigUserAccount h itlu GetByHandle h -> Intra.getBrigUserByHandle h diff --git a/services/spar/src/Spar/Sem/ScimExternalIdStore.hs b/services/spar/src/Spar/Sem/ScimExternalIdStore.hs index 7ea4fe759c5..604c089d393 100644 --- a/services/spar/src/Spar/Sem/ScimExternalIdStore.hs +++ b/services/spar/src/Spar/Sem/ScimExternalIdStore.hs @@ -22,6 +22,8 @@ module Spar.Sem.ScimExternalIdStore insert, lookup, delete, + insertStatus, + lookupStatus, ) where @@ -29,12 +31,17 @@ import Data.Id (TeamId, UserId) import Imports (Maybe, Show) import Polysemy import Polysemy.Check (deriveGenericK) +import Spar.Scim.Types import Wire.API.User.Identity (Email) +import Wire.API.User.Scim data ScimExternalIdStore m a where Insert :: TeamId -> Email -> UserId -> ScimExternalIdStore m () Lookup :: TeamId -> Email -> ScimExternalIdStore m (Maybe UserId) Delete :: TeamId -> Email -> ScimExternalIdStore m () + -- NB: the fact that we are using `Email` in some cases here and `ValidExternalId` in others has historical reasons (this table was only used for non-saml accounts in the past, now it is used for *all* scim-managed accounts). the interface would work equally well with just `Text` here (for unvalidated scim external id). + InsertStatus :: TeamId -> ValidExternalId -> UserId -> ScimUserCreationStatus -> ScimExternalIdStore m () + LookupStatus :: TeamId -> ValidExternalId -> ScimExternalIdStore m (Maybe (UserId, ScimUserCreationStatus)) deriving instance Show (ScimExternalIdStore m a) diff --git a/services/spar/src/Spar/Sem/ScimExternalIdStore/Cassandra.hs b/services/spar/src/Spar/Sem/ScimExternalIdStore/Cassandra.hs index 6dad02d5fad..73c192dafdf 100644 --- a/services/spar/src/Spar/Sem/ScimExternalIdStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/ScimExternalIdStore/Cassandra.hs @@ -23,11 +23,15 @@ module Spar.Sem.ScimExternalIdStore.Cassandra where import Cassandra +import Data.Bifunctor (second) import Data.Id import Imports import Polysemy +import Spar.Data.Instances () +import Spar.Scim.Types (ScimUserCreationStatus (ScimUserCreated)) import Spar.Sem.ScimExternalIdStore (ScimExternalIdStore (..)) import Wire.API.User.Identity +import Wire.API.User.Scim (ValidExternalId, runValidExternalIdUnsafe) scimExternalIdStoreToCassandra :: forall m r a. @@ -40,6 +44,8 @@ scimExternalIdStoreToCassandra = Insert tid em uid -> insertScimExternalId tid em uid Lookup tid em -> lookupScimExternalId tid em Delete tid em -> deleteScimExternalId tid em + InsertStatus tid veid buid status -> insertScimExternalIdStatus tid veid buid status + LookupStatus tid veid -> lookupScimExternalIdStatus tid veid -- | If a scim externalId does not have an associated saml idp issuer, it cannot be stored in -- table @spar.user@. In those cases, and only in those cases, we store the mapping to @@ -67,3 +73,19 @@ deleteScimExternalId tid (fromEmail -> email) = where delete :: PrepQuery W (TeamId, Text) () delete = "DELETE FROM scim_external WHERE team = ? and external_id = ?" + +insertScimExternalIdStatus :: (HasCallStack, MonadClient m) => TeamId -> ValidExternalId -> UserId -> ScimUserCreationStatus -> m () +insertScimExternalIdStatus tid veid uid status = + retry x5 . write insert $ params LocalQuorum (tid, runValidExternalIdUnsafe veid, uid, status) + where + insert :: PrepQuery W (TeamId, Text, UserId, ScimUserCreationStatus) () + insert = "INSERT INTO scim_external (team, external_id, user, creation_status) VALUES (?, ?, ?, ?)" + +lookupScimExternalIdStatus :: (HasCallStack, MonadClient m) => TeamId -> ValidExternalId -> m (Maybe (UserId, ScimUserCreationStatus)) +lookupScimExternalIdStatus tid veid = do + mResult <- retry x1 . query1 sel $ params LocalQuorum (tid, runValidExternalIdUnsafe veid) + -- if the user exists and the status is not present, we assume the user was created successfully + pure $ mResult <&> second (fromMaybe ScimUserCreated) + where + sel :: PrepQuery R (TeamId, Text) (UserId, Maybe ScimUserCreationStatus) + sel = "SELECT user, creation_status FROM scim_external WHERE team = ? and external_id = ?" diff --git a/services/spar/src/Spar/Sem/ScimExternalIdStore/Mem.hs b/services/spar/src/Spar/Sem/ScimExternalIdStore/Mem.hs index 03b742c3a60..3af1a26437d 100644 --- a/services/spar/src/Spar/Sem/ScimExternalIdStore/Mem.hs +++ b/services/spar/src/Spar/Sem/ScimExternalIdStore/Mem.hs @@ -27,14 +27,18 @@ import qualified Data.Map as M import Imports import Polysemy import Polysemy.State +import Spar.Scim (runValidExternalIdUnsafe) +import Spar.Scim.Types (ScimUserCreationStatus) import Spar.Sem.ScimExternalIdStore -import Wire.API.User.Identity (Email) +import Wire.API.User (fromEmail) scimExternalIdStoreToMem :: Sem (ScimExternalIdStore ': r) a -> - Sem r (Map (TeamId, Email) UserId, a) + Sem r (Map (TeamId, Text) (UserId, Maybe ScimUserCreationStatus), a) scimExternalIdStoreToMem = (runState mempty .) $ reinterpret $ \case - Insert tid em uid -> modify $ M.insert (tid, em) uid - Lookup tid em -> gets $ M.lookup (tid, em) - Delete tid em -> modify $ M.delete (tid, em) + Insert tid em uid -> modify $ M.insert (tid, fromEmail em) (uid, Nothing) + Lookup tid em -> fmap fst <$> gets (M.lookup (tid, fromEmail em)) + Delete tid em -> modify $ M.delete (tid, fromEmail em) + InsertStatus tid veid uid status -> modify $ M.insert (tid, runValidExternalIdUnsafe veid) (uid, Just status) + LookupStatus tid veid -> ((=<<) (\(uid, mStatus) -> (uid,) <$> mStatus)) <$> gets (M.lookup (tid, runValidExternalIdUnsafe veid)) diff --git a/services/spar/src/Spar/Sem/ScimExternalIdStore/Spec.hs b/services/spar/src/Spar/Sem/ScimExternalIdStore/Spec.hs index 7593f11c7e9..38ad7834c00 100644 --- a/services/spar/src/Spar/Sem/ScimExternalIdStore/Spec.hs +++ b/services/spar/src/Spar/Sem/ScimExternalIdStore/Spec.hs @@ -24,6 +24,7 @@ import Data.Id import Imports import Polysemy import Polysemy.Check +import Spar.Scim.Types (ScimUserCreationStatus) import qualified Spar.Sem.ScimExternalIdStore as E import Test.Hspec import Test.Hspec.QuickCheck @@ -45,15 +46,17 @@ propsForInterpreter interpreter extract lower = do prop "insert/lookup" $ prop_insertLookup (Just $ show . void . extract) lower prop "insert/insert" $ prop_insertInsert (Just $ show . void . extract) lower +-- FUTUREWORK: Add prop tests for missing operations + -- | All the constraints we need to generalize properties in this module. -- A regular type synonym doesn't work due to dreaded impredicative -- polymorphism. class - (Arbitrary UserId, CoArbitrary UserId, Functor f, Member E.ScimExternalIdStore r, forall z. Show z => Show (f z), forall z. Eq z => Eq (f z)) => + (Arbitrary UserId, CoArbitrary UserId, Arbitrary ScimUserCreationStatus, CoArbitrary ScimUserCreationStatus, Functor f, Member E.ScimExternalIdStore r, forall z. Show z => Show (f z), forall z. Eq z => Eq (f z)) => PropConstraints r f instance - (CoArbitrary UserId, Functor f, Member E.ScimExternalIdStore r, forall z. Show z => Show (f z), forall z. Eq z => Eq (f z)) => + (CoArbitrary UserId, CoArbitrary ScimUserCreationStatus, Functor f, Member E.ScimExternalIdStore r, forall z. Show z => Show (f z), forall z. Eq z => Eq (f z)) => PropConstraints r f prop_insertLookup :: diff --git a/services/spar/test/Arbitrary.hs b/services/spar/test/Arbitrary.hs index 44d8f38ddac..1908c2c3dff 100644 --- a/services/spar/test/Arbitrary.hs +++ b/services/spar/test/Arbitrary.hs @@ -31,6 +31,7 @@ import SAML2.WebSSO.Test.Arbitrary () import SAML2.WebSSO.Types import Servant.API.ContentTypes import Spar.Scim +import Spar.Scim.Types (ScimUserCreationStatus) import qualified Spar.Sem.IdPConfigStore as E import Test.QuickCheck import URI.ByteString @@ -115,3 +116,5 @@ instance CoArbitrary (IdPConfig WireIdP) instance CoArbitrary IdPMetadata where coarbitrary = coarbitrary . show + +instance CoArbitrary ScimUserCreationStatus diff --git a/services/spar/test/Test/Spar/Intra/BrigSpec.hs b/services/spar/test/Test/Spar/Intra/BrigSpec.hs index 2b993e38bf5..129ba720eca 100644 --- a/services/spar/test/Test/Spar/Intra/BrigSpec.hs +++ b/services/spar/test/Test/Spar/Intra/BrigSpec.hs @@ -20,7 +20,6 @@ module Test.Spar.Intra.BrigSpec where import Arbitrary () -import Control.Lens ((^.)) import Imports import SAML2.WebSSO as SAML import Spar.Intra.BrigApp @@ -70,12 +69,3 @@ spec = do it "roundtrips" . property $ \(x :: ValidExternalId) -> (veidFromUserSSOId @(Either String) . veidToUserSSOId) x === Right x - -instance Arbitrary ValidExternalId where - arbitrary = do - muref <- arbitrary - case muref of - Just uref -> case emailFromSAMLNameID $ uref ^. SAML.uidSubject of - Just email -> pure $ EmailAndUref email uref - Nothing -> pure $ UrefOnly uref - Nothing -> EmailOnly <$> arbitrary From 26e62f9aa5cc1fab38c1495e0d0bf6f9f836c09c Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 11 Mar 2024 11:04:19 +0100 Subject: [PATCH 041/117] Lazily attempt to get rabbitmq channel When sending an empty list of notification, we don't want to fail if federation is disabled. --- integration/test/Test/Conversation.hs | 7 +++++++ .../Galley/Intra/BackendNotificationQueue.hs | 19 ++++++++++--------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index 1d5587f40ae..81a15c7e16f 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -864,3 +864,10 @@ testConversationWithFedV0 = do withWebSocket bob $ \ws -> do void $ changeConversationName alice conv "foobar" >>= getJSON 200 void $ awaitMatch isConvNameChangeNotif ws + +testConversationWithoutFederation :: HasCallStack => App () +testConversationWithoutFederation = withModifiedBackend + (def {galleyCfg = removeField "federator" >=> removeField "rabbitmq"}) + $ \domain -> do + [alice, bob] <- createAndConnectUsers [domain, domain] + void $ postConversation alice (defProteus {qualifiedUsers = [bob]}) >>= getJSON 201 diff --git a/services/galley/src/Galley/Intra/BackendNotificationQueue.hs b/services/galley/src/Galley/Intra/BackendNotificationQueue.hs index cefe3cdc1e4..0323a7dff45 100644 --- a/services/galley/src/Galley/Intra/BackendNotificationQueue.hs +++ b/services/galley/src/Galley/Intra/BackendNotificationQueue.hs @@ -74,11 +74,8 @@ enqueueNotificationsConcurrently :: f (Remote x) -> (Remote [x] -> FedQueueClient c a) -> ExceptT FederationError App [Remote a] -enqueueNotificationsConcurrently m xs f = do - chanVar <- getChannel - lift $ pooledForConcurrentlyN 8 (bucketRemote xs) $ \r -> - qualifyAs r - <$> enqueueSingleNotification (tDomain r) m chanVar (f r) +enqueueNotificationsConcurrently m xs f = + enqueueNotificationsConcurrentlyBuckets m (bucketRemote xs) f enqueueNotificationsConcurrentlyBuckets :: (Foldable f) => @@ -87,10 +84,14 @@ enqueueNotificationsConcurrentlyBuckets :: (Remote x -> FedQueueClient c a) -> ExceptT FederationError App [Remote a] enqueueNotificationsConcurrentlyBuckets m xs f = do - chanVar <- getChannel - lift $ pooledForConcurrentlyN 8 (toList xs) $ \r -> - qualifyAs r - <$> enqueueSingleNotification (tDomain r) m chanVar (f r) + case toList xs of + -- only attempt to get a channel if there is at least one notification to send + [] -> pure [] + _ -> do + chanVar <- getChannel + lift $ pooledForConcurrentlyN 8 (toList xs) $ \r -> + qualifyAs r + <$> enqueueSingleNotification (tDomain r) m chanVar (f r) data NoRabbitMqChannel = NoRabbitMqChannel deriving (Show) From 009896eac406c616fc8293a8c03e0d0b0046ab22 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 11 Mar 2024 11:07:32 +0100 Subject: [PATCH 042/117] Add CHANGELOG entry --- changelog.d/3-bug-fixes/enqueue-lazy | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/3-bug-fixes/enqueue-lazy diff --git a/changelog.d/3-bug-fixes/enqueue-lazy b/changelog.d/3-bug-fixes/enqueue-lazy new file mode 100644 index 00000000000..7902f17adf3 --- /dev/null +++ b/changelog.d/3-bug-fixes/enqueue-lazy @@ -0,0 +1 @@ +Fix crash when enqueing an empty list of notifications and federation is disabled From cb4eb8da857719ae3bece93bfcf4291bc45854be Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 11 Mar 2024 11:08:57 +0100 Subject: [PATCH 043/117] Revert "Add CHANGELOG entry" This reverts commit 009896eac406c616fc8293a8c03e0d0b0046ab22. --- changelog.d/3-bug-fixes/enqueue-lazy | 1 - 1 file changed, 1 deletion(-) delete mode 100644 changelog.d/3-bug-fixes/enqueue-lazy diff --git a/changelog.d/3-bug-fixes/enqueue-lazy b/changelog.d/3-bug-fixes/enqueue-lazy deleted file mode 100644 index 7902f17adf3..00000000000 --- a/changelog.d/3-bug-fixes/enqueue-lazy +++ /dev/null @@ -1 +0,0 @@ -Fix crash when enqueing an empty list of notifications and federation is disabled From 08662e0c41ddcdb89c7921dc3b91572de18fb39d Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 11 Mar 2024 11:08:59 +0100 Subject: [PATCH 044/117] Revert "Lazily attempt to get rabbitmq channel" This reverts commit 26e62f9aa5cc1fab38c1495e0d0bf6f9f836c09c. --- integration/test/Test/Conversation.hs | 7 ------- .../Galley/Intra/BackendNotificationQueue.hs | 19 +++++++++---------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index 81a15c7e16f..1d5587f40ae 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -864,10 +864,3 @@ testConversationWithFedV0 = do withWebSocket bob $ \ws -> do void $ changeConversationName alice conv "foobar" >>= getJSON 200 void $ awaitMatch isConvNameChangeNotif ws - -testConversationWithoutFederation :: HasCallStack => App () -testConversationWithoutFederation = withModifiedBackend - (def {galleyCfg = removeField "federator" >=> removeField "rabbitmq"}) - $ \domain -> do - [alice, bob] <- createAndConnectUsers [domain, domain] - void $ postConversation alice (defProteus {qualifiedUsers = [bob]}) >>= getJSON 201 diff --git a/services/galley/src/Galley/Intra/BackendNotificationQueue.hs b/services/galley/src/Galley/Intra/BackendNotificationQueue.hs index 0323a7dff45..cefe3cdc1e4 100644 --- a/services/galley/src/Galley/Intra/BackendNotificationQueue.hs +++ b/services/galley/src/Galley/Intra/BackendNotificationQueue.hs @@ -74,8 +74,11 @@ enqueueNotificationsConcurrently :: f (Remote x) -> (Remote [x] -> FedQueueClient c a) -> ExceptT FederationError App [Remote a] -enqueueNotificationsConcurrently m xs f = - enqueueNotificationsConcurrentlyBuckets m (bucketRemote xs) f +enqueueNotificationsConcurrently m xs f = do + chanVar <- getChannel + lift $ pooledForConcurrentlyN 8 (bucketRemote xs) $ \r -> + qualifyAs r + <$> enqueueSingleNotification (tDomain r) m chanVar (f r) enqueueNotificationsConcurrentlyBuckets :: (Foldable f) => @@ -84,14 +87,10 @@ enqueueNotificationsConcurrentlyBuckets :: (Remote x -> FedQueueClient c a) -> ExceptT FederationError App [Remote a] enqueueNotificationsConcurrentlyBuckets m xs f = do - case toList xs of - -- only attempt to get a channel if there is at least one notification to send - [] -> pure [] - _ -> do - chanVar <- getChannel - lift $ pooledForConcurrentlyN 8 (toList xs) $ \r -> - qualifyAs r - <$> enqueueSingleNotification (tDomain r) m chanVar (f r) + chanVar <- getChannel + lift $ pooledForConcurrentlyN 8 (toList xs) $ \r -> + qualifyAs r + <$> enqueueSingleNotification (tDomain r) m chanVar (f r) data NoRabbitMqChannel = NoRabbitMqChannel deriving (Show) From d9a09d4f5baeb92e610473084d74513536161cb4 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 11 Mar 2024 12:53:43 +0100 Subject: [PATCH 045/117] Welcome notification bug (#3907) * Add failing test reproducing the issue * Only create one welcome notification per user * Add CHANGELOG entry * Lint * Test notifications for both clients --- changelog.d/3-bug-fixes/welcome-notifications | 1 + integration/integration.cabal | 1 + integration/test/Test/MLS/Notifications.hs | 30 +++++++++++++++++++ services/galley/src/Galley/API/MLS/Welcome.hs | 17 +++++++++-- 4 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 changelog.d/3-bug-fixes/welcome-notifications create mode 100644 integration/test/Test/MLS/Notifications.hs diff --git a/changelog.d/3-bug-fixes/welcome-notifications b/changelog.d/3-bug-fixes/welcome-notifications new file mode 100644 index 00000000000..443c5694026 --- /dev/null +++ b/changelog.d/3-bug-fixes/welcome-notifications @@ -0,0 +1 @@ +Fix bug where welcome notifications were generated for each client instead of for each user diff --git a/integration/integration.cabal b/integration/integration.cabal index 5ffb61552cb..1a447c9b7f1 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -128,6 +128,7 @@ library Test.MLS Test.MLS.KeyPackage Test.MLS.Message + Test.MLS.Notifications Test.MLS.One2One Test.MLS.SubConversation Test.MLS.Unreachable diff --git a/integration/test/Test/MLS/Notifications.hs b/integration/test/Test/MLS/Notifications.hs new file mode 100644 index 00000000000..ad0595a48c6 --- /dev/null +++ b/integration/test/Test/MLS/Notifications.hs @@ -0,0 +1,30 @@ +module Test.MLS.Notifications where + +import API.Gundeck +import MLS.Util +import Notifications +import SetupHelpers +import Testlib.Prelude + +testWelcomeNotification :: HasCallStack => App () +testWelcomeNotification = do + [alice, bob] <- createAndConnectUsers [OwnDomain, OtherDomain] + [alice1, alice2, bob1, bob2] <- traverse (createMLSClient def) [alice, alice, bob, bob] + traverse_ uploadNewKeyPackage [alice2, bob1, bob2] + + void $ createNewGroup alice1 + notif <- withWebSocket bob $ \ws -> do + void $ createAddCommit alice1 [alice, bob] >>= sendAndConsumeCommitBundle + awaitMatch isWelcomeNotif ws + + notifId <- notif %. "id" & asString + + for_ [bob1, bob2] $ \cid -> + getNotifications + bob + def + { since = Just notifId, + client = Just cid.client, + size = Just 10000 + } + >>= getJSON 200 diff --git a/services/galley/src/Galley/API/MLS/Welcome.hs b/services/galley/src/Galley/API/MLS/Welcome.hs index 02f336562a1..188d73e6d0d 100644 --- a/services/galley/src/Galley/API/MLS/Welcome.hs +++ b/services/galley/src/Galley/API/MLS/Welcome.hs @@ -26,12 +26,15 @@ import Data.Aeson qualified as A import Data.Domain import Data.Id import Data.Json.Util +import Data.List1 +import Data.Map qualified as Map import Data.Qualified import Data.Time import Galley.API.Push import Galley.Effects.ExternalAccess import Galley.Effects.FederatorAccess -import Imports +import Gundeck.Types.Push.V2 (RecipientClients (..)) +import Imports hiding (cs) import Network.Wai.Utilities.JSONResponse import Polysemy import Polysemy.Input @@ -49,7 +52,7 @@ import Wire.API.MLS.Serialisation import Wire.API.MLS.SubConversation import Wire.API.MLS.Welcome import Wire.API.Message -import Wire.NotificationSubsystem (NotificationSubsystem) +import Wire.NotificationSubsystem sendWelcomes :: ( Member FederatorAccess r, @@ -88,9 +91,17 @@ sendLocalWelcomes :: Local [(UserId, ClientId)] -> Sem r () sendLocalWelcomes qcnv qusr con now welcome lclients = do + -- only create one notification per user + let rcpts = + map (\(u, cs) -> Recipient u (RecipientClientsSome (List1 cs))) + . Map.assocs + . foldr + (\(u, c) -> Map.insertWith (<>) u (pure c)) + mempty + $ tUnqualified lclients let e = Event qcnv Nothing qusr now $ EdMLSWelcome welcome.raw runMessagePush lclients (Just qcnv) $ - newMessagePush mempty con defMessageMetadata (tUnqualified lclients) e + newMessagePush mempty con defMessageMetadata rcpts e sendRemoteWelcomes :: ( Member FederatorAccess r, From 59cfe692435b61b25b6515d92fd9e16db7663212 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 11 Mar 2024 15:26:53 +0100 Subject: [PATCH 046/117] Lazily attempt to get rabbitmq channel (#3936) * Lazily attempt to get rabbitmq channel When sending an empty list of notification, we don't want to fail if federation is disabled. * Add CHANGELOG entry --- changelog.d/3-bug-fixes/enqueue-lazy | 1 + integration/test/Test/Conversation.hs | 7 +++++++ .../Galley/Intra/BackendNotificationQueue.hs | 19 ++++++++++--------- 3 files changed, 18 insertions(+), 9 deletions(-) create mode 100644 changelog.d/3-bug-fixes/enqueue-lazy diff --git a/changelog.d/3-bug-fixes/enqueue-lazy b/changelog.d/3-bug-fixes/enqueue-lazy new file mode 100644 index 00000000000..7902f17adf3 --- /dev/null +++ b/changelog.d/3-bug-fixes/enqueue-lazy @@ -0,0 +1 @@ +Fix crash when enqueing an empty list of notifications and federation is disabled diff --git a/integration/test/Test/Conversation.hs b/integration/test/Test/Conversation.hs index 1d5587f40ae..81a15c7e16f 100644 --- a/integration/test/Test/Conversation.hs +++ b/integration/test/Test/Conversation.hs @@ -864,3 +864,10 @@ testConversationWithFedV0 = do withWebSocket bob $ \ws -> do void $ changeConversationName alice conv "foobar" >>= getJSON 200 void $ awaitMatch isConvNameChangeNotif ws + +testConversationWithoutFederation :: HasCallStack => App () +testConversationWithoutFederation = withModifiedBackend + (def {galleyCfg = removeField "federator" >=> removeField "rabbitmq"}) + $ \domain -> do + [alice, bob] <- createAndConnectUsers [domain, domain] + void $ postConversation alice (defProteus {qualifiedUsers = [bob]}) >>= getJSON 201 diff --git a/services/galley/src/Galley/Intra/BackendNotificationQueue.hs b/services/galley/src/Galley/Intra/BackendNotificationQueue.hs index cefe3cdc1e4..0323a7dff45 100644 --- a/services/galley/src/Galley/Intra/BackendNotificationQueue.hs +++ b/services/galley/src/Galley/Intra/BackendNotificationQueue.hs @@ -74,11 +74,8 @@ enqueueNotificationsConcurrently :: f (Remote x) -> (Remote [x] -> FedQueueClient c a) -> ExceptT FederationError App [Remote a] -enqueueNotificationsConcurrently m xs f = do - chanVar <- getChannel - lift $ pooledForConcurrentlyN 8 (bucketRemote xs) $ \r -> - qualifyAs r - <$> enqueueSingleNotification (tDomain r) m chanVar (f r) +enqueueNotificationsConcurrently m xs f = + enqueueNotificationsConcurrentlyBuckets m (bucketRemote xs) f enqueueNotificationsConcurrentlyBuckets :: (Foldable f) => @@ -87,10 +84,14 @@ enqueueNotificationsConcurrentlyBuckets :: (Remote x -> FedQueueClient c a) -> ExceptT FederationError App [Remote a] enqueueNotificationsConcurrentlyBuckets m xs f = do - chanVar <- getChannel - lift $ pooledForConcurrentlyN 8 (toList xs) $ \r -> - qualifyAs r - <$> enqueueSingleNotification (tDomain r) m chanVar (f r) + case toList xs of + -- only attempt to get a channel if there is at least one notification to send + [] -> pure [] + _ -> do + chanVar <- getChannel + lift $ pooledForConcurrentlyN 8 (toList xs) $ \r -> + qualifyAs r + <$> enqueueSingleNotification (tDomain r) m chanVar (f r) data NoRabbitMqChannel = NoRabbitMqChannel deriving (Show) From 387fc9d808698ca6590a0dc744692f3db7e9d6f8 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 11 Mar 2024 17:04:26 +0100 Subject: [PATCH 047/117] WPB-6524 Added optional api proxy attribute to deeplink json in nginz chart (#3933) --- changelog.d/3-bug-fixes/WPB-6524 | 1 + charts/nginz/templates/conf/_deeplink.json.tpl | 9 +++++++++ 2 files changed, 10 insertions(+) create mode 100644 changelog.d/3-bug-fixes/WPB-6524 diff --git a/changelog.d/3-bug-fixes/WPB-6524 b/changelog.d/3-bug-fixes/WPB-6524 new file mode 100644 index 00000000000..472e85809eb --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-6524 @@ -0,0 +1 @@ +Optional `apiProxy` attribute added to `deeplink.json` in nginz chart diff --git a/charts/nginz/templates/conf/_deeplink.json.tpl b/charts/nginz/templates/conf/_deeplink.json.tpl index da5ddb19a6d..5a11f07b1a6 100644 --- a/charts/nginz/templates/conf/_deeplink.json.tpl +++ b/charts/nginz/templates/conf/_deeplink.json.tpl @@ -15,6 +15,15 @@ "websiteURL": {{ .websiteURL | quote }} {{- end }} }, + {{- if hasKey . "apiProxy" }} + {{- with .apiProxy }} + "apiProxy" : { + "host" : {{ .host | quote }}, + "port" : {{ .port }}, + "needsAuthentication" : {{ .needsAuthentication }} + }, + {{- end }} + {{- end }} "title" : {{ .title | quote }} } {{- end }} From a5716e4d93d03e6f30513157bf138aae8b31ff3b Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Mon, 11 Mar 2024 17:23:14 +0100 Subject: [PATCH 048/117] avoid IO Exception when group state not set (#3939) --- changelog.d/3-bug-fixes/WPB-7023 | 5 +++++ services/galley/src/Galley/Cassandra/Conversation.hs | 4 ++-- services/galley/src/Galley/Cassandra/Queries.hs | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 changelog.d/3-bug-fixes/WPB-7023 diff --git a/changelog.d/3-bug-fixes/WPB-7023 b/changelog.d/3-bug-fixes/WPB-7023 new file mode 100644 index 00000000000..69cab46b547 --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-7023 @@ -0,0 +1,5 @@ +Avoid IO Exception when querying + + GET /converations/{cnv_domain}/{cnv}/groupinfo + +with public group state not set in galley.converation. diff --git a/services/galley/src/Galley/Cassandra/Conversation.hs b/services/galley/src/Galley/Cassandra/Conversation.hs index 2d24adb63b2..098a4771cac 100644 --- a/services/galley/src/Galley/Cassandra/Conversation.hs +++ b/services/galley/src/Galley/Cassandra/Conversation.hs @@ -198,8 +198,8 @@ conversationMeta conv = getGroupInfo :: ConvId -> Client (Maybe GroupInfoData) getGroupInfo cid = do - runIdentity - <$$> retry + (runIdentity =<<) + <$> retry x1 ( query1 Cql.selectGroupInfo diff --git a/services/galley/src/Galley/Cassandra/Queries.hs b/services/galley/src/Galley/Cassandra/Queries.hs index 560d8d9a19f..ef6e26f5a4a 100644 --- a/services/galley/src/Galley/Cassandra/Queries.hs +++ b/services/galley/src/Galley/Cassandra/Queries.hs @@ -305,7 +305,7 @@ deleteConv = "delete from conversation using timestamp 32503680000000000 where c markConvDeleted :: PrepQuery W (Identity ConvId) () markConvDeleted = {- `IF EXISTS`, but that requires benchmarking -} "update conversation set deleted = true where conv = ?" -selectGroupInfo :: PrepQuery R (Identity ConvId) (Identity GroupInfoData) +selectGroupInfo :: PrepQuery R (Identity ConvId) (Identity (Maybe GroupInfoData)) selectGroupInfo = "select public_group_state from conversation where conv = ?" updateGroupInfo :: PrepQuery W (GroupInfoData, ConvId) () From 709ee55bd2d7c2ae229feefbe10532a87ee1e40b Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Tue, 12 Mar 2024 13:40:39 +0100 Subject: [PATCH 049/117] Make ejpd data model exhaustive (#3875) * Nit-pick. * Code layout. * Remove duplicated helper. * Pick easier defPassword. * Refactor EJPD response structure. * Test: assets. * Implement: assets. * Move tests to /integration. --- changelog.d/5-internal/wpb-6329_ejpd_stuff | 6 + integration/default.nix | 4 + integration/integration.cabal | 3 + integration/test/API/BrigInternal.hs | 10 + integration/test/API/Cargohold.hs | 23 ++- integration/test/API/Common.hs | 4 +- integration/test/SetupHelpers.hs | 47 ++++- integration/test/Test/AssetDownload.hs | 21 +-- integration/test/Test/EJPD.hs | 172 ++++++++++++++++++ integration/test/Testlib/Assertions.hs | 92 +++++++++- .../src/Wire/API/Routes/Internal/Brig/EJPD.hs | 27 ++- .../src/Wire/API/Routes/Internal/Cargohold.hs | 7 +- services/brig/src/Brig/API/Internal.hs | 27 +-- services/brig/src/Brig/App.hs | 3 + services/brig/src/Brig/User/EJPD.hs | 56 ++++-- .../brig/test/integration/API/Internal.hs | 58 +----- .../test/integration/API/Internal/Util.hs | 88 +-------- .../cargohold/src/CargoHold/API/Public.hs | 18 +- services/cargohold/src/CargoHold/API/V3.hs | 4 + 19 files changed, 476 insertions(+), 194 deletions(-) create mode 100644 changelog.d/5-internal/wpb-6329_ejpd_stuff create mode 100644 integration/test/Test/EJPD.hs diff --git a/changelog.d/5-internal/wpb-6329_ejpd_stuff b/changelog.d/5-internal/wpb-6329_ejpd_stuff new file mode 100644 index 00000000000..6da6f5b419d --- /dev/null +++ b/changelog.d/5-internal/wpb-6329_ejpd_stuff @@ -0,0 +1,6 @@ +Add assets to output of ejpd-info end-point in stern; also: + +- [brig] now talks to carghold for profile picture extraction; +- [integration] migrate ejpd tests; +- [integration] enhanced `shouldMatch` shows a diff on failure now; +- [integration] added `shouldMatchLeniently` for rule-based canonicalization of arguments diff --git a/integration/default.nix b/integration/default.nix index 36f503e3c9c..a259708844e 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -4,6 +4,7 @@ # dependencies are added or removed. { mkDerivation , aeson +, aeson-diff , aeson-pretty , array , async @@ -72,6 +73,7 @@ , warp-tls , websockets , wire-message-proto-lens +, wreq , xml , yaml }: @@ -91,6 +93,7 @@ mkDerivation { ]; libraryHaskellDepends = [ aeson + aeson-diff aeson-pretty array async @@ -155,6 +158,7 @@ mkDerivation { warp-tls websockets wire-message-proto-lens + wreq xml yaml ]; diff --git a/integration/integration.cabal b/integration/integration.cabal index 1a447c9b7f1..bcafb9ff147 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -118,6 +118,7 @@ library Test.Connection Test.Conversation Test.Demo + Test.EJPD Test.Errors Test.ExternalPartner Test.FeatureFlags @@ -166,6 +167,7 @@ library build-depends: , aeson + , aeson-diff , aeson-pretty , array , async @@ -230,5 +232,6 @@ library , warp-tls , websockets , wire-message-proto-lens + , wreq , xml , yaml diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index 5eef85edea8..d538bb35561 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -243,3 +243,13 @@ getClientsFull user users = do val <- make users baseRequest user Brig Unversioned do joinHttpPath ["i", "clients", "full"] >>= submit "POST" . addJSONObject ["users" .= val] + +-- | https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/post_i_ejpd_request +getEJPDInfo :: (HasCallStack, MakesValue dom) => dom -> [String] -> String -> App Response +getEJPDInfo dom handles mode = do + req <- rawBaseRequest dom Brig Unversioned "/i/ejpd-request" + let query = case mode of + "" -> [] + "include_contacts" -> [("include_contacts", "true")] + bad -> error $ show bad + submit "POST" $ req & addJSONObject ["ejpd_request" .= handles] & addQueryParams query diff --git a/integration/test/API/Cargohold.hs b/integration/test/API/Cargohold.hs index 595ce75327a..0fe767fea35 100644 --- a/integration/test/API/Cargohold.hs +++ b/integration/test/API/Cargohold.hs @@ -37,7 +37,10 @@ uploadAssetV3 user isPublic retention mimeType bdy = do multipartMixedMime = "multipart/mixed; boundary=" <> multipartBoundary uploadAsset :: (HasCallStack, MakesValue user) => user -> App Response -uploadAsset user = do +uploadAsset = flip uploadFreshAsset "Hello World!" + +uploadFreshAsset :: (HasCallStack, MakesValue user) => user -> String -> App Response +uploadFreshAsset user payload = do uid <- user & objId req <- baseRequest user Cargohold Versioned "/assets" bdy <- txtAsset @@ -51,7 +54,7 @@ uploadAsset user = do buildUploadAssetRequestBody True (Nothing :: Maybe String) - (LBSC.pack "Hello World!") + (LBSC.pack payload) textPlainMime textPlainMime :: MIME.MIMEType @@ -104,13 +107,25 @@ instance MakesValue loc => IsAssetLocation loc where noRedirect :: Request -> Request noRedirect r = r {redirectCount = 0} -downloadAsset' :: (HasCallStack, MakesValue user, IsAssetLocation loc, IsAssetToken tok) => user -> loc -> tok -> App Response +downloadAsset' :: + (HasCallStack, MakesValue user, IsAssetLocation loc, IsAssetToken tok) => + user -> + loc -> + tok -> + App Response downloadAsset' user loc tok = do locPath <- locationPathFragment loc req <- baseRequest user Cargohold Unversioned $ locPath submit "GET" $ req & tokenParam tok & noRedirect -downloadAsset :: (HasCallStack, MakesValue user, MakesValue key, MakesValue assetDomain) => user -> assetDomain -> key -> String -> (HTTP.Request -> HTTP.Request) -> App Response +downloadAsset :: + (HasCallStack, MakesValue user, MakesValue key, MakesValue assetDomain) => + user -> + assetDomain -> + key -> + String -> + (HTTP.Request -> HTTP.Request) -> + App Response downloadAsset user assetDomain key zHostHeader trans = do domain <- objDomain assetDomain key' <- asString key diff --git a/integration/test/API/Common.hs b/integration/test/API/Common.hs index 623a2c56579..066c360a422 100644 --- a/integration/test/API/Common.hs +++ b/integration/test/API/Common.hs @@ -14,8 +14,10 @@ teamRole "admin" = 5951 teamRole "owner" = 8191 teamRole bad = error $ "unknown team role: " <> bad +-- | please don't use special shell characters like '!' here. it makes writing shell lines +-- that use test data a lot less straight-forward. defPassword :: String -defPassword = "hunter2!" +defPassword = "hunter2." randomEmail :: App String randomEmail = do diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 0c263d969e3..0181d9325c8 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -5,6 +5,7 @@ module SetupHelpers where import API.Brig import API.BrigInternal +import API.Cargohold import API.Common import API.Galley import API.GalleyInternal (legalholdWhitelistTeam) @@ -14,8 +15,10 @@ import Data.Aeson hiding ((.=)) import qualified Data.Aeson.Types as Aeson import qualified Data.ByteString.Base64.URL as B64Url import Data.ByteString.Char8 (unpack) +import qualified Data.CaseInsensitive as CI import Data.Default import Data.Function +import Data.String.Conversions (cs) import Data.UUID.V1 (nextUUID) import Data.UUID.V4 (nextRandom) import GHC.Stack @@ -90,7 +93,7 @@ connectTwoUsers alice bob = do bindResponse (postConnection alice bob) (\resp -> resp.status `shouldMatchInt` 201) bindResponse (putConnection bob alice "accepted") (\resp -> resp.status `shouldMatchInt` 200) -connectUsers :: HasCallStack => [Value] -> App () +connectUsers :: (HasCallStack, MakesValue usr) => [usr] -> App () connectUsers users = traverse_ (uncurry connectTwoUsers) $ do t <- tails users (a, others) <- maybeToList (uncons t) @@ -326,3 +329,45 @@ randomScimUser = do "userName" .= handle, "displayName" .= handle ] + +-- | This adds one random asset to the `assets` field in the user record and returns an asset +-- key. The asset carries a fresh UUIDv4 in text form (even though it is typed 'preview` and +-- `image'). +uploadProfilePicture :: (HasCallStack, MakesValue usr) => usr -> App (String, String, String) +uploadProfilePicture usr = do + payload <- ("asset_contents=" <>) <$> randomId + asset <- bindResponse (uploadFreshAsset usr payload) (getJSON 201) + dom <- asset %. "domain" & asString + key <- asset %. "key" & asString + Success (oldAssets :: [Value]) <- bindResponse (getSelf usr) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "assets" <&> fromJSON + bindResponse + (putSelf usr def {assets = Just (object ["key" .= key, "size" .= "preview", "type" .= "image"] : oldAssets)}) + assertSuccess + pure (dom, key, payload) + +-- | Take a calling user (any user will do) and an asset domain and key, and return a +-- (temporarily valid) s3 url plus asset payload (if created with `uploadProfilePicture`, +-- that's a UUIDv4). +downloadProfilePicture :: (HasCallStack, MakesValue caller) => caller -> String -> String -> App (String, String) +downloadProfilePicture caller assetDomain assetKey = do + locurl <- bindResponse (downloadAsset caller caller assetKey assetDomain noRedirect) $ \resp -> do + resp.status `shouldMatchInt` 302 + maybe + (error "no location header in 302 response!?") + (pure . cs) + (lookup (CI.mk (cs "Location")) resp.headers) + + payload <- bindResponse (downloadAsset caller caller assetKey assetDomain id) $ \resp -> do + resp.status `shouldMatchInt` 200 + pure $ cs resp.body + + pure (locurl, payload) + +-- | Call 'uploadProfilePicture' and 'downloadPicture', returning the return value of the +-- latter. +uploadDownloadProfilePicture :: (HasCallStack, MakesValue usr) => usr -> App (String, String) +uploadDownloadProfilePicture usr = do + (dom, key, _payload) <- uploadProfilePicture usr + downloadProfilePicture usr dom key diff --git a/integration/test/Test/AssetDownload.hs b/integration/test/Test/AssetDownload.hs index 2d73fb7ff9e..68b60c85453 100644 --- a/integration/test/Test/AssetDownload.hs +++ b/integration/test/Test/AssetDownload.hs @@ -2,8 +2,6 @@ module Test.AssetDownload where import API.Cargohold import GHC.Stack -import Network.HTTP.Client (Request (redirectCount)) -import qualified Network.HTTP.Client as HTTP import SetupHelpers import Testlib.Prelude @@ -28,16 +26,16 @@ testDownloadAssetMultiIngressS3DownloadUrl = do -- multi-ingress disabled key <- doUploadAsset user - bindResponse (downloadAsset user user key "nginz-https.example.com" noRedirects) $ \resp -> do + bindResponse (downloadAsset user user key "nginz-https.example.com" noRedirect) $ \resp -> do resp.status `shouldMatchInt` 302 - bindResponse (downloadAsset user user key "red.example.com" noRedirects) $ \resp -> do + bindResponse (downloadAsset user user key "red.example.com" noRedirect) $ \resp -> do resp.status `shouldMatchInt` 302 - bindResponse (downloadAsset user user key "green.example.com" noRedirects) $ \resp -> do + bindResponse (downloadAsset user user key "green.example.com" noRedirect) $ \resp -> do resp.status `shouldMatchInt` 302 - bindResponse (downloadAsset user user key "unknown.example.com" noRedirects) $ \resp -> do + bindResponse (downloadAsset user user key "unknown.example.com" noRedirect) $ \resp -> do resp.status `shouldMatchInt` 302 -- multi-ingress enabled @@ -45,25 +43,22 @@ testDownloadAssetMultiIngressS3DownloadUrl = do user' <- randomUser domain def key' <- doUploadAsset user' - bindResponse (downloadAsset user' user' key' "nginz-https.example.com" noRedirects) $ \resp -> do + bindResponse (downloadAsset user' user' key' "nginz-https.example.com" noRedirect) $ \resp -> do resp.status `shouldMatchInt` 404 resp.json %. "label" `shouldMatch` "not-found" - bindResponse (downloadAsset user' user' key' "red.example.com" noRedirects) $ \resp -> do + bindResponse (downloadAsset user' user' key' "red.example.com" noRedirect) $ \resp -> do resp.status `shouldMatchInt` 302 locationHeaderHost resp `shouldMatch` "s3-download.red.example.com" - bindResponse (downloadAsset user' user' key' "green.example.com" noRedirects) $ \resp -> do + bindResponse (downloadAsset user' user' key' "green.example.com" noRedirect) $ \resp -> do resp.status `shouldMatchInt` 302 locationHeaderHost resp `shouldMatch` "s3-download.green.example.com" - bindResponse (downloadAsset user' user' key' "unknown.example.com" noRedirects) $ \resp -> do + bindResponse (downloadAsset user' user' key' "unknown.example.com" noRedirect) $ \resp -> do resp.status `shouldMatchInt` 404 resp.json %. "label" `shouldMatch` "not-found" where - noRedirects :: HTTP.Request -> HTTP.Request - noRedirects req = (req {redirectCount = 0}) - modifyConfig :: ServiceOverrides modifyConfig = def diff --git a/integration/test/Test/EJPD.hs b/integration/test/Test/EJPD.hs new file mode 100644 index 00000000000..b20e74a6634 --- /dev/null +++ b/integration/test/Test/EJPD.hs @@ -0,0 +1,172 @@ +{-# OPTIONS -Wno-ambiguous-fields #-} +module Test.EJPD (testEJPDRequest) where + +import API.Brig +import qualified API.BrigInternal as BI +import API.Gundeck +import Control.Lens hiding ((.=)) +import Control.Monad.Reader +import qualified Data.Aeson as A +import Data.Aeson.Lens +import Data.String.Conversions (cs) +import qualified Data.UUID as UUID +import qualified Data.UUID.V4 as UUID +import qualified Network.Wreq as Wreq +import SetupHelpers +import Testlib.JSON +import Testlib.Prelude + +-- | Create some teams & users, and return their expected ejpd response values. +setupEJPD :: HasCallStack => App (A.Value, A.Value, A.Value, A.Value, A.Value) +setupEJPD = + do + (owner1, _tid1, [usr1, usr2]) <- createTeam OwnDomain 3 + handle1 <- liftIO $ UUID.nextRandom <&> ("usr1-handle-" <>) . UUID.toString + handle2 <- liftIO $ UUID.nextRandom <&> ("usr2-handle-" <>) . UUID.toString + void $ putHandle usr1 handle1 + void $ putHandle usr2 handle2 + email3 <- liftIO $ UUID.nextRandom <&> \uuid -> "usr3-" <> UUID.toString uuid <> "@example.com" + email4 <- liftIO $ UUID.nextRandom <&> \uuid -> "usr4-" <> UUID.toString uuid <> "@example.com" + email5 <- liftIO $ UUID.nextRandom <&> \uuid -> "usr5-" <> UUID.toString uuid <> "@example.com" + usr3 <- randomUser OwnDomain def {BI.email = Just email3, BI.name = Just "usr3"} + usr4 <- randomUser OwnDomain def {BI.email = Just email4, BI.name = Just "usr4"} + usr5 <- randomUser OwnDomain def {BI.email = Just email5, BI.name = Just "usr5"} + handle3 <- liftIO $ UUID.nextRandom <&> ("usr3-handle-" <>) . UUID.toString + handle4 <- liftIO $ UUID.nextRandom <&> ("usr4-handle-" <>) . UUID.toString + handle5 <- liftIO $ UUID.nextRandom <&> ("usr5-handle-" <>) . UUID.toString + void $ putHandle usr3 handle3 + void $ putHandle usr4 handle4 + void $ putHandle usr5 handle5 + + connectTwoUsers usr3 usr5 + connectTwoUsers usr2 usr4 + connectTwoUsers usr4 usr5 + + toks1 <- do + cl11 <- objId $ addClient (usr1 %. "qualified_id") def >>= getJSON 201 + bindResponse (postPushToken usr1 cl11 def) $ \resp -> do + resp.status `shouldMatchInt` 201 + tok <- resp.json %. "token" & asString + pure [tok] + toks2 <- do + cl21 <- objId $ addClient (usr2 %. "qualified_id") def >>= getJSON 201 + cl22 <- objId $ addClient (usr2 %. "qualified_id") def >>= getJSON 201 + t1 <- bindResponse (postPushToken usr2 cl21 def) $ \resp -> do + resp.status `shouldMatchInt` 201 + resp.json %. "token" & asString + t2 <- bindResponse (postPushToken usr2 cl22 def) $ \resp -> do + resp.status `shouldMatchInt` 201 + resp.json %. "token" & asString + pure [t1, t2] + toks4 <- do + cl41 <- objId $ addClient (usr4 %. "qualified_id") def >>= getJSON 201 + bindResponse (postPushToken usr4 cl41 def) $ \resp -> do + resp.status `shouldMatchInt` 201 + tok <- resp.json %. "token" & asString + pure [tok] + + assets1 <- do + a1 <- uploadDownloadProfilePicture usr1 + a2 <- uploadDownloadProfilePicture usr1 + pure $ snd <$> [a1, a2] + assets2 <- do + (: []) . snd <$> uploadDownloadProfilePicture usr2 + assets3 <- do + (: []) . snd <$> uploadDownloadProfilePicture usr3 + assets4 <- do + (: []) . snd <$> uploadDownloadProfilePicture usr4 + + (convs1, convs2, convs4) <- do + -- FUTUREWORKI(fisx): implement this (create both team convs and regular convs) + pure (Nothing, Nothing, Nothing) + + let usr2contacts = Just $ (,"accepted") <$> [ejpd4] + usr3contacts = Just $ (,"accepted") <$> [ejpd5] + usr4contacts = Just $ (,"accepted") <$> [ejpd2, ejpd5] + usr5contacts = Just $ (,"accepted") <$> [ejpd3, ejpd4] + + ejpd0 = mkUsr owner1 Nothing [] Nothing (Just ([ejpd1, ejpd2], "list_complete")) Nothing Nothing + ejpd1 = mkUsr usr1 (Just handle1) toks1 Nothing (Just ([ejpd0, ejpd2], "list_complete")) convs1 (Just assets1) + ejpd2 = mkUsr usr2 (Just handle2) toks2 usr2contacts (Just ([ejpd0, ejpd1], "list_complete")) convs2 (Just assets2) + ejpd3 = mkUsr usr3 (Just handle3) [] usr3contacts Nothing Nothing (Just assets3) + ejpd4 = mkUsr usr4 (Just handle4) toks4 usr4contacts Nothing convs4 (Just assets4) + ejpd5 = mkUsr usr5 (Just handle5) [] usr5contacts Nothing Nothing Nothing + + pure (ejpd1, ejpd2, ejpd3, ejpd4, ejpd5) + where + -- Return value is a 'EJPDResponseItem'. + mkUsr :: + HasCallStack => + A.Value {- user -} -> + Maybe String {- handle (in case usr is not up to date, we pass this separately) -} -> + [String {- push tokens -}] -> + Maybe [(A.Value {- ejpd response item of contact -}, String {- relation -})] -> + Maybe ([A.Value {- ejpd response item -}], String {- pagination flag -}) -> + Maybe [(String {- conv name -}, String {- conv id -})] -> + Maybe [String {- asset url -}] -> + A.Value + mkUsr usr handle toks contacts teamContacts convs assets = result + where + result = + object + [ -- (We know we have "id", but using ^? instead of ^. avoids the need for a Monoid instance for Value.) + "ejpd_response_user_id" .= (usr ^? key (fromString "id")), + "ejpd_response_team_id" .= (usr ^? key (fromString "team")), + "ejpd_response_name" .= (usr ^? key (fromString "name")), + "ejpd_response_handle" .= handle, + "ejpd_response_email" .= (usr ^? key (fromString "email")), + "ejpd_response_phone" .= (usr ^? key (fromString "phone")), + "ejpd_response_push_tokens" .= toks, + "ejpd_response_contacts" .= (trimContacts _1 <$> contacts), + "ejpd_response_team_contacts" .= (teamContacts & _Just . _1 %~ trimContacts id), + "ejpd_response_conversations" .= convs, + "ejpd_response_assets" .= assets + ] + + trimContacts :: forall x. Lens' x A.Value -> [x] -> [x] + trimContacts lns = + fmap + ( lns + %~ ( \case + trimmable@(A.Object _) -> trimItem trimmable + other -> error $ show other + ) + ) + + trimItem :: A.Value -> A.Value + trimItem = + (key (fromString "ejpd_response_contacts") .~ A.Null) + . (key (fromString "ejpd_response_team_contacts") .~ A.Null) + . (key (fromString "ejpd_response_conversations") .~ A.Null) + +testEJPDRequest :: HasCallStack => App () +testEJPDRequest = do + (usr1, usr2, usr3, usr4, usr5) <- setupEJPD + + let check :: HasCallStack => [A.Value] -> App () + check want = do + let handle = cs . (^?! (key (fromString "ejpd_response_handle") . _String)) + have <- BI.getEJPDInfo OwnDomain (handle <$> want) "include_contacts" + have.json `shouldMatchSpecial` object ["ejpd_response" .= want] + + shouldMatchSpecial :: (MakesValue a, MakesValue b, HasCallStack) => a -> b -> App () + shouldMatchSpecial = shouldMatchWithRules [minBound ..] resolveAssetLinks + + -- query params and even the uuid in the path of asset urls may differ between actual + -- and expected value because they are re-generated non-deterministically. so we fetch + -- the actual content. + resolveAssetLinks :: A.Value -> App (Maybe A.Value) + resolveAssetLinks = \case + (A.String (cs -> url)) | isProbablyAssetUrl url -> (Just . toJSON) <$> fetchIt url + _ -> pure Nothing + where + isProbablyAssetUrl :: String -> Bool + isProbablyAssetUrl url = all (`isInfixOf` url) ["http", "://", "/dummy-bucket/v3/persistent/"] + + fetchIt :: String -> App String + fetchIt url = liftIO $ (cs . view Wreq.responseBody) <$> Wreq.get url + + check [usr1] + check [usr2] + check [usr3] + check [usr4, usr5] diff --git a/integration/test/Testlib/Assertions.hs b/integration/test/Testlib/Assertions.hs index 2668a84b745..ac86c962147 100644 --- a/integration/test/Testlib/Assertions.hs +++ b/integration/test/Testlib/Assertions.hs @@ -2,11 +2,17 @@ module Testlib.Assertions where +import Control.Applicative ((<|>)) import Control.Exception as E +import Control.Lens ((^?)) +import qualified Control.Lens.Plated as LP import Control.Monad.Reader import Data.Aeson (Value) import qualified Data.Aeson as Aeson +import qualified Data.Aeson.Diff as AD import qualified Data.Aeson.Encode.Pretty as Aeson +import qualified Data.Aeson.KeyMap as Aeson +import Data.Aeson.Lens (_Array, _Object) import qualified Data.ByteString.Base64 as B64 import qualified Data.ByteString.Lazy as BS import Data.Char @@ -14,6 +20,7 @@ import Data.Foldable import Data.Hex import Data.List import qualified Data.Map as Map +import Data.Maybe (isJust, mapMaybe) import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import qualified Data.Text.Lazy as TL @@ -52,13 +59,94 @@ shouldMatch :: -- | The expected value b -> App () -a `shouldMatch` b = do +shouldMatch = shouldMatchWithMsg Nothing + +shouldMatchWithMsg :: + (MakesValue a, MakesValue b, HasCallStack) => + -- | Message to be added to failure report + Maybe String -> + -- | The actual value + a -> + -- | The expected value + b -> + App () +shouldMatchWithMsg msg a b = do xa <- make a xb <- make b unless (xa == xb) do pa <- prettyJSON xa pb <- prettyJSON xb - assertFailure $ "Actual:\n" <> pa <> "\nExpected:\n" <> pb + diff <- -- show diff, but only in the interesting cases. + if (isJust (xa ^? _Object) && isJust (xb ^? _Object)) + || (isJust (xa ^? _Array) && isJust (xb ^? _Array)) + then ("\nDiff:\n" <>) <$> prettyJSON (AD.diff xa xb) + else pure "" + assertFailure $ (maybe "" (<> "\n") msg) <> "Actual:\n" <> pa <> "\nExpected:\n" <> pb <> diff + +-- | apply some canonicalization transformations that *usually* do not change semantics before +-- comparing. +shouldMatchLeniently :: (MakesValue a, MakesValue b, HasCallStack) => a -> b -> App () +shouldMatchLeniently = shouldMatchWithRules [EmptyArrayIsNull, RemoveNullFieldsFromObjects] (const $ pure Nothing) + +-- | apply *all* canonicalization transformations before comparing. some of these may not be +-- valid on your input, see 'LenientMatchRule' for details. +shouldMatchSloppily :: (MakesValue a, MakesValue b, HasCallStack) => a -> b -> App () +shouldMatchSloppily = shouldMatchWithRules [minBound ..] (const $ pure Nothing) + +-- | apply *all* canonicalization transformations before comparing. some of these may not be +-- valid on your input, see 'LenientMatchRule' for details. +shouldMatchALittle :: (MakesValue a, MakesValue b, HasCallStack) => (Aeson.Value -> App (Maybe Aeson.Value)) -> a -> b -> App () +shouldMatchALittle = shouldMatchWithRules [minBound ..] + +data LenientMatchRule + = EmptyArrayIsNull + | ArraysAreSets + | RemoveNullFieldsFromObjects + deriving (Eq, Ord, Show, Bounded, Enum) + +shouldMatchWithRules :: + (MakesValue a, MakesValue b, HasCallStack) => + [LenientMatchRule] -> + (Aeson.Value -> App (Maybe Aeson.Value)) -> + a -> + b -> + App () +shouldMatchWithRules rules customRules a b = do + xa <- make a + xb <- make b + simplify xa `shouldMatch` simplify xb + where + simplify :: Aeson.Value -> App Aeson.Value + simplify = LP.rewriteM $ (\v -> foldM (tryApplyRule v) Nothing compiledRules) + + tryApplyRule :: + Aeson.Value -> + Maybe Aeson.Value -> + (Aeson.Value -> App (Maybe Aeson.Value)) -> + App (Maybe Aeson.Value) + tryApplyRule v bresult arule = (bresult <|>) <$> arule v + + compiledRules :: [Aeson.Value -> App (Maybe Aeson.Value)] + compiledRules = customRules : ((\r v -> pure $ runRule r v) <$> rules) + + runRule :: LenientMatchRule -> Aeson.Value -> Maybe Aeson.Value + runRule EmptyArrayIsNull = \case + Aeson.Array arr + | arr == mempty -> + Just Aeson.Null + _ -> Nothing + runRule ArraysAreSets = \case + Aeson.Array (toList -> arr) -> + let arr' = sort arr + in if arr == arr' then Nothing else Just $ Aeson.toJSON arr' + _ -> Nothing + runRule RemoveNullFieldsFromObjects = \case + Aeson.Object (Aeson.toList -> obj) + | any ((== Aeson.Null) . snd) obj -> + let rmNulls (_, Aeson.Null) = Nothing + rmNulls (k, v) = Just (k, v) + in Just . Aeson.Object . Aeson.fromList $ mapMaybe rmNulls obj + _ -> Nothing shouldMatchBase64 :: (MakesValue a, MakesValue b, HasCallStack) => diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/EJPD.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/EJPD.hs index 93db38b2974..d34bd9fb78a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig/EJPD.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig/EJPD.hs @@ -21,13 +21,26 @@ module Wire.API.Routes.Internal.Brig.EJPD ( EJPDRequestBody (EJPDRequestBody, ejpdRequestBody), EJPDResponseBody (EJPDResponseBody, ejpdResponseBody), - EJPDResponseItem (EJPDResponseItem, ejpdResponseHandle, ejpdResponsePushTokens, ejpdResponseContacts), + EJPDResponseItem + ( EJPDResponseItem, + ejpdResponseUserId, + ejpdResponseTeamId, + ejpdResponseName, + ejpdResponseHandle, + ejpdResponseEmail, + ejpdResponsePhone, + ejpdResponsePushTokens, + ejpdResponseContacts, + ejpdResponseTeamContacts, + ejpdResponseConversations, + ejpdResponseAssets + ), ) where import Data.Aeson hiding (json) import Data.Handle (Handle) -import Data.Id (TeamId, UserId) +import Data.Id (ConvId, TeamId, UserId) import Data.OpenApi (ToSchema) import Deriving.Swagger (CamelToSnake, CustomSwagger (..), FieldLabelModifier, StripSuffix) import Imports hiding (head) @@ -57,7 +70,9 @@ data EJPDResponseItem = EJPDResponseItem ejpdResponsePhone :: Maybe Phone, ejpdResponsePushTokens :: Set Text, -- 'Wire.API.Push.V2.Token.Token', but that would produce an orphan instance. ejpdResponseContacts :: Maybe (Set (Relation, EJPDResponseItem)), - ejpdResponseTeamContacts :: Maybe (Set EJPDResponseItem, NewListType) + ejpdResponseTeamContacts :: Maybe (Set EJPDResponseItem, NewListType), + ejpdResponseConversations :: Maybe (Set (Text, ConvId)), -- name, id + ejpdResponseAssets :: Maybe (Set Text) -- urls pointing to s3 resources } deriving stock (Eq, Ord, Show, Generic) deriving (Arbitrary) via (GenericUniform EJPDResponseItem) @@ -86,7 +101,9 @@ instance ToJSON EJPDResponseItem where "ejpd_response_phone" .= ejpdResponsePhone rspi, "ejpd_response_push_tokens" .= ejpdResponsePushTokens rspi, "ejpd_response_contacts" .= ejpdResponseContacts rspi, - "ejpd_response_team_contacts" .= ejpdResponseTeamContacts rspi + "ejpd_response_team_contacts" .= ejpdResponseTeamContacts rspi, + "ejpd_response_conversations" .= ejpdResponseConversations rspi, + "ejpd_response_assets" .= ejpdResponseAssets rspi ] instance FromJSON EJPDResponseItem where @@ -101,3 +118,5 @@ instance FromJSON EJPDResponseItem where <*> obj .: "ejpd_response_push_tokens" <*> obj .:? "ejpd_response_contacts" <*> obj .:? "ejpd_response_team_contacts" + <*> obj .:? "ejpd_response_conversations" + <*> obj .:? "ejpd_response_assets" diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs index cb9599b441e..592e72dc61a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Cargohold.hs @@ -22,12 +22,15 @@ import Data.OpenApi import Imports import Servant import Servant.OpenApi +import Wire.API.Asset import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Named type InternalAPI = "i" - :> "status" - :> MultiVerb 'GET '() '[RespondEmpty 200 "OK"] () + :> ( "status" :> MultiVerb 'GET '() '[RespondEmpty 200 "OK"] () + :<|> Named "iGetAsset" ("assets" :> Capture "key" AssetKey :> Get '[Servant.JSON] Text) + ) swaggerDoc :: OpenApi swaggerDoc = diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 659db42be15..c04173cf918 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -100,6 +100,7 @@ import Wire.API.User.Activation import Wire.API.User.Client import Wire.API.User.RichInfo import Wire.NotificationSubsystem +import Wire.Rpc import Wire.Sem.Concurrency import Wire.Sem.Paging.Cassandra (InternalPaging) @@ -108,20 +109,21 @@ import Wire.Sem.Paging.Cassandra (InternalPaging) servantSitemap :: forall r p. - ( Member BlacklistStore r, + ( Member BlacklistPhonePrefixStore r, + Member BlacklistStore r, Member CodeStore r, - Member BlacklistPhonePrefixStore r, - Member PasswordResetStore r, - Member GalleyProvider r, - Member (UserPendingActivationStore p) r, - Member FederationConfigStore r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, - Member TinyLog r, Member (Concurrency 'Unsafe) r, + Member (ConnectionStore InternalPaging) r, + Member (Embed HttpClientIO) r, + Member FederationConfigStore r, + Member GalleyProvider r, Member (Input (Local ())) r, Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member NotificationSubsystem r, + Member PasswordResetStore r, + Member Rpc r, + Member TinyLog r, + Member (UserPendingActivationStore p) r ) => ServerT BrigIRoutes.API (Handler r) servantSitemap = @@ -142,7 +144,10 @@ istatusAPI :: forall r. ServerT BrigIRoutes.IStatusAPI (Handler r) istatusAPI = Named @"get-status" (pure NoContent) ejpdAPI :: - (Member GalleyProvider r, Member NotificationSubsystem r) => + ( Member GalleyProvider r, + Member NotificationSubsystem r, + Member Rpc r + ) => ServerT BrigIRoutes.EJPD_API (Handler r) ejpdAPI = Brig.User.EJPD.ejpdRequest diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 1b41a473806..4467ad7c34f 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -37,6 +37,7 @@ module Brig.App galley, galleyEndpoint, gundeckEndpoint, + cargoholdEndpoint, federator, casClient, userTemplates, @@ -164,6 +165,7 @@ data Env = Env _galley :: RPC.Request, _galleyEndpoint :: Endpoint, _gundeckEndpoint :: Endpoint, + _cargoholdEndpoint :: Endpoint, _federator :: Maybe Endpoint, -- FUTUREWORK: should we use a better type here? E.g. to avoid fresh connections all the time? _casClient :: Cas.ClientState, _smtpEnv :: Maybe SMTP.SMTP, @@ -264,6 +266,7 @@ newEnv o = do _galley = mkEndpoint $ Opt.galley o, _galleyEndpoint = Opt.galley o, _gundeckEndpoint = Opt.gundeck o, + _cargoholdEndpoint = Opt.cargohold o, _federator = Opt.federatorInternal o, _casClient = cas, _smtpEnv = emailSMTP, diff --git a/services/brig/src/Brig/User/EJPD.hs b/services/brig/src/Brig/User/EJPD.hs index ae7538b6b5f..31392bfd84b 100644 --- a/services/brig/src/Brig/User/EJPD.hs +++ b/services/brig/src/Brig/User/EJPD.hs @@ -20,6 +20,8 @@ -- manually.) module Brig.User.EJPD (ejpdRequest) where +import Bilge.Request +import Bilge.Response import Brig.API.Handler import Brig.API.User (lookupHandle) import Brig.App @@ -27,50 +29,54 @@ import Brig.Data.Connection qualified as Conn import Brig.Data.User (lookupUser) import Brig.Effects.GalleyProvider (GalleyProvider) import Brig.Effects.GalleyProvider qualified as GalleyProvider -import Brig.Types.User (HavePendingInvitations (NoPendingInvitations)) import Control.Error hiding (bool) import Control.Lens (view, (^.)) +import Data.Aeson qualified as A +import Data.ByteString.Conversion import Data.Handle (Handle) import Data.Id (UserId) import Data.Set qualified as Set import Imports hiding (head) -import Polysemy +import Network.HTTP.Types.Method +import Polysemy (Member) import Servant.OpenApi.Internal.Orphans () import Wire.API.Connection (Relation, RelationWithHistory (..), relationDropHistory) import Wire.API.Push.Token qualified as PushTok import Wire.API.Routes.Internal.Brig.EJPD (EJPDRequestBody (EJPDRequestBody), EJPDResponseBody (EJPDResponseBody), EJPDResponseItem (EJPDResponseItem)) import Wire.API.Team.Member qualified as Team -import Wire.API.User (User, userDisplayName, userEmail, userHandle, userId, userPhone, userTeam) +import Wire.API.User import Wire.NotificationSubsystem +import Wire.Rpc ejpdRequest :: forall r. ( Member GalleyProvider r, - Member NotificationSubsystem r + Member NotificationSubsystem r, + Member Rpc r ) => Maybe Bool -> EJPDRequestBody -> - Handler r EJPDResponseBody -ejpdRequest includeContacts (EJPDRequestBody handles) = do - ExceptT $ Right . EJPDResponseBody . catMaybes <$> forM handles (go1 (fromMaybe False includeContacts)) + (Handler r) EJPDResponseBody +ejpdRequest (fromMaybe False -> includeContacts) (EJPDRequestBody handles) = do + ExceptT $ Right . EJPDResponseBody . catMaybes <$> forM handles go1 where -- find uid given handle - go1 :: Bool -> Handle -> (AppT r) (Maybe EJPDResponseItem) - go1 includeContacts' handle = do + go1 :: Handle -> (AppT r) (Maybe EJPDResponseItem) + go1 handle = do mbUid <- wrapClient $ lookupHandle handle mbUsr <- maybe (pure Nothing) (wrapClient . lookupUser NoPendingInvitations) mbUid - maybe (pure Nothing) (fmap Just . go2 includeContacts') mbUsr + maybe (pure Nothing) (fmap Just . go2 includeContacts) mbUsr -- construct response item given uid go2 :: Bool -> User -> (AppT r) EJPDResponseItem - go2 includeContacts' target = do + go2 reallyIncludeContacts target = do let uid = userId target ptoks <- PushTok.tokenText . view PushTok.token <$$> liftSem (getPushTokens uid) mbContacts <- - if includeContacts' + if reallyIncludeContacts then do contacts :: [(UserId, RelationWithHistory)] <- wrapClient $ Conn.lookupContactListWithRelation uid @@ -85,7 +91,7 @@ ejpdRequest includeContacts (EJPDRequestBody handles) = do pure Nothing mbTeamContacts <- - case (includeContacts', userTeam target) of + case (reallyIncludeContacts, userTeam target) of (True, Just tid) -> do memberList <- liftSem $ GalleyProvider.getTeamMembers tid let members = (view Team.userId <$> (memberList ^. Team.teamMembers)) \\ [uid] @@ -99,6 +105,28 @@ ejpdRequest includeContacts (EJPDRequestBody handles) = do _ -> do pure Nothing + mbConversations <- do + -- FUTUREWORK(fisx) + pure Nothing + + mbAssets <- do + urls <- forM (userAssets target) $ \(asset :: Asset) -> do + cgh <- asks (view cargoholdEndpoint) + let key = toByteString' $ assetKey asset + resp <- liftSem $ rpcWithRetries "cargohold" cgh (method GET . paths ["/i/assets", key]) + pure $ + case (statusCode resp, responseJsonEither resp) of + (200, Right (A.String loc)) -> loc + _ -> + cs $ + "could not fetch asset: " + <> show key + <> ", error: " + <> show (statusCode resp, responseBody resp) + pure $ case urls of + [] -> Nothing + something -> Just (Set.fromList something) + pure $ EJPDResponseItem uid @@ -110,3 +138,5 @@ ejpdRequest includeContacts (EJPDRequestBody handles) = do (Set.fromList ptoks) mbContacts mbTeamContacts + mbConversations + mbAssets diff --git a/services/brig/test/integration/API/Internal.hs b/services/brig/test/integration/API/Internal.hs index b0f7704123b..b4d730fcd94 100644 --- a/services/brig/test/integration/API/Internal.hs +++ b/services/brig/test/integration/API/Internal.hs @@ -34,13 +34,13 @@ import Cassandra qualified as Cass import Cassandra.Util import Control.Exception (ErrorCall (ErrorCall), throwIO) import Control.Lens ((^.), (^?!)) +import Data.Aeson qualified as Aeson import Data.Aeson.Lens qualified as Aeson import Data.Aeson.Types qualified as Aeson import Data.ByteString.Conversion (toByteString') import Data.Default import Data.Id import Data.Qualified -import Data.Set qualified as Set import GHC.TypeLits (KnownSymbol) import Imports import System.IO.Temp @@ -48,20 +48,16 @@ import Test.Tasty import Test.Tasty.HUnit import Util import Util.Options (Endpoint) -import Wire.API.Connection qualified as Conn -import Wire.API.Routes.Internal.Brig import Wire.API.Team.Feature import Wire.API.Team.Feature qualified as ApiFt -import Wire.API.Team.Member qualified as Team import Wire.API.User import Wire.API.User.Client tests :: Opt.Opts -> Manager -> Cass.ClientState -> Brig -> Endpoint -> Gundeck -> Galley -> IO TestTree -tests opts mgr db brig brigep gundeck galley = do +tests opts mgr db brig brigep _gundeck galley = do pure $ testGroup "api/internal" $ - [ test mgr "ejpd requests" $ testEJPDRequest mgr brig brigep gundeck, - test mgr "account features: conferenceCalling" $ + [ test mgr "account features: conferenceCalling" $ testFeatureConferenceCallingByAccount opts mgr db brig brigep galley, test mgr "suspend and unsuspend user" $ testSuspendUser db brig, test mgr "suspend non existing user and verify no db entry" $ @@ -98,54 +94,6 @@ setAccountStatus brig u s = . json (AccountStatusUpdate s) ) -testEJPDRequest :: (TestConstraints m) => Manager -> Brig -> Endpoint -> Gundeck -> m () -testEJPDRequest mgr brig brigep gundeck = do - (handle1, mkUsr1, handle2, mkUsr2, mkUsr3) <- scaffolding brig gundeck - - do - let req = EJPDRequestBody [handle1] - want = - EJPDResponseBody - [ mkUsr1 Nothing Nothing - ] - have <- ejpdRequestClient brigep mgr Nothing req - liftIO $ assertEqual "" want have - - do - let req = EJPDRequestBody [handle1, handle2] - want = - EJPDResponseBody - [ mkUsr1 Nothing Nothing, - mkUsr2 Nothing Nothing - ] - have <- ejpdRequestClient brigep mgr Nothing req - liftIO $ assertEqual "" want have - - do - let req = EJPDRequestBody [handle2] - want = - EJPDResponseBody - [ mkUsr2 - (Just (Set.fromList [(Conn.Accepted, mkUsr1 Nothing Nothing)])) - Nothing - ] - have <- ejpdRequestClient brigep mgr (Just True) req - liftIO $ assertEqual "" want have - - do - let req = EJPDRequestBody [handle1, handle2] - want = - EJPDResponseBody - [ mkUsr1 - (Just (Set.fromList [(Conn.Accepted, mkUsr2 Nothing Nothing)])) - (Just (Set.fromList [mkUsr3 Nothing Nothing], Team.NewListComplete)), - mkUsr2 - (Just (Set.fromList [(Conn.Accepted, mkUsr1 Nothing Nothing)])) - Nothing - ] - have <- ejpdRequestClient brigep mgr (Just True) req - liftIO $ assertEqual "" want have - testFeatureConferenceCallingByAccount :: forall m. (TestConstraints m) => Opt.Opts -> Manager -> Cass.ClientState -> Brig -> Endpoint -> Galley -> m () testFeatureConferenceCallingByAccount (Opt.optSettings -> settings) mgr db brig brigep galley = do let check :: (HasCallStack) => ApiFt.WithStatusNoLock ApiFt.ConferenceCallingConfig -> m () diff --git a/services/brig/test/integration/API/Internal/Util.hs b/services/brig/test/integration/API/Internal/Util.hs index 5a55b04461b..733c23620b2 100644 --- a/services/brig/test/integration/API/Internal/Util.hs +++ b/services/brig/test/integration/API/Internal/Util.hs @@ -20,108 +20,27 @@ module API.Internal.Util ( TestConstraints, - MkUsr, - scaffolding, - ejpdRequestClient, getAccountConferenceCallingConfigClient, putAccountConferenceCallingConfigClient, deleteAccountConferenceCallingConfigClient, ) where -import API.Team.Util (createPopulatedBindingTeamWithNamesAndHandles) import Bilge hiding (host, port) -import Control.Lens (view, (^.)) -import Control.Monad.Catch (MonadCatch, MonadThrow, throwM) -import Data.ByteString.Base16 qualified as B16 -import Data.Handle (Handle) +import Control.Lens ((^.)) +import Control.Monad.Catch (MonadCatch) import Data.Id -import Data.List1 qualified as List1 import Data.Proxy (Proxy (Proxy)) -import Data.Set qualified as Set -import Data.Text.Encoding qualified as T import Imports import Servant.API ((:>)) import Servant.API.ContentTypes (NoContent) import Servant.Client qualified as Client -import System.Random (randomIO) -import Util import Util.Options (Endpoint, host, port) -import Wire.API.Connection -import Wire.API.Push.V2.Token qualified as PushToken import Wire.API.Routes.Internal.Brig as IAPI import Wire.API.Team.Feature qualified as Public -import Wire.API.Team.Member qualified as Team -import Wire.API.User type TestConstraints m = (MonadFail m, MonadCatch m, MonadIO m, MonadHttp m) -type MkUsr = - Maybe (Set (Relation, EJPDResponseItem)) -> - Maybe (Set EJPDResponseItem, Team.NewListType) -> - EJPDResponseItem - -scaffolding :: - forall m. - (TestConstraints m) => - Brig -> - Gundeck -> - m (Handle, MkUsr, Handle, MkUsr, MkUsr) -scaffolding brig gundeck = do - (_tid, usr1, [usr3]) <- createPopulatedBindingTeamWithNamesAndHandles brig 1 - (_handle1, usr2) <- createUserWithHandle brig - connectUsers brig (userId usr1) (List1.singleton $ userId usr2) - tok1 <- registerPushToken gundeck $ userId usr1 - tok2 <- registerPushToken gundeck $ userId usr2 - tok3 <- registerPushToken gundeck $ userId usr2 - pure - ( fromJust $ userHandle usr1, - mkUsr usr1 (Set.fromList [tok1]), - fromJust $ userHandle usr2, - mkUsr usr2 (Set.fromList [tok2, tok3]), - mkUsr usr3 Set.empty - ) - where - mkUsr :: User -> Set Text -> MkUsr - mkUsr usr toks = - EJPDResponseItem - (userId usr) - (userTeam usr) - (userDisplayName usr) - (userHandle usr) - (userEmail usr) - (userPhone usr) - toks - - registerPushToken :: Gundeck -> UserId -> m Text - registerPushToken gd u = do - t <- randomToken - rsp <- registerPushTokenRequest gd u t - responseJsonEither rsp - & either - (error . show) - (pure . PushToken.tokenText . view PushToken.token) - - registerPushTokenRequest :: Gundeck -> UserId -> PushToken.PushToken -> m ResponseLBS - registerPushTokenRequest gd u t = do - post - ( gd - . path "/push/tokens" - . contentJson - . zUser u - . zConn "random" - . json t - ) - - randomToken :: m PushToken.PushToken - randomToken = liftIO $ do - c <- liftIO $ ClientId <$> (randomIO :: IO Word64) - tok <- (PushToken.Token . T.decodeUtf8) . B16.encode <$> randomBytes 32 - pure $ PushToken.pushToken PushToken.APNSSandbox (PushToken.AppName "test") tok c - -ejpdRequestClientM :: Maybe Bool -> EJPDRequestBody -> Client.ClientM EJPDResponseBody -ejpdRequestClientM = Client.client (Proxy @("i" :> IAPI.EJPDRequest)) - getAccountConferenceCallingConfigClientM :: UserId -> Client.ClientM (Public.WithStatusNoLock Public.ConferenceCallingConfig) getAccountConferenceCallingConfigClientM = Client.client (Proxy @("i" :> IAPI.GetAccountConferenceCallingConfig)) @@ -131,9 +50,6 @@ putAccountConferenceCallingConfigClientM = Client.client (Proxy @("i" :> IAPI.Pu deleteAccountConferenceCallingConfigClientM :: UserId -> Client.ClientM NoContent deleteAccountConferenceCallingConfigClientM = Client.client (Proxy @("i" :> IAPI.DeleteAccountConferenceCallingConfig)) -ejpdRequestClient :: (HasCallStack, MonadThrow m, MonadIO m) => Endpoint -> Manager -> Maybe Bool -> EJPDRequestBody -> m EJPDResponseBody -ejpdRequestClient brigep mgr includeContacts ejpdReqBody = runHereClientM brigep mgr (ejpdRequestClientM includeContacts ejpdReqBody) >>= either throwM pure - getAccountConferenceCallingConfigClient :: (HasCallStack, MonadIO m) => Endpoint -> Manager -> UserId -> m (Either Client.ClientError (Public.WithStatusNoLock Public.ConferenceCallingConfig)) getAccountConferenceCallingConfigClient brigep mgr uid = runHereClientM brigep mgr (getAccountConferenceCallingConfigClientM uid) diff --git a/services/cargohold/src/CargoHold/API/Public.hs b/services/cargohold/src/CargoHold/API/Public.hs index 4435fc9e3e1..0430c110ef3 100644 --- a/services/cargohold/src/CargoHold/API/Public.hs +++ b/services/cargohold/src/CargoHold/API/Public.hs @@ -27,6 +27,7 @@ import qualified CargoHold.Types.V3 as V3 import Control.Lens import Control.Monad.Trans.Except (throwE) import Data.ByteString.Builder +import qualified Data.ByteString.Builder as Builder import qualified Data.ByteString.Lazy as LBS import Data.Domain import Data.Id @@ -36,12 +37,13 @@ import Imports hiding (head) import qualified Network.HTTP.Types as HTTP import Servant.API import Servant.Server hiding (Handler) -import URI.ByteString +import URI.ByteString as URI import Wire.API.Asset import Wire.API.Federation.API import Wire.API.Routes.AssetBody import Wire.API.Routes.Internal.Brig (brigInternalClient) import Wire.API.Routes.Internal.Cargohold +import Wire.API.Routes.Named import Wire.API.Routes.Public.Cargohold import Wire.API.User (AccountStatus (Active), AccountStatusResp (..)) @@ -74,7 +76,19 @@ servantSitemap = :<|> deleteAssetV4 internalSitemap :: ServerT InternalAPI Handler -internalSitemap = pure () +internalSitemap = + pure () + :<|> Named @"iGetAsset" iDownloadAssetV3 + +-- | Like 'downloadAssetV3' below, but it works without user session token, and has a +-- different route type. +iDownloadAssetV3 :: V3.AssetKey -> Handler Text +iDownloadAssetV3 key = do + render <$> V3.downloadUnsafe key Nothing + where + -- (NB: don't use HttpsUrl here, as in some test environments we legitimately use "http"!) + render :: URI.URI -> Text + render = cs . Builder.toLazyByteString . URI.serializeURIRef class HasLocation (tag :: PrincipalTag) where assetLocation :: Local AssetKey -> [Text] diff --git a/services/cargohold/src/CargoHold/API/V3.hs b/services/cargohold/src/CargoHold/API/V3.hs index d96d772d5ce..4b4c58f374a 100644 --- a/services/cargohold/src/CargoHold/API/V3.hs +++ b/services/cargohold/src/CargoHold/API/V3.hs @@ -18,6 +18,7 @@ module CargoHold.API.V3 ( upload, download, + downloadUnsafe, checkMetadata, delete, renewToken, @@ -112,6 +113,9 @@ download own key tok mbHost = runMaybeT $ do checkMetadata (Just own) key tok lift $ genSignedURL (S3.mkKey key) mbHost +downloadUnsafe :: V3.AssetKey -> Maybe Text -> Handler URI +downloadUnsafe key mbHost = genSignedURL (S3.mkKey key) mbHost + checkMetadata :: Maybe V3.Principal -> V3.AssetKey -> Maybe V3.AssetToken -> MaybeT Handler () checkMetadata mown key tok = do s3 <- lift (S3.getMetadataV3 key) >>= maybe mzero pure From dd93615d830a52de2b68882c232dfc7e940c63e0 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Tue, 12 Mar 2024 14:55:32 +0100 Subject: [PATCH 050/117] DO NOT MERGE THIS COMMIT EITHER!!! --- hack/bin/integration-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hack/bin/integration-test.sh b/hack/bin/integration-test.sh index 0667ebeac28..944a1f515e0 100755 --- a/hack/bin/integration-test.sh +++ b/hack/bin/integration-test.sh @@ -57,7 +57,7 @@ summary() { mkdir -p ~/.parallel && touch ~/.parallel/will-cite printf '%s\n' "${tests[@]}" | parallel echo "Running helm tests for {}..." printf '%s\n' "${tests[@]}" | parallel -P "${HELM_PARALLELISM}" \ - helm test -n "${NAMESPACE}" "${CHART}" --timeout 900s --filter name="${CHART}-{}-integration" '> logs-{};' \ + helm test -n "${NAMESPACE}" "${CHART}" --timeout 3600s --filter name="${CHART}-{}-integration" '> logs-{};' \ echo '$? > stat-{};' \ echo "==== Done testing {}. ====" '};' \ kubectl -n "${NAMESPACE}" logs "${CHART}-{}-integration" '>> logs-{};' From 95d6fbfa5f096b0692eba5cfc2b8396bf0f38662 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Tue, 12 Mar 2024 14:56:22 +0100 Subject: [PATCH 051/117] Revert "DO NOT MERGE THIS COMMIT EITHER!!!" This reverts commit dd93615d830a52de2b68882c232dfc7e940c63e0. --- hack/bin/integration-test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hack/bin/integration-test.sh b/hack/bin/integration-test.sh index 944a1f515e0..0667ebeac28 100755 --- a/hack/bin/integration-test.sh +++ b/hack/bin/integration-test.sh @@ -57,7 +57,7 @@ summary() { mkdir -p ~/.parallel && touch ~/.parallel/will-cite printf '%s\n' "${tests[@]}" | parallel echo "Running helm tests for {}..." printf '%s\n' "${tests[@]}" | parallel -P "${HELM_PARALLELISM}" \ - helm test -n "${NAMESPACE}" "${CHART}" --timeout 3600s --filter name="${CHART}-{}-integration" '> logs-{};' \ + helm test -n "${NAMESPACE}" "${CHART}" --timeout 900s --filter name="${CHART}-{}-integration" '> logs-{};' \ echo '$? > stat-{};' \ echo "==== Done testing {}. ====" '};' \ kubectl -n "${NAMESPACE}" logs "${CHART}-{}-integration" '>> logs-{};' From bf5b6ff3083dce26abd329a97ff4984a81e1d251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Wed, 13 Mar 2024 10:25:56 +0100 Subject: [PATCH 052/117] [WPB-6783] Support unblocking an MLS 1-to-1 conversation (#3940) * A test confirming unblocking isn't implemented for MLS 1-1 conversations * Cosmetics in the old code * Introduce a qualified internal Galley endpoint for unblocking * Rework the Brig invocation of conv unblocking Moved the logic from `Brig.IO.Intra` to the `GalleyProvider` effect * Rename test function for clarity * Fix unblocking local and remote users * Add a changelog * Assert message received by all Bob's clients --- changelog.d/2-features/WPB-6783 | 1 + integration/test/Test/MLS/One2One.hs | 66 +++++++++++++++++-- .../src/Wire/API/Routes/Internal/Galley.hs | 28 +++++++- services/brig/src/Brig/API/Connection.hs | 17 +++-- .../brig/src/Brig/API/Connection/Remote.hs | 32 +++++---- services/brig/src/Brig/API/Federation.hs | 4 +- services/brig/src/Brig/API/Internal.hs | 3 +- .../brig/src/Brig/Effects/GalleyProvider.hs | 13 +++- .../src/Brig/Effects/GalleyProvider/RPC.hs | 62 ++++++++++++----- services/brig/src/Brig/IO/Intra.hs | 37 ----------- services/galley/src/Galley/API/Internal.hs | 2 + services/galley/src/Galley/API/Query.hs | 66 ++++++++++++++++++- services/galley/src/Galley/API/Update.hs | 36 +++++++++- 13 files changed, 280 insertions(+), 87 deletions(-) create mode 100644 changelog.d/2-features/WPB-6783 diff --git a/changelog.d/2-features/WPB-6783 b/changelog.d/2-features/WPB-6783 new file mode 100644 index 00000000000..c746b345e37 --- /dev/null +++ b/changelog.d/2-features/WPB-6783 @@ -0,0 +1 @@ +Support unblocking a user in an MLS 1-to-1 conversation diff --git a/integration/test/Test/MLS/One2One.hs b/integration/test/Test/MLS/One2One.hs index 1e2f10e02ee..aac9725d9c9 100644 --- a/integration/test/Test/MLS/One2One.hs +++ b/integration/test/Test/MLS/One2One.hs @@ -21,6 +21,7 @@ import API.Brig import API.Galley import qualified Data.ByteString.Base64 as Base64 import qualified Data.ByteString.Char8 as B8 +import qualified Data.Set as Set import MLS.Util import Notifications import SetupHelpers @@ -67,7 +68,7 @@ testMLSOne2OneBlocked otherDomain = do testMLSOne2OneBlockedAfterConnected :: HasCallStack => One2OneScenario -> App () testMLSOne2OneBlockedAfterConnected scenario = do alice <- randomUser OwnDomain def - let otherDomain = one2OneScenarioDomain scenario + let otherDomain = one2OneScenarioUserDomain scenario convDomain = one2OneScenarioConvDomain scenario bob <- createMLSOne2OnePartner otherDomain alice convDomain conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 @@ -101,6 +102,61 @@ testMLSOne2OneBlockedAfterConnected scenario = do void $ postMLSMessage mp.sender mp.message >>= getJSON 201 awaitAnyEvent 2 ws `shouldMatch` (Nothing :: Maybe Value) +-- | Alice and Bob are initially connected, then Alice blocks Bob, and finally +-- Alice unblocks Bob. +testMLSOne2OneUnblocked :: HasCallStack => One2OneScenario -> App () +testMLSOne2OneUnblocked scenario = do + alice <- randomUser OwnDomain def + let otherDomain = one2OneScenarioUserDomain scenario + convDomain = one2OneScenarioConvDomain scenario + bob <- createMLSOne2OnePartner otherDomain alice convDomain + conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + do + convId <- conv %. "qualified_id" + bobConv <- getMLSOne2OneConversation bob alice >>= getJSON 200 + convId `shouldMatch` (bobConv %. "qualified_id") + + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] + traverse_ uploadNewKeyPackage [bob1] + resetGroup alice1 conv + withWebSocket bob1 $ \ws -> do + commit <- createAddCommit alice1 [bob] + void $ sendAndConsumeCommitBundle commit + let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" + n <- awaitMatch isMessage ws + nPayload n %. "data" `shouldMatch` B8.unpack (Base64.encode (fold commit.welcome)) + + -- Alice blocks Bob + void $ putConnection alice bob "blocked" >>= getBody 200 + void $ getMLSOne2OneConversation alice bob >>= getJSON 403 + + -- Reset the group membership in the test setup as only 'bob1' is left in + -- reality, even though the test state believes 'alice1' is still part of the + -- conversation. + modifyMLSState $ \s -> s {members = Set.singleton bob1} + + -- Bob creates a new client and adds it to the one-to-one conversation just so + -- that the epoch advances. + bob2 <- createMLSClient def bob + traverse_ uploadNewKeyPackage [bob2] + void $ createAddCommit bob1 [bob] >>= sendAndConsumeCommitBundle + + -- Alice finally unblocks Bob + void $ putConnection alice bob "accepted" >>= getBody 200 + void $ getMLSOne2OneConversation alice bob >>= getJSON 200 + + -- Alice rejoins via an external commit + void $ createExternalCommit alice1 Nothing >>= sendAndConsumeCommitBundle + + -- Check that an application message can get to Bob + withWebSockets [bob1, bob2] $ \wss -> do + mp <- createApplicationMessage alice1 "hello, I've always been here" + void $ sendAndConsumeMessage mp + let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-message-add" + forM_ wss $ \ws -> do + n <- awaitMatch isMessage ws + nPayload n %. "data" `shouldMatch` B8.unpack (Base64.encode mp.message) + testGetMLSOne2OneSameTeam :: App () testGetMLSOne2OneSameTeam = do (alice, _, _) <- createTeam OwnDomain 1 @@ -122,9 +178,9 @@ instance TestCases One2OneScenario where MkTestCase "[domain=other;conv=other]" One2OneScenarioRemoteConv ] -one2OneScenarioDomain :: One2OneScenario -> Domain -one2OneScenarioDomain One2OneScenarioLocal = OwnDomain -one2OneScenarioDomain _ = OtherDomain +one2OneScenarioUserDomain :: One2OneScenario -> Domain +one2OneScenarioUserDomain One2OneScenarioLocal = OwnDomain +one2OneScenarioUserDomain _ = OtherDomain one2OneScenarioConvDomain :: One2OneScenario -> Domain one2OneScenarioConvDomain One2OneScenarioLocal = OwnDomain @@ -134,7 +190,7 @@ one2OneScenarioConvDomain One2OneScenarioRemoteConv = OtherDomain testMLSOne2One :: HasCallStack => One2OneScenario -> App () testMLSOne2One scenario = do alice <- randomUser OwnDomain def - let otherDomain = one2OneScenarioDomain scenario + let otherDomain = one2OneScenarioUserDomain scenario convDomain = one2OneScenarioConvDomain scenario bob <- createMLSOne2OnePartner otherDomain alice convDomain [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs index b6be1bade6a..d07f48258dc 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Galley.hs @@ -517,7 +517,7 @@ type IConversationAPI = -- - MemberJoin event to other, if the conversation existed and only the other was member -- before :<|> Named - "conversation-unblock" + "conversation-unblock-unqualified" ( CanThrow 'InvalidOperation :> CanThrow 'ConvNotFound :> ZLocalUser @@ -527,6 +527,21 @@ type IConversationAPI = :> "unblock" :> Put '[Servant.JSON] Conversation ) + -- This endpoint can lead to the following events being sent: + -- - MemberJoin event to you, if the conversation existed and had < 2 members before + -- - MemberJoin event to other, if the conversation existed and only the other was member + -- before + :<|> Named + "conversation-unblock" + ( CanThrow 'InvalidOperation + :> CanThrow 'ConvNotFound + :> ZLocalUser + :> ZOptConn + :> "conversations" + :> QualifiedCapture "cnv" ConvId + :> "unblock" + :> Put '[Servant.JSON] () + ) :<|> Named "conversation-meta" ( CanThrow 'ConvNotFound @@ -545,6 +560,17 @@ type IConversationAPI = :> QualifiedCapture "user" UserId :> Get '[Servant.JSON] Conversation ) + :<|> Named + "conversation-mls-one-to-one-established" + ( CanThrow 'NotConnected + :> CanThrow 'MLSNotEnabled + :> ZLocalUser + :> "conversations" + :> "mls-one2one" + :> QualifiedCapture "user" UserId + :> "established" + :> Get '[Servant.JSON] Bool + ) swaggerDoc :: OpenApi swaggerDoc = diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index 1144931940f..b7ba931541f 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -341,8 +341,8 @@ updateConnectionToLocalUser self other newStatus conn = do mlsEnabled <- view (settings . enableMLS) liftSem $ when (fromMaybe False mlsEnabled) $ do let mlsConvId = one2OneConvId BaseProtocolMLSTag (tUntagged self) (tUntagged other) - mlsConvEstablished <- isMLSOne2OneEstablished self (tUntagged other) - when mlsConvEstablished $ Intra.blockConv self mlsConvId + isEstablished <- isMLSOne2OneEstablished self (tUntagged other) + when (isEstablished == Established) $ Intra.blockConv self mlsConvId wrapClient $ Just <$> Data.updateConnection s2o BlockedWithHistory unblock :: UserConnection -> UserConnection -> Relation -> ExceptT ConnectionError (AppT r) (Maybe UserConnection) @@ -353,7 +353,13 @@ updateConnectionToLocalUser self other newStatus conn = do lift . Log.info $ logLocalConnection (tUnqualified self) (qUnqualified (ucTo s2o)) . msg (val "Unblocking connection") - cnv <- lift $ traverse (Intra.unblockConv self conn) (ucConvId s2o) + cnv <- lift . liftSem $ traverse (unblockConversation self conn) (ucConvId s2o) + mlsEnabled <- view (settings . enableMLS) + lift . liftSem $ when (fromMaybe False mlsEnabled) $ do + let mlsConvId = one2OneConvId BaseProtocolMLSTag (tUntagged self) (tUntagged other) + isEstablished <- isMLSOne2OneEstablished self (tUntagged other) + when (isEstablished == NotAMember || isEstablished == Established) . void $ + unblockConversation self conn mlsConvId when (ucStatus o2s == Sent && new == Accepted) . lift $ do o2s' <- wrapClient $ @@ -413,7 +419,8 @@ mkRelationWithHistory oldRel = \case updateConnectionInternal :: forall r. - ( Member NotificationSubsystem r, + ( Member GalleyProvider r, + Member NotificationSubsystem r, Member TinyLog r, Member (Embed HttpClientIO) r ) => @@ -480,7 +487,7 @@ updateConnectionInternal = \case unblockDirected :: UserConnection -> UserConnection -> ExceptT ConnectionError (AppT r) () unblockDirected uconn uconnRev = do lfrom <- qualifyLocal (ucFrom uconnRev) - void . lift . for (ucConvId uconn) $ Intra.unblockConv lfrom Nothing + void . lift . liftSem . for (ucConvId uconn) $ unblockConversation lfrom Nothing uconnRevRel :: RelationWithHistory <- relationWithHistory lfrom (ucTo uconnRev) uconnRev' <- lift . wrapClient $ Data.updateConnection uconnRev (undoRelationHistory uconnRevRel) connName <- lift . wrapClient $ Data.lookupName (tUnqualified lfrom) diff --git a/services/brig/src/Brig/API/Connection/Remote.hs b/services/brig/src/Brig/API/Connection/Remote.hs index 5f41c261e5c..5b6240a09ec 100644 --- a/services/brig/src/Brig/API/Connection/Remote.hs +++ b/services/brig/src/Brig/API/Connection/Remote.hs @@ -152,7 +152,7 @@ desiredMembership a r = -- -- Returns the connection, and whether it was updated or not. transitionTo :: - (Member NotificationSubsystem r, Member GalleyProvider r) => + (Member GalleyProvider r, Member NotificationSubsystem r) => Local UserId -> Maybe ConnId -> Remote UserId -> @@ -192,14 +192,18 @@ transitionTo self mzcon other (Just connection) (Just rel) actor = do fromMaybe (one2OneConvId BaseProtocolProteusTag (tUntagged self) (tUntagged other)) $ ucConvId connection - lift $ updateOne2OneConv self Nothing other proteusConvId (desiredMembership actor rel) actor + desiredMem = desiredMembership actor rel + lift $ updateOne2OneConv self Nothing other proteusConvId desiredMem actor mlsEnabled <- view (settings . enableMLS) when (fromMaybe False mlsEnabled) $ do let mlsConvId = one2OneConvId BaseProtocolMLSTag (tUntagged self) (tUntagged other) - mlsConvEstablished <- lift . liftSem $ isMLSOne2OneEstablished self (tUntagged other) - let desiredMem = desiredMembership actor rel - lift . when (mlsConvEstablished && desiredMem == Excluded) $ - updateOne2OneConv self Nothing other mlsConvId desiredMem actor + isEstablished <- lift . liftSem $ isMLSOne2OneEstablished self (tUntagged other) + lift + . when + ( isEstablished == Established + || (isEstablished == NotAMember && ucStatus connection == Blocked && rel == Accepted) + ) + $ updateOne2OneConv self Nothing other mlsConvId desiredMem actor -- update connection connection' <- lift $ wrapClient $ Data.updateConnection connection (relationWithHistory rel) @@ -220,7 +224,7 @@ pushEvent self mzcon connection = do liftSem $ Intra.onConnectionEvent (tUnqualified self) mzcon event performLocalAction :: - (Member NotificationSubsystem r, Member GalleyProvider r) => + (Member GalleyProvider r, Member NotificationSubsystem r) => Local UserId -> Maybe ConnId -> Remote UserId -> @@ -276,7 +280,7 @@ performLocalAction self mzcon other mconnection action = do -- B connects & A reacts: Accepted Accepted -- @ performRemoteAction :: - (Member NotificationSubsystem r, Member GalleyProvider r) => + (Member GalleyProvider r, Member NotificationSubsystem r) => Local UserId -> Remote UserId -> Maybe UserConnection -> @@ -294,9 +298,9 @@ performRemoteAction self other mconnection action = do reaction _ = Nothing createConnectionToRemoteUser :: - ( Member FederationConfigStore r, - Member NotificationSubsystem r, - Member GalleyProvider r + ( Member GalleyProvider r, + Member FederationConfigStore r, + Member NotificationSubsystem r ) => Local UserId -> ConnId -> @@ -309,9 +313,9 @@ createConnectionToRemoteUser self zcon other = do fst <$> performLocalAction self (Just zcon) other mconnection LocalConnect updateConnectionToRemoteUser :: - ( Member NotificationSubsystem r, - Member FederationConfigStore r, - Member GalleyProvider r + ( Member GalleyProvider r, + Member NotificationSubsystem r, + Member FederationConfigStore r ) => Local UserId -> Remote UserId -> diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index cc44dd3b250..067d14389f5 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -111,8 +111,8 @@ getFederationStatus _ request = do sendConnectionAction :: ( Member FederationConfigStore r, - Member NotificationSubsystem r, - Member GalleyProvider r + Member GalleyProvider r, + Member NotificationSubsystem r ) => Domain -> NewConnectionRequest -> diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index c04173cf918..9adc85b9af0 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -677,7 +677,8 @@ revokeIdentityH Nothing (Just phone) = lift $ NoContent <$ API.revokeIdentity (R revokeIdentityH bade badp = throwStd (badRequest ("need exactly one of email, phone: " <> Imports.cs (show (bade, badp)))) updateConnectionInternalH :: - ( Member NotificationSubsystem r, + ( Member GalleyProvider r, + Member NotificationSubsystem r, Member TinyLog r, Member (Embed HttpClientIO) r ) => diff --git a/services/brig/src/Brig/Effects/GalleyProvider.hs b/services/brig/src/Brig/Effects/GalleyProvider.hs index c45d58a81b2..24843dbaa0b 100644 --- a/services/brig/src/Brig/Effects/GalleyProvider.hs +++ b/services/brig/src/Brig/Effects/GalleyProvider.hs @@ -36,6 +36,12 @@ import Wire.API.Team.Member qualified as Team import Wire.API.Team.Role import Wire.API.Team.SearchVisibility +data MLSOneToOneEstablished + = Established + | NotEstablished + | NotAMember + deriving (Eq, Show) + data GalleyProvider m a where CreateSelfConv :: UserId -> @@ -109,6 +115,11 @@ data GalleyProvider m a where IsMLSOne2OneEstablished :: Local UserId -> Qualified UserId -> - GalleyProvider m Bool + GalleyProvider m MLSOneToOneEstablished + UnblockConversation :: + Local UserId -> + Maybe ConnId -> + Qualified ConvId -> + GalleyProvider m Conversation makeSem ''GalleyProvider diff --git a/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs b/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs index 84d6ba98cf9..447f8386b41 100644 --- a/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs +++ b/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs @@ -19,7 +19,7 @@ module Brig.Effects.GalleyProvider.RPC where import Bilge hiding (head, options, requestId) import Brig.API.Types -import Brig.Effects.GalleyProvider (GalleyProvider (..)) +import Brig.Effects.GalleyProvider (GalleyProvider (..), MLSOneToOneEstablished (..)) import Brig.RPC hiding (galleyRequest) import Brig.Team.Types (ShowOrHideInvitationUrl (..)) import Control.Error (hush) @@ -48,7 +48,6 @@ import Servant.API (toHeader) import System.Logger (field, msg, val) import Util.Options import Wire.API.Conversation hiding (Member) -import Wire.API.Conversation.Protocol import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Routes.Version import Wire.API.Team @@ -93,6 +92,7 @@ interpretGalleyProviderToRpc disabledVersions galleyEndpoint = GetVerificationCodeEnabled id' -> getVerificationCodeEnabled id' GetExposeInvitationURLsToTeamAdmin id' -> getTeamExposeInvitationURLsToTeamAdmin id' IsMLSOne2OneEstablished lusr qother -> checkMLSOne2OneEstablished lusr qother + UnblockConversation lusr mconn qcnv -> unblockConversation v lusr mconn qcnv galleyRequest :: (Member Rpc r, Member (Input Endpoint) r) => (Request -> Request) -> Sem r (Response (Maybe LByteString)) galleyRequest req = do @@ -537,22 +537,17 @@ checkMLSOne2OneEstablished :: ) => Local UserId -> Qualified UserId -> - Sem r Bool + Sem r MLSOneToOneEstablished checkMLSOne2OneEstablished self (Qualified other otherDomain) = do debug $ remote "galley" . msg (val "Get the MLS one-to-one conversation") - response <- galleyRequest req - case HTTP.statusCode (HTTP.responseStatus response) of - 403 -> pure False - 400 -> pure False - _ {- 200 is assumed -} -> do - conv <- decodeBodyOrThrow @Conversation "galley" response - let mEpoch = case cnvProtocol conv of - ProtocolProteus -> Nothing - ProtocolMLS meta -> Just . cnvmlsEpoch $ meta - ProtocolMixed meta -> Just . cnvmlsEpoch $ meta - pure $ case mEpoch of - Nothing -> False - Just (Epoch e) -> e > 0 + responseSelf <- galleyRequest req + case HTTP.statusCode (HTTP.responseStatus responseSelf) of + 200 -> do + established <- decodeBodyOrThrow @Bool "galley" responseSelf + pure $ if established then Established else NotEstablished + 403 -> pure NotAMember + 400 -> pure NotEstablished + _ -> pure NotEstablished where req = method GET @@ -561,6 +556,39 @@ checkMLSOne2OneEstablished self (Qualified other otherDomain) = do "conversations", "mls-one2one", toByteString' otherDomain, - toByteString' other + toByteString' other, + "established" ] . zUser (tUnqualified self) + +unblockConversation :: + ( Member (Error ParseException) r, + Member (Input Endpoint) r, + Member Rpc r, + Member TinyLog r + ) => + Version -> + Local UserId -> + Maybe ConnId -> + Qualified ConvId -> + Sem r Conversation +unblockConversation v lusr mconn (Qualified cnv cdom) = do + debug $ + remote "galley" + . field "conv" (toByteString cnv) + . field "domain" (toByteString cdom) + . msg (val "Unblocking conversation") + void $ galleyRequest putReq + galleyRequest getReq >>= decodeBodyOrThrow @Conversation "galley" + where + putReq = + method PUT + . paths ["i", "conversations", toByteString' cdom, toByteString' cnv, "unblock"] + . zUser (tUnqualified lusr) + . maybe id (header "Z-Connection" . fromConnId) mconn + . expect2xx + getReq = + method GET + . paths [toHeader v, "conversations", toByteString' cdom, toByteString' cnv] + . zUser (tUnqualified lusr) + . expect2xx diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index 23d7d3bbb0a..2dbd9109b0e 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -29,7 +29,6 @@ module Brig.IO.Intra createConnectConv, acceptConnectConv, blockConv, - unblockConv, upsertOne2OneConversation, -- * Clients @@ -669,42 +668,6 @@ blockConv lusr qcnv = do . zUser (tUnqualified lusr) . expect2xx --- | Calls 'Galley.API.unblockConvH'. -unblockLocalConv :: - ( Member (Embed HttpClientIO) r, - Member TinyLog r - ) => - Local UserId -> - Maybe ConnId -> - ConvId -> - Sem r Conversation -unblockLocalConv lusr conn cnv = do - Log.debug $ - remote "galley" - . field "conv" (toByteString cnv) - . msg (val "Unblocking conversation") - embed $ galleyRequest PUT req >>= decodeBody "galley" - where - req = - paths ["/i/conversations", toByteString' cnv, "unblock"] - . zUser (tUnqualified lusr) - . maybe id (header "Z-Connection" . fromConnId) conn - . expect2xx - -unblockConv :: - ( Member (Embed HttpClientIO) r, - Member TinyLog r - ) => - Local UserId -> - Maybe ConnId -> - Qualified ConvId -> - AppT r Conversation -unblockConv luid conn = - foldQualified - luid - (liftSem . unblockLocalConv luid conn . tUnqualified) - (const (throwM federationNotImplemented)) - upsertOne2OneConversation :: ( MonadReader Env m, MonadIO m, diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 15d0107d009..5e66acec806 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -127,9 +127,11 @@ conversationAPI = <@> mkNamedAPI @"conversation-accept-v2" Update.acceptConv <@> mkNamedAPI @"conversation-block-unqualified" Update.blockConvUnqualified <@> mkNamedAPI @"conversation-block" Update.blockConv + <@> mkNamedAPI @"conversation-unblock-unqualified" Update.unblockConvUnqualified <@> mkNamedAPI @"conversation-unblock" Update.unblockConv <@> mkNamedAPI @"conversation-meta" Query.getConversationMeta <@> mkNamedAPI @"conversation-mls-one-to-one" Query.getMLSOne2OneConversation + <@> mkNamedAPI @"conversation-mls-one-to-one-established" Query.isMLSOne2OneEstablished legalholdWhitelistedTeamsAPI :: API ILegalholdWhitelistedTeamsAPI GalleyEffects legalholdWhitelistedTeamsAPI = mkAPI $ \tid -> hoistAPIHandler Imports.id (base tid) diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index 37b2f295dc7..aaa01a9daa4 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -38,6 +38,7 @@ module Galley.API.Query getMLSSelfConversation, getMLSSelfConversationWithError, getMLSOne2OneConversation, + isMLSOne2OneEstablished, ) where @@ -65,6 +66,7 @@ import Galley.API.Mapping qualified as Mapping import Galley.API.One2One import Galley.API.Util import Galley.Data.Conversation qualified as Data +import Galley.Data.Conversation.Types qualified as Data import Galley.Data.Types (Code (codeConversation)) import Galley.Data.Types qualified as Data import Galley.Effects @@ -89,6 +91,7 @@ import System.Logger.Class qualified as Logger import Wire.API.Conversation hiding (Member) import Wire.API.Conversation qualified as Public import Wire.API.Conversation.Code +import Wire.API.Conversation.Protocol import Wire.API.Conversation.Role import Wire.API.Conversation.Role qualified as Public import Wire.API.Error @@ -792,7 +795,7 @@ getRemoteMLSOne2OneConversation lself qother rconv = do -- a conversation can only be remote if it is hosted on the other user's domain rother <- if qDomain qother == tDomain rconv - then pure (toRemoteUnsafe (tDomain rconv) (qUnqualified qother)) + then pure (qualifyAs rconv (qUnqualified qother)) else throw (InternalErrorWithDescription "Unexpected 1-1 conversation domain") resp <- @@ -806,6 +809,67 @@ getRemoteMLSOne2OneConversation lself qother rconv = do throw (FederationUnexpectedBody "Backend mismatch when retrieving a remote 1-1 conversation") GetOne2OneConversationNotConnected -> throwS @'NotConnected +-- | Check if an MLS 1-1 conversation has been established, namely if its epoch +-- is non-zero. The conversation will only be stored in the database when its +-- first commit arrives. +-- +-- For the federated case, we do not make the assumption that the other backend +-- uses the same function to calculate the conversation ID and corresponding +-- group ID, however we /do/ assume that the two backends agree on which of the +-- two is responsible for hosting the conversation. +isMLSOne2OneEstablished :: + ( Member ConversationStore r, + Member (Input Env) r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member (ErrorS 'MLSNotEnabled) r, + Member (ErrorS 'NotConnected) r, + Member FederatorAccess r + ) => + Local UserId -> + Qualified UserId -> + Sem r Bool +isMLSOne2OneEstablished lself qother = do + assertMLSEnabled + let convId = one2OneConvId BaseProtocolMLSTag (tUntagged lself) qother + foldQualified + lself + isLocalMLSOne2OneEstablished + (isRemoteMLSOne2OneEstablished lself qother) + convId + +isLocalMLSOne2OneEstablished :: + Member ConversationStore r => + Local ConvId -> + Sem r Bool +isLocalMLSOne2OneEstablished lconv = do + mconv <- E.getConversation (tUnqualified lconv) + pure $ case mconv of + Nothing -> False + Just conv -> do + let meta = fst <$> Data.mlsMetadata conv + maybe False ((> 0) . epochNumber . cnvmlsEpoch) meta + +isRemoteMLSOne2OneEstablished :: + ( Member (ErrorS 'NotConnected) r, + Member (Error FederationError) r, + Member (Error InternalError) r, + Member FederatorAccess r + ) => + Local UserId -> + Qualified UserId -> + Remote conv -> + Sem r Bool +isRemoteMLSOne2OneEstablished lself qother rconv = do + conv <- getRemoteMLSOne2OneConversation lself qother rconv + pure . (> 0) $ case cnvProtocol conv of + ProtocolProteus -> 0 + ProtocolMLS meta -> ep meta + ProtocolMixed meta -> ep meta + where + ep :: ConversationMLSData -> Word64 + ep = epochNumber . cnvmlsEpoch + ------------------------------------------------------------------------------- -- Helpers diff --git a/services/galley/src/Galley/API/Update.hs b/services/galley/src/Galley/API/Update.hs index 809f0376188..9e74a97aaa6 100644 --- a/services/galley/src/Galley/API/Update.hs +++ b/services/galley/src/Galley/API/Update.hs @@ -23,6 +23,7 @@ module Galley.API.Update blockConv, blockConvUnqualified, unblockConv, + unblockConvUnqualified, checkReusableCode, joinConversationByReusableCode, joinConversationById, @@ -175,8 +176,8 @@ blockConv :: blockConv lusr qcnv = foldQualified lusr - (\lcnv -> blockConvUnqualified (tUnqualified lusr) (tUnqualified lcnv)) - (\rcnv -> blockRemoteConv lusr rcnv) + (blockConvUnqualified (tUnqualified lusr) . tUnqualified) + (blockRemoteConv lusr) qcnv blockConvUnqualified :: @@ -208,6 +209,26 @@ blockRemoteConv (tUnqualified -> usr) rcnv = do E.deleteMembersInRemoteConversation rcnv [usr] unblockConv :: + ( Member ConversationStore r, + Member (Error InternalError) r, + Member (ErrorS 'ConvNotFound) r, + Member (ErrorS 'InvalidOperation) r, + Member NotificationSubsystem r, + Member (Input UTCTime) r, + Member MemberStore r, + Member TinyLog r + ) => + Local UserId -> + Maybe ConnId -> + Qualified ConvId -> + Sem r () +unblockConv lusr conn = + foldQualified + lusr + (void . unblockConvUnqualified lusr conn . tUnqualified) + (unblockRemoteConv lusr) + +unblockConvUnqualified :: ( Member ConversationStore r, Member (Error InternalError) r, Member (ErrorS 'ConvNotFound) r, @@ -221,7 +242,7 @@ unblockConv :: Maybe ConnId -> ConvId -> Sem r Conversation -unblockConv lusr conn cnv = do +unblockConvUnqualified lusr conn cnv = do conv <- E.getConversation cnv >>= noteS @'ConvNotFound unless (Data.convType conv `elem` [ConnectConv, One2OneConv]) $ @@ -229,6 +250,15 @@ unblockConv lusr conn cnv = do conv' <- acceptOne2One lusr conv conn conversationView lusr conv' +unblockRemoteConv :: + ( Member MemberStore r + ) => + Local UserId -> + Remote ConvId -> + Sem r () +unblockRemoteConv lusr rcnv = do + E.createMembersInRemoteConversation rcnv [tUnqualified lusr] + -- conversation updates handleUpdateResult :: UpdateResult Event -> Response From 8f823f2517f14119c921c2563a9d8f35836f48d4 Mon Sep 17 00:00:00 2001 From: Igor Ranieri Elland <54423+elland@users.noreply.github.com> Date: Wed, 13 Mar 2024 13:04:55 +0100 Subject: [PATCH 053/117] Update http2, wai. (#3911) This silences `ConnectionIsClosed` log spam. --- libs/http2-manager/default.nix | 5 +- libs/http2-manager/http2-manager.cabal | 3 +- .../src/HTTP2/Client/Manager/Internal.hs | 69 +++++++++---------- .../test/Test/HTTP2/Client/ManagerSpec.hs | 66 ++++++++---------- nix/haskell-pins.nix | 28 +++++++- nix/manual-overrides.nix | 1 - 6 files changed, 89 insertions(+), 83 deletions(-) diff --git a/libs/http2-manager/default.nix b/libs/http2-manager/default.nix index 782b3605f14..3674bc56d2a 100644 --- a/libs/http2-manager/default.nix +++ b/libs/http2-manager/default.nix @@ -19,7 +19,7 @@ , stm , streaming-commons , text -, time-manager +, utf8-string }: mkDerivation { pname = "http2-manager"; @@ -36,7 +36,7 @@ mkDerivation { stm streaming-commons text - time-manager + utf8-string ]; testHaskellDepends = [ async @@ -51,7 +51,6 @@ mkDerivation { random stm streaming-commons - time-manager ]; testToolDepends = [ hspec-discover ]; description = "Managed connection pool for HTTP2"; diff --git a/libs/http2-manager/http2-manager.cabal b/libs/http2-manager/http2-manager.cabal index 3aed0c465ba..b6d02869a97 100644 --- a/libs/http2-manager/http2-manager.cabal +++ b/libs/http2-manager/http2-manager.cabal @@ -44,7 +44,7 @@ library , stm , streaming-commons , text - , time-manager + , utf8-string default-language: Haskell2010 @@ -85,4 +85,3 @@ test-suite http2-manager-tests , random , stm , streaming-commons - , time-manager diff --git a/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs b/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs index 0dd00173c8c..ee8bc878ec2 100644 --- a/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs +++ b/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs @@ -15,6 +15,7 @@ import Control.Monad import Control.Monad.IO.Class import Data.ByteString import qualified Data.ByteString as BS +import Data.ByteString.UTF8 as UTF8 import Data.IORef import Data.Map import qualified Data.Map as Map @@ -23,13 +24,11 @@ import Data.Streaming.Network import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import Data.Unique -import Foreign.Marshal.Alloc (mallocBytes) import GHC.IO.Exception import qualified Network.HTTP2.Client as HTTP2 import qualified Network.Socket as NS import qualified OpenSSL.Session as SSL import System.IO.Error -import qualified System.TimeManager import System.Timeout import Prelude @@ -291,9 +290,9 @@ startPersistentHTTP2Connection :: startPersistentHTTP2Connection ctx (tlsEnabled, hostname, port) cl removeTrailingDot tcpConnectTimeout sendReqMVar = do liveReqs <- newIORef mempty let clientConfig = - HTTP2.ClientConfig + HTTP2.defaultClientConfig { HTTP2.scheme = if tlsEnabled then "https" else "http", - HTTP2.authority = hostname, + HTTP2.authority = UTF8.toString hostname, HTTP2.cacheLimit = cl } -- Sends error to requests which show up too late, i.e. after the @@ -333,7 +332,7 @@ startPersistentHTTP2Connection ctx (tlsEnabled, hostname, port) cl removeTrailin bracket connectTCPWithTimeout NS.close $ \sock -> do bracket (mkTransport sock transportConfig) cleanupTransport $ \transport -> bracket (allocHTTP2Config transport) HTTP2.freeSimpleConfig $ \http2Cfg -> do - let runAction = HTTP2.run clientConfig http2Cfg $ \sendReq -> do + let runAction = HTTP2.run clientConfig http2Cfg $ \sendReq _aux -> do handleRequests liveReqs sendReq -- Any request threads still hanging about after 'runAction' finishes -- are canceled with 'ConnectionAlreadyClosed'. @@ -397,7 +396,7 @@ type SendReqFn = HTTP2.Request -> (HTTP2.Response -> IO ()) -> IO () data Transport = InsecureTransport NS.Socket - | SecureTransport SSL.SSL + | SecureTransport SSL.SSL NS.Socket data TLSParams = TLSParams { context :: SSL.SSLContext, @@ -414,11 +413,11 @@ mkTransport sock (Just TLSParams {..}) = do SSL.setTlsextHostName ssl hostnameStr SSL.enableHostnameValidation ssl hostnameStr SSL.connect ssl - pure $ SecureTransport ssl + pure $ SecureTransport ssl sock cleanupTransport :: Transport -> IO () cleanupTransport (InsecureTransport _) = pure () -cleanupTransport (SecureTransport ssl) = SSL.shutdown ssl SSL.Unidirectional +cleanupTransport (SecureTransport ssl _) = SSL.shutdown ssl SSL.Unidirectional data ConnectionAlreadyClosed = ConnectionAlreadyClosed deriving (Show) @@ -430,33 +429,29 @@ bufsize = 4096 allocHTTP2Config :: Transport -> IO HTTP2.Config allocHTTP2Config (InsecureTransport sock) = HTTP2.allocSimpleConfig sock bufsize -allocHTTP2Config (SecureTransport ssl) = do - buf <- mallocBytes bufsize - timmgr <- System.TimeManager.initialize $ 30 * 1000000 - -- Sometimes the frame header says that the payload length is 0. Reading 0 - -- bytes multiple times seems to be causing errors in openssl. I cannot figure - -- out why. The previous implementation didn't try to read from the socket - -- when trying to read 0 bytes, so special handling for 0 maintains that - -- behaviour. - let readData acc 0 = pure acc - readData acc n = do - -- Handling SSL.ConnectionAbruptlyTerminated as a stream end - -- (some sites terminate SSL connection right after returning the data). - chunk <- SSL.read ssl n `catch` \(_ :: SSL.ConnectionAbruptlyTerminated) -> pure mempty - let chunkLen = BS.length chunk - if - | chunkLen == 0 || chunkLen == n -> - pure (acc <> chunk) - | chunkLen > n -> - error "openssl: SSL.read returned more bytes than asked for, this is probably a bug" - | otherwise -> - readData (acc <> chunk) (n - chunkLen) - pure - HTTP2.Config - { HTTP2.confWriteBuffer = buf, - HTTP2.confBufferSize = bufsize, - HTTP2.confSendAll = SSL.write ssl, - HTTP2.confReadN = readData mempty, - HTTP2.confPositionReadMaker = HTTP2.defaultPositionReadMaker, - HTTP2.confTimeoutManager = timmgr +allocHTTP2Config (SecureTransport ssl sock) = do + config <- HTTP2.allocSimpleConfig sock bufsize + pure $ + config + { HTTP2.confSendAll = SSL.write ssl, + HTTP2.confReadN = readData mempty } + where + -- Sometimes the frame header says that the payload length is 0. Reading 0 + -- bytes multiple times seems to be causing errors in openssl. I cannot figure + -- out why. The previous implementation didn't try to read from the socket + -- when trying to read 0 bytes, so special handling for 0 maintains that + -- behaviour. + readData acc 0 = pure acc + readData acc n = do + -- Handling SSL.ConnectionAbruptlyTerminated as a stream end + -- (some sites terminate SSL connection right after returning the data). + chunk <- SSL.read ssl n `catch` \(_ :: SSL.ConnectionAbruptlyTerminated) -> pure mempty + let chunkLen = BS.length chunk + if + | chunkLen == 0 || chunkLen == n -> + pure (acc <> chunk) + | chunkLen > n -> + error "openssl: SSL.read returned more bytes than asked for, this is probably a bug" + | otherwise -> + readData (acc <> chunk) (n - chunkLen) diff --git a/libs/http2-manager/test/Test/HTTP2/Client/ManagerSpec.hs b/libs/http2-manager/test/Test/HTTP2/Client/ManagerSpec.hs index f839619b9bb..7b1010e4066 100644 --- a/libs/http2-manager/test/Test/HTTP2/Client/ManagerSpec.hs +++ b/libs/http2-manager/test/Test/HTTP2/Client/ManagerSpec.hs @@ -26,7 +26,6 @@ import qualified Data.Map as Map import Data.Maybe (isJust) import Data.Streaming.Network (bindPortTCP, bindRandomPortTCP) import Data.Unique -import Foreign.Marshal.Alloc (mallocBytes) import GHC.IO.Exception import HTTP2.Client.Manager import HTTP2.Client.Manager.Internal @@ -37,7 +36,6 @@ import qualified Network.HTTP2.Server as Server import Network.Socket import qualified OpenSSL.Session as SSL import System.Random (randomRIO) -import qualified System.TimeManager import Test.Hspec echoTest :: Http2Manager -> TLSEnabled -> Int -> Expectation @@ -270,38 +268,30 @@ withTestServerOnSocket mCtx action (serverPort, listenSock) = do bracket (async $ testServerOnSocket mCtx listenSock acceptedConns liveConns) cleanupServer $ \serverThread -> action TestServer {..} -allocServerConfig :: Either Socket SSL.SSL -> IO Server.Config -allocServerConfig (Left sock) = HTTP2.allocSimpleConfig sock 4096 -allocServerConfig (Right ssl) = do - buf <- mallocBytes bufsize - timmgr <- System.TimeManager.initialize $ 30 * 1000000 - -- Sometimes the frame header says that the payload length is 0. Reading 0 - -- bytes multiple times seems to be causing errors in openssl. I cannot figure - -- out why. The previous implementation didn't try to read from the socket - -- when trying to read 0 bytes, so special handling for 0 maintains that - -- behaviour. - let readData prevChunk 0 = pure prevChunk - readData prevChunk n = do - -- Handling SSL.ConnectionAbruptlyTerminated as a stream end - -- (some sites terminate SSL connection right after returning the data). - chunk <- SSL.read ssl n `catch` \(_ :: SSL.ConnectionAbruptlyTerminated) -> pure mempty - let chunkLen = BS.length chunk - if - | chunkLen == 0 || chunkLen == n -> - pure (prevChunk <> chunk) - | chunkLen > n -> - error "openssl: SSL.read returned more bytes than asked for, this is probably a bug" - | otherwise -> - readData (prevChunk <> chunk) (n - chunkLen) - pure - Server.Config - { Server.confWriteBuffer = buf, - Server.confBufferSize = bufsize, - Server.confSendAll = SSL.write ssl, - Server.confReadN = readData mempty, - Server.confPositionReadMaker = Server.defaultPositionReadMaker, - Server.confTimeoutManager = timmgr +allocServerConfig :: (Socket, Maybe SSL.SSL) -> IO Server.Config +allocServerConfig (sock, Nothing) = + HTTP2.allocSimpleConfig sock 4096 +allocServerConfig (sock, Just ssl) = do + config <- HTTP2.allocSimpleConfig sock 4096 + pure $ + config + { Server.confReadN = readData mempty, + Server.confSendAll = SSL.write ssl } + where + readData prevChunk 0 = pure prevChunk + readData prevChunk n = do + -- Handling SSL.ConnectionAbruptlyTerminated as a stream end + -- (some sites terminate SSL connection right after returning the data). + chunk <- SSL.read ssl n `catch` \(_ :: SSL.ConnectionAbruptlyTerminated) -> pure mempty + let chunkLen = BS.length chunk + if + | chunkLen == 0 || chunkLen == n -> + pure (prevChunk <> chunk) + | chunkLen > n -> + error "openssl: SSL.read returned more bytes than asked for, this is probably a bug" + | otherwise -> + readData (prevChunk <> chunk) (n - chunkLen) testServerOnSocket :: Maybe SSL.SSLContext -> Socket -> IORef Int -> IORef (Map Unique (Async ())) -> IO () testServerOnSocket mCtx listenSock connsCounter conns = do @@ -309,20 +299,20 @@ testServerOnSocket mCtx listenSock connsCounter conns = do forever $ do (sock, _) <- accept listenSock serverCfgParam <- case mCtx of - Nothing -> pure $ Left sock + Nothing -> pure $ (sock, Nothing) Just ctx -> do ssl <- SSL.connection ctx sock SSL.accept ssl - pure (Right ssl) + pure (sock, Just ssl) connKey <- newUnique modifyIORef connsCounter (+ 1) let shutdownSSL = case serverCfgParam of - Left _ -> pure () - Right ssl -> SSL.shutdown ssl SSL.Bidirectional + (_sock, Just ssl) -> SSL.shutdown ssl SSL.Bidirectional + _ -> pure () cleanup cfg = do Server.freeSimpleConfig cfg `finally` (shutdownSSL `finally` close sock) thread <- async $ bracket (allocServerConfig serverCfgParam) cleanup $ \cfg -> do - Server.run cfg testServer `finally` modifyIORef conns (Map.delete connKey) + Server.run Server.defaultServerConfig cfg testServer `finally` modifyIORef conns (Map.delete connKey) modifyIORef conns $ Map.insert connKey thread testServer :: Server.Request -> Server.Aux -> (Server.Response -> [Server.PushPromise] -> IO ()) -> IO () diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 1b0505fa7c7..835013ef132 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -140,6 +140,17 @@ let }; }; + # We forked to add a handler for the ConnectionIsClosed signal + # since it was threated as a halting exception instead of a + # clean exit. + http2 = { + src = fetchgit { + url = "https://github.com/wireapp/http2"; + rev = "9cad270779bbcd9e6297b9ff05a4a7eb83bca069"; + sha256 = "sha256-c+PzfZZUxo/tE8oH1ZmKwbUMAq34kGD1OoBWorNLG38="; + }; + }; + # PR: https://gitlab.com/twittner/cql/-/merge_requests/11 cql = { src = fetchgit { @@ -232,18 +243,25 @@ let hash = "sha256-E35PVxi/4iJFfWts3td52KKZKQt4dj9KFP3SvWG77Cc="; }; }; + # PR: https://github.com/yesodweb/wai/pull/958 warp = { src = fetchgit { url = "https://github.com/wireapp/wai"; - rev = "bedd6a835f6d98128880465c30e8115fa986e3f6"; - sha256 = "sha256-0r/d9YwcKZIZd10EhL2TP+W14Wjk0/S8Q4pVvZuZLaY="; + rev = "a48f8f31ad42f26057d7b96d70f897c1a3f69a3c"; + sha256 = "sha256-fFkiKLlViiV+F1wdQXak3RI454kgWvyRsoDz6g4c5Ks="; }; packages = { "warp" = "warp"; + "warp-tls" = "warp-tls"; + "wai-app-static" = "wai-app-static"; + "wai" = "wai"; + "wai-extra" = "wai-extra"; + "wai-websockets" = "wai-websockets"; }; }; }; + hackagePins = { # Major re-write upstream, we should get rid of this dependency rather than # adapt to upstream, this will go away when completing servantification. @@ -252,6 +270,12 @@ let sha256 = "sha256-DSMckKIeVE/buSMg8Mq+mUm1bYPYB7veA11Ns7vTBbc="; }; + # http2 now depends on this, might be removable after next nixpkgs bump + network-control = { + version = "0.0.2"; + sha256 = "sha256-0EvnVu7cktMmSRVk9Ufm0oE4JLQrKLSRYpFpgcJguY0="; + }; + # these are not yet in nixpkgs ghc-source-gen = { version = "0.4.4.0"; diff --git a/nix/manual-overrides.nix b/nix/manual-overrides.nix index 51d0f437aea..f17bdaf4673 100644 --- a/nix/manual-overrides.nix +++ b/nix/manual-overrides.nix @@ -54,7 +54,6 @@ hself: hsuper: { optparse-generic = hsuper.optparse-generic_1_5_2; th-abstraction = hsuper.th-abstraction_0_5_0_0; tls = hsuper.tls_1_9_0; - warp-tls = hsuper.warp-tls_3_4_3; # ----------------- # flags and patches From 4a69fa6a39761a713748f104230fc7df2fe1a3af Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Wed, 13 Mar 2024 14:02:35 +0100 Subject: [PATCH 054/117] Update smallstep-accomp Helm chart (#3932) (#3944) * Update smallstep-accomp Helm chart (#3932) Add an exemplary configuration setup for smallstep for End-to-End Identity. Document new values in Helm chart. * Sync missing requirements file Looks like some parts weren't backported. This commit aligns the Helm charts on both branches. --------- Co-authored-by: Stefan Matting --- .../2-features/WPB-6997-smallstep-accomp | 1 + charts/smallstep-accomp/Chart.yaml | 4 + charts/smallstep-accomp/README.md | 111 ++++++++++ charts/smallstep-accomp/requirements.yaml | 4 + .../smallstep-accomp/templates/_helpers.tpl | 3 + .../templates/ca-password.yaml | 12 + charts/smallstep-accomp/templates/certs.yaml | 13 ++ .../smallstep-accomp/templates/secrets.yaml | 14 ++ .../templates/server-block-configmap.yaml | 30 +++ .../templates/step-config.yaml | 9 + charts/smallstep-accomp/values.yaml | 208 ++++++++++++++++++ 11 files changed, 409 insertions(+) create mode 100644 changelog.d/2-features/WPB-6997-smallstep-accomp create mode 100644 charts/smallstep-accomp/Chart.yaml create mode 100644 charts/smallstep-accomp/README.md create mode 100644 charts/smallstep-accomp/requirements.yaml create mode 100644 charts/smallstep-accomp/templates/_helpers.tpl create mode 100644 charts/smallstep-accomp/templates/ca-password.yaml create mode 100644 charts/smallstep-accomp/templates/certs.yaml create mode 100644 charts/smallstep-accomp/templates/secrets.yaml create mode 100644 charts/smallstep-accomp/templates/server-block-configmap.yaml create mode 100644 charts/smallstep-accomp/templates/step-config.yaml create mode 100644 charts/smallstep-accomp/values.yaml diff --git a/changelog.d/2-features/WPB-6997-smallstep-accomp b/changelog.d/2-features/WPB-6997-smallstep-accomp new file mode 100644 index 00000000000..009652bc588 --- /dev/null +++ b/changelog.d/2-features/WPB-6997-smallstep-accomp @@ -0,0 +1 @@ +Add E2EI configuration setup to smallstep-accomp chart diff --git a/charts/smallstep-accomp/Chart.yaml b/charts/smallstep-accomp/Chart.yaml new file mode 100644 index 00000000000..6dad899102f --- /dev/null +++ b/charts/smallstep-accomp/Chart.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +description: Accompanying chart for Smallstep E2EI support +name: smallstep-accomp +version: 1.0.4 diff --git a/charts/smallstep-accomp/README.md b/charts/smallstep-accomp/README.md new file mode 100644 index 00000000000..3165073192c --- /dev/null +++ b/charts/smallstep-accomp/README.md @@ -0,0 +1,111 @@ +# smallstep-acomp - Helm chart accompanying smallstep + +This Helm chart is meant to be installed alongside the [step-certificates Helm +chart](https://smallstep.github.io/helm-charts) in the same namespace. It has been tested with Helm +chart version `1.25.0` and image + +``` +image: + repository: cr.step.sm/smallstep/step-ca + tag: "0.25.3-rc7" +``` + +This Helm chart provides: + +- A reverse-proxy for Certificate Revocation List (CR) distribution endpoints to federating smallstep + servers +- Smallstep server configuration for the End-to-End Identity setup + + +## Reverse-proxy for CRL distribution points + +This Helm chart installs a reverse proxy that proxies the Certificate Revocation List (CRL) +Distribution Point of the Smallstep servers CRL Certificate Authority (CA) from federating domains +and the own domain. This reverse proxy is required for a working End-to-End Identity setup. + +The Helm chart deploys a nginx server that reverse-proxies +`https:///proxyCrl/` to `https://{other_acme_domain}/crl` +as well as an ingress for the `/proxyCrl` endpoint. For example if `upstreams.proxiedHosts` is set +to `[acme.alpha.example.com, acme.beta.example.com]` and the host for the Smallstep server on the +own domain is `acme.alpha.example.com` this helm chart will forward requests + +- `https://acme.alpha.example.com/proxyCrl/acme.alpha.example.com` to `https://acme.alpha.example.com/crl` +- `https://acme.alpha.example.com/proxyCrl/acme.beta.example.com` to `https://acme.beta.example.com/crl` + +| Name | Description | +| ------------------------- | ----------------------------------------------------------------------------------------- | +| `upstreams.enable` | Set to `false` in case you want to write custom nginx server block for the upstream rules | +| `upstreams.dnsResolver` | DNS server that nginx uses to resolve the proxied hostnames | +| `upstreams.proxiedHosts` | List of smallstep hostnames to proxy. Please also include the own smallstep host here | +| `nginx.ingress.enable` | Set to `false` in case you'd like to define a custom ingress for the /proxyCrl endpoint | +| `nginx.ingress.hostname` | Hostname of the step-ca server | + +For more details on `nginx.*` parameters see README.md documentation in the `nginx` dependency chart. + +## Smallstep server configuration for the End-to-End Identity setup + +This Helm chart helps to create configuration file for step-ca. If `stepConfig.enabled` is `true` a +configmap that contains a `ca.json` will be created. The name of that configmap is compatible with the +step-certificates Helm chart, so that it can be directly used. However since step-ca is deployed +from a seperate Helm release updating and deploying a configuration won't cause an automatic reload +of the step-ca server. It is therefore recommended to manually restart step-ca after configuartion +changes if this Helm chart is used for these purposes. + +For references see: + +- [[1] Configuring `step-ca`](https://smallstep.com/docs/step-ca/configuration/) +- [[2] Configuring `step-ca` Provisioners - ACME for Wire messenger clients ](https://smallstep.com/docs/step-ca/provisioners/#acme-for-wire-messenger-clients) + +| Parameter | Description | +|------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------| +| `stepConfig.enabled` | Create a configmap with configuration file for `step-certificates` Helm chart. | +| | If `true` then almost all `stepConfig.*` parameters are required. | +| `stepConfig.configTemplate` | Template for the configuration file. Overwrite this if the default value is not generic enough for your use case. | +| `stepConfig.address` | See [1] | +| `stepConfig.dnsName` | Used in `dnsNames` config entry (See [1]) and to define the CRL URL. | +| `stepConfig.additionalDNSNames` | Optional. Additional entries to `dnsNames` configuration field | +| `stepConfig.root` | See [1]. Public key of the Root CA | +| `stepConfig.crt` | See [1]. Public key of the Intermediate CA | +| `stepConfig.key` | See [1]. Private key of the Intermediate CA | +| `stepConfig.federatedRoots` | See [1]. Add all cross-signed Intermediate CA certs from federating domains here. | +| `stepConfig.db` | See [1] | +| `stepConfig.tls` | See [1] | +| `stepConfig.logger` | See [1] | +| `stepConfig.authority.claims` | See [1] | +| `stepConfig.authority.jwk` | JSON string of the JWK provisioner to use. A JWK provisioner can be created | +| | by running `step ca init` then copying it out of the generated `ca.json`, discarding the `ca.json`. | +| `stepConfig.authority.acme.name` | Name of the ACME provisioner. Default: `"keycloakteams"` | +| `stepConfig.authority.acme.claims` | See [1] | +| `stepConfig.authority.acme.dpop.key` | See [2]. Public half of the DPoP signature key bundle configured of the Wire deployment. | +| | Use the same value as `brig.secrets.dpopSigKeyBundle` value of the `wire-server` Helm chart. | +| | Base64 encoded string of the PEM encoded public key. | +| `stepConfig.authority.acme.dpop.wireDomain` | Set this to the federation domain of the backend | +| `stepConfig.authority.acme.oidc.clientId` | Name of the OIDC client. Default: "wireapp". | +| `stepConfig.authority.acme.oidc.discoveryBaseUrl` | OpenID Connect Discovery endpoint. The OIDC provider must respond with its configuration when `/.well-known/openid-configuration` | +| | is appended to the URL. For Keycloak this URL is of format `https:///auth/realms/`. | +| `stepConfig.authority.acme.oidc.issuerUrl` | For Keycloak this must be of the format `https:///auth/realms/?client_id=wireapp` | +| `stepConfig.authority.acme.oidc.signatureAlgorithms` | See [2] | +| `stepConfig.authority.acme.oidc.transform` | See [2]. Has sensible default. There shouldn't be any need to customize this setting. | +| `stepConfig.authority.acme.x509.organization` | Set this to the federation domain of the backend | +| `stepConfig.authority.acme.x509.template` | See [2]. Go template for a client certificate which is evaluated by step-ca. | +| | This string is evaluated as template of the Helm chart first. | +| | Has a sensible default. There shouldn't be a need to customize this setting. | + +| Parameter | Description | +|-----------------------|-------------------------------------------------------------------------------------------------------| +| `caPassword.enabled` | If `true` generate Secret with a name that the `step-certificates` Helm chart will automatically use. | +| | The Helm chart will mount this at `/home/step/secrets/passwords/password`. | +| `caPassword.password` | Password that decrypts the intermediate CA private key | + +| Parameter | Description | +|---------------------------|-------------------------------------------------------------------------------------------------------| +| `existingSecrets.enabled` | If `true` generate Secret with a name that the `step-certificates` Helm chart will automatically use. | +| `existingSecrets.data` | Map from filename to content. Each entry will be mounted as file `/home/step/secrets/` | +| | Add the private key of the Intermediate CA here. | + +| Parameter | Description | +|-------------------------|-----------------------------------------------------------------------------------------------------| +| `existingCerts.enabled` | If `true` generate ConfigMap with a name that the Helm chart will automatically use. | +| `existingCerts.data` | Map from filename to content. Each entry will be mounted as file `/home/step/certs/` | +| `existingCerts.data` | Use it to make public keys of the Root, intermediate CA as well as the cross-signed certs available | +| | to step-ca. Each entry will be mounted as file `/home/step/certs/` | diff --git a/charts/smallstep-accomp/requirements.yaml b/charts/smallstep-accomp/requirements.yaml new file mode 100644 index 00000000000..e9d0780c6e9 --- /dev/null +++ b/charts/smallstep-accomp/requirements.yaml @@ -0,0 +1,4 @@ +dependencies: +- name: nginx + version: 15.10.4 + repository: https://charts.bitnami.com/bitnami diff --git a/charts/smallstep-accomp/templates/_helpers.tpl b/charts/smallstep-accomp/templates/_helpers.tpl new file mode 100644 index 00000000000..fb5cb93c9ce --- /dev/null +++ b/charts/smallstep-accomp/templates/_helpers.tpl @@ -0,0 +1,3 @@ +{{- define "fullname" -}} +smallstep-step-certificates +{{- end -}} diff --git a/charts/smallstep-accomp/templates/ca-password.yaml b/charts/smallstep-accomp/templates/ca-password.yaml new file mode 100644 index 00000000000..cd1bdc962a9 --- /dev/null +++ b/charts/smallstep-accomp/templates/ca-password.yaml @@ -0,0 +1,12 @@ +{{- if .Values.caPassword.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: smallstep-step-certificates-ca-password + labels: + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" +type: Opaque +data: + password: {{ .Values.caPassword.password | b64enc | quote }} +{{- end }} diff --git a/charts/smallstep-accomp/templates/certs.yaml b/charts/smallstep-accomp/templates/certs.yaml new file mode 100644 index 00000000000..c9ef0ce45a9 --- /dev/null +++ b/charts/smallstep-accomp/templates/certs.yaml @@ -0,0 +1,13 @@ +{{- if .Values.existingCerts.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: smallstep-step-certificates-certs + labels: + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" +data: + {{- range $key, $value := .Values.existingCerts.data }} + {{ $key }}: {{ $value | quote }} + {{- end }} +{{- end }} diff --git a/charts/smallstep-accomp/templates/secrets.yaml b/charts/smallstep-accomp/templates/secrets.yaml new file mode 100644 index 00000000000..8448fbc7f8f --- /dev/null +++ b/charts/smallstep-accomp/templates/secrets.yaml @@ -0,0 +1,14 @@ +{{- if .Values.existingSecrets.enabled }} +apiVersion: v1 +kind: Secret +metadata: + name: smallstep-step-certificates-secrets + labels: + chart: "{{ .Chart.Name }}-{{ .Chart.Version }}" + release: "{{ .Release.Name }}" +type: Opaque +data: + {{- range $key, $value := .Values.existingSecrets.data }} + {{ $key }}: {{ $value | b64enc | quote }} + {{- end }} +{{- end }} diff --git a/charts/smallstep-accomp/templates/server-block-configmap.yaml b/charts/smallstep-accomp/templates/server-block-configmap.yaml new file mode 100644 index 00000000000..366dad7e92e --- /dev/null +++ b/charts/smallstep-accomp/templates/server-block-configmap.yaml @@ -0,0 +1,30 @@ +{{- if and .Values.upstreams.enabled .Values.nginx.existingServerBlockConfigmap }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ .Values.nginx.existingServerBlockConfigmap }} + labels: + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: {{ .Release.Name }} + heritage: {{ .Release.Service }} +data: + server.conf: | + resolver {{ .Values.upstreams.dnsResolver }}; + + server { + listen 0.0.0.0:8080; + + {{- range .Values.upstreams.proxiedHosts }} + + location /proxyCrl/{{ . }} { + proxy_redirect off; + proxy_set_header X-Forwarded-Host $http_host; + proxy_set_header Host {{ . }}; + proxy_hide_header Content-Type; + add_header Content-Type application/pkix-crl; + proxy_pass "https://{{ . }}/crl"; + } + + {{- end }} + } +{{- end }} diff --git a/charts/smallstep-accomp/templates/step-config.yaml b/charts/smallstep-accomp/templates/step-config.yaml new file mode 100644 index 00000000000..0cb957fa88c --- /dev/null +++ b/charts/smallstep-accomp/templates/step-config.yaml @@ -0,0 +1,9 @@ +{{- if .Values.stepConfig.enabled }} +apiVersion: v1 +kind: ConfigMap +metadata: + name: smallstep-step-certificates-config +data: + ca.json: |- + {{(tpl .Values.stepConfig.configTemplate .) | fromYaml | toJson }} +{{- end }} diff --git a/charts/smallstep-accomp/values.yaml b/charts/smallstep-accomp/values.yaml new file mode 100644 index 00000000000..2f99e7b8334 --- /dev/null +++ b/charts/smallstep-accomp/values.yaml @@ -0,0 +1,208 @@ +nginx: + existingServerBlockConfigmap: "smallstep-accomp-server-block" + + service: + type: ClusterIP + + ingress: + enabled: true + # ingressClassName: "nginx" + + # hostname: "acme.alpha.example.com" + path: "/proxyCrl" + pathType: "Prefix" + + # extraTls: + # - + # hosts: [ "acme.alpha.example.com" ] + # secretName: "smallstep-step-certificates-ingress-cert" + # + +upstreams: + enabled: true + # dnsResolver: 9.9.9.9 + + # Note: include the smallstep host of the own domain here as well + proxiedHosts: [] + # proxiedHosts: + # - acme.alpha.example.com + # - acme.beta.example.com + # - acme.gamma.example.com + + +caPassword: + enabled: true + password: "...." + +existingSecrets: + enabled: false + # data: + # ca.key: foobar + +existingCerts: + enabled: false + # data: + # ca.crt: "-----BEGIN CERTIFICATE-----...." + # root_ca.crt: "-----BEGIN CERTIFICATE-----...." + # ca-other2-cross-signed.crt: "-----BEGIN CERTIFICATE-----...." + # ca-other3-cross-signed.crt: "-----BEGIN CERTIFICATE-----...." + +stepConfig: + enabled: true + + address: "0.0.0.0:9000" + + # dnsName: acme.alpha.example.com + + # additionalDNSNames: + # - localhost + + root: /home/step/certs/root_ca.crt + crt: /home/step/certs/ca.crt + key: /home/step/secrets/ca.key + + federatedRoots: + - /home/step/certs/ca.crt + + # federatedRoots: + # - /home/step/certs/ca.crt + # - /home/step/certs/acme.beta.example.com-xsigned-by-acme.alpha.example.com + + db: + badgerFileLoadingMode: "" + dataSource: /home/step/db + type: badgerv2 + + tls: + cipherSuites: + - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + maxVersion: 1.3 + minVersion: 1.2 + renegotiation: false + + logger: + format: text + + authority: + claims: + maxTLSCertDuration: 87701h + + # jwk: |- + # { + # "type": "JWK", + # "name": "..example.com", + # "key": { ... }, + # "encryptedKey": "e..." + # } + + acme: + name: keycloakteams + + claims: + allowRenewalAfterExpiry: false + defaultTLSCertDuration: 2160h + disableRenewal: false + maxTLSCertDuration: 2160h + minTLSCertDuration: 60s + + dpop: + # key: + wireDomain: alpha.example.com + + oidc: + clientId: wireapp + # discoveryBaseUrl: https://keycloak.example.com/auth/realms/master + # issuerUrl: https://keycloak.example.com/auth/realms/master?client_id=wireapp + signatureAlgorithms: + - RS256 + - ES256 + - ES384 + - EdDSA + transform: | + { + "name": "{{ .name }}", + "preferred_username": "wireapp://%40{{ .preferred_username }}" + } + + x509: + # organization: alpha.example.com + template: | + { + "subject": { + "organization": {{ required "stepConfig.authority.acme.x509.organization is missing" .Values.stepConfig.authority.acme.x509.organization | toJson }}, + "commonName": {{ "{{" }} toJson .Oidc.name {{ "}}" }} + }, + "uris": [{{ "{{" }} toJson .Oidc.preferred_username {{ "}}" }}, {{ "{{" }} toJson .Dpop.sub {{ "}}" }}], + "keyUsage": ["digitalSignature"], + "extKeyUsage": ["clientAuth"], + "crlDistributionPoints": {{ tpl "[ https://{{ required \"stepConfig.dnsName is missing\" .Values.stepConfig.dnsName }}/crl ]" . | fromYamlArray | toJson }} + } + + configTemplate: |- + address: {{ required "stepConfig.address is missing" .Values.stepConfig.address }} + + dnsNames: + - {{ required "stepConfig.dnsName is missing" .Values.stepConfig.dnsName }} + {{- if .Values.stepConfig.additionalDNSNames }} + {{- .Values.stepConfig.additionalDNSNames | toYaml | nindent 2 }} + {{- end }} + + crt: {{ required "stepConfig.crt is missing" .Values.stepConfig.crt }} + key: {{ required "stepConfig.key is missing" .Values.stepConfig.key }} + root: {{ required "stepConfig.root is missing" .Values.stepConfig.root }} + + federatedRoots: + {{- required "stepConfig.federatedRoots is missing" .Values.stepConfig.federatedRoots | toYaml | nindent 2 }} + + crl: + enabled: true + generateOnRevoke: true + idpURL: https://{{ required "stepConfig.dnsName is missing" .Values.stepConfig.dnsName }}/crl + + db: + {{ required "stepConfig.db is missing" .Values.stepConfig.db | toYaml | nindent 2 }} + + tls: + {{ required "stepConfig.tls is missing" .Values.stepConfig.tls | toYaml | nindent 2 }} + + logger: + {{ required "stepConfig.logger is missing" .Values.stepConfig.logger | toYaml | nindent 2 }} + + authority: + claims: + {{ required "stepConfig.authority.claims is missing" .Values.stepConfig.authority.claims | toYaml | nindent 4 }} + provisioners: + - {{ required "stepConfig.authority.jwk is missing" .Values.stepConfig.authority.jwk | fromJson | toYaml | nindent 6 }} + - name: {{ required "stepConfig.authority.acme.name is missing" .Values.stepConfig.authority.acme.name }} + type: ACME + forceCN: true + challenges: + - wire-oidc-01 + - wire-dpop-01 + claims: + {{ required "stepConfig.authority.acme.claims is missing" .Values.stepConfig.authority.acme.claims | toYaml | nindent 8 }} + options: + wire: + dpop: + key: {{ required "stepConfig.authority.acme.dpop.key is missing" .Values.stepConfig.authority.acme.dpop.key }} + target: https://{{ required "stepConfig.authority.acme.dpop.wireDomain" .Values.stepConfig.authority.acme.dpop.wireDomain }}/clients/{{ "{{" }}.DeviceID{{ "}}" }}/access-token + oidc: + config: + clientId: {{ required "stepConfig.authority.acme.oidc.clientId is missing" .Values.stepConfig.authority.acme.oidc.clientId }} + signatureAlgorithms: + {{ required "stepConfig.authority.acme.oidc.signatureAlgorithms is missing" .Values.stepConfig.authority.acme.oidc.signatureAlgorithms | toYaml | nindent 14 }} + provider: + discoveryBaseUrl: {{ required "stepConfig.authority.acme.oidc.discoveryBaseUrl is missing" .Values.stepConfig.authority.acme.oidc.discoveryBaseUrl }} + id_token_signing_alg_values_supported: + {{ required "stepConfig.authority.acme.oidc.signatureAlgorithms is missing" .Values.stepConfig.authority.acme.oidc.signatureAlgorithms | toYaml | nindent 14 }} + issuerUrl: {{ required "stepConfig.authority.acme.oidc.issuerUrl is missing" .Values.stepConfig.authority.acme.oidc.issuerUrl }} + transform: {{ required "stepConfig.authority.acme.oidc.transform is missing" .Values.stepConfig.authority.acme.oidc.transform | toJson }} + x509: + template: {{ (tpl .Values.stepConfig.authority.acme.x509.template .) | toJson }} + + {{- if .Values.stepConfig.extraConfig }} + {{ .Values.stepConfig.extraconfig | toYaml }} + {{- end }} + + + From ba0d7f15c459d9417698a76b928e5ac8637d751a Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Thu, 14 Mar 2024 15:03:52 +0100 Subject: [PATCH 055/117] smallstep-accomp: Resolve proxy target on request (#3946) (#3947) Usually, proxy targets are resolved when nginx is started. This can lead to strange behavior if the target either doesn't exist (yet) or the DNS entry changes while nginx is running. This little trick with the indirection via a variable should trigger the lookup(s) while nginx is running. The default behavior of the `resolver` directive is to update the target according to its TTL in the configured DNS server. --- changelog.d/5-internal/smallstep-accomp-target-resolving | 4 ++++ .../templates/server-block-configmap.yaml | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 changelog.d/5-internal/smallstep-accomp-target-resolving diff --git a/changelog.d/5-internal/smallstep-accomp-target-resolving b/changelog.d/5-internal/smallstep-accomp-target-resolving new file mode 100644 index 00000000000..3d8f4fa8b4e --- /dev/null +++ b/changelog.d/5-internal/smallstep-accomp-target-resolving @@ -0,0 +1,4 @@ +Ensure that targets of the smallstep nginx proxy are resolved at runtime via the +configured DNS server. This has two benefits: The target gets adjusted when it's +changed at the DNS server. And, nginx doesn't fail to start when the target +doesn't exist yet. diff --git a/charts/smallstep-accomp/templates/server-block-configmap.yaml b/charts/smallstep-accomp/templates/server-block-configmap.yaml index 366dad7e92e..a6765595d31 100644 --- a/charts/smallstep-accomp/templates/server-block-configmap.yaml +++ b/charts/smallstep-accomp/templates/server-block-configmap.yaml @@ -17,12 +17,16 @@ data: {{- range .Values.upstreams.proxiedHosts }} location /proxyCrl/{{ . }} { + # This indirection is required to make the resolver check the domain. + # Otherwise, broken upstreams lead to broken deployments. + set $backend "{{ . }}"; + proxy_redirect off; proxy_set_header X-Forwarded-Host $http_host; - proxy_set_header Host {{ . }}; + proxy_set_header Host $backend; proxy_hide_header Content-Type; add_header Content-Type application/pkix-crl; - proxy_pass "https://{{ . }}/crl"; + proxy_pass "https://$backend/crl"; } {{- end }} From 479a302b4420623d07de6ea38d99a08281e4d1a9 Mon Sep 17 00:00:00 2001 From: Mathias Staab <71255223+mastaab@users.noreply.github.com> Date: Thu, 14 Mar 2024 19:28:00 +0100 Subject: [PATCH 056/117] add cors annotations to the crl-proxy ingress (#3956) --- changelog.d/5-internal/WPB-7155 | 2 ++ charts/smallstep-accomp/values.yaml | 6 +++++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 changelog.d/5-internal/WPB-7155 diff --git a/changelog.d/5-internal/WPB-7155 b/changelog.d/5-internal/WPB-7155 new file mode 100644 index 00000000000..f5d04caee31 --- /dev/null +++ b/changelog.d/5-internal/WPB-7155 @@ -0,0 +1,2 @@ +In order for the CRL-proxy to function correctly, it needs to have CORS headers set. +We are now setting the CORS headers on the ingress level. \ No newline at end of file diff --git a/charts/smallstep-accomp/values.yaml b/charts/smallstep-accomp/values.yaml index 2f99e7b8334..e4e3ad18437 100644 --- a/charts/smallstep-accomp/values.yaml +++ b/charts/smallstep-accomp/values.yaml @@ -16,7 +16,11 @@ nginx: # - # hosts: [ "acme.alpha.example.com" ] # secretName: "smallstep-step-certificates-ingress-cert" - # + + # annotations: + # nginx.ingress.kubernetes.io/cors-allow-origin: https://webapp.acme.alpha.example.com + # nginx.ingress.kubernetes.io/cors-expose-headers: Replay-Nonce, Location + # nginx.ingress.kubernetes.io/enable-cors: 'true' upstreams: enabled: true From 022aa394de057d650fbead42a9acbca742611e7f Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 18 Mar 2024 09:02:21 +0100 Subject: [PATCH 057/117] Fix openapi docs of UTCTime and UTCTimeMillis (#3899) * Fix openapi docs of UTCTime and UTCTimeMillis * Use different names for UTCTime and UTCTimeMillis --- changelog.d/4-docs/utctime-swagger | 1 + libs/types-common/src/Data/Json/Util.hs | 23 +++++++++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 changelog.d/4-docs/utctime-swagger diff --git a/changelog.d/4-docs/utctime-swagger b/changelog.d/4-docs/utctime-swagger new file mode 100644 index 00000000000..20e0bbdd460 --- /dev/null +++ b/changelog.d/4-docs/utctime-swagger @@ -0,0 +1 @@ +Distinguish UTCTime and UTCTimeMillis in swagger diff --git a/libs/types-common/src/Data/Json/Util.hs b/libs/types-common/src/Data/Json/Util.hs index 2c898487f04..f6c990081ec 100644 --- a/libs/types-common/src/Data/Json/Util.hs +++ b/libs/types-common/src/Data/Json/Util.hs @@ -101,17 +101,24 @@ newtype UTCTimeMillis = UTCTimeMillis {fromUTCTimeMillis :: UTCTime} deriving (FromJSON, ToJSON, S.ToSchema) via Schema UTCTimeMillis instance ToSchema UTCTimeMillis where - schema = UTCTimeMillis <$> showUTCTimeMillis .= utcTimeTextSchema - -utcTimeTextSchema :: ValueSchemaP NamedSwaggerDoc Text UTCTime -utcTimeTextSchema = - parsedText "UTCTime" (Atto.parseOnly (Atto.utcTime <* Atto.endOfInput)) + schema = + UTCTimeMillis + <$> showUTCTimeMillis + .= ( utcTimeTextSchema "UTCTimeMillis" + & doc . S.schema + %~ (S.format ?~ "yyyy-mm-ddThh:MM:ss.qqqZ") + . (S.example ?~ "2021-05-12T10:52:02.671Z") + ) + +utcTimeTextSchema :: Text -> ValueSchemaP NamedSwaggerDoc Text UTCTime +utcTimeTextSchema name = + parsedText name (Atto.parseOnly (Atto.utcTime <* Atto.endOfInput)) & doc . S.schema - %~ (S.format ?~ "yyyy-mm-ddThh:MM:ss.qqq") - . (S.example ?~ "2021-05-12T10:52:02.671Z") + %~ (S.format ?~ "yyyy-mm-ddThh:MM:ssZ") + . (S.example ?~ "2021-05-12T10:52:02Z") utcTimeSchema :: ValueSchema NamedSwaggerDoc UTCTime -utcTimeSchema = showUTCTime .= utcTimeTextSchema +utcTimeSchema = showUTCTime .= utcTimeTextSchema "UTCTime" {-# INLINE toUTCTimeMillis #-} toUTCTimeMillis :: UTCTime -> UTCTimeMillis From b85133fc86d7826b3671979ca3f73d3e7a20cbac Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Mon, 18 Mar 2024 15:06:50 +0100 Subject: [PATCH 058/117] Refactor and test user event serialisation (#3912) * Move brig event serialisation code * Implement schemas for user events * Move user events to wire-api * Add golden test examples for user events * Add user event golden tests These JSON files were generated using the old serialisation code. * Fix user event serialisation golden tests --- changelog.d/5-internal/user-events | 1 + libs/brig-types/brig-types.cabal | 2 - libs/brig-types/default.nix | 2 - libs/brig-types/src/Brig/Types/User/Event.hs | 223 --------- libs/types-common/src/Data/Id.hs | 9 +- libs/wire-api/default.nix | 2 + libs/wire-api/src/Wire/API/UserEvent.hs | 451 ++++++++++++++++++ .../golden/Test/Wire/API/Golden/Manual.hs | 21 + .../Test/Wire/API/Golden/Manual/UserEvent.hs | 249 ++++++++++ .../test/golden/testObject_UserEvent_1.json | 20 + .../test/golden/testObject_UserEvent_10.json | 4 + .../test/golden/testObject_UserEvent_11.json | 11 + .../test/golden/testObject_UserEvent_12.json | 16 + .../test/golden/testObject_UserEvent_13.json | 5 + .../test/golden/testObject_UserEvent_14.json | 4 + .../test/golden/testObject_UserEvent_15.json | 3 + .../test/golden/testObject_UserEvent_16.json | 15 + .../test/golden/testObject_UserEvent_17.json | 6 + .../test/golden/testObject_UserEvent_2.json | 20 + .../test/golden/testObject_UserEvent_3.json | 4 + .../test/golden/testObject_UserEvent_4.json | 4 + .../test/golden/testObject_UserEvent_5.json | 8 + .../test/golden/testObject_UserEvent_6.json | 14 + .../test/golden/testObject_UserEvent_7.json | 7 + .../test/golden/testObject_UserEvent_8.json | 7 + .../test/golden/testObject_UserEvent_9.json | 4 + libs/wire-api/wire-api.cabal | 3 + services/brig/brig.cabal | 1 + services/brig/src/Brig/API/Client.hs | 4 +- services/brig/src/Brig/API/Connection.hs | 43 +- .../brig/src/Brig/API/Connection/Remote.hs | 4 +- services/brig/src/Brig/API/Federation.hs | 2 +- services/brig/src/Brig/API/Internal.hs | 2 +- services/brig/src/Brig/API/Properties.hs | 8 +- services/brig/src/Brig/API/User.hs | 2 +- services/brig/src/Brig/IO/Intra.hs | 137 +----- services/brig/src/Brig/IO/Logging.hs | 34 ++ .../brig/src/Brig/InternalEvent/Process.hs | 4 +- .../API/Teams/LegalHold/DisabledByDefault.hs | 8 +- .../integration/API/Teams/LegalHold/Util.hs | 29 +- 40 files changed, 966 insertions(+), 427 deletions(-) create mode 100644 changelog.d/5-internal/user-events delete mode 100644 libs/brig-types/src/Brig/Types/User/Event.hs create mode 100644 libs/wire-api/src/Wire/API/UserEvent.hs create mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs create mode 100644 libs/wire-api/test/golden/testObject_UserEvent_1.json create mode 100644 libs/wire-api/test/golden/testObject_UserEvent_10.json create mode 100644 libs/wire-api/test/golden/testObject_UserEvent_11.json create mode 100644 libs/wire-api/test/golden/testObject_UserEvent_12.json create mode 100644 libs/wire-api/test/golden/testObject_UserEvent_13.json create mode 100644 libs/wire-api/test/golden/testObject_UserEvent_14.json create mode 100644 libs/wire-api/test/golden/testObject_UserEvent_15.json create mode 100644 libs/wire-api/test/golden/testObject_UserEvent_16.json create mode 100644 libs/wire-api/test/golden/testObject_UserEvent_17.json create mode 100644 libs/wire-api/test/golden/testObject_UserEvent_2.json create mode 100644 libs/wire-api/test/golden/testObject_UserEvent_3.json create mode 100644 libs/wire-api/test/golden/testObject_UserEvent_4.json create mode 100644 libs/wire-api/test/golden/testObject_UserEvent_5.json create mode 100644 libs/wire-api/test/golden/testObject_UserEvent_6.json create mode 100644 libs/wire-api/test/golden/testObject_UserEvent_7.json create mode 100644 libs/wire-api/test/golden/testObject_UserEvent_8.json create mode 100644 libs/wire-api/test/golden/testObject_UserEvent_9.json create mode 100644 services/brig/src/Brig/IO/Logging.hs diff --git a/changelog.d/5-internal/user-events b/changelog.d/5-internal/user-events new file mode 100644 index 00000000000..b2e75283007 --- /dev/null +++ b/changelog.d/5-internal/user-events @@ -0,0 +1 @@ +Use schema-profunctor for user event serialisation and introduce golden tests diff --git a/libs/brig-types/brig-types.cabal b/libs/brig-types/brig-types.cabal index d7a933c90bb..4d4d0640dd1 100644 --- a/libs/brig-types/brig-types.cabal +++ b/libs/brig-types/brig-types.cabal @@ -23,7 +23,6 @@ library Brig.Types.Team.LegalHold Brig.Types.Test.Arbitrary Brig.Types.User - Brig.Types.User.Event other-modules: Paths_brig_types hs-source-dirs: src @@ -85,7 +84,6 @@ library , imports , QuickCheck >=2.9 , text >=0.11 - , tinylog , types-common >=0.16 , wire-api diff --git a/libs/brig-types/default.nix b/libs/brig-types/default.nix index 173b83591b0..78932b5d379 100644 --- a/libs/brig-types/default.nix +++ b/libs/brig-types/default.nix @@ -19,7 +19,6 @@ , tasty-hunit , tasty-quickcheck , text -, tinylog , types-common , wire-api }: @@ -38,7 +37,6 @@ mkDerivation { imports QuickCheck text - tinylog types-common wire-api ]; diff --git a/libs/brig-types/src/Brig/Types/User/Event.hs b/libs/brig-types/src/Brig/Types/User/Event.hs deleted file mode 100644 index 96aed364a76..00000000000 --- a/libs/brig-types/src/Brig/Types/User/Event.hs +++ /dev/null @@ -1,223 +0,0 @@ -{-# LANGUAGE RecordWildCards #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Brig.Types.User.Event where - -import Data.ByteString.Conversion -import Data.Handle (Handle) -import Data.Id -import Data.Qualified -import Imports -import System.Logger.Class -import Wire.API.Connection -import Wire.API.Properties -import Wire.API.User -import Wire.API.User.Client -import Wire.API.User.Client.Prekey - -data Event - = UserEvent !UserEvent - | ConnectionEvent !ConnectionEvent - | PropertyEvent !PropertyEvent - | ClientEvent !ClientEvent - -data UserEvent - = UserCreated !User - | -- | A user is activated when the first user identity (email address or phone number) - -- is verified. {#RefActivationEvent} - UserActivated !User - | -- | Account & API access of a user has been suspended. - UserSuspended !UserId - | -- | Account & API access of a previously suspended user - -- has been restored. - UserResumed !UserId - | -- | The user account has been deleted. - UserDeleted !(Qualified UserId) - | UserUpdated !UserUpdatedData - | UserIdentityUpdated !UserIdentityUpdatedData - | UserIdentityRemoved !UserIdentityRemovedData - | UserLegalHoldDisabled !UserId - | UserLegalHoldEnabled !UserId - | LegalHoldClientRequested LegalHoldClientRequestedData - -data ConnectionEvent = ConnectionUpdated - { ucConn :: !UserConnection, - ucPrev :: !(Maybe Relation), - ucName :: !(Maybe Name) - } - -data PropertyEvent - = PropertySet !UserId !PropertyKey !PropertyValue - | PropertyDeleted !UserId !PropertyKey - | PropertiesCleared !UserId - -data ClientEvent - = ClientAdded !UserId !Client - | ClientRemoved !UserId !ClientId - -data UserUpdatedData = UserUpdatedData - { eupId :: !UserId, - eupName :: !(Maybe Name), - -- | DEPRECATED - eupPict :: !(Maybe Pict), - eupAccentId :: !(Maybe ColourId), - eupAssets :: !(Maybe [Asset]), - eupHandle :: !(Maybe Handle), - eupLocale :: !(Maybe Locale), - eupManagedBy :: !(Maybe ManagedBy), - eupSSOId :: !(Maybe UserSSOId), - eupSSOIdRemoved :: Bool, - eupSupportedProtocols :: !(Maybe (Set BaseProtocolTag)) - } - deriving stock (Show) - -data UserIdentityUpdatedData = UserIdentityUpdatedData - { eiuId :: !UserId, - eiuEmail :: !(Maybe Email), - eiuPhone :: !(Maybe Phone) - } - deriving stock (Show) - -data UserIdentityRemovedData = UserIdentityRemovedData - { eirId :: !UserId, - eirEmail :: !(Maybe Email), - eirPhone :: !(Maybe Phone) - } - deriving stock (Show) - -data LegalHoldClientRequestedData = LegalHoldClientRequestedData - { -- | the user that is under legalhold - lhcTargetUser :: !UserId, - -- | the last prekey of the user that is under legalhold - lhcLastPrekey :: !LastPrekey, - -- | the client id of the legalhold device - lhcClientId :: !ClientId - } - deriving stock (Show) - -emailRemoved :: UserId -> Email -> UserEvent -emailRemoved u e = - UserIdentityRemoved $ UserIdentityRemovedData u (Just e) Nothing - -phoneRemoved :: UserId -> Phone -> UserEvent -phoneRemoved u p = - UserIdentityRemoved $ UserIdentityRemovedData u Nothing (Just p) - -emailUpdated :: UserId -> Email -> UserEvent -emailUpdated u e = - UserIdentityUpdated $ UserIdentityUpdatedData u (Just e) Nothing - -phoneUpdated :: UserId -> Phone -> UserEvent -phoneUpdated u p = - UserIdentityUpdated $ UserIdentityUpdatedData u Nothing (Just p) - -handleUpdated :: UserId -> Handle -> UserEvent -handleUpdated u h = - UserUpdated $ (emptyUserUpdatedData u) {eupHandle = Just h} - -localeUpdate :: UserId -> Locale -> UserEvent -localeUpdate u loc = - UserUpdated $ (emptyUserUpdatedData u) {eupLocale = Just loc} - -managedByUpdate :: UserId -> ManagedBy -> UserEvent -managedByUpdate u mb = - UserUpdated $ (emptyUserUpdatedData u) {eupManagedBy = Just mb} - -supportedProtocolUpdate :: UserId -> Set BaseProtocolTag -> UserEvent -supportedProtocolUpdate u prots = - UserUpdated $ (emptyUserUpdatedData u) {eupSupportedProtocols = Just prots} - -profileUpdated :: UserId -> UserUpdate -> UserEvent -profileUpdated u UserUpdate {..} = - UserUpdated $ - (emptyUserUpdatedData u) - { eupName = uupName, - eupPict = uupPict, - eupAccentId = uupAccentId, - eupAssets = uupAssets - } - -emptyUpdate :: UserId -> UserEvent -emptyUpdate = UserUpdated . emptyUserUpdatedData - -emptyUserUpdatedData :: UserId -> UserUpdatedData -emptyUserUpdatedData u = - UserUpdatedData - { eupId = u, - eupName = Nothing, - eupPict = Nothing, - eupAccentId = Nothing, - eupAssets = Nothing, - eupHandle = Nothing, - eupLocale = Nothing, - eupManagedBy = Nothing, - eupSSOId = Nothing, - eupSSOIdRemoved = False, - eupSupportedProtocols = Nothing - } - -connEventUserId :: ConnectionEvent -> UserId -connEventUserId ConnectionUpdated {..} = ucFrom ucConn - -propEventUserId :: PropertyEvent -> UserId -propEventUserId (PropertySet u _ _) = u -propEventUserId (PropertyDeleted u _) = u -propEventUserId (PropertiesCleared u) = u - -logConnection :: UserId -> Qualified UserId -> Msg -> Msg -logConnection from (Qualified toUser toDomain) = - "connection.from" .= toByteString from - ~~ "connection.to" .= toByteString toUser - ~~ "connection.to_domain" .= toByteString toDomain - -logLocalConnection :: UserId -> UserId -> Msg -> Msg -logLocalConnection from to = - "connection.from" .= toByteString from - ~~ "connection.to" .= toByteString to - -instance ToBytes Event where - bytes (UserEvent e) = bytes e - bytes (ConnectionEvent e) = bytes e - bytes (PropertyEvent e) = bytes e - bytes (ClientEvent e) = bytes e - -instance ToBytes UserEvent where - bytes (UserCreated u) = val "user.new: " +++ toByteString (userId u) - bytes (UserActivated u) = val "user.activate: " +++ toByteString (userId u) - bytes (UserUpdated u) = val "user.update: " +++ toByteString (eupId u) - bytes (UserIdentityUpdated u) = val "user.update: " +++ toByteString (eiuId u) - bytes (UserIdentityRemoved u) = val "user.identity-remove: " +++ toByteString (eirId u) - bytes (UserSuspended u) = val "user.suspend: " +++ toByteString u - bytes (UserResumed u) = val "user.resume: " +++ toByteString u - bytes (UserDeleted u) = val "user.delete: " +++ toByteString (qUnqualified u) +++ val "@" +++ toByteString (qDomain u) - bytes (UserLegalHoldDisabled u) = val "user.legalhold-disable: " +++ toByteString u - bytes (UserLegalHoldEnabled u) = val "user.legalhold-enable: " +++ toByteString u - bytes (LegalHoldClientRequested payload) = val "user.legalhold-request: " +++ show payload - -instance ToBytes ConnectionEvent where - bytes e@ConnectionUpdated {} = val "user.connection: " +++ toByteString (connEventUserId e) - -instance ToBytes PropertyEvent where - bytes e@PropertySet {} = val "user.properties-set: " +++ toByteString (propEventUserId e) - bytes e@PropertyDeleted {} = val "user.properties-delete: " +++ toByteString (propEventUserId e) - bytes e@PropertiesCleared {} = val "user.properties-clear: " +++ toByteString (propEventUserId e) - -instance ToBytes ClientEvent where - bytes (ClientAdded u _) = val "user.client-add: " +++ toByteString u - bytes (ClientRemoved u _) = val "user.client-remove: " +++ toByteString u diff --git a/libs/types-common/src/Data/Id.hs b/libs/types-common/src/Data/Id.hs index c707a4ea02d..2f57ba3d920 100644 --- a/libs/types-common/src/Data/Id.hs +++ b/libs/types-common/src/Data/Id.hs @@ -38,6 +38,7 @@ module Data.Id ScimTokenId, parseIdFromText, idToText, + idObjectSchema, IdObject (..), -- * Client IDs @@ -444,7 +445,7 @@ newtype IdObject a = IdObject {fromIdObject :: a} deriving (ToJSON, FromJSON, S.ToSchema) via Schema (IdObject a) instance ToSchema a => ToSchema (IdObject a) where - schema = - object "Id" $ - IdObject - <$> fromIdObject .= field "id" schema + schema = idObjectSchema (IdObject <$> fromIdObject .= schema) + +idObjectSchema :: ValueSchemaP NamedSwaggerDoc a b -> ValueSchemaP NamedSwaggerDoc a b +idObjectSchema sch = object "Id" (field "id" sch) diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index 87dc1bf9b84..581ac8b9612 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -93,6 +93,7 @@ , tasty-quickcheck , text , time +, tinylog , transitive-anns , types-common , unliftio @@ -190,6 +191,7 @@ mkDerivation { tagged text time + tinylog transitive-anns types-common unordered-containers diff --git a/libs/wire-api/src/Wire/API/UserEvent.hs b/libs/wire-api/src/Wire/API/UserEvent.hs new file mode 100644 index 00000000000..6ed42e3690c --- /dev/null +++ b/libs/wire-api/src/Wire/API/UserEvent.hs @@ -0,0 +1,451 @@ +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} + +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Wire.API.UserEvent where + +import Control.Lens.TH +import Data.Aeson qualified as A +import Data.Aeson.KeyMap qualified as KM +import Data.ByteString.Conversion +import Data.Handle (Handle) +import Data.Id +import Data.Json.Util +import Data.Qualified +import Data.Schema +import Imports +import System.Logger.Message hiding (field, (.=)) +import Wire.API.Connection +import Wire.API.Properties +import Wire.API.User +import Wire.API.User.Client +import Wire.API.User.Client.Prekey + +data Event + = UserEvent !UserEvent + | ConnectionEvent !ConnectionEvent + | PropertyEvent !PropertyEvent + | ClientEvent !ClientEvent + deriving stock (Eq, Show) + +eventType :: Event -> EventType +eventType (UserEvent (UserCreated _)) = EventTypeUserCreated +eventType (UserEvent (UserActivated _)) = EventTypeUserActivated +eventType (UserEvent (UserSuspended _)) = EventTypeUserSuspended +eventType (UserEvent (UserResumed _)) = EventTypeUserResumed +eventType (UserEvent (UserDeleted _)) = EventTypeUserDeleted +eventType (UserEvent (UserUpdated _)) = EventTypeUserUpdated +eventType (UserEvent (UserIdentityUpdated _)) = EventTypeUserUpdated +eventType (UserEvent (UserIdentityRemoved _)) = EventTypeUserIdentityRemoved +eventType (UserEvent (UserLegalHoldDisabled _)) = EventTypeUserLegalholdDisabled +eventType (UserEvent (UserLegalHoldEnabled _)) = EventTypeUserLegalholdEnabled +eventType (UserEvent (LegalHoldClientRequested _)) = EventTypeUserLegalholdRequested +eventType (ConnectionEvent _) = EventTypeConnection +eventType (PropertyEvent (PropertySet _ _)) = EventTypePropertiesSet +eventType (PropertyEvent (PropertyDeleted _)) = EventTypePropertiesDeleted +eventType (PropertyEvent PropertiesCleared) = EventTypePropertiesCleared +eventType (ClientEvent (ClientAdded _)) = EventTypeClientAdded +eventType (ClientEvent (ClientRemoved _)) = EventTypeClientRemoved + +data EventType + = EventTypeUserCreated + | EventTypeUserActivated + | EventTypeUserUpdated + | EventTypeUserIdentityRemoved + | EventTypeUserSuspended + | EventTypeUserResumed + | EventTypeUserDeleted + | EventTypeUserLegalholdEnabled + | EventTypeUserLegalholdDisabled + | EventTypeUserLegalholdRequested + | EventTypePropertiesSet + | EventTypePropertiesDeleted + | EventTypePropertiesCleared + | EventTypeClientAdded + | EventTypeClientRemoved + | EventTypeConnection + deriving stock (Eq, Enum, Bounded) + +instance ToSchema EventType where + schema = + enum @Text "EventType" $ + mconcat + [ element "user.new" EventTypeUserCreated, + element "user.activate" EventTypeUserActivated, + element "user.update" EventTypeUserUpdated, + element "user.identity-remove" EventTypeUserIdentityRemoved, + element "user.suspend" EventTypeUserSuspended, + element "user.resume" EventTypeUserResumed, + element "user.delete" EventTypeUserDeleted, + element "user.legalhold-enable" EventTypeUserLegalholdEnabled, + element "user.legalhold-disable" EventTypeUserLegalholdDisabled, + element "user.legalhold-request" EventTypeUserLegalholdRequested, + element "user.properties-set" EventTypePropertiesSet, + element "user.properties-delete" EventTypePropertiesDeleted, + element "user.properties-clear" EventTypePropertiesCleared, + element "user.client-add" EventTypeClientAdded, + element "user.client-remove" EventTypeClientRemoved, + element "user.connection" EventTypeConnection + ] + +data UserEvent + = UserCreated !User + | -- | A user is activated when the first user identity (email address or phone number) + -- is verified. {#RefActivationEvent} + UserActivated !User + | -- | Account & API access of a user has been suspended. + UserSuspended !UserId + | -- | Account & API access of a previously suspended user + -- has been restored. + UserResumed !UserId + | -- | The user account has been deleted. + UserDeleted !(Qualified UserId) + | UserUpdated !UserUpdatedData + | UserIdentityUpdated !UserIdentityUpdatedData + | UserIdentityRemoved !UserIdentityRemovedData + | UserLegalHoldDisabled !UserId + | UserLegalHoldEnabled !UserId + | LegalHoldClientRequested LegalHoldClientRequestedData + deriving stock (Eq, Show) + +data ConnectionEvent = ConnectionUpdated + { ucConn :: !UserConnection, + ucName :: !(Maybe Name) + } + deriving stock (Eq, Show) + +data PropertyEvent + = PropertySet !PropertyKey !A.Value + | PropertyDeleted !PropertyKey + | PropertiesCleared + deriving stock (Eq, Show) + +data ClientEvent + = ClientAdded !Client + | ClientRemoved !ClientId + deriving stock (Eq, Show) + +data UserUpdatedData = UserUpdatedData + { eupId :: !UserId, + eupName :: !(Maybe Name), + -- | DEPRECATED + eupPict :: !(Maybe Pict), + eupAccentId :: !(Maybe ColourId), + eupAssets :: !(Maybe [Asset]), + eupHandle :: !(Maybe Handle), + eupLocale :: !(Maybe Locale), + eupManagedBy :: !(Maybe ManagedBy), + eupSSOId :: !(Maybe UserSSOId), + eupSSOIdRemoved :: Bool, + eupSupportedProtocols :: !(Maybe (Set BaseProtocolTag)) + } + deriving stock (Eq, Show) + +data UserIdentityUpdatedData = UserIdentityUpdatedData + { eiuId :: !UserId, + eiuEmail :: !(Maybe Email), + eiuPhone :: !(Maybe Phone) + } + deriving stock (Eq, Show) + +data UserIdentityRemovedData = UserIdentityRemovedData + { eirId :: !UserId, + eirEmail :: !(Maybe Email), + eirPhone :: !(Maybe Phone) + } + deriving stock (Eq, Show) + +data LegalHoldClientRequestedData = LegalHoldClientRequestedData + { -- | the user that is under legalhold + lhcTargetUser :: !UserId, + -- | the last prekey of the user that is under legalhold + lhcLastPrekey :: !LastPrekey, + -- | the client id of the legalhold device + lhcClientId :: !ClientId + } + deriving stock (Eq, Show) + +emailRemoved :: UserId -> Email -> UserEvent +emailRemoved u e = + UserIdentityRemoved $ UserIdentityRemovedData u (Just e) Nothing + +phoneRemoved :: UserId -> Phone -> UserEvent +phoneRemoved u p = + UserIdentityRemoved $ UserIdentityRemovedData u Nothing (Just p) + +emailUpdated :: UserId -> Email -> UserEvent +emailUpdated u e = + UserIdentityUpdated $ UserIdentityUpdatedData u (Just e) Nothing + +phoneUpdated :: UserId -> Phone -> UserEvent +phoneUpdated u p = + UserIdentityUpdated $ UserIdentityUpdatedData u Nothing (Just p) + +handleUpdated :: UserId -> Handle -> UserEvent +handleUpdated u h = + UserUpdated $ (emptyUserUpdatedData u) {eupHandle = Just h} + +localeUpdate :: UserId -> Locale -> UserEvent +localeUpdate u loc = + UserUpdated $ (emptyUserUpdatedData u) {eupLocale = Just loc} + +managedByUpdate :: UserId -> ManagedBy -> UserEvent +managedByUpdate u mb = + UserUpdated $ (emptyUserUpdatedData u) {eupManagedBy = Just mb} + +supportedProtocolUpdate :: UserId -> Set BaseProtocolTag -> UserEvent +supportedProtocolUpdate u prots = + UserUpdated $ (emptyUserUpdatedData u) {eupSupportedProtocols = Just prots} + +profileUpdated :: UserId -> UserUpdate -> UserEvent +profileUpdated u UserUpdate {..} = + UserUpdated $ + (emptyUserUpdatedData u) + { eupName = uupName, + eupPict = uupPict, + eupAccentId = uupAccentId, + eupAssets = uupAssets + } + +emptyUpdate :: UserId -> UserEvent +emptyUpdate = UserUpdated . emptyUserUpdatedData + +emptyUserUpdatedData :: UserId -> UserUpdatedData +emptyUserUpdatedData u = + UserUpdatedData + { eupId = u, + eupName = Nothing, + eupPict = Nothing, + eupAccentId = Nothing, + eupAssets = Nothing, + eupHandle = Nothing, + eupLocale = Nothing, + eupManagedBy = Nothing, + eupSSOId = Nothing, + eupSSOIdRemoved = False, + eupSupportedProtocols = Nothing + } + +-- Event schema + +$(makePrisms ''Event) +$(makePrisms ''UserEvent) +$(makePrisms ''PropertyEvent) +$(makePrisms ''ClientEvent) + +eventObjectSchema :: ObjectSchema SwaggerDoc Event +eventObjectSchema = + snd + <$> bind + (eventType .= field "type" schema) + ( dispatch $ \case + EventTypeUserCreated -> + tag _UserEvent (tag _UserCreated (noId .= userSchema)) + EventTypeUserActivated -> + tag _UserEvent (tag _UserActivated userSchema) + EventTypeUserUpdated -> + tag + _UserEvent + ( tag + _UserUpdated + ( field + "user" + ( object + "UserUpdatedData" + ( UserUpdatedData + <$> eupId .= field "id" schema + <*> eupName .= maybe_ (optField "name" schema) + <*> eupPict .= maybe_ (optField "picture" schema) -- DEPRECATED + <*> eupAccentId .= maybe_ (optField "accent_id" schema) + <*> eupAssets .= maybe_ (optField "assets" (array schema)) + <*> eupHandle .= maybe_ (optField "handle" schema) + <*> eupLocale .= maybe_ (optField "locale" schema) + <*> eupManagedBy .= maybe_ (optField "managed_by" schema) + <*> eupSSOId .= maybe_ (optField "sso_id" genericToSchema) + <*> eupSSOIdRemoved .= field "sso_id_deleted" schema + <*> eupSupportedProtocols + .= maybe_ + ( optField + "supported_protocols" + (set schema) + ) + ) + ) + ) + <|> tag + _UserIdentityUpdated + ( field + "user" + ( object + "UserIdentityUpdatedData" + ( UserIdentityUpdatedData + <$> eiuId .= field "id" schema + <*> eiuEmail .= maybe_ (optField "email" schema) + <*> eiuPhone .= maybe_ (optField "phone" schema) + ) + ) + ) + ) + EventTypeUserIdentityRemoved -> + tag + _UserEvent + ( tag + _UserIdentityRemoved + ( field + "user" + ( object + "UserIdentityRemovedData" + ( UserIdentityRemovedData + <$> eirId .= field "id" schema + <*> eirEmail .= maybe_ (optField "email" schema) + <*> eirPhone .= maybe_ (optField "phone" schema) + ) + ) + ) + ) + EventTypeUserSuspended -> tag _UserEvent (tag _UserSuspended (field "id" schema)) + EventTypeUserResumed -> tag _UserEvent (tag _UserResumed (field "id" schema)) + EventTypeUserDeleted -> + tag + _UserEvent + ( tag + _UserDeleted + ( field "qualified_id" schema + <* qUnqualified .= field "id" schema + ) + ) + EventTypeUserLegalholdEnabled -> + tag + _UserEvent + ( tag _UserLegalHoldEnabled (field "id" schema) + ) + EventTypeUserLegalholdDisabled -> + tag + _UserEvent + ( tag _UserLegalHoldDisabled (field "id" schema) + ) + EventTypeUserLegalholdRequested -> + tag + _UserEvent + ( tag + _LegalHoldClientRequested + ( LegalHoldClientRequestedData + <$> lhcTargetUser .= field "id" schema + <*> lhcLastPrekey .= field "last_prekey" schema + <*> lhcClientId .= field "client" (idObjectSchema schema) + ) + ) + EventTypePropertiesSet -> + tag + _PropertyEvent + ( tag + _PropertySet + ( (,) + <$> fst .= field "key" genericToSchema + <*> snd .= field "value" jsonValue + ) + ) + EventTypePropertiesDeleted -> + tag + _PropertyEvent + ( tag + _PropertyDeleted + (field "key" genericToSchema) + ) + EventTypePropertiesCleared -> + tag + _PropertyEvent + ( tag + _PropertiesCleared + (pure ()) + ) + EventTypeClientAdded -> + tag + _ClientEvent + ( tag + _ClientAdded + (field "client" schema) + ) + EventTypeClientRemoved -> + tag + _ClientEvent + ( tag + _ClientRemoved + (field "client" (idObjectSchema schema)) + ) + EventTypeConnection -> + tag + _ConnectionEvent + ( ConnectionUpdated + <$> ucConn .= field "connection" schema + <*> ucName .= maybe_ (optField "user" (object "UserName" (field "name" schema))) + ) + ) + where + noId :: User -> User + noId u = u {userIdentity = Nothing} + + userSchema :: ObjectSchema SwaggerDoc User + userSchema = field "user" schema + +instance ToJSONObject Event where + toJSONObject = KM.fromList . fold . schemaOut eventObjectSchema + +instance ToSchema Event where + schema = object "UserEvent" eventObjectSchema + +deriving via (Schema Event) instance A.ToJSON Event + +deriving via (Schema Event) instance A.FromJSON Event + +-- Logging + +connEventUserId :: ConnectionEvent -> UserId +connEventUserId ConnectionUpdated {..} = ucFrom ucConn + +instance ToBytes Event where + bytes (UserEvent e) = bytes e + bytes (ConnectionEvent e) = bytes e + bytes (PropertyEvent e) = bytes e + bytes (ClientEvent e) = bytes e + +instance ToBytes UserEvent where + bytes (UserCreated u) = val "user.new: " +++ toByteString (userId u) + bytes (UserActivated u) = val "user.activate: " +++ toByteString (userId u) + bytes (UserUpdated u) = val "user.update: " +++ toByteString (eupId u) + bytes (UserIdentityUpdated u) = val "user.update: " +++ toByteString (eiuId u) + bytes (UserIdentityRemoved u) = val "user.identity-remove: " +++ toByteString (eirId u) + bytes (UserSuspended u) = val "user.suspend: " +++ toByteString u + bytes (UserResumed u) = val "user.resume: " +++ toByteString u + bytes (UserDeleted u) = val "user.delete: " +++ toByteString (qUnqualified u) +++ val "@" +++ toByteString (qDomain u) + bytes (UserLegalHoldDisabled u) = val "user.legalhold-disable: " +++ toByteString u + bytes (UserLegalHoldEnabled u) = val "user.legalhold-enable: " +++ toByteString u + bytes (LegalHoldClientRequested payload) = val "user.legalhold-request: " +++ show payload + +instance ToBytes ConnectionEvent where + bytes e@ConnectionUpdated {} = val "user.connection: " +++ toByteString (connEventUserId e) + +instance ToBytes PropertyEvent where + bytes PropertySet {} = val "user.properties-set" + bytes PropertyDeleted {} = val "user.properties-delete" + bytes PropertiesCleared {} = val "user.properties-clear" + +instance ToBytes ClientEvent where + bytes (ClientAdded _) = val "user.client-add" + bytes (ClientRemoved _) = val "user.client-remove" diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs index ed40d1157bd..40e7f101a28 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs @@ -44,6 +44,7 @@ import Test.Wire.API.Golden.Manual.SubConversation import Test.Wire.API.Golden.Manual.TeamSize import Test.Wire.API.Golden.Manual.Token import Test.Wire.API.Golden.Manual.UserClientPrekeyMap +import Test.Wire.API.Golden.Manual.UserEvent import Test.Wire.API.Golden.Manual.UserIdList import Test.Wire.API.Golden.Runner import Wire.API.Routes.Version @@ -197,5 +198,25 @@ tests = [ (testObject_ConversationRemoveMembers_1, "testObject_ConversationRemoveMembers_1.json"), (testObject_ConversationRemoveMembers_2, "testObject_ConversationRemoveMembers_2.json"), (testObject_ConversationRemoveMembers_3, "testObject_ConversationRemoveMembers_3.json") + ], + testGroup "UserEvent" $ + testObjects + [ (testObject_UserEvent_1, "testObject_UserEvent_1.json"), + (testObject_UserEvent_2, "testObject_UserEvent_2.json"), + (testObject_UserEvent_3, "testObject_UserEvent_3.json"), + (testObject_UserEvent_4, "testObject_UserEvent_4.json"), + (testObject_UserEvent_5, "testObject_UserEvent_5.json"), + (testObject_UserEvent_6, "testObject_UserEvent_6.json"), + (testObject_UserEvent_7, "testObject_UserEvent_7.json"), + (testObject_UserEvent_8, "testObject_UserEvent_8.json"), + (testObject_UserEvent_9, "testObject_UserEvent_9.json"), + (testObject_UserEvent_10, "testObject_UserEvent_10.json"), + (testObject_UserEvent_11, "testObject_UserEvent_11.json"), + (testObject_UserEvent_12, "testObject_UserEvent_12.json"), + (testObject_UserEvent_13, "testObject_UserEvent_13.json"), + (testObject_UserEvent_14, "testObject_UserEvent_14.json"), + (testObject_UserEvent_15, "testObject_UserEvent_15.json"), + (testObject_UserEvent_16, "testObject_UserEvent_16.json"), + (testObject_UserEvent_17, "testObject_UserEvent_17.json") ] ] diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs new file mode 100644 index 00000000000..3c063f6fde5 --- /dev/null +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs @@ -0,0 +1,249 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2024 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Test.Wire.API.Golden.Manual.UserEvent + ( testObject_UserEvent_1, + testObject_UserEvent_2, + testObject_UserEvent_3, + testObject_UserEvent_4, + testObject_UserEvent_5, + testObject_UserEvent_6, + testObject_UserEvent_7, + testObject_UserEvent_8, + testObject_UserEvent_9, + testObject_UserEvent_10, + testObject_UserEvent_11, + testObject_UserEvent_12, + testObject_UserEvent_13, + testObject_UserEvent_14, + testObject_UserEvent_15, + testObject_UserEvent_16, + testObject_UserEvent_17, + ) +where + +import Data.Aeson (toJSON) +import Data.Domain +import Data.ISO3166_CountryCodes +import Data.Id +import Data.Json.Util +import Data.LanguageCodes as L +import Data.Qualified +import Data.UUID qualified as UUID (fromString) +import Imports +import Wire.API.Connection +import Wire.API.Properties +import Wire.API.User +import Wire.API.User.Client +import Wire.API.User.Client.Prekey +import Wire.API.UserEvent + +testObject_UserEvent_1 :: Event +testObject_UserEvent_1 = UserEvent (UserCreated alice) + +testObject_UserEvent_2 :: Event +testObject_UserEvent_2 = UserEvent (UserActivated alice) + +testObject_UserEvent_3 :: Event +testObject_UserEvent_3 = + UserEvent + ( UserSuspended + (Id (fromJust (UUID.fromString "dd56271c-181a-43f5-874b-1a8951f7fcc7"))) + ) + +testObject_UserEvent_4 :: Event +testObject_UserEvent_4 = + UserEvent + ( UserSuspended + (Id (fromJust (UUID.fromString "3ddb960e-8ea3-4d14-95bc-97f9da795ca6"))) + ) + +testObject_UserEvent_5 :: Event +testObject_UserEvent_5 = + UserEvent + ( UserDeleted + ( Qualified + (Id (fromJust (UUID.fromString "78f9ba2e-a6b0-48c6-a644-662617bb8bcc"))) + (Domain "bar.example.com") + ) + ) + +testObject_UserEvent_6 :: Event +testObject_UserEvent_6 = + UserEvent + ( UserUpdated + ( UserUpdatedData + alice.userId + (Just alice.userDisplayName) + (Just alice.userPict) + (Just alice.userAccentId) + (Just alice.userAssets) + alice.userHandle + (Just alice.userLocale) + (Just alice.userManagedBy) + Nothing + False + (Just mempty) + ) + ) + +testObject_UserEvent_7 :: Event +testObject_UserEvent_7 = + UserEvent + ( UserIdentityUpdated + ( UserIdentityUpdatedData + alice.userId + (Just (Email "alice" "foo.example.com")) + Nothing + ) + ) + +testObject_UserEvent_8 :: Event +testObject_UserEvent_8 = + UserEvent + ( UserIdentityRemoved + ( UserIdentityRemovedData + alice.userId + (Just (Email "alice" "foo.example.com")) + Nothing + ) + ) + +testObject_UserEvent_9 :: Event +testObject_UserEvent_9 = UserEvent (UserLegalHoldDisabled alice.userId) + +testObject_UserEvent_10 :: Event +testObject_UserEvent_10 = UserEvent (UserLegalHoldEnabled alice.userId) + +testObject_UserEvent_11 :: Event +testObject_UserEvent_11 = + UserEvent + ( LegalHoldClientRequested + ( LegalHoldClientRequestedData + alice.userId + (lastPrekey "foo") + (ClientId 3728) + ) + ) + +testObject_UserEvent_12 :: Event +testObject_UserEvent_12 = + ConnectionEvent + ( ConnectionUpdated + ( UserConnection + bob.userId + bob.userQualifiedId + Accepted + (fromJust (readUTCTimeMillis "2007-02-03T10:51:17.329Z")) + Nothing + ) + (Just (Name "hi bob")) + ) + +testObject_UserEvent_13 :: Event +testObject_UserEvent_13 = + PropertyEvent + ( PropertySet (PropertyKey "a") (toJSON (39 :: Int)) + ) + +testObject_UserEvent_14 :: Event +testObject_UserEvent_14 = + PropertyEvent + ( PropertyDeleted (PropertyKey "a") + ) + +testObject_UserEvent_15 :: Event +testObject_UserEvent_15 = PropertyEvent PropertiesCleared + +testObject_UserEvent_16 :: Event +testObject_UserEvent_16 = + ClientEvent + ( ClientAdded + ( Client + (ClientId 2839) + PermanentClientType + (fromJust (readUTCTimeMillis "2007-02-03T10:51:17.329Z")) + (Just DesktopClient) + (Just "%*") + Nothing + (Just "bazz") + (ClientCapabilityList mempty) + mempty + Nothing + ) + ) + +testObject_UserEvent_17 :: Event +testObject_UserEvent_17 = ClientEvent (ClientRemoved (ClientId 2839)) + +-------------------------------------------------------------------------------- + +alice :: User +alice = + User + { userId = Id (fromJust (UUID.fromString "539d9183-32a5-4fc4-ba5c-4634454e7585")), + userQualifiedId = + Qualified + { qUnqualified = Id (fromJust (UUID.fromString "539d9183-32a5-4fc4-ba5c-4634454e7585")), + qDomain = Domain {_domainText = "foo.example.com"} + }, + userIdentity = Nothing, + userDisplayName = Name "alice", + userPict = Pict {fromPict = []}, + userAssets = [], + userAccentId = ColourId {fromColourId = 1}, + userDeleted = True, + userLocale = + Locale + { lLanguage = Language L.TN, + lCountry = Just (Country {fromCountry = SB}) + }, + userService = Nothing, + userHandle = Nothing, + userExpire = Nothing, + userTeam = Nothing, + userManagedBy = ManagedByWire, + userSupportedProtocols = defSupportedProtocols + } + +bob :: User +bob = + User + { userId = Id (fromJust (UUID.fromString "284d1c86-5117-4c58-aa18-c0068f3f7d8c")), + userQualifiedId = + Qualified + { qUnqualified = Id (fromJust (UUID.fromString "284d1c86-5117-4c58-aa18-c0068f3f7d8c")), + qDomain = Domain {_domainText = "baz.example.com"} + }, + userIdentity = Nothing, + userDisplayName = Name "bob", + userPict = Pict {fromPict = []}, + userAssets = [], + userAccentId = ColourId {fromColourId = 2}, + userDeleted = False, + userLocale = + Locale + { lLanguage = Language L.CA, + lCountry = Just (Country {fromCountry = JP}) + }, + userService = Nothing, + userHandle = Nothing, + userExpire = Nothing, + userTeam = Nothing, + userManagedBy = ManagedByWire, + userSupportedProtocols = defSupportedProtocols + } diff --git a/libs/wire-api/test/golden/testObject_UserEvent_1.json b/libs/wire-api/test/golden/testObject_UserEvent_1.json new file mode 100644 index 00000000000..09940cd4e9e --- /dev/null +++ b/libs/wire-api/test/golden/testObject_UserEvent_1.json @@ -0,0 +1,20 @@ +{ + "type": "user.new", + "user": { + "accent_id": 1, + "assets": [], + "deleted": true, + "id": "539d9183-32a5-4fc4-ba5c-4634454e7585", + "locale": "tn-SB", + "managed_by": "wire", + "name": "alice", + "picture": [], + "qualified_id": { + "domain": "foo.example.com", + "id": "539d9183-32a5-4fc4-ba5c-4634454e7585" + }, + "supported_protocols": [ + "proteus" + ] + } +} diff --git a/libs/wire-api/test/golden/testObject_UserEvent_10.json b/libs/wire-api/test/golden/testObject_UserEvent_10.json new file mode 100644 index 00000000000..c8aceb979de --- /dev/null +++ b/libs/wire-api/test/golden/testObject_UserEvent_10.json @@ -0,0 +1,4 @@ +{ + "id": "539d9183-32a5-4fc4-ba5c-4634454e7585", + "type": "user.legalhold-enable" +} diff --git a/libs/wire-api/test/golden/testObject_UserEvent_11.json b/libs/wire-api/test/golden/testObject_UserEvent_11.json new file mode 100644 index 00000000000..3a391a8781b --- /dev/null +++ b/libs/wire-api/test/golden/testObject_UserEvent_11.json @@ -0,0 +1,11 @@ +{ + "client": { + "id": "e90" + }, + "id": "539d9183-32a5-4fc4-ba5c-4634454e7585", + "last_prekey": { + "id": 65535, + "key": "foo" + }, + "type": "user.legalhold-request" +} diff --git a/libs/wire-api/test/golden/testObject_UserEvent_12.json b/libs/wire-api/test/golden/testObject_UserEvent_12.json new file mode 100644 index 00000000000..f342079b4ea --- /dev/null +++ b/libs/wire-api/test/golden/testObject_UserEvent_12.json @@ -0,0 +1,16 @@ +{ + "connection": { + "from": "284d1c86-5117-4c58-aa18-c0068f3f7d8c", + "last_update": "2007-02-03T10:51:17.329Z", + "qualified_to": { + "domain": "baz.example.com", + "id": "284d1c86-5117-4c58-aa18-c0068f3f7d8c" + }, + "status": "accepted", + "to": "284d1c86-5117-4c58-aa18-c0068f3f7d8c" + }, + "type": "user.connection", + "user": { + "name": "hi bob" + } +} diff --git a/libs/wire-api/test/golden/testObject_UserEvent_13.json b/libs/wire-api/test/golden/testObject_UserEvent_13.json new file mode 100644 index 00000000000..25a0a1dd05b --- /dev/null +++ b/libs/wire-api/test/golden/testObject_UserEvent_13.json @@ -0,0 +1,5 @@ +{ + "key": "a", + "type": "user.properties-set", + "value": 39 +} diff --git a/libs/wire-api/test/golden/testObject_UserEvent_14.json b/libs/wire-api/test/golden/testObject_UserEvent_14.json new file mode 100644 index 00000000000..0b359af6e4b --- /dev/null +++ b/libs/wire-api/test/golden/testObject_UserEvent_14.json @@ -0,0 +1,4 @@ +{ + "key": "a", + "type": "user.properties-delete" +} diff --git a/libs/wire-api/test/golden/testObject_UserEvent_15.json b/libs/wire-api/test/golden/testObject_UserEvent_15.json new file mode 100644 index 00000000000..529b84e9598 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_UserEvent_15.json @@ -0,0 +1,3 @@ +{ + "type": "user.properties-clear" +} diff --git a/libs/wire-api/test/golden/testObject_UserEvent_16.json b/libs/wire-api/test/golden/testObject_UserEvent_16.json new file mode 100644 index 00000000000..88168e5d582 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_UserEvent_16.json @@ -0,0 +1,15 @@ +{ + "client": { + "capabilities": { + "capabilities": [] + }, + "class": "desktop", + "id": "b17", + "label": "%*", + "mls_public_keys": {}, + "model": "bazz", + "time": "2007-02-03T10:51:17.329Z", + "type": "permanent" + }, + "type": "user.client-add" +} diff --git a/libs/wire-api/test/golden/testObject_UserEvent_17.json b/libs/wire-api/test/golden/testObject_UserEvent_17.json new file mode 100644 index 00000000000..9ddeee4ce50 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_UserEvent_17.json @@ -0,0 +1,6 @@ +{ + "client": { + "id": "b17" + }, + "type": "user.client-remove" +} diff --git a/libs/wire-api/test/golden/testObject_UserEvent_2.json b/libs/wire-api/test/golden/testObject_UserEvent_2.json new file mode 100644 index 00000000000..36ec06060ef --- /dev/null +++ b/libs/wire-api/test/golden/testObject_UserEvent_2.json @@ -0,0 +1,20 @@ +{ + "type": "user.activate", + "user": { + "accent_id": 1, + "assets": [], + "deleted": true, + "id": "539d9183-32a5-4fc4-ba5c-4634454e7585", + "locale": "tn-SB", + "managed_by": "wire", + "name": "alice", + "picture": [], + "qualified_id": { + "domain": "foo.example.com", + "id": "539d9183-32a5-4fc4-ba5c-4634454e7585" + }, + "supported_protocols": [ + "proteus" + ] + } +} diff --git a/libs/wire-api/test/golden/testObject_UserEvent_3.json b/libs/wire-api/test/golden/testObject_UserEvent_3.json new file mode 100644 index 00000000000..2dfeaa90444 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_UserEvent_3.json @@ -0,0 +1,4 @@ +{ + "id": "dd56271c-181a-43f5-874b-1a8951f7fcc7", + "type": "user.suspend" +} diff --git a/libs/wire-api/test/golden/testObject_UserEvent_4.json b/libs/wire-api/test/golden/testObject_UserEvent_4.json new file mode 100644 index 00000000000..ab6fc97882e --- /dev/null +++ b/libs/wire-api/test/golden/testObject_UserEvent_4.json @@ -0,0 +1,4 @@ +{ + "id": "3ddb960e-8ea3-4d14-95bc-97f9da795ca6", + "type": "user.suspend" +} diff --git a/libs/wire-api/test/golden/testObject_UserEvent_5.json b/libs/wire-api/test/golden/testObject_UserEvent_5.json new file mode 100644 index 00000000000..34d8c0d81a1 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_UserEvent_5.json @@ -0,0 +1,8 @@ +{ + "id": "78f9ba2e-a6b0-48c6-a644-662617bb8bcc", + "qualified_id": { + "domain": "bar.example.com", + "id": "78f9ba2e-a6b0-48c6-a644-662617bb8bcc" + }, + "type": "user.delete" +} diff --git a/libs/wire-api/test/golden/testObject_UserEvent_6.json b/libs/wire-api/test/golden/testObject_UserEvent_6.json new file mode 100644 index 00000000000..328b2cb2193 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_UserEvent_6.json @@ -0,0 +1,14 @@ +{ + "type": "user.update", + "user": { + "accent_id": 1, + "assets": [], + "id": "539d9183-32a5-4fc4-ba5c-4634454e7585", + "locale": "tn-SB", + "managed_by": "wire", + "name": "alice", + "picture": [], + "sso_id_deleted": false, + "supported_protocols": [] + } +} diff --git a/libs/wire-api/test/golden/testObject_UserEvent_7.json b/libs/wire-api/test/golden/testObject_UserEvent_7.json new file mode 100644 index 00000000000..71c1b5a163a --- /dev/null +++ b/libs/wire-api/test/golden/testObject_UserEvent_7.json @@ -0,0 +1,7 @@ +{ + "type": "user.update", + "user": { + "email": "alice@foo.example.com", + "id": "539d9183-32a5-4fc4-ba5c-4634454e7585" + } +} diff --git a/libs/wire-api/test/golden/testObject_UserEvent_8.json b/libs/wire-api/test/golden/testObject_UserEvent_8.json new file mode 100644 index 00000000000..9998a51fdbc --- /dev/null +++ b/libs/wire-api/test/golden/testObject_UserEvent_8.json @@ -0,0 +1,7 @@ +{ + "type": "user.identity-remove", + "user": { + "email": "alice@foo.example.com", + "id": "539d9183-32a5-4fc4-ba5c-4634454e7585" + } +} diff --git a/libs/wire-api/test/golden/testObject_UserEvent_9.json b/libs/wire-api/test/golden/testObject_UserEvent_9.json new file mode 100644 index 00000000000..fc025dd9ad6 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_UserEvent_9.json @@ -0,0 +1,4 @@ +{ + "id": "539d9183-32a5-4fc4-ba5c-4634454e7585", + "type": "user.legalhold-disable" +} diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index b191dbfbdec..81859cef566 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -232,6 +232,7 @@ library Wire.API.User.Saml Wire.API.User.Scim Wire.API.User.Search + Wire.API.UserEvent Wire.API.UserMap Wire.API.Util.Aeson Wire.API.VersionInfo @@ -316,6 +317,7 @@ library , tagged , text >=0.11 , time >=1.4 + , tinylog , transitive-anns , types-common >=0.16 , unordered-containers >=0.2 @@ -587,6 +589,7 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Manual.TeamSize Test.Wire.API.Golden.Manual.Token Test.Wire.API.Golden.Manual.UserClientPrekeyMap + Test.Wire.API.Golden.Manual.UserEvent Test.Wire.API.Golden.Manual.UserIdList Test.Wire.API.Golden.Protobuf Test.Wire.API.Golden.Run diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 6158288d854..4e0c272153d 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -149,6 +149,7 @@ library Brig.InternalEvent.Types Brig.IO.Intra Brig.IO.Journal + Brig.IO.Logging Brig.Locale Brig.Options Brig.Phone diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 9eeda792e29..17071f56ef3 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -69,7 +69,6 @@ import Brig.Options qualified as Opt import Brig.Queue qualified as Queue import Brig.Types.Intra import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) -import Brig.Types.User.Event import Brig.User.Auth qualified as UserAuth import Brig.User.Auth.Cookie qualified as Auth import Brig.User.Email @@ -108,6 +107,7 @@ import Wire.API.User qualified as Code import Wire.API.User.Client import Wire.API.User.Client.DPoPAccessToken import Wire.API.User.Client.Prekey +import Wire.API.UserEvent import Wire.API.UserMap (QualifiedUserMap (QualifiedUserMap, qualifiedUserMap), UserMap (userMap)) import Wire.NotificationSubsystem import Wire.Sem.Concurrency @@ -211,7 +211,7 @@ addClientWithReAuthPolicy policy u con new = do lift $ do for_ old $ execDelete u con liftSem $ GalleyProvider.newClient u (clientId clt) - liftSem $ Intra.onClientEvent u con (ClientAdded u clt) + liftSem $ Intra.onClientEvent u con (ClientAdded clt) when (clientType clt == LegalHoldClientType) $ liftSem $ Intra.onUserEvent u con (UserLegalHoldEnabled u) when (count > 1) $ for_ (userEmail usr) $ diff --git a/services/brig/src/Brig/API/Connection.hs b/services/brig/src/Brig/API/Connection.hs index b7ba931541f..c5b8de1c4c2 100644 --- a/services/brig/src/Brig/API/Connection.hs +++ b/services/brig/src/Brig/API/Connection.hs @@ -45,9 +45,9 @@ import Brig.Effects.FederationConfigStore import Brig.Effects.GalleyProvider import Brig.Effects.GalleyProvider qualified as GalleyProvider import Brig.IO.Intra qualified as Intra +import Brig.IO.Logging import Brig.Options import Brig.Types.Connection -import Brig.Types.User.Event import Control.Error import Control.Lens (view) import Control.Monad.Catch (throwM) @@ -69,6 +69,7 @@ import Wire.API.Error import Wire.API.Error.Brig qualified as E import Wire.API.Routes.Public.Util (ResponseForExistedCreated (..)) import Wire.API.User +import Wire.API.UserEvent import Wire.NotificationSubsystem ensureNotSameTeam :: Member GalleyProvider r => Local UserId -> Local UserId -> (ConnectionM r) () @@ -121,10 +122,10 @@ createConnectionToLocalUser self conn target = do Just rs -> rs Nothing -> do checkLimit self - Created <$> insert Nothing Nothing + Created <$> insert where - insert :: Maybe UserConnection -> Maybe UserConnection -> ExceptT ConnectionError (AppT r) UserConnection - insert s2o o2s = lift $ do + insert :: ExceptT ConnectionError (AppT r) UserConnection + insert = lift $ do Log.info $ logConnection (tUnqualified self) (tUntagged target) . msg (val "Creating connection") @@ -132,9 +133,8 @@ createConnectionToLocalUser self conn target = do s2o' <- wrapClient $ Data.insertConnection self (tUntagged target) SentWithHistory qcnv o2s' <- wrapClient $ Data.insertConnection target (tUntagged self) PendingWithHistory qcnv e2o <- - ConnectionUpdated o2s' (ucStatus <$> o2s) - <$> wrapClient (Data.lookupName (tUnqualified self)) - let e2s = ConnectionUpdated s2o' (ucStatus <$> s2o) Nothing + ConnectionUpdated o2s' <$> wrapClient (Data.lookupName (tUnqualified self)) + let e2s = ConnectionUpdated s2o' Nothing liftSem $ mapM_ (Intra.onConnectionEvent (tUnqualified self) (Just conn)) [e2o, e2s] pure s2o' @@ -149,9 +149,9 @@ createConnectionToLocalUser self conn target = do (_, Blocked) -> change s2o SentWithHistory (_, Sent) -> accept s2o o2s (_, Accepted) -> accept s2o o2s - (_, Ignored) -> resend s2o o2s - (_, Pending) -> resend s2o o2s - (_, Cancelled) -> resend s2o o2s + (_, Ignored) -> resend s2o + (_, Pending) -> resend s2o + (_, Cancelled) -> resend s2o accept :: UserConnection -> UserConnection -> ExceptT ConnectionError (AppT r) (ResponseForExistedCreated UserConnection) accept s2o o2s = do @@ -169,21 +169,19 @@ createConnectionToLocalUser self conn target = do else Data.updateConnection o2s AcceptedWithHistory e2o <- lift . wrapClient $ - ConnectionUpdated o2s' (Just $ ucStatus o2s) - <$> Data.lookupName (tUnqualified self) - let e2s = ConnectionUpdated s2o' (Just $ ucStatus s2o) Nothing + ConnectionUpdated o2s' <$> Data.lookupName (tUnqualified self) + let e2s = ConnectionUpdated s2o' Nothing lift $ liftSem $ mapM_ (Intra.onConnectionEvent (tUnqualified self) (Just conn)) [e2o, e2s] pure $ Existed s2o' - resend :: UserConnection -> UserConnection -> ExceptT ConnectionError (AppT r) (ResponseForExistedCreated UserConnection) - resend s2o o2s = do + resend :: UserConnection -> ExceptT ConnectionError (AppT r) (ResponseForExistedCreated UserConnection) + resend s2o = do unless (ucStatus s2o `elem` [Sent, Accepted]) $ checkLimit self lift . Log.info $ logLocalConnection (tUnqualified self) (qUnqualified (ucTo s2o)) . msg (val "Resending connection request") - s2o' <- insert (Just s2o) (Just o2s) - pure $ Existed s2o' + Existed <$> insert change :: UserConnection -> RelationWithHistory -> ExceptT ConnectionError (AppT r) (ResponseForExistedCreated UserConnection) change c s = Existed <$> lift (wrapClient $ Data.updateConnection c s) @@ -305,7 +303,7 @@ updateConnectionToLocalUser self other newStatus conn = do _ -> throwE $ InvalidTransition (tUnqualified self) let s2oUserConn = s2o' lift . liftSem . for_ s2oUserConn $ \c -> - let e2s = ConnectionUpdated c (Just $ ucStatus s2o) Nothing + let e2s = ConnectionUpdated c Nothing in Intra.onConnectionEvent (tUnqualified self) conn e2s pure s2oUserConn where @@ -327,7 +325,7 @@ updateConnectionToLocalUser self other newStatus conn = do then Data.updateConnection o2s AcceptedWithHistory else Data.updateConnection o2s BlockedWithHistory e2o <- - ConnectionUpdated o2s' (Just $ ucStatus o2s) + ConnectionUpdated o2s' <$> wrapClient (Data.lookupName (tUnqualified self)) liftSem $ Intra.onConnectionEvent (tUnqualified self) conn e2o lift . wrapClient $ Just <$> Data.updateConnection s2o AcceptedWithHistory @@ -368,7 +366,7 @@ updateConnectionToLocalUser self other newStatus conn = do else Data.updateConnection o2s BlockedWithHistory e2o :: ConnectionEvent <- wrapClient $ - ConnectionUpdated o2s' (Just $ ucStatus o2s) + ConnectionUpdated o2s' <$> Data.lookupName (tUnqualified self) -- TODO: is this correct? shouldnt o2s be sent to other? liftSem $ Intra.onConnectionEvent (tUnqualified self) conn e2o @@ -382,7 +380,7 @@ updateConnectionToLocalUser self other newStatus conn = do lfrom <- qualifyLocal (ucFrom s2o) lift $ traverse_ (liftSem . Intra.blockConv lfrom) (ucConvId s2o) o2s' <- lift . wrapClient $ Data.updateConnection o2s CancelledWithHistory - let e2o = ConnectionUpdated o2s' (Just $ ucStatus o2s) Nothing + let e2o = ConnectionUpdated o2s' Nothing lift $ liftSem $ Intra.onConnectionEvent (tUnqualified self) conn e2o change s2o Cancelled @@ -454,7 +452,7 @@ updateConnectionInternal = \case lfrom <- qualifyLocal (ucFrom uconn) traverse_ (liftSem . Intra.blockConv lfrom) (ucConvId uconn) uconn' <- wrapClient $ Data.updateConnection uconn (mkRelationWithHistory (ucStatus uconn) MissingLegalholdConsent) - let ev = ConnectionUpdated uconn' (Just $ ucStatus uconn) Nothing + let ev = ConnectionUpdated uconn' Nothing liftSem $ Intra.onConnectionEvent (tUnqualified self) Nothing ev removeLHBlocksInvolving :: Local UserId -> ExceptT ConnectionError (AppT r) () @@ -494,7 +492,6 @@ updateConnectionInternal = \case let connEvent = ConnectionUpdated { ucConn = uconnRev', - ucPrev = Just $ ucStatus uconnRev, ucName = connName } lift $ liftSem $ Intra.onConnectionEvent (ucFrom uconn) Nothing connEvent diff --git a/services/brig/src/Brig/API/Connection/Remote.hs b/services/brig/src/Brig/API/Connection/Remote.hs index 5b6240a09ec..c96ecd24a5b 100644 --- a/services/brig/src/Brig/API/Connection/Remote.hs +++ b/services/brig/src/Brig/API/Connection/Remote.hs @@ -33,7 +33,6 @@ import Brig.Effects.GalleyProvider import Brig.Federation.Client as Federation import Brig.IO.Intra qualified as Intra import Brig.Options -import Brig.Types.User.Event import Control.Comonad import Control.Error.Util ((??)) import Control.Lens (view) @@ -52,6 +51,7 @@ import Wire.API.Federation.API.Brig import Wire.API.Routes.Internal.Galley.ConversationsIntra import Wire.API.Routes.Public.Util (ResponseForExistedCreated (..)) import Wire.API.User +import Wire.API.UserEvent import Wire.NotificationSubsystem data LocalConnectionAction @@ -220,7 +220,7 @@ pushEvent :: UserConnection -> AppT r () pushEvent self mzcon connection = do - let event = ConnectionUpdated connection Nothing Nothing + let event = ConnectionUpdated connection Nothing liftSem $ Intra.onConnectionEvent (tUnqualified self) mzcon event performLocalAction :: diff --git a/services/brig/src/Brig/API/Federation.hs b/services/brig/src/Brig/API/Federation.hs index 067d14389f5..9a6559663c6 100644 --- a/services/brig/src/Brig/API/Federation.hs +++ b/services/brig/src/Brig/API/Federation.hs @@ -36,7 +36,6 @@ import Brig.Effects.FederationConfigStore qualified as E import Brig.Effects.GalleyProvider (GalleyProvider) import Brig.IO.Intra (notify) import Brig.Options -import Brig.Types.User.Event import Brig.User.API.Handle import Brig.User.Search.SearchIndex qualified as Q import Control.Error.Util @@ -69,6 +68,7 @@ import Wire.API.User (UserProfile) import Wire.API.User.Client import Wire.API.User.Client.Prekey import Wire.API.User.Search hiding (searchPolicy) +import Wire.API.UserEvent import Wire.API.UserMap (UserMap) import Wire.NotificationSubsystem import Wire.Sem.Concurrency diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 9adc85b9af0..315ad9ae22f 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -58,7 +58,6 @@ import Brig.Types.Connection import Brig.Types.Intra import Brig.Types.Team.LegalHold (LegalHoldClientRequest (..)) import Brig.Types.User -import Brig.Types.User.Event (UserEvent (UserUpdated), UserUpdatedData (eupSSOId, eupSSOIdRemoved), emptyUserUpdatedData) import Brig.User.API.Search qualified as Search import Brig.User.EJPD qualified import Brig.User.Search.Index qualified as Index @@ -99,6 +98,7 @@ import Wire.API.User import Wire.API.User.Activation import Wire.API.User.Client import Wire.API.User.RichInfo +import Wire.API.UserEvent import Wire.NotificationSubsystem import Wire.Rpc import Wire.Sem.Concurrency diff --git a/services/brig/src/Brig/API/Properties.hs b/services/brig/src/Brig/API/Properties.hs index 3443ace2956..814b899962a 100644 --- a/services/brig/src/Brig/API/Properties.hs +++ b/services/brig/src/Brig/API/Properties.hs @@ -30,25 +30,25 @@ import Brig.App import Brig.Data.Properties (PropertiesDataError) import Brig.Data.Properties qualified as Data import Brig.IO.Intra qualified as Intra -import Brig.Types.User.Event import Control.Error import Data.Id import Imports import Polysemy import Wire.API.Properties +import Wire.API.UserEvent import Wire.NotificationSubsystem setProperty :: (Member NotificationSubsystem r) => UserId -> ConnId -> PropertyKey -> PropertyValue -> ExceptT PropertiesDataError (AppT r) () setProperty u c k v = do wrapClientE $ Data.insertProperty u k (propertyRaw v) - lift $ liftSem $ Intra.onPropertyEvent u c (PropertySet u k v) + lift $ liftSem $ Intra.onPropertyEvent u c (PropertySet k (propertyValue v)) deleteProperty :: (Member NotificationSubsystem r) => UserId -> ConnId -> PropertyKey -> AppT r () deleteProperty u c k = do wrapClient $ Data.deleteProperty u k - liftSem $ Intra.onPropertyEvent u c (PropertyDeleted u k) + liftSem $ Intra.onPropertyEvent u c (PropertyDeleted k) clearProperties :: (Member NotificationSubsystem r) => UserId -> ConnId -> AppT r () clearProperties u c = do wrapClient $ Data.clearProperties u - liftSem $ Intra.onPropertyEvent u c (PropertiesCleared u) + liftSem $ Intra.onPropertyEvent u c PropertiesCleared diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 8b70410265d..4b565ce4509 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -132,7 +132,6 @@ import Brig.Team.Types (ShowOrHideInvitationUrl (..)) import Brig.Types.Activation (ActivationPair) import Brig.Types.Connection import Brig.Types.Intra -import Brig.Types.User.Event import Brig.User.Auth.Cookie (listCookies, revokeAllCookies) import Brig.User.Email import Brig.User.Handle @@ -189,6 +188,7 @@ import Wire.API.User.Activation import Wire.API.User.Client import Wire.API.User.Password import Wire.API.User.RichInfo +import Wire.API.UserEvent import Wire.NotificationSubsystem import Wire.Sem.Concurrency import Wire.Sem.Paging.Cassandra (InternalPaging) diff --git a/services/brig/src/Brig/IO/Intra.hs b/services/brig/src/Brig/IO/Intra.hs index 2dbd9109b0e..c8e82c586d0 100644 --- a/services/brig/src/Brig/IO/Intra.hs +++ b/services/brig/src/Brig/IO/Intra.hs @@ -57,20 +57,19 @@ import Brig.Effects.ConnectionStore (ConnectionStore) import Brig.Effects.ConnectionStore qualified as E import Brig.Federation.Client (notifyUserDeleted, sendConnectionAction) import Brig.IO.Journal qualified as Journal +import Brig.IO.Logging import Brig.RPC -import Brig.Types.User.Event import Brig.User.Search.Index qualified as Search import Control.Error (ExceptT, runExceptT) import Control.Lens (view, (.~), (?~), (^.), (^?)) import Control.Monad.Catch import Control.Monad.Trans.Except (throwE) import Data.Aeson hiding (json) -import Data.Aeson.KeyMap qualified as KeyMap import Data.Aeson.Lens import Data.ByteString.Conversion import Data.ByteString.Lazy qualified as BL import Data.Id -import Data.Json.Util (toUTCTimeMillis, (#)) +import Data.Json.Util import Data.List.NonEmpty (NonEmpty (..)) import Data.List1 (List1, singleton) import Data.Proxy @@ -91,13 +90,13 @@ import Wire.API.Conversation hiding (Member) import Wire.API.Event.Conversation (Connect (Connect)) import Wire.API.Federation.API.Brig import Wire.API.Federation.Error -import Wire.API.Properties import Wire.API.Routes.Internal.Galley.ConversationsIntra import Wire.API.Routes.Internal.Galley.TeamsIntra (GuardLegalholdPolicyConflicts (GuardLegalholdPolicyConflicts)) import Wire.API.Team.LegalHold (LegalholdProtectee) import Wire.API.Team.Member qualified as Team import Wire.API.User import Wire.API.User.Client +import Wire.API.UserEvent import Wire.NotificationSubsystem import Wire.Rpc import Wire.Sem.Logger qualified as Log @@ -171,7 +170,7 @@ onClientEvent orig conn e = do let event = ClientEvent e let rcps = Recipient orig V2.RecipientClientsAll :| [] pushNotifications - [ newPush1 (Just orig) (toPushFormat event) rcps + [ newPush1 (Just orig) (toJSONObject event) rcps & pushConn .~ conn & pushApsData .~ toApsData event ] @@ -302,7 +301,7 @@ notifyUserDeletionLocals deleted conn event = do Cancelled now (ucConvId uc) - let e = ConnectionUpdated ucCancelled Nothing Nothing + let e = ConnectionUpdated ucCancelled Nothing onConnectionEvent deleted conn e ) @@ -372,7 +371,7 @@ notify :: notify (toList -> events) orig route conn recipients = do rs <- (\u -> Recipient u RecipientClientsAll) <$$> recipients let pushes = flip map events $ \event -> - newPush1 (Just orig) (toPushFormat event) rs + newPush1 (Just orig) (toJSONObject event) rs & pushConn .~ conn & pushRoute .~ route & pushApsData .~ toApsData event @@ -422,130 +421,8 @@ notifyContacts events orig route conn = do view Team.userId <$> mems ^. Team.teamMembers screenMemberList _ = [] --- Event Serialisation: - -toPushFormat :: Event -> Object -toPushFormat (UserEvent (UserCreated u)) = - KeyMap.fromList - [ "type" .= ("user.new" :: Text), - "user" .= SelfProfile (u {userIdentity = Nothing}) - ] -toPushFormat (UserEvent (UserActivated u)) = - KeyMap.fromList - [ "type" .= ("user.activate" :: Text), - "user" .= SelfProfile u - ] -toPushFormat (UserEvent (UserUpdated (UserUpdatedData i n pic acc ass hdl loc mb ssoId ssoIdDel prots))) = - KeyMap.fromList - [ "type" .= ("user.update" :: Text), - "user" - .= object - ( "id" .= i - # "name" .= n - # "picture" .= pic -- DEPRECATED - # "accent_id" .= acc - # "assets" .= ass - # "handle" .= hdl - # "locale" .= loc - # "managed_by" .= mb - # "sso_id" .= ssoId - # "sso_id_deleted" .= ssoIdDel - # "supported_protocols" .= prots - # [] - ) - ] -toPushFormat (UserEvent (UserIdentityUpdated UserIdentityUpdatedData {..})) = - KeyMap.fromList - [ "type" .= ("user.update" :: Text), - "user" - .= object - ( "id" .= eiuId - # "email" .= eiuEmail - # "phone" .= eiuPhone - # [] - ) - ] -toPushFormat (UserEvent (UserIdentityRemoved (UserIdentityRemovedData i e p))) = - KeyMap.fromList - [ "type" .= ("user.identity-remove" :: Text), - "user" - .= object - ( "id" .= i - # "email" .= e - # "phone" .= p - # [] - ) - ] -toPushFormat (ConnectionEvent (ConnectionUpdated uc _ name)) = - KeyMap.fromList $ - "type" .= ("user.connection" :: Text) - # "connection" .= uc - # "user" .= case name of - Just n -> Just $ object ["name" .= n] - Nothing -> Nothing - # [] -toPushFormat (UserEvent (UserSuspended i)) = - KeyMap.fromList - [ "type" .= ("user.suspend" :: Text), - "id" .= i - ] -toPushFormat (UserEvent (UserResumed i)) = - KeyMap.fromList - [ "type" .= ("user.resume" :: Text), - "id" .= i - ] -toPushFormat (UserEvent (UserDeleted qid)) = - KeyMap.fromList - [ "type" .= ("user.delete" :: Text), - "id" .= qUnqualified qid, - "qualified_id" .= qid - ] -toPushFormat (UserEvent (UserLegalHoldDisabled i)) = - KeyMap.fromList - [ "type" .= ("user.legalhold-disable" :: Text), - "id" .= i - ] -toPushFormat (UserEvent (UserLegalHoldEnabled i)) = - KeyMap.fromList - [ "type" .= ("user.legalhold-enable" :: Text), - "id" .= i - ] -toPushFormat (PropertyEvent (PropertySet _ k v)) = - KeyMap.fromList - [ "type" .= ("user.properties-set" :: Text), - "key" .= k, - "value" .= propertyValue v - ] -toPushFormat (PropertyEvent (PropertyDeleted _ k)) = - KeyMap.fromList - [ "type" .= ("user.properties-delete" :: Text), - "key" .= k - ] -toPushFormat (PropertyEvent (PropertiesCleared _)) = - KeyMap.fromList - [ "type" .= ("user.properties-clear" :: Text) - ] -toPushFormat (ClientEvent (ClientAdded _ c)) = - KeyMap.fromList - [ "type" .= ("user.client-add" :: Text), - "client" .= c - ] -toPushFormat (ClientEvent (ClientRemoved _ clientId)) = - KeyMap.fromList - [ "type" .= ("user.client-remove" :: Text), - "client" .= IdObject clientId - ] -toPushFormat (UserEvent (LegalHoldClientRequested payload)) = - let LegalHoldClientRequestedData targetUser lastPrekey' clientId = payload - in KeyMap.fromList - [ "type" .= ("user.legalhold-request" :: Text), - "id" .= targetUser, - "last_prekey" .= lastPrekey', - "client" .= IdObject clientId - ] - toApsData :: Event -> Maybe V2.ApsData -toApsData (ConnectionEvent (ConnectionUpdated uc _ name)) = +toApsData (ConnectionEvent (ConnectionUpdated uc name)) = case (ucStatus uc, name) of (MissingLegalholdConsent, _) -> Nothing (Pending, n) -> apsConnRequest <$> n diff --git a/services/brig/src/Brig/IO/Logging.hs b/services/brig/src/Brig/IO/Logging.hs new file mode 100644 index 00000000000..ec733caa119 --- /dev/null +++ b/services/brig/src/Brig/IO/Logging.hs @@ -0,0 +1,34 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Brig.IO.Logging where + +import Data.ByteString.Conversion +import Data.Id +import Data.Qualified +import System.Logger + +logConnection :: UserId -> Qualified UserId -> Msg -> Msg +logConnection from (Qualified toUser toDomain) = + "connection.from" .= toByteString from + ~~ "connection.to" .= toByteString toUser + ~~ "connection.to_domain" .= toByteString toDomain + +logLocalConnection :: UserId -> UserId -> Msg -> Msg +logLocalConnection from to = + "connection.from" .= toByteString from + ~~ "connection.to" .= toByteString to diff --git a/services/brig/src/Brig/InternalEvent/Process.hs b/services/brig/src/Brig/InternalEvent/Process.hs index 9bca2320e37..b0e0ba1c870 100644 --- a/services/brig/src/Brig/InternalEvent/Process.hs +++ b/services/brig/src/Brig/InternalEvent/Process.hs @@ -28,7 +28,6 @@ import Brig.IO.Intra qualified as Intra import Brig.InternalEvent.Types import Brig.Options (defDeleteThrottleMillis, setDeleteThrottleMillis) import Brig.Provider.API qualified as API -import Brig.Types.User.Event import Control.Lens (view) import Control.Monad.Catch import Data.ByteString.Conversion @@ -41,6 +40,7 @@ import Polysemy.Input (Input) import Polysemy.Time import Polysemy.TinyLog as Log import System.Logger.Class (field, msg, val, (~~)) +import Wire.API.UserEvent import Wire.NotificationSubsystem import Wire.Sem.Delay import Wire.Sem.Paging.Cassandra (InternalPaging) @@ -63,7 +63,7 @@ onEvent :: onEvent n = handleTimeout $ case n of DeleteClient clientId uid mcon -> do rmClient uid clientId - Intra.onClientEvent uid mcon (ClientRemoved uid clientId) + Intra.onClientEvent uid mcon (ClientRemoved clientId) DeleteUser uid -> do Log.info $ msg (val "Processing user delete event") diff --git a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs index 8be3b158517..5f85b3a1259 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs @@ -31,7 +31,6 @@ import Bilge hiding (accept, head, timeout, trace) import Bilge.Assert import Brig.Types.Intra (UserSet (..)) import Brig.Types.Test.Arbitrary () -import Brig.Types.User.Event qualified as Ev import Cassandra.Exec qualified as Cql import Control.Category ((>>>)) import Control.Concurrent.Chan @@ -71,6 +70,7 @@ import Wire.API.Team.Permission import Wire.API.Team.Role import Wire.API.User.Client import Wire.API.User.Client qualified as Client +import Wire.API.UserEvent qualified as Ev tests :: IO TestSetup -> TestTree tests s = @@ -237,7 +237,7 @@ testApproveLegalHoldDevice = do UserLegalHoldEnabled userStatus let pluck = \case - Ev.ClientAdded _ eClient -> do + Ev.ClientAdded eClient -> do clientId eClient @?= someClientId clientType eClient @?= LegalHoldClientType clientClass eClient @?= Just LegalHoldClient @@ -316,7 +316,7 @@ testDisableLegalHoldForUser = do requestLegalHoldDevice owner member tid !!! testResponse 201 Nothing approveLegalHoldDevice (Just defPassword) member member tid !!! testResponse 200 Nothing assertNotification mws $ \case - Ev.ClientAdded _ client -> do + Ev.ClientAdded client -> do clientId client @?= someClientId clientType client @?= LegalHoldClientType clientClass client @?= Just LegalHoldClient @@ -332,7 +332,7 @@ testDisableLegalHoldForUser = do assertEqual "method" "POST" (requestMethod req) assertEqual "path" (pathInfo req) ["legalhold", "remove"] assertNotification mws $ \case - Ev.ClientEvent (Ev.ClientRemoved _ clientId') -> clientId' @?= someClientId + Ev.ClientEvent (Ev.ClientRemoved clientId') -> clientId' @?= someClientId _ -> assertBool "Unexpected event" False assertNotification mws $ \case Ev.UserEvent (Ev.UserLegalHoldDisabled uid) -> uid @?= member diff --git a/services/galley/test/integration/API/Teams/LegalHold/Util.hs b/services/galley/test/integration/API/Teams/LegalHold/Util.hs index f4362f81507..e0b2d06481b 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/Util.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/Util.hs @@ -11,7 +11,6 @@ import API.Util import Bilge hiding (accept, head, timeout, trace) import Bilge.Assert import Brig.Types.Test.Arbitrary () -import Brig.Types.User.Event qualified as Ev import Control.Concurrent.Async qualified as Async import Control.Concurrent.Chan import Control.Concurrent.Timeout hiding (threadDelay) @@ -61,6 +60,7 @@ import Wire.API.Team.LegalHold.External import Wire.API.Team.Member qualified as Team import Wire.API.User (UserProfile (..)) import Wire.API.User.Client +import Wire.API.UserEvent qualified as Ev -------------------------------------------------------------------- -- setup helpers @@ -487,26 +487,6 @@ requestLegalHoldDevice' g zusr uid tid = do ---------------------------------------------------------------------- -- test helpers -deriving instance Show Ev.Event - -deriving instance Show Ev.UserEvent - -deriving instance Show Ev.ClientEvent - -deriving instance Show Ev.PropertyEvent - -deriving instance Show Ev.ConnectionEvent - --- (partial implementation, just good enough to make the tests work) -instance FromJSON Ev.Event where - parseJSON ev = flip (withObject "Ev.Event") ev $ \o -> do - typ :: Text <- o .: "type" - if - | typ `elem` ["user.legalhold-request", "user.legalhold-enable", "user.legalhold-disable"] -> Ev.UserEvent <$> Aeson.parseJSON ev - | typ `elem` ["user.client-add", "user.client-remove"] -> Ev.ClientEvent <$> Aeson.parseJSON ev - | typ == "user.connection" -> Ev.ConnectionEvent <$> Aeson.parseJSON ev - | otherwise -> fail $ "Ev.Event: unsupported event type: " <> show typ - -- (partial implementation, just good enough to make the tests work) instance FromJSON Ev.UserEvent where parseJSON = withObject "Ev.UserEvent" $ \o -> do @@ -528,11 +508,9 @@ instance FromJSON Ev.ClientEvent where parseJSON = withObject "Ev.ClientEvent" $ \o -> do tag :: Text <- o .: "type" case tag of - "user.client-add" -> Ev.ClientAdded fakeuid <$> o .: "client" - "user.client-remove" -> Ev.ClientRemoved fakeuid <$> (o .: "client" >>= withObject "id" (.: "id")) + "user.client-add" -> Ev.ClientAdded <$> o .: "client" + "user.client-remove" -> Ev.ClientRemoved <$> (o .: "client" >>= withObject "id" (.: "id")) x -> fail $ "Ev.ClientEvent: unsupported event type: " ++ show x - where - fakeuid = read @UserId "6980fb5e-ba64-11eb-a339-0b3625bf01be" instance FromJSON Ev.ConnectionEvent where parseJSON = Aeson.withObject "ConnectionEvent" $ \o -> do @@ -542,7 +520,6 @@ instance FromJSON Ev.ConnectionEvent where Ev.ConnectionUpdated <$> o .: "connection" <*> pure Nothing - <*> pure Nothing x -> fail $ "unspported event type: " ++ show x assertNotification :: (HasCallStack, FromJSON a, MonadIO m) => WS.WebSocket -> (a -> Assertion) -> m () From 9904df9a78a9239f5a1878c3ce336e26627e2aa9 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Tue, 19 Mar 2024 11:29:43 +0100 Subject: [PATCH 059/117] New shell script hack to create session tokens (and logout if you like). (#3962) --- hack/bin/get-session-token | 44 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100755 hack/bin/get-session-token diff --git a/hack/bin/get-session-token b/hack/bin/get-session-token new file mode 100755 index 00000000000..e611bf84d7f --- /dev/null +++ b/hack/bin/get-session-token @@ -0,0 +1,44 @@ +#!/usr/bin/env bash + +#set -x +set -e -o pipefail + +WIRE_COOKIEJAR=/tmp/get-session-token.cookiejar + +#WIRE_HOST=https://prod-nginz-https.wire.com +#WIRE_USER='...' +#WIRE_PASSWORD='...' + +# run eg. './create_test_team_admins.sh -c', and fill user and password from the output: +WIRE_HOST=http://localhost:8080 +WIRE_USER='...' +WIRE_PASSWORD='...' + +function wire_login () { + curl -b $WIRE_COOKIEJAR -c $WIRE_COOKIEJAR -X POST \ + --header 'Content-Type: application/json' \ + --header 'Accept: application/json' \ + -d '{"email":"'"$WIRE_USER"'","password":"'"$WIRE_PASSWORD"'"}' \ + $WIRE_HOST/login'?persist=false' +} + +function wire_logout () { + curl -b $WIRE_COOKIEJAR -c $WIRE_COOKIEJAR -v -X POST \ + --header 'Content-Type: application/json' \ + --header 'Accept: application/json' \ + --header "Authorization: Bearer $BEARER" \ + "{$WIRE_HOST}/access/logout" +} + +export RESP +RESP=$(wire_login) +echo "[$RESP]" + +export BEARER +BEARER=$(echo "$RESP" | jq -r .access_token) +echo "Authorization: Bearer $BEARER" + +#cat $WIRE_COOKIEJAR +#export RESP +#RESP=$(wire_logout) +#echo "[$RESP]" From 5fc5e6cd6a0bcb739b4c2c2577f77a7579db5a9f Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Wed, 20 Mar 2024 09:36:58 +0100 Subject: [PATCH 060/117] Bump hscim version and deps (#3943) * bump hscim version & dependencies (for hackage upload). * Don't -Werror in hscim, hackage wouldn't accept that for libraries. (anyway cabal.project turns it back on again inside this repo.) --- libs/hscim/CHANGELOG | 3 ++ libs/hscim/default.nix | 2 +- libs/hscim/hscim.cabal | 80 +++++++++++++++++++++--------------------- 3 files changed, 44 insertions(+), 41 deletions(-) diff --git a/libs/hscim/CHANGELOG b/libs/hscim/CHANGELOG index a9fcfb821fa..6da7d28ec7e 100644 --- a/libs/hscim/CHANGELOG +++ b/libs/hscim/CHANGELOG @@ -1,3 +1,6 @@ +0.4.0: + - update dependencies + 0.3.6: - fix serialization: json attributes in scim are case-insensitive diff --git a/libs/hscim/default.nix b/libs/hscim/default.nix index 175f532a31e..85fc2e7cd5e 100644 --- a/libs/hscim/default.nix +++ b/libs/hscim/default.nix @@ -47,7 +47,7 @@ }: mkDerivation { pname = "hscim"; - version = "0.3.6"; + version = "0.4.0.2"; src = gitignoreSource ./.; isLibrary = true; isExecutable = true; diff --git a/libs/hscim/hscim.cabal b/libs/hscim/hscim.cabal index 0a0484ca848..a2a5a9c19b4 100644 --- a/libs/hscim/hscim.cabal +++ b/libs/hscim/hscim.cabal @@ -1,6 +1,6 @@ cabal-version: 1.12 name: hscim -version: 0.3.6 +version: 0.4.0.2 synopsis: hscim json schema and server implementation description: The README file will answer all the questions you might have @@ -81,42 +81,42 @@ library TypeOperators TypeSynonymInstances - ghc-options: -Wall -Werror -Wredundant-constraints -Wunused-packages + ghc-options: -Wall -Wredundant-constraints -Wunused-packages build-depends: - aeson - , aeson-qq - , attoparsec - , base - , bytestring - , case-insensitive - , email-validate - , hashable - , hspec - , hspec-expectations - , hspec-wai - , http-api-data - , http-media - , http-types - , list-t - , microlens - , mmorph - , mtl - , network-uri - , retry - , scientific - , servant - , servant-client - , servant-client-core - , servant-server - , stm - , stm-containers - , string-conversions - , template-haskell - , text - , time - , uuid - , wai - , wai-extra + aeson >=2.1.2 && <2.2 + , aeson-qq >=0.8.4 && <0.9 + , attoparsec >=0.14.4 && <0.15 + , base >=4.17.2 && <4.18 + , bytestring >=0.10.4 && <0.12 + , case-insensitive >=1.2.1 && <1.3 + , email-validate >=2.3.2 && <2.4 + , hashable >=1.4.3 && <1.5 + , hspec >=2.10.10 && <2.11 + , hspec-expectations >=0.8.2 && <0.9 + , hspec-wai >=0.11.1 && <0.12 + , http-api-data >=0.5 && <0.6 + , http-media >=0.8.1 && <0.9 + , http-types >=0.12.3 && <0.13 + , list-t >=1.0.5 && <1.1 + , microlens >=0.4.13 && <0.5 + , mmorph >=1.2.0 && <1.3 + , mtl >=2.2.2 && <2.3 + , network-uri >=2.6.4 && <2.7 + , retry >=0.9.3 && <0.10 + , scientific >=0.3.7 && <0.4 + , servant >=0.19.1 && <0.20 + , servant-client >=0.19 && <0.20 + , servant-client-core >=0.19 && <0.20 + , servant-server >=0.19.2 && <0.20 + , stm >=2.5.1 && <2.6 + , stm-containers >=1.2.0 && <1.3 + , string-conversions >=0.4.0 && <0.5 + , template-haskell >=2.19.0 && <2.20 + , text >=2.0.2 && <2.1 + , time >=1.12.2 && <1.13 + , uuid >=1.3.15 && <1.4 + , wai >=3.2.3 && <3.3 + , wai-extra >=3.1.13 && <3.2 default-language: Haskell2010 @@ -143,8 +143,8 @@ executable hscim-server TypeSynonymInstances ghc-options: - -Wall -Werror -threaded -rtsopts -with-rtsopts=-N - -Wredundant-constraints -Wunused-packages + -Wall -threaded -rtsopts -with-rtsopts=-N -Wredundant-constraints + -Wunused-packages build-depends: base @@ -198,8 +198,8 @@ test-suite spec TypeSynonymInstances ghc-options: - -Wall -Werror -threaded -rtsopts -with-rtsopts=-N - -Wredundant-constraints -Wunused-packages + -Wall -threaded -rtsopts -with-rtsopts=-N -Wredundant-constraints + -Wunused-packages build-tool-depends: hspec-discover:hspec-discover build-depends: From 2fd07d4fb9dd5bb8448431e88fc5bdc3892e5bce Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Wed, 20 Mar 2024 11:47:14 +0100 Subject: [PATCH 061/117] Revert http2 pin. (#3965) * Remove trailing whitespace. * Revert "Update http2, wai. (#3911)" This reverts commit 8f823f2517f14119c921c2563a9d8f35836f48d4. --- libs/http2-manager/default.nix | 5 +- libs/http2-manager/http2-manager.cabal | 3 +- .../src/HTTP2/Client/Manager/Internal.hs | 69 ++++++++++--------- .../test/Test/HTTP2/Client/ManagerSpec.hs | 66 ++++++++++-------- nix/haskell-pins.nix | 30 +------- nix/manual-overrides.nix | 1 + 6 files changed, 84 insertions(+), 90 deletions(-) diff --git a/libs/http2-manager/default.nix b/libs/http2-manager/default.nix index 3674bc56d2a..782b3605f14 100644 --- a/libs/http2-manager/default.nix +++ b/libs/http2-manager/default.nix @@ -19,7 +19,7 @@ , stm , streaming-commons , text -, utf8-string +, time-manager }: mkDerivation { pname = "http2-manager"; @@ -36,7 +36,7 @@ mkDerivation { stm streaming-commons text - utf8-string + time-manager ]; testHaskellDepends = [ async @@ -51,6 +51,7 @@ mkDerivation { random stm streaming-commons + time-manager ]; testToolDepends = [ hspec-discover ]; description = "Managed connection pool for HTTP2"; diff --git a/libs/http2-manager/http2-manager.cabal b/libs/http2-manager/http2-manager.cabal index b6d02869a97..3aed0c465ba 100644 --- a/libs/http2-manager/http2-manager.cabal +++ b/libs/http2-manager/http2-manager.cabal @@ -44,7 +44,7 @@ library , stm , streaming-commons , text - , utf8-string + , time-manager default-language: Haskell2010 @@ -85,3 +85,4 @@ test-suite http2-manager-tests , random , stm , streaming-commons + , time-manager diff --git a/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs b/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs index ee8bc878ec2..0dd00173c8c 100644 --- a/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs +++ b/libs/http2-manager/src/HTTP2/Client/Manager/Internal.hs @@ -15,7 +15,6 @@ import Control.Monad import Control.Monad.IO.Class import Data.ByteString import qualified Data.ByteString as BS -import Data.ByteString.UTF8 as UTF8 import Data.IORef import Data.Map import qualified Data.Map as Map @@ -24,11 +23,13 @@ import Data.Streaming.Network import qualified Data.Text as Text import qualified Data.Text.Encoding as Text import Data.Unique +import Foreign.Marshal.Alloc (mallocBytes) import GHC.IO.Exception import qualified Network.HTTP2.Client as HTTP2 import qualified Network.Socket as NS import qualified OpenSSL.Session as SSL import System.IO.Error +import qualified System.TimeManager import System.Timeout import Prelude @@ -290,9 +291,9 @@ startPersistentHTTP2Connection :: startPersistentHTTP2Connection ctx (tlsEnabled, hostname, port) cl removeTrailingDot tcpConnectTimeout sendReqMVar = do liveReqs <- newIORef mempty let clientConfig = - HTTP2.defaultClientConfig + HTTP2.ClientConfig { HTTP2.scheme = if tlsEnabled then "https" else "http", - HTTP2.authority = UTF8.toString hostname, + HTTP2.authority = hostname, HTTP2.cacheLimit = cl } -- Sends error to requests which show up too late, i.e. after the @@ -332,7 +333,7 @@ startPersistentHTTP2Connection ctx (tlsEnabled, hostname, port) cl removeTrailin bracket connectTCPWithTimeout NS.close $ \sock -> do bracket (mkTransport sock transportConfig) cleanupTransport $ \transport -> bracket (allocHTTP2Config transport) HTTP2.freeSimpleConfig $ \http2Cfg -> do - let runAction = HTTP2.run clientConfig http2Cfg $ \sendReq _aux -> do + let runAction = HTTP2.run clientConfig http2Cfg $ \sendReq -> do handleRequests liveReqs sendReq -- Any request threads still hanging about after 'runAction' finishes -- are canceled with 'ConnectionAlreadyClosed'. @@ -396,7 +397,7 @@ type SendReqFn = HTTP2.Request -> (HTTP2.Response -> IO ()) -> IO () data Transport = InsecureTransport NS.Socket - | SecureTransport SSL.SSL NS.Socket + | SecureTransport SSL.SSL data TLSParams = TLSParams { context :: SSL.SSLContext, @@ -413,11 +414,11 @@ mkTransport sock (Just TLSParams {..}) = do SSL.setTlsextHostName ssl hostnameStr SSL.enableHostnameValidation ssl hostnameStr SSL.connect ssl - pure $ SecureTransport ssl sock + pure $ SecureTransport ssl cleanupTransport :: Transport -> IO () cleanupTransport (InsecureTransport _) = pure () -cleanupTransport (SecureTransport ssl _) = SSL.shutdown ssl SSL.Unidirectional +cleanupTransport (SecureTransport ssl) = SSL.shutdown ssl SSL.Unidirectional data ConnectionAlreadyClosed = ConnectionAlreadyClosed deriving (Show) @@ -429,29 +430,33 @@ bufsize = 4096 allocHTTP2Config :: Transport -> IO HTTP2.Config allocHTTP2Config (InsecureTransport sock) = HTTP2.allocSimpleConfig sock bufsize -allocHTTP2Config (SecureTransport ssl sock) = do - config <- HTTP2.allocSimpleConfig sock bufsize - pure $ - config - { HTTP2.confSendAll = SSL.write ssl, - HTTP2.confReadN = readData mempty +allocHTTP2Config (SecureTransport ssl) = do + buf <- mallocBytes bufsize + timmgr <- System.TimeManager.initialize $ 30 * 1000000 + -- Sometimes the frame header says that the payload length is 0. Reading 0 + -- bytes multiple times seems to be causing errors in openssl. I cannot figure + -- out why. The previous implementation didn't try to read from the socket + -- when trying to read 0 bytes, so special handling for 0 maintains that + -- behaviour. + let readData acc 0 = pure acc + readData acc n = do + -- Handling SSL.ConnectionAbruptlyTerminated as a stream end + -- (some sites terminate SSL connection right after returning the data). + chunk <- SSL.read ssl n `catch` \(_ :: SSL.ConnectionAbruptlyTerminated) -> pure mempty + let chunkLen = BS.length chunk + if + | chunkLen == 0 || chunkLen == n -> + pure (acc <> chunk) + | chunkLen > n -> + error "openssl: SSL.read returned more bytes than asked for, this is probably a bug" + | otherwise -> + readData (acc <> chunk) (n - chunkLen) + pure + HTTP2.Config + { HTTP2.confWriteBuffer = buf, + HTTP2.confBufferSize = bufsize, + HTTP2.confSendAll = SSL.write ssl, + HTTP2.confReadN = readData mempty, + HTTP2.confPositionReadMaker = HTTP2.defaultPositionReadMaker, + HTTP2.confTimeoutManager = timmgr } - where - -- Sometimes the frame header says that the payload length is 0. Reading 0 - -- bytes multiple times seems to be causing errors in openssl. I cannot figure - -- out why. The previous implementation didn't try to read from the socket - -- when trying to read 0 bytes, so special handling for 0 maintains that - -- behaviour. - readData acc 0 = pure acc - readData acc n = do - -- Handling SSL.ConnectionAbruptlyTerminated as a stream end - -- (some sites terminate SSL connection right after returning the data). - chunk <- SSL.read ssl n `catch` \(_ :: SSL.ConnectionAbruptlyTerminated) -> pure mempty - let chunkLen = BS.length chunk - if - | chunkLen == 0 || chunkLen == n -> - pure (acc <> chunk) - | chunkLen > n -> - error "openssl: SSL.read returned more bytes than asked for, this is probably a bug" - | otherwise -> - readData (acc <> chunk) (n - chunkLen) diff --git a/libs/http2-manager/test/Test/HTTP2/Client/ManagerSpec.hs b/libs/http2-manager/test/Test/HTTP2/Client/ManagerSpec.hs index 7b1010e4066..f839619b9bb 100644 --- a/libs/http2-manager/test/Test/HTTP2/Client/ManagerSpec.hs +++ b/libs/http2-manager/test/Test/HTTP2/Client/ManagerSpec.hs @@ -26,6 +26,7 @@ import qualified Data.Map as Map import Data.Maybe (isJust) import Data.Streaming.Network (bindPortTCP, bindRandomPortTCP) import Data.Unique +import Foreign.Marshal.Alloc (mallocBytes) import GHC.IO.Exception import HTTP2.Client.Manager import HTTP2.Client.Manager.Internal @@ -36,6 +37,7 @@ import qualified Network.HTTP2.Server as Server import Network.Socket import qualified OpenSSL.Session as SSL import System.Random (randomRIO) +import qualified System.TimeManager import Test.Hspec echoTest :: Http2Manager -> TLSEnabled -> Int -> Expectation @@ -268,30 +270,38 @@ withTestServerOnSocket mCtx action (serverPort, listenSock) = do bracket (async $ testServerOnSocket mCtx listenSock acceptedConns liveConns) cleanupServer $ \serverThread -> action TestServer {..} -allocServerConfig :: (Socket, Maybe SSL.SSL) -> IO Server.Config -allocServerConfig (sock, Nothing) = - HTTP2.allocSimpleConfig sock 4096 -allocServerConfig (sock, Just ssl) = do - config <- HTTP2.allocSimpleConfig sock 4096 - pure $ - config - { Server.confReadN = readData mempty, - Server.confSendAll = SSL.write ssl +allocServerConfig :: Either Socket SSL.SSL -> IO Server.Config +allocServerConfig (Left sock) = HTTP2.allocSimpleConfig sock 4096 +allocServerConfig (Right ssl) = do + buf <- mallocBytes bufsize + timmgr <- System.TimeManager.initialize $ 30 * 1000000 + -- Sometimes the frame header says that the payload length is 0. Reading 0 + -- bytes multiple times seems to be causing errors in openssl. I cannot figure + -- out why. The previous implementation didn't try to read from the socket + -- when trying to read 0 bytes, so special handling for 0 maintains that + -- behaviour. + let readData prevChunk 0 = pure prevChunk + readData prevChunk n = do + -- Handling SSL.ConnectionAbruptlyTerminated as a stream end + -- (some sites terminate SSL connection right after returning the data). + chunk <- SSL.read ssl n `catch` \(_ :: SSL.ConnectionAbruptlyTerminated) -> pure mempty + let chunkLen = BS.length chunk + if + | chunkLen == 0 || chunkLen == n -> + pure (prevChunk <> chunk) + | chunkLen > n -> + error "openssl: SSL.read returned more bytes than asked for, this is probably a bug" + | otherwise -> + readData (prevChunk <> chunk) (n - chunkLen) + pure + Server.Config + { Server.confWriteBuffer = buf, + Server.confBufferSize = bufsize, + Server.confSendAll = SSL.write ssl, + Server.confReadN = readData mempty, + Server.confPositionReadMaker = Server.defaultPositionReadMaker, + Server.confTimeoutManager = timmgr } - where - readData prevChunk 0 = pure prevChunk - readData prevChunk n = do - -- Handling SSL.ConnectionAbruptlyTerminated as a stream end - -- (some sites terminate SSL connection right after returning the data). - chunk <- SSL.read ssl n `catch` \(_ :: SSL.ConnectionAbruptlyTerminated) -> pure mempty - let chunkLen = BS.length chunk - if - | chunkLen == 0 || chunkLen == n -> - pure (prevChunk <> chunk) - | chunkLen > n -> - error "openssl: SSL.read returned more bytes than asked for, this is probably a bug" - | otherwise -> - readData (prevChunk <> chunk) (n - chunkLen) testServerOnSocket :: Maybe SSL.SSLContext -> Socket -> IORef Int -> IORef (Map Unique (Async ())) -> IO () testServerOnSocket mCtx listenSock connsCounter conns = do @@ -299,20 +309,20 @@ testServerOnSocket mCtx listenSock connsCounter conns = do forever $ do (sock, _) <- accept listenSock serverCfgParam <- case mCtx of - Nothing -> pure $ (sock, Nothing) + Nothing -> pure $ Left sock Just ctx -> do ssl <- SSL.connection ctx sock SSL.accept ssl - pure (sock, Just ssl) + pure (Right ssl) connKey <- newUnique modifyIORef connsCounter (+ 1) let shutdownSSL = case serverCfgParam of - (_sock, Just ssl) -> SSL.shutdown ssl SSL.Bidirectional - _ -> pure () + Left _ -> pure () + Right ssl -> SSL.shutdown ssl SSL.Bidirectional cleanup cfg = do Server.freeSimpleConfig cfg `finally` (shutdownSSL `finally` close sock) thread <- async $ bracket (allocServerConfig serverCfgParam) cleanup $ \cfg -> do - Server.run Server.defaultServerConfig cfg testServer `finally` modifyIORef conns (Map.delete connKey) + Server.run cfg testServer `finally` modifyIORef conns (Map.delete connKey) modifyIORef conns $ Map.insert connKey thread testServer :: Server.Request -> Server.Aux -> (Server.Response -> [Server.PushPromise] -> IO ()) -> IO () diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 835013ef132..1edd473eade 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -140,17 +140,6 @@ let }; }; - # We forked to add a handler for the ConnectionIsClosed signal - # since it was threated as a halting exception instead of a - # clean exit. - http2 = { - src = fetchgit { - url = "https://github.com/wireapp/http2"; - rev = "9cad270779bbcd9e6297b9ff05a4a7eb83bca069"; - sha256 = "sha256-c+PzfZZUxo/tE8oH1ZmKwbUMAq34kGD1OoBWorNLG38="; - }; - }; - # PR: https://gitlab.com/twittner/cql/-/merge_requests/11 cql = { src = fetchgit { @@ -243,25 +232,18 @@ let hash = "sha256-E35PVxi/4iJFfWts3td52KKZKQt4dj9KFP3SvWG77Cc="; }; }; - # PR: https://github.com/yesodweb/wai/pull/958 warp = { src = fetchgit { url = "https://github.com/wireapp/wai"; - rev = "a48f8f31ad42f26057d7b96d70f897c1a3f69a3c"; - sha256 = "sha256-fFkiKLlViiV+F1wdQXak3RI454kgWvyRsoDz6g4c5Ks="; + rev = "bedd6a835f6d98128880465c30e8115fa986e3f6"; + sha256 = "sha256-0r/d9YwcKZIZd10EhL2TP+W14Wjk0/S8Q4pVvZuZLaY="; }; packages = { "warp" = "warp"; - "warp-tls" = "warp-tls"; - "wai-app-static" = "wai-app-static"; - "wai" = "wai"; - "wai-extra" = "wai-extra"; - "wai-websockets" = "wai-websockets"; }; }; }; - hackagePins = { # Major re-write upstream, we should get rid of this dependency rather than # adapt to upstream, this will go away when completing servantification. @@ -270,12 +252,6 @@ let sha256 = "sha256-DSMckKIeVE/buSMg8Mq+mUm1bYPYB7veA11Ns7vTBbc="; }; - # http2 now depends on this, might be removable after next nixpkgs bump - network-control = { - version = "0.0.2"; - sha256 = "sha256-0EvnVu7cktMmSRVk9Ufm0oE4JLQrKLSRYpFpgcJguY0="; - }; - # these are not yet in nixpkgs ghc-source-gen = { version = "0.4.4.0"; @@ -285,7 +261,7 @@ let version = "5.0.18.4"; sha256 = "sha256-gIc4hpdUfTS33rZPfzwLfVcXkQaglmsljqViyYdihdk="; }; - # dependency of hoogle + # dependency of hoogle safe = { version = "0.3.20"; sha256 = "sha256-PGwjhrRnkH8cLhd7fHTZFd6ts9abp0w5sLlV8ke1yXU="; diff --git a/nix/manual-overrides.nix b/nix/manual-overrides.nix index f17bdaf4673..51d0f437aea 100644 --- a/nix/manual-overrides.nix +++ b/nix/manual-overrides.nix @@ -54,6 +54,7 @@ hself: hsuper: { optparse-generic = hsuper.optparse-generic_1_5_2; th-abstraction = hsuper.th-abstraction_0_5_0_0; tls = hsuper.tls_1_9_0; + warp-tls = hsuper.warp-tls_3_4_3; # ----------------- # flags and patches From 75f65ddf3e24a5281b773112227c09be25fa4560 Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Wed, 20 Mar 2024 16:41:45 +0100 Subject: [PATCH 062/117] gundeck: Make notificationTTL configurable (#3960) Decreasing the notification TTL can e.g. save database space on test environments. --- changelog.d/2-features/gundeck-configure-notificationTTL | 3 +++ charts/gundeck/templates/configmap.yaml | 2 +- charts/gundeck/values.yaml | 5 +++++ 3 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 changelog.d/2-features/gundeck-configure-notificationTTL diff --git a/changelog.d/2-features/gundeck-configure-notificationTTL b/changelog.d/2-features/gundeck-configure-notificationTTL new file mode 100644 index 00000000000..c7dc6f5a2c5 --- /dev/null +++ b/changelog.d/2-features/gundeck-configure-notificationTTL @@ -0,0 +1,3 @@ +Make gundeck's notificationTTL configurable. The value defines how long +notifications are (at most) stored in the database. Decreasing this value e.g. +helps to safe database space on test environments. diff --git a/charts/gundeck/templates/configmap.yaml b/charts/gundeck/templates/configmap.yaml index cac6782ab9a..bd49b906760 100644 --- a/charts/gundeck/templates/configmap.yaml +++ b/charts/gundeck/templates/configmap.yaml @@ -55,7 +55,7 @@ data: settings: httpPoolSize: 1024 - notificationTTL: 2419200 + notificationTTL: {{ required "config.notificationTTL" .notificationTTL }} bulkPush: {{ .bulkPush }} {{- if hasKey . "perNativePushConcurrency" }} perNativePushConcurrency: {{ .perNativePushConcurrency }} diff --git a/charts/gundeck/values.yaml b/charts/gundeck/values.yaml index 75f3ce54ef7..80816a0eaad 100644 --- a/charts/gundeck/values.yaml +++ b/charts/gundeck/values.yaml @@ -57,6 +57,11 @@ config: # the database if notifications have inlined payloads. internalPageSize: 100 + # TTL of stored notifications in Seconds. After this period, notifications + # will be deleted and thus not delivered. + # The default is 28 days. + notificationTTL: 2419200 + serviceAccount: # When setting this to 'false', either make sure that a service account named # 'gundeck' exists or change the 'name' field to 'default' From 53989b55425e75fbc0532129710514282b8954db Mon Sep 17 00:00:00 2001 From: Igor Ranieri Elland <54423+elland@users.noreply.github.com> Date: Thu, 21 Mar 2024 11:09:28 +0100 Subject: [PATCH 063/117] Parallelise new integration tests (#3951) --- integration/test/Testlib/Run.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index b3275b0f7fd..0a50c6429a1 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -16,7 +16,6 @@ import Data.Functor import Data.List import Data.PEM import Data.Time.Clock -import Data.Traversable (for) import RunAllTests import System.Directory import System.Environment @@ -147,7 +146,8 @@ runTests tests mXMLOutput cfg = do runCodensity (createGlobalEnv cfg) $ \genv -> withAsync displayOutput $ \displayThread -> do - report <- fmap mconcat $ for tests $ \(qname, _, _, action) -> do + -- Currently 4 seems to be stable, more seems to create more timeouts. + report <- fmap mconcat $ pooledForConcurrentlyN 4 tests $ \(qname, _, _, action) -> do (mErr, tm) <- withTime (runTest genv action) case mErr of Left err -> do From 232a882bbacf6ec2ee482e52df2064ed6ed7413f Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Thu, 21 Mar 2024 11:48:30 +0100 Subject: [PATCH 064/117] Recover "WPB-5204 Remove unused APNS_VOIP code (#3695)" (#3967) * Recover "WPB-5204 Remove unused APNS_VOIP code (#3695)" This reverts commit 133a7409f152a60611eec9d97db11feb6551aa3a. * Fix: do not choke on deprecated APNSVoIP* transport tokens. --- .../wpb6583-clean-up-apns-cruft-in-gundeck | 1 + .../src/Gundeck/Types/Push/V2.hs | 3 -- libs/wire-api/src/Wire/API/Push/V2/Token.hs | 8 +---- .../golden/Test/Wire/API/Golden/Generated.hs | 6 ---- .../Push_2eToken_2eTransport_user.hs | 8 +---- .../golden/Test/Wire/API/Golden/Manual.hs | 4 --- .../Test/Wire/API/Golden/Manual/Token.hs | 29 ----------------- ...bject_Push_2eToken_2eTransport_user_4.json | 1 - ...bject_Push_2eToken_2eTransport_user_5.json | 1 - .../test/golden/testObject_Token_1.json | 6 ---- libs/wire-api/test/unit/Test/Wire/API/MLS.hs | 2 +- libs/wire-api/wire-api.cabal | 1 - services/brig/docs/swagger-v3.json | 4 +-- services/brig/docs/swagger-v4.json | 4 +-- services/gundeck/src/Gundeck/Aws.hs | 19 +++--------- services/gundeck/src/Gundeck/Aws/Arn.hs | 4 --- services/gundeck/src/Gundeck/Instances.hs | 23 +++++++------- services/gundeck/src/Gundeck/Push.hs | 23 +++----------- services/gundeck/src/Gundeck/Push/Data.hs | 31 ++++++++++++------- .../src/Gundeck/Push/Native/Serialise.hs | 9 ------ services/gundeck/test/integration/API.hs | 5 ++- services/gundeck/test/unit/Native.hs | 2 -- 22 files changed, 48 insertions(+), 146 deletions(-) create mode 100644 changelog.d/5-internal/wpb6583-clean-up-apns-cruft-in-gundeck delete mode 100644 libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Token.hs delete mode 100644 libs/wire-api/test/golden/testObject_Push_2eToken_2eTransport_user_4.json delete mode 100644 libs/wire-api/test/golden/testObject_Push_2eToken_2eTransport_user_5.json delete mode 100644 libs/wire-api/test/golden/testObject_Token_1.json diff --git a/changelog.d/5-internal/wpb6583-clean-up-apns-cruft-in-gundeck b/changelog.d/5-internal/wpb6583-clean-up-apns-cruft-in-gundeck new file mode 100644 index 00000000000..524f1e60a52 --- /dev/null +++ b/changelog.d/5-internal/wpb6583-clean-up-apns-cruft-in-gundeck @@ -0,0 +1 @@ +Remove support for push token transport types APNSVoIP, APNSVoIPSandbox from gundeck. \ No newline at end of file diff --git a/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs b/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs index 0dc12e508e8..aedfc7f0164 100644 --- a/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs +++ b/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs @@ -178,17 +178,14 @@ newtype ApsLocKey = ApsLocKey {fromLocKey :: Text} data ApsPreference = ApsStdPreference - | ApsVoIPPreference deriving (Eq, Show, Generic) deriving (Arbitrary) via GenericUniform ApsPreference instance ToJSON ApsPreference where - toJSON ApsVoIPPreference = "voip" toJSON ApsStdPreference = "std" instance FromJSON ApsPreference where parseJSON = withText "ApsPreference" $ \case - "voip" -> pure ApsVoIPPreference "std" -> pure ApsStdPreference x -> fail $ "Invalid preference: " ++ show x diff --git a/libs/wire-api/src/Wire/API/Push/V2/Token.hs b/libs/wire-api/src/Wire/API/Push/V2/Token.hs index 0cf7b292af4..79f282b4d0f 100644 --- a/libs/wire-api/src/Wire/API/Push/V2/Token.hs +++ b/libs/wire-api/src/Wire/API/Push/V2/Token.hs @@ -115,8 +115,6 @@ data Transport = GCM | APNS | APNSSandbox - | APNSVoIP - | APNSVoIPSandbox deriving stock (Eq, Ord, Show, Bounded, Enum, Generic) deriving (Arbitrary) via (GenericUniform Transport) deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema Transport) @@ -127,9 +125,7 @@ instance ToSchema Transport where mconcat [ element "GCM" GCM, element "APNS" APNS, - element "APNS_SANDBOX" APNSSandbox, - element "APNS_VOIP" APNSVoIP, - element "APNS_VOIP_SANDBOX" APNSVoIPSandbox + element "APNS_SANDBOX" APNSSandbox ] instance FromByteString Transport where @@ -138,8 +134,6 @@ instance FromByteString Transport where "GCM" -> pure GCM "APNS" -> pure APNS "APNS_SANDBOX" -> pure APNSSandbox - "APNS_VOIP" -> pure APNSVoIP - "APNS_VOIP_SANDBOX" -> pure APNSVoIPSandbox x -> fail $ "Invalid push transport: " <> show x newtype Token = Token diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs index a6003b36d81..52cde0922bd 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs @@ -791,12 +791,6 @@ tests = ), ( Test.Wire.API.Golden.Generated.Push_2eToken_2eTransport_user.testObject_Push_2eToken_2eTransport_user_3, "testObject_Push_2eToken_2eTransport_user_3.json" - ), - ( Test.Wire.API.Golden.Generated.Push_2eToken_2eTransport_user.testObject_Push_2eToken_2eTransport_user_4, - "testObject_Push_2eToken_2eTransport_user_4.json" - ), - ( Test.Wire.API.Golden.Generated.Push_2eToken_2eTransport_user.testObject_Push_2eToken_2eTransport_user_5, - "testObject_Push_2eToken_2eTransport_user_5.json" ) ], testGroup "Golden: Token_user" $ diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Push_2eToken_2eTransport_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Push_2eToken_2eTransport_user.hs index 96739ba620c..fc7c1ed7f14 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Push_2eToken_2eTransport_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/Push_2eToken_2eTransport_user.hs @@ -17,7 +17,7 @@ module Test.Wire.API.Golden.Generated.Push_2eToken_2eTransport_user where -import Wire.API.Push.Token (Transport (APNS, APNSSandbox, APNSVoIP, APNSVoIPSandbox, GCM)) +import Wire.API.Push.Token (Transport (APNS, APNSSandbox, GCM)) import Wire.API.Push.Token qualified as Push.Token (Transport) testObject_Push_2eToken_2eTransport_user_1 :: Push.Token.Transport @@ -28,9 +28,3 @@ testObject_Push_2eToken_2eTransport_user_2 = APNS testObject_Push_2eToken_2eTransport_user_3 :: Push.Token.Transport testObject_Push_2eToken_2eTransport_user_3 = APNSSandbox - -testObject_Push_2eToken_2eTransport_user_4 :: Push.Token.Transport -testObject_Push_2eToken_2eTransport_user_4 = APNSVoIP - -testObject_Push_2eToken_2eTransport_user_5 :: Push.Token.Transport -testObject_Push_2eToken_2eTransport_user_5 = APNSVoIPSandbox diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs index 40e7f101a28..f8f6b0f7ab6 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual.hs @@ -42,7 +42,6 @@ import Test.Wire.API.Golden.Manual.QualifiedUserClientPrekeyMap import Test.Wire.API.Golden.Manual.SearchResultContact import Test.Wire.API.Golden.Manual.SubConversation import Test.Wire.API.Golden.Manual.TeamSize -import Test.Wire.API.Golden.Manual.Token import Test.Wire.API.Golden.Manual.UserClientPrekeyMap import Test.Wire.API.Golden.Manual.UserEvent import Test.Wire.API.Golden.Manual.UserIdList @@ -145,9 +144,6 @@ tests = testGroup "GroupId" $ testObjects [(testObject_GroupId_1, "testObject_GroupId_1.json")], - testGroup "PushToken" $ - testObjects - [(testObject_Token_1, "testObject_Token_1.json")], testGroup "TeamSize" $ testObjects [ (testObject_TeamSize_1, "testObject_TeamSize_1.json"), diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Token.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Token.hs deleted file mode 100644 index 2fa8207ddc9..00000000000 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/Token.hs +++ /dev/null @@ -1,29 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Test.Wire.API.Golden.Manual.Token where - -import Data.Id -import Wire.API.Push.V2.Token - -testObject_Token_1 :: PushToken -testObject_Token_1 = - pushToken - APNSVoIPSandbox - (AppName {appNameText = "j{\110746\SOH_\1084873M"}) - (Token {tokenText = "K"}) - (ClientId {clientToWord64 = 6}) diff --git a/libs/wire-api/test/golden/testObject_Push_2eToken_2eTransport_user_4.json b/libs/wire-api/test/golden/testObject_Push_2eToken_2eTransport_user_4.json deleted file mode 100644 index d177fe0e9d7..00000000000 --- a/libs/wire-api/test/golden/testObject_Push_2eToken_2eTransport_user_4.json +++ /dev/null @@ -1 +0,0 @@ -"APNS_VOIP" diff --git a/libs/wire-api/test/golden/testObject_Push_2eToken_2eTransport_user_5.json b/libs/wire-api/test/golden/testObject_Push_2eToken_2eTransport_user_5.json deleted file mode 100644 index fd689b4ac10..00000000000 --- a/libs/wire-api/test/golden/testObject_Push_2eToken_2eTransport_user_5.json +++ /dev/null @@ -1 +0,0 @@ -"APNS_VOIP_SANDBOX" diff --git a/libs/wire-api/test/golden/testObject_Token_1.json b/libs/wire-api/test/golden/testObject_Token_1.json deleted file mode 100644 index 36f8ff69bd8..00000000000 --- a/libs/wire-api/test/golden/testObject_Token_1.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "app": "j{𛂚\u0001_􈷉M", - "client": "6", - "token": "K", - "transport": "APNS_VOIP_SANDBOX" -} diff --git a/libs/wire-api/test/unit/Test/Wire/API/MLS.hs b/libs/wire-api/test/unit/Test/Wire/API/MLS.hs index d0ff8a27a3b..b1f51f3b259 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/MLS.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/MLS.hs @@ -308,7 +308,7 @@ spawn cp minput = do in snd <$> concurrently writeInput readOutput case (mout, ex) of (Just out, ExitSuccess) -> pure out - _ -> assertFailure "Failed spawning process" + _ -> assertFailure $ "Failed spawning process\n" <> show mout <> "\n" <> show ex cli :: String -> FilePath -> [String] -> CreateProcess cli store tmp args = diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 81859cef566..625c8fec75e 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -587,7 +587,6 @@ test-suite wire-api-golden-tests Test.Wire.API.Golden.Manual.SearchResultContact Test.Wire.API.Golden.Manual.SubConversation Test.Wire.API.Golden.Manual.TeamSize - Test.Wire.API.Golden.Manual.Token Test.Wire.API.Golden.Manual.UserClientPrekeyMap Test.Wire.API.Golden.Manual.UserEvent Test.Wire.API.Golden.Manual.UserIdList diff --git a/services/brig/docs/swagger-v3.json b/services/brig/docs/swagger-v3.json index e252a739717..b844341e756 100644 --- a/services/brig/docs/swagger-v3.json +++ b/services/brig/docs/swagger-v3.json @@ -14,9 +14,7 @@ "enum": [ "GCM", "APNS", - "APNS_SANDBOX", - "APNS_VOIP", - "APNS_VOIP_SANDBOX" + "APNS_SANDBOX" ], "type": "string" }, diff --git a/services/brig/docs/swagger-v4.json b/services/brig/docs/swagger-v4.json index 937aafdefc9..7ff1394f344 100644 --- a/services/brig/docs/swagger-v4.json +++ b/services/brig/docs/swagger-v4.json @@ -4828,9 +4828,7 @@ "enum": [ "GCM", "APNS", - "APNS_SANDBOX", - "APNS_VOIP", - "APNS_VOIP_SANDBOX" + "APNS_SANDBOX" ], "type": "string" }, diff --git a/services/gundeck/src/Gundeck/Aws.hs b/services/gundeck/src/Gundeck/Aws.hs index ea5fe968866..b1636f33b7c 100644 --- a/services/gundeck/src/Gundeck/Aws.hs +++ b/services/gundeck/src/Gundeck/Aws.hs @@ -369,17 +369,12 @@ newtype Attributes = Attributes -- Note [VoIP TTLs] -- ~~~~~~~~~~~~~~~~ --- The TTL message attributes for APNS_VOIP and APNS_VOIP_SANDBOX are not --- documented but appear to work. The reason might be that TTLs were --- introduced before support for VoIP notifications. There is a catch, --- however. For GCM, APNS and APNS_SANDBOX, SNS treats the TTL "0" +-- For GCM, APNS and APNS_SANDBOX, SNS treats the TTL "0" -- specially, i.e. it forwards it to the provider where it has a special --- meaning. That does not appear to be the case for APNS_VOIP and --- APNS_VOIP_SANDBOX, for which the TTL is interpreted normally, which means --- if the TTL is lower than the "dwell time" in SNS, the notification is --- never sent to the provider. So we must specify a reasonably large TTL --- for transient VoIP notifications, so that they are not discarded --- already by SNS. +-- meaning. Which means if the TTL is lower than the "dwell time" in SNS, +-- the notification is never sent to the provider. So we must specify a +-- reasonably large TTL for transient VoIP notifications, so that they are +-- not discarded already by SNS. -- -- cf. http://docs.aws.amazon.com/sns/latest/dg/sns-ttl.html @@ -395,13 +390,9 @@ timeToLive t s = Attributes (Endo (ttlAttr s)) ttlNow GCM = "0" ttlNow APNS = "0" ttlNow APNSSandbox = "0" - ttlNow APNSVoIP = "15" -- See note [VoIP TTLs] - ttlNow APNSVoIPSandbox = "15" -- See note [VoIP TTLs] ttlKey GCM = "AWS.SNS.MOBILE.GCM.TTL" ttlKey APNS = "AWS.SNS.MOBILE.APNS.TTL" ttlKey APNSSandbox = "AWS.SNS.MOBILE.APNS_SANDBOX.TTL" - ttlKey APNSVoIP = "AWS.SNS.MOBILE.APNS_VOIP.TTL" - ttlKey APNSVoIPSandbox = "AWS.SNS.MOBILE.APNS_VOIP_SANDBOX.TTL" publish :: EndpointArn -> LT.Text -> Attributes -> Amazon (Either PublishError ()) publish arn txt attrs = do diff --git a/services/gundeck/src/Gundeck/Aws/Arn.hs b/services/gundeck/src/Gundeck/Aws/Arn.hs index 17588d08106..6c09b4bf362 100644 --- a/services/gundeck/src/Gundeck/Aws/Arn.hs +++ b/services/gundeck/src/Gundeck/Aws/Arn.hs @@ -135,8 +135,6 @@ arnTransportText :: Transport -> Text arnTransportText GCM = "GCM" arnTransportText APNS = "APNS" arnTransportText APNSSandbox = "APNS_SANDBOX" -arnTransportText APNSVoIP = "APNS_VOIP" -arnTransportText APNSVoIPSandbox = "APNS_VOIP_SANDBOX" -- Parsers -------------------------------------------------------------------- @@ -165,7 +163,5 @@ endpointTopicParser = do transportParser :: Parser Transport transportParser = string "GCM" $> GCM - <|> string "APNS_VOIP_SANDBOX" $> APNSVoIPSandbox - <|> string "APNS_VOIP" $> APNSVoIP <|> string "APNS_SANDBOX" $> APNSSandbox <|> string "APNS" $> APNS diff --git a/services/gundeck/src/Gundeck/Instances.hs b/services/gundeck/src/Gundeck/Instances.hs index 83ab2a692b4..8b5b334f15f 100644 --- a/services/gundeck/src/Gundeck/Instances.hs +++ b/services/gundeck/src/Gundeck/Instances.hs @@ -34,21 +34,22 @@ import Gundeck.Aws.Arn (EndpointArn) import Gundeck.Types import Imports -instance Cql Transport where +-- | We provide a instance for `Either Int Transport` so we can handle (ie., gracefully ignore +-- rather than crash on) deprecated values in cassandra. See "Gundeck.Push.Data". +instance Cql (Either Int32 Transport) where ctype = Tagged IntColumn - toCql GCM = CqlInt 0 - toCql APNS = CqlInt 1 - toCql APNSSandbox = CqlInt 2 - toCql APNSVoIP = CqlInt 3 - toCql APNSVoIPSandbox = CqlInt 4 + toCql (Right GCM) = CqlInt 0 + toCql (Right APNS) = CqlInt 1 + toCql (Right APNSSandbox) = CqlInt 2 + toCql (Left i) = CqlInt i -- (this is weird, but it's helpful for cleaning up deprecated tokens.) fromCql (CqlInt i) = case i of - 0 -> pure GCM - 1 -> pure APNS - 2 -> pure APNSSandbox - 3 -> pure APNSVoIP - 4 -> pure APNSVoIPSandbox + 0 -> pure $ Right GCM + 1 -> pure $ Right APNS + 2 -> pure $ Right APNSSandbox + 3 -> pure (Left 3) -- `APNSVoIPV1` tokens are deprecated and will be ignored + 4 -> pure (Left 4) -- `APNSVoIPSandboxV1` tokens are deprecated and will be ignored n -> Left $ "unexpected transport: " ++ show n fromCql _ = Left "transport: int expected" diff --git a/services/gundeck/src/Gundeck/Push.hs b/services/gundeck/src/Gundeck/Push.hs index 7beb3b56076..56ae375680e 100644 --- a/services/gundeck/src/Gundeck/Push.hs +++ b/services/gundeck/src/Gundeck/Push.hs @@ -374,31 +374,16 @@ nativeTargets psh rcps' alreadySent = null (psh ^. pushConnections) || a ^. addrConn `elem` psh ^. pushConnections -- Apply transport preference in case of alternative transports for the - -- same client (currently only APNS vs APNS VoIP). If no explicit - -- preference is given, the default preference depends on the priority. + -- same client. If no explicit preference is given, the default preference depends on the priority. preference as = let pref = psh ^. pushNativeAps >>= view apsPreference in filter (pick (fromMaybe defPreference pref)) as where pick pr a = case a ^. addrTransport of GCM -> True - APNS -> pr == ApsStdPreference || notAny a APNSVoIP - APNSSandbox -> pr == ApsStdPreference || notAny a APNSVoIPSandbox - APNSVoIP -> pr == ApsVoIPPreference || notAny a APNS - APNSVoIPSandbox -> pr == ApsVoIPPreference || notAny a APNSSandbox - notAny a t = - not - ( any - ( \a' -> - addrEqualClient a a' - && a ^. addrApp == a' ^. addrApp - && a' ^. addrTransport == t - ) - as - ) - defPreference = case psh ^. pushNativePriority of - LowPriority -> ApsStdPreference - HighPriority -> ApsVoIPPreference + APNS -> pr == ApsStdPreference + APNSSandbox -> pr == ApsStdPreference + defPreference = ApsStdPreference check :: Either SomeException [a] -> m [a] check (Left e) = mntgtLogErr e >> pure [] check (Right r) = pure r diff --git a/services/gundeck/src/Gundeck/Push/Data.hs b/services/gundeck/src/Gundeck/Push/Data.hs index c688f64f4db..fa495b0e1fe 100644 --- a/services/gundeck/src/Gundeck/Push/Data.hs +++ b/services/gundeck/src/Gundeck/Push/Data.hs @@ -38,26 +38,29 @@ import System.Logger.Class qualified as Log lookup :: (MonadClient m, MonadLogger m) => UserId -> Consistency -> m [Address] lookup u c = foldM mk [] =<< retry x1 (query q (params c (Identity u))) where - q :: PrepQuery R (Identity UserId) (UserId, Transport, AppName, Token, Maybe EndpointArn, ConnId, Maybe ClientId) + q :: PrepQuery R (Identity UserId) (UserId, Either Int32 Transport, AppName, Token, Maybe EndpointArn, ConnId, Maybe ClientId) q = "select usr, transport, app, ptoken, arn, connection, client from user_push where usr = ?" mk as r = maybe as (: as) <$> mkAddr r insert :: MonadClient m => UserId -> Transport -> AppName -> Token -> EndpointArn -> ConnId -> ClientId -> m () -insert u t a p e o c = retry x5 $ write q (params LocalQuorum (u, t, a, p, e, o, c)) +insert u t a p e o c = retry x5 $ write q (params LocalQuorum (u, Right t, a, p, e, o, c)) where - q :: PrepQuery W (UserId, Transport, AppName, Token, EndpointArn, ConnId, ClientId) () + q :: PrepQuery W (UserId, Either Int32 Transport, AppName, Token, EndpointArn, ConnId, ClientId) () q = "insert into user_push (usr, transport, app, ptoken, arn, connection, client) values (?, ?, ?, ?, ?, ?, ?)" updateArn :: MonadClient m => UserId -> Transport -> AppName -> Token -> EndpointArn -> m () -updateArn uid transport app token arn = retry x5 $ write q (params LocalQuorum (arn, uid, transport, app, token)) +updateArn uid transport app token arn = retry x5 $ write q (params LocalQuorum (arn, uid, Right transport, app, token)) where - q :: PrepQuery W (EndpointArn, UserId, Transport, AppName, Token) () + q :: PrepQuery W (EndpointArn, UserId, Either Int32 Transport, AppName, Token) () q = {- `IF EXISTS`, but that requires benchmarking -} "update user_push set arn = ? where usr = ? and transport = ? and app = ? and ptoken = ?" delete :: MonadClient m => UserId -> Transport -> AppName -> Token -> m () -delete u t a p = retry x5 $ write q (params LocalQuorum (u, t, a, p)) +delete u t = deleteAux u (Right t) + +deleteAux :: MonadClient m => UserId -> Either Int32 Transport -> AppName -> Token -> m () +deleteAux u t a p = retry x5 $ write q (params LocalQuorum (u, t, a, p)) where - q :: PrepQuery W (UserId, Transport, AppName, Token) () + q :: PrepQuery W (UserId, Either Int32 Transport, AppName, Token) () q = "delete from user_push where usr = ? and transport = ? and app = ? and ptoken = ?" erase :: MonadClient m => UserId -> m () @@ -68,16 +71,20 @@ erase u = retry x5 $ write q (params LocalQuorum (Identity u)) mkAddr :: (MonadClient m, MonadLogger m) => - (UserId, Transport, AppName, Token, Maybe EndpointArn, ConnId, Maybe ClientId) -> + (UserId, Either Int32 Transport, AppName, Token, Maybe EndpointArn, ConnId, Maybe ClientId) -> m (Maybe Address) -mkAddr (usr, trp, app, tok, arn, con, clt) = case (clt, arn) of - (Just c, Just a) -> pure $! Just $! Address usr a con (pushToken trp app tok c) +mkAddr (usr, trp, app, tok, arn, con, clt) = case (trp, clt, arn) of + (Right t, Just c, Just a) -> pure $! Just $! Address usr a con (pushToken t app tok c) _ -> do Log.info $ field "user" (toByteString usr) ~~ field "transport" (show trp) ~~ field "app" (appNameText app) ~~ field "token" (tokenText tok) - ~~ msg (val "Deleting legacy push token without a client or ARN.") - delete usr trp app tok + ~~ msg + ( val + "Deleting legacy push token without a client or ARN, or with deprecated \ + \APNSVoIP* transports (transport type not shown in this message)." + ) + deleteAux usr trp app tok pure Nothing diff --git a/services/gundeck/src/Gundeck/Push/Native/Serialise.hs b/services/gundeck/src/Gundeck/Push/Native/Serialise.hs index bf9e0e491cc..07f783c36d9 100644 --- a/services/gundeck/src/Gundeck/Push/Native/Serialise.hs +++ b/services/gundeck/src/Gundeck/Push/Native/Serialise.hs @@ -54,8 +54,6 @@ renderText t prio x = case t of GCM -> trim "GCM" (jsonString gcmJson) APNS -> trim "APNS" (jsonString stdApnsJson) APNSSandbox -> trim "APNS_SANDBOX" (jsonString stdApnsJson) - APNSVoIP -> trim "APNS_VOIP" (jsonString voipApnsJson) - APNSVoIPSandbox -> trim "APNS_VOIP_SANDBOX" (jsonString voipApnsJson) where gcmJson = object @@ -67,11 +65,6 @@ renderText t prio x = case t of [ "aps" .= apsDict, "data" .= x ] - voipApnsJson = - object - [ "aps" .= object [], - "data" .= x - ] -- https://developer.apple.com/documentation/usernotifications/modifying_content_in_newly_delivered_notifications -- Must contain `mutable-content: 1` and include an alert dictionary with title, subtitle, or body information. -- Since we have no useful data here, we send a default payload that gets overridden by the client @@ -94,8 +87,6 @@ maxPayloadSize :: Transport -> Int64 maxPayloadSize GCM = 4096 maxPayloadSize APNS = 4096 maxPayloadSize APNSSandbox = 4096 -maxPayloadSize APNSVoIP = 5120 -maxPayloadSize APNSVoIPSandbox = 5120 gcmPriority :: Priority -> Text gcmPriority LowPriority = "normal" diff --git a/services/gundeck/test/integration/API.hs b/services/gundeck/test/integration/API.hs index b0c3a63186f..4ef4bd327b8 100644 --- a/services/gundeck/test/integration/API.hs +++ b/services/gundeck/test/integration/API.hs @@ -838,9 +838,8 @@ testSharePushToken = do gcmTok <- Token . T.decodeUtf8 . toByteString' <$> randomId apsTok <- Token . T.decodeUtf8 . B16.encode <$> randomBytes 32 let tok1 = pushToken GCM "test" gcmTok - let tok2 = pushToken APNSVoIP "com.wire.dev.ent" apsTok - let tok3 = pushToken APNS "com.wire.int.ent" apsTok - forM_ [tok1, tok2, tok3] $ \tk -> do + let tok2 = pushToken APNS "com.wire.int.ent" apsTok + forM_ [tok1, tok2] $ \tk -> do u1 <- randomUser u2 <- randomUser c1 <- randomClientId diff --git a/services/gundeck/test/unit/Native.hs b/services/gundeck/test/unit/Native.hs index 2e525f7cf1f..500ec668ff6 100644 --- a/services/gundeck/test/unit/Native.hs +++ b/services/gundeck/test/unit/Native.hs @@ -73,8 +73,6 @@ instance FromJSON SnsNotification where [("GCM", String n)] -> parseGcm n [("APNS", String n)] -> parseApns APNS n [("APNS_SANDBOX", String n)] -> parseApns APNSSandbox n - [("APNS_VOIP", String n)] -> parseApns APNSVoIP n - [("APNS_VOIP_SANDBOX", String n)] -> parseApns APNSVoIPSandbox n _ -> mempty where parseApns t n = From d06867831a0b4b5c22d4e8647a194c2547423243 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Mon, 25 Mar 2024 15:16:37 +0100 Subject: [PATCH 065/117] WPB-6717 ES Credentials (#3959) --- Makefile | 12 +++--- changelog.d/2-features/WPB-6717 | 1 + charts/brig/templates/configmap.yaml | 6 +++ charts/brig/templates/secret.yaml | 6 +++ .../elasticsearch-ephemeral/templates/es.yaml | 5 +++ .../templates/create-index.yaml | 33 ++++++++++++--- .../templates/migrate-data.yaml | 19 +++++++-- .../elasticsearch-index/templates/secret.yaml | 20 +++++++++ charts/elasticsearch-index/values.yaml | 2 + .../templates/integration-integration.yaml | 2 +- deploy/dockerephemeral/docker-compose.yaml | 16 ++++---- .../dockerephemeral/federation-v0/brig.yaml | 3 +- .../src/developer/reference/config-options.md | 28 +++++++++++++ hack/helm_vars/wire-server/values.yaml.gotmpl | 10 +++++ libs/types-common/src/Data/Credentials.hs | 31 ++++++++++++++ libs/types-common/src/Util/Options.hs | 11 +++-- libs/types-common/types-common.cabal | 1 + services/brig/brig.integration.yaml | 2 + services/brig/src/Brig/App.hs | 25 ++++++----- services/brig/src/Brig/Index/Eval.hs | 41 ++++++++++++------- services/brig/src/Brig/Index/Migrations.hs | 15 ++++--- services/brig/src/Brig/Index/Options.hs | 31 +++++++++++--- services/brig/src/Brig/Options.hs | 6 ++- services/brig/test/integration/API/Search.hs | 8 ++-- .../brig/test/integration/API/Search/Util.hs | 6 +++ .../brig/test/integration/Index/Create.hs | 19 ++++++--- .../resources/elasticsearch-credentials.yaml | 2 + 27 files changed, 287 insertions(+), 74 deletions(-) create mode 100644 changelog.d/2-features/WPB-6717 create mode 100644 charts/elasticsearch-index/templates/secret.yaml create mode 100644 libs/types-common/src/Data/Credentials.hs create mode 100644 services/brig/test/resources/elasticsearch-credentials.yaml diff --git a/Makefile b/Makefile index 17e11e3aec0..f774012f3e6 100644 --- a/Makefile +++ b/Makefile @@ -294,9 +294,9 @@ db-reset: c ./dist/gundeck-schema --keyspace gundeck_test2 --replication-factor 1 --reset ./dist/spar-schema --keyspace spar_test2 --replication-factor 1 --reset ./integration/scripts/integration-dynamic-backends-db-schemas.sh --replication-factor 1 --reset - ./dist/brig-index reset --elasticsearch-index-prefix directory --elasticsearch-server http://localhost:9200 > /dev/null - ./dist/brig-index reset --elasticsearch-index-prefix directory2 --elasticsearch-server http://localhost:9200 > /dev/null - ./integration/scripts/integration-dynamic-backends-brig-index.sh --elasticsearch-server http://localhost:9200 > /dev/null + ./dist/brig-index reset --elasticsearch-index-prefix directory --elasticsearch-server http://localhost:9200 --elasticsearch-credentials ./services/brig/test/resources/elasticsearch-credentials.yaml > /dev/null + ./dist/brig-index reset --elasticsearch-index-prefix directory2 --elasticsearch-server http://localhost:9200 --elasticsearch-credentials ./services/brig/test/resources/elasticsearch-credentials.yaml > /dev/null + ./integration/scripts/integration-dynamic-backends-brig-index.sh --elasticsearch-server http://localhost:9200 --elasticsearch-credentials ./services/brig/test/resources/elasticsearch-credentials.yaml > /dev/null @@ -312,9 +312,9 @@ db-migrate: c ./dist/gundeck-schema --keyspace gundeck_test2 --replication-factor 1 > /dev/null ./dist/spar-schema --keyspace spar_test2 --replication-factor 1 > /dev/null ./integration/scripts/integration-dynamic-backends-db-schemas.sh --replication-factor 1 > /dev/null - ./dist/brig-index reset --elasticsearch-index-prefix directory --elasticsearch-server http://localhost:9200 > /dev/null - ./dist/brig-index reset --elasticsearch-index-prefix directory2 --elasticsearch-server http://localhost:9200 > /dev/null - ./integration/scripts/integration-dynamic-backends-brig-index.sh --elasticsearch-server http://localhost:9200 > /dev/null + ./dist/brig-index reset --elasticsearch-index-prefix directory --elasticsearch-server http://localhost:9200 --elasticsearch-credentials ./services/brig/test/resources/elasticsearch-credentials.yaml > /dev/null + ./dist/brig-index reset --elasticsearch-index-prefix directory2 --elasticsearch-server http://localhost:9200 --elasticsearch-credentials ./services/brig/test/resources/elasticsearch-credentials.yaml > /dev/null + ./integration/scripts/integration-dynamic-backends-brig-index.sh --elasticsearch-server http://localhost:9200 --elasticsearch-credentials ./services/brig/test/resources/elasticsearch-credentials.yaml > /dev/null ################################# ## dependencies diff --git a/changelog.d/2-features/WPB-6717 b/changelog.d/2-features/WPB-6717 new file mode 100644 index 00000000000..6720334b245 --- /dev/null +++ b/changelog.d/2-features/WPB-6717 @@ -0,0 +1 @@ +Support for Elasticsearch password authentication diff --git a/charts/brig/templates/configmap.yaml b/charts/brig/templates/configmap.yaml index e128169dbe4..7065407f57c 100644 --- a/charts/brig/templates/configmap.yaml +++ b/charts/brig/templates/configmap.yaml @@ -38,6 +38,12 @@ data: {{- if .elasticsearch.additionalWriteIndex }} additionalWriteIndex: {{ .elasticsearch.additionalWriteIndex }} {{- end }} + {{- if $.Values.secrets.elasticsearch }} + credentials: /etc/wire/brig/secrets/elasticsearch-credentials.yaml + {{- end }} + {{- if $.Values.secrets.elasticsearchAdditional }} + additionalCredentials: /etc/wire/brig/secrets/elasticsearch-additional-credentials.yaml + {{- end }} cargohold: host: cargohold diff --git a/charts/brig/templates/secret.yaml b/charts/brig/templates/secret.yaml index a4e51228b60..c2359979f57 100644 --- a/charts/brig/templates/secret.yaml +++ b/charts/brig/templates/secret.yaml @@ -35,4 +35,10 @@ data: rabbitmqUsername: {{ .rabbitmq.username | b64enc | quote }} rabbitmqPassword: {{ .rabbitmq.password | b64enc | quote }} {{- end }} + {{- if .elasticsearch }} + elasticsearch-credentials.yaml: {{ .elasticsearch | toYaml | b64enc }} + {{- end }} + {{- if .elasticsearchAdditional }} + elasticsearch-additional-credentials.yaml: {{ .elasticsearchAdditional | toYaml | b64enc }} + {{- end }} {{- end }} diff --git a/charts/elasticsearch-ephemeral/templates/es.yaml b/charts/elasticsearch-ephemeral/templates/es.yaml index 79526560ad1..4a82cbd28bf 100644 --- a/charts/elasticsearch-ephemeral/templates/es.yaml +++ b/charts/elasticsearch-ephemeral/templates/es.yaml @@ -32,6 +32,11 @@ spec: value: "single-node" - name: "action.auto_create_index" value: ".watches,.triggered_watches,.watcher-history-*,pod-*,node-*" + - name: "xpack.security.enabled" + value: "true" + # setting the password here is ok, as this chart is only used for integration tests on CI + - name: "ELASTIC_PASSWORD" + value: "changeme" ports: - containerPort: 9200 name: http diff --git a/charts/elasticsearch-index/templates/create-index.yaml b/charts/elasticsearch-index/templates/create-index.yaml index 9e2c5fda798..19ddd6854e0 100644 --- a/charts/elasticsearch-index/templates/create-index.yaml +++ b/charts/elasticsearch-index/templates/create-index.yaml @@ -21,20 +21,34 @@ spec: chart: "{{.Chart.Name}}-{{.Chart.Version}}" spec: restartPolicy: OnFailure + {{- if hasKey .Values.secrets "elasticsearch" }} + volumes: + - name: elasticsearch-index-secrets + secret: + secretName: elasticsearch-index + {{- end }} initContainers: # Creates index in elasticsearch only when it doesn't exist. # Does nothing if the index exists. - name: brig-index-create image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" - imagePullPolicy: {{ default "" .Values.imagePullPolicy | quote }} - {{- if eq (include "includeSecurityContext" .) "true" }} + {{- if hasKey .Values.secrets "elasticsearch" }} + volumeMounts: + - name: "elasticsearch-index-secrets" + mountPath: "/etc/wire/elasticsearch-index/secrets" + {{- end }} + {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 12 }} - {{- end }} + {{- end }} args: - create - --elasticsearch-server - "http://{{ required "missing elasticsearch-index.elasticsearch.host!" .Values.elasticsearch.host }}:{{ .Values.elasticsearch.port }}" + {{- if hasKey .Values.secrets "elasticsearch" }} + - --elasticsearch-credentials + - "/etc/wire/elasticsearch-index/secrets/elasticsearch-credentials.yaml" + {{- end }} - --elasticsearch-index - "{{ or (.Values.elasticsearch.additionalWriteIndex) (.Values.elasticsearch.index) }}" - --elasticsearch-shards=5 @@ -48,13 +62,22 @@ spec: - name: brig-index-update-mapping image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" imagePullPolicy: {{ default "" .Values.imagePullPolicy | quote }} - {{- if eq (include "includeSecurityContext" .) "true" }} + {{- if hasKey .Values.secrets "elasticsearch" }} + volumeMounts: + - name: "elasticsearch-index-secrets" + mountPath: "/etc/wire/elasticsearch-index/secrets" + {{- end }} + {{- if eq (include "includeSecurityContext" .) "true" }} securityContext: {{- toYaml .Values.podSecurityContext | nindent 12 }} - {{- end }} + {{- end }} args: - update-mapping - --elasticsearch-server - "http://{{ required "missing elasticsearch-index.elasticsearch.host!" .Values.elasticsearch.host }}:{{ .Values.elasticsearch.port }}" + {{- if hasKey .Values.secrets "elasticsearch" }} + - --elasticsearch-credentials + - "/etc/wire/elasticsearch-index/secrets/elasticsearch-credentials.yaml" + {{- end }} - --elasticsearch-index - "{{ or (.Values.elasticsearch.additionalWriteIndex) (.Values.elasticsearch.index) }}" diff --git a/charts/elasticsearch-index/templates/migrate-data.yaml b/charts/elasticsearch-index/templates/migrate-data.yaml index 3d54e1f51b8..b3cb21dae3a 100644 --- a/charts/elasticsearch-index/templates/migrate-data.yaml +++ b/charts/elasticsearch-index/templates/migrate-data.yaml @@ -31,6 +31,10 @@ spec: - migrate-data - --elasticsearch-server - "http://{{ required "missing elasticsearch-index.elasticsearch.host!" .Values.elasticsearch.host }}:{{ .Values.elasticsearch.port }}" + {{- if hasKey .Values.secrets "elasticsearch" }} + - --elasticsearch-credentials + - "/etc/wire/elasticsearch-index/secrets/elasticsearch-credentials.yaml" + {{- end }} - --elasticsearch-index - "{{ or (.Values.elasticsearch.additionalWriteIndex) (.Values.elasticsearch.index) }}" - --cassandra-host @@ -47,14 +51,23 @@ spec: - --tls-ca-certificate-file - /certs/{{- (include "tlsSecretRef" .Values | fromYaml).key }} {{- end }} - {{- if eq (include "useCassandraTLS" .Values) "true" }} volumeMounts: + {{- if hasKey .Values.secrets "elasticsearch" }} + - name: "elasticsearch-index-secrets" + mountPath: "/etc/wire/elasticsearch-index/secrets" + {{- end }} + {{- if eq (include "useCassandraTLS" .Values) "true" }} - name: elasticsearch-index-migrate-cassandra-client-ca mountPath: "/certs" {{- end }} - {{- if eq (include "useCassandraTLS" .Values) "true" }} volumes: + {{- if hasKey .Values.secrets "elasticsearch" }} + - name: elasticsearch-index-secrets + secret: + secretName: elasticsearch-index + {{- end }} + {{- if eq (include "useCassandraTLS" .Values) "true" }} - name: elasticsearch-index-migrate-cassandra-client-ca secret: secretName: {{ (include "tlsSecretRef" .Values | fromYaml).name }} - {{- end}} + {{- end}} diff --git a/charts/elasticsearch-index/templates/secret.yaml b/charts/elasticsearch-index/templates/secret.yaml new file mode 100644 index 00000000000..cda93a046bc --- /dev/null +++ b/charts/elasticsearch-index/templates/secret.yaml @@ -0,0 +1,20 @@ +{{- if hasKey .Values.secrets "elasticsearch" }} +apiVersion: v1 +kind: Secret +metadata: + name: elasticsearch-index + labels: + chart: {{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }} + release: "{{ .Release.Name }}" + heritage: "{{ .Release.Service }}" + annotations: + "helm.sh/hook": pre-install,pre-upgrade + "helm.sh/hook-delete-policy": "before-hook-creation" +type: Opaque +data: + {{- with .Values.secrets }} + {{- if .elasticsearch }} + elasticsearch-credentials.yaml: {{ .elasticsearch | toYaml | b64enc }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/elasticsearch-index/values.yaml b/charts/elasticsearch-index/values.yaml index 93e8a97ef6f..876edd92e4a 100644 --- a/charts/elasticsearch-index/values.yaml +++ b/charts/elasticsearch-index/values.yaml @@ -30,3 +30,5 @@ podSecurityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault + +secrets: {} diff --git a/charts/integration/templates/integration-integration.yaml b/charts/integration/templates/integration-integration.yaml index 5199c03def4..5f6a98f7595 100644 --- a/charts/integration/templates/integration-integration.yaml +++ b/charts/integration/templates/integration-integration.yaml @@ -127,7 +127,7 @@ spec: --tls-ca-certificate-file /certs/{{- (include "tlsSecretRef" .Values.config | fromYaml).key }} {{ end }} - integration-dynamic-backends-brig-index.sh --elasticsearch-server http://{{ .Values.config.elasticsearch.host }}:9200 + integration-dynamic-backends-brig-index.sh --elasticsearch-server http://elastic:changeme@{{ .Values.config.elasticsearch.host }}:9200 integration-dynamic-backends-ses.sh {{ .Values.config.sesEndpointUrl }} integration-dynamic-backends-s3.sh {{ .Values.config.s3EndpointUrl }} {{- range $name, $dynamicBackend := .Values.config.dynamicBackends }} diff --git a/deploy/dockerephemeral/docker-compose.yaml b/deploy/dockerephemeral/docker-compose.yaml index 2ac1a1843e8..2ad299a6d19 100644 --- a/deploy/dockerephemeral/docker-compose.yaml +++ b/deploy/dockerephemeral/docker-compose.yaml @@ -159,10 +159,13 @@ services: elasticsearch: container_name: demo_wire_elasticsearch - #image: elasticsearch:5.6 - image: julialongtin/elasticsearch:0.0.9-amd64 - # https://hub.docker.com/_/elastic is deprecated, but 6.2.4 did not work without further changes. - # image: docker.elastic.co/elasticsearch/elasticsearch:6.2.4 + build: + context: . + dockerfile_inline: | + FROM julialongtin/elasticsearch:0.0.9-amd64 + RUN /usr/share/elasticsearch/bin/elasticsearch-plugin install x-pack -b + # this seems to be necessary to run X-Pack on Alpine (https://discuss.elastic.co/t/elasticsearch-failing-to-start-due-to-x-pack/85125/7) + RUN rm -rf /usr/share/elasticsearch/plugins/x-pack/platform/linux-x86_64 ulimits: nofile: soft: 65536 @@ -171,10 +174,9 @@ services: - "127.0.0.1:9200:9200" - "127.0.0.1:9300:9300" environment: + - "xpack.ml.enabled=false" + - "xpack.security.enabled=true" - "bootstrap.system_call_filter=false" -# ES_JVM_OPTIONS is reserved, so... -# what's present in the jvm.options file by default. -# - "JVM_OPTIONS_ES=-Xmx2g -Xms2g" - "JVM_OPTIONS_ES=-Xmx512m -Xms512m" - "discovery.type=single-node" networks: diff --git a/deploy/dockerephemeral/federation-v0/brig.yaml b/deploy/dockerephemeral/federation-v0/brig.yaml index 06dfefe80e3..6c2216b3c1a 100644 --- a/deploy/dockerephemeral/federation-v0/brig.yaml +++ b/deploy/dockerephemeral/federation-v0/brig.yaml @@ -10,7 +10,8 @@ cassandra: # filterNodesByDatacentre: datacenter1 elasticsearch: - url: http://demo_wire_elasticsearch:9200 + # FUTUREWORK: use separate ES v0 instance + url: http://elastic:changeme@demo_wire_elasticsearch:9200 index: directory_test rabbitmq: diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 9b376c49e8c..857dee00806 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -853,3 +853,31 @@ accessible to services (and not the private key.) The corresponding Cassandra options are described in Cassandra's documentation: [client_encryption_options](https://cassandra.apache.org/doc/stable/cassandra/configuration/cass_yaml_file.html#client_encryption_options) + +## Configure Elasticsearch basic authentication + +When the Wire backend is configured to work against a custom Elasticsearch instance, it may be desired to enable basic authentication for the internal communication between the Wire backend and the ES instance. To do so the Elasticsearch credentials can be set in wire-server's secrets for `brig` and `elasticsearch-index` as follows: + +```yaml +brig: + secrets: + elasticsearch: + username: elastic + password: changeme + +elasticsearch-index: + secrets: + elasticsearch: + username: elastic + password: changeme +``` + +In some cases an additional Elasticsearch instance is needed (e.g. for index migrations). To configure credentials for the additional ES instance add the secret as follows: + +```yaml +brig: + secrets: + elasticsearchAdditional: + username: elastic + password: changeme +``` diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 437159b7263..d215f8efd6a 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -38,6 +38,10 @@ elasticsearch-index: name: "cassandra-jks-keystore" key: "ca.crt" {{- end }} + secrets: + elasticsearch: + username: "elastic" + password: "changeme" brig: replicaCount: 1 @@ -151,6 +155,12 @@ brig: rabbitmq: username: {{ .Values.rabbitmqUsername }} password: {{ .Values.rabbitmqPassword }} + elasticsearch: + username: "elastic" + password: "changeme" + elasticsearchAdditional: + username: "elastic" + password: "changeme" tests: enableFederationTests: true {{- if .Values.uploadXml }} diff --git a/libs/types-common/src/Data/Credentials.hs b/libs/types-common/src/Data/Credentials.hs new file mode 100644 index 00000000000..5423b574e7a --- /dev/null +++ b/libs/types-common/src/Data/Credentials.hs @@ -0,0 +1,31 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2024 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Data.Credentials where + +import Data.Aeson (FromJSON) +import Data.Text +import Imports + +-- | Generic credentials for authenticating a user. Usually used for deserializing from a secret yaml file. +data Credentials = Credentials + { username :: Text, + password :: Text + } + deriving stock (Generic) + +instance FromJSON Credentials diff --git a/libs/types-common/src/Util/Options.hs b/libs/types-common/src/Util/Options.hs index 74437b78a27..f9beac14583 100644 --- a/libs/types-common/src/Util/Options.hs +++ b/libs/types-common/src/Util/Options.hs @@ -79,14 +79,19 @@ urlPort u = do makeLenses ''AWSEndpoint newtype FilePathSecrets = FilePathSecrets FilePath - deriving (Eq, Show, FromJSON) + deriving (Eq, Show, FromJSON, IsString) -loadSecret :: FromJSON a => FilePathSecrets -> IO (Either String a) +initCredentials :: (MonadIO m, FromJSON a) => FilePathSecrets -> m a +initCredentials secretFile = do + dat <- loadSecret secretFile + pure $ either (\e -> error $ "Could not load secrets from " ++ show secretFile ++ ": " ++ e) id dat + +loadSecret :: (MonadIO m, FromJSON a) => FilePathSecrets -> m (Either String a) loadSecret (FilePathSecrets p) = do path <- canonicalizePath p exists <- doesFileExist path if exists - then over _Left show . decodeEither' <$> BS.readFile path + then liftIO $ over _Left show . decodeEither' <$> BS.readFile path else pure (Left "File doesn't exist") -- | Get configuration options from the command line or configuration file. diff --git a/libs/types-common/types-common.cabal b/libs/types-common/types-common.cabal index 0c13025aabb..dc15cfbc2e2 100644 --- a/libs/types-common/types-common.cabal +++ b/libs/types-common/types-common.cabal @@ -15,6 +15,7 @@ library exposed-modules: Data.Code Data.CommaSeparatedList + Data.Credentials Data.Domain Data.ETag Data.Handle diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml index 71e83cf664c..a536c77626d 100644 --- a/services/brig/brig.integration.yaml +++ b/services/brig/brig.integration.yaml @@ -12,6 +12,8 @@ cassandra: elasticsearch: url: http://127.0.0.1:9200 index: directory_test + credentials: test/resources/elasticsearch-credentials.yaml + additionalCredentials: test/resources/elasticsearch-credentials.yaml rabbitmq: host: 127.0.0.1 diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 4467ad7c34f..b9f5a099cfc 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -96,7 +96,7 @@ import Bilge.IO import Bilge.RPC (HasRequestId (..)) import Brig.AWS qualified as AWS import Brig.Calling qualified as Calling -import Brig.Options (Opts, Settings) +import Brig.Options (Opts, Settings (..)) import Brig.Options qualified as Opt import Brig.Provider.Template import Brig.Queue.Stomp qualified as Stomp @@ -118,6 +118,7 @@ import Control.Lens hiding (index, (.=)) import Control.Monad.Catch import Control.Monad.Trans.Resource import Data.ByteString.Conversion +import Data.Credentials (Credentials (..)) import Data.Domain import Data.Metrics (Metrics) import Data.Metrics.Middleware qualified as Metrics @@ -128,7 +129,6 @@ import Data.Text.Encoding (encodeUtf8) import Data.Text.Encoding qualified as Text import Data.Text.IO qualified as Text import Data.Time.Clock -import Data.Yaml (FromJSON) import Database.Bloodhound qualified as ES import HTTP2.Client.Manager (Http2Manager, http2ManagerWithSSLCtx) import Imports @@ -259,6 +259,8 @@ newEnv o = do kpLock <- newMVar () rabbitChan <- traverse (Q.mkRabbitMqChannelMVar lgr) o.rabbitmq let allDisabledVersions = foldMap expandVersionExp (Opt.setDisabledAPIVersions sett) + mEsCreds <- for (Opt.credentials (Opt.elasticsearch o)) initCredentials + mEsAddCreds <- for (Opt.additionalCredentials (Opt.elasticsearch o)) initCredentials pure $! Env @@ -294,7 +296,7 @@ newEnv o = do _zauthEnv = zau, _digestMD5 = md5, _digestSHA256 = sha256, - _indexEnv = mkIndexEnv o lgr mgr mtr (Opt.galley o), + _indexEnv = mkIndexEnv o lgr mgr mtr mEsCreds mEsAddCreds (Opt.galley o), _randomPrekeyLocalLock = prekeyLocalLock, _keyPackageLocalLock = kpLock, _rabbitmqChannel = rabbitChan, @@ -313,14 +315,16 @@ newEnv o = do pure (Nothing, Just smtp) mkEndpoint service = RPC.host (encodeUtf8 (service ^. host)) . RPC.port (service ^. port) $ RPC.empty -mkIndexEnv :: Opts -> Logger -> Manager -> Metrics -> Endpoint -> IndexEnv -mkIndexEnv o lgr mgr mtr galleyEp = - let bhe = ES.mkBHEnv (ES.Server (Opt.url (Opt.elasticsearch o))) mgr +mkIndexEnv :: Opts -> Logger -> Manager -> Metrics -> Maybe Credentials -> Maybe Credentials -> Endpoint -> IndexEnv +mkIndexEnv o lgr mgr mtr mCreds mAddCreds galleyEp = + let mkBhe url mcs = + let bhe = ES.mkBHEnv (ES.Server url) mgr + in maybe bhe (\creds -> bhe {ES.bhRequestHook = ES.basicAuthHook (ES.EsUsername creds.username) (ES.EsPassword creds.password)}) mcs lgr' = Log.clone (Just "index.brig") lgr mainIndex = ES.IndexName $ Opt.index (Opt.elasticsearch o) additionalIndex = ES.IndexName <$> Opt.additionalWriteIndex (Opt.elasticsearch o) - additionalBhe = flip ES.mkBHEnv mgr . ES.Server <$> Opt.additionalWriteIndexUrl (Opt.elasticsearch o) - in IndexEnv mtr lgr' bhe Nothing mainIndex additionalIndex additionalBhe galleyEp mgr + additionalBhe = flip mkBhe mAddCreds <$> Opt.additionalWriteIndexUrl (Opt.elasticsearch o) + in IndexEnv mtr lgr' (mkBhe (Opt.url (Opt.elasticsearch o)) mCreds) Nothing mainIndex additionalIndex additionalBhe galleyEp mgr initZAuth :: Opts -> IO ZAuth.Env initZAuth o = do @@ -409,11 +413,6 @@ initCassandra o g = (Just schemaVersion) g -initCredentials :: (FromJSON a) => FilePathSecrets -> IO a -initCredentials secretFile = do - dat <- loadSecret secretFile - pure $ either (\e -> error $ "Could not load secrets from " ++ show secretFile ++ ": " ++ e) id dat - userTemplates :: (MonadReader Env m) => Maybe Locale -> m (Locale, UserTemplates) userTemplates l = forLocale l <$> view usrTemplates diff --git a/services/brig/src/Brig/Index/Eval.hs b/services/brig/src/Brig/Index/Eval.hs index ed412d8d0d2..8cae3079b05 100644 --- a/services/brig/src/Brig/Index/Eval.hs +++ b/services/brig/src/Brig/Index/Eval.hs @@ -32,37 +32,46 @@ import Control.Monad.Catch import Control.Retry import Data.Aeson (FromJSON) import Data.Aeson qualified as Aeson +import Data.Credentials (Credentials (..)) import Data.Metrics qualified as Metrics import Database.Bloodhound qualified as ES import Imports import Network.HTTP.Client as HTTP import System.Logger qualified as Log import System.Logger.Class (Logger, MonadLogger (..)) +import Util.Options (initCredentials) runCommand :: Logger -> Command -> IO () runCommand l = \case Create es galley -> do - e <- initIndex es galley + mCreds <- for (es ^. esCredentials) initCredentials + e <- initIndex es mCreds galley runIndexIO e $ createIndexIfNotPresent (mkCreateIndexSettings es) Reset es galley -> do - e <- initIndex es galley + mCreds <- for (es ^. esCredentials) initCredentials + e <- initIndex es mCreds galley runIndexIO e $ resetIndex (mkCreateIndexSettings es) Reindex es cas galley -> do - e <- initIndex es galley + mCreds <- for (es ^. esCredentials) initCredentials + e <- initIndex es mCreds galley c <- initDb cas runReindexIO e c reindexAll ReindexSameOrNewer es cas galley -> do - e <- initIndex es galley + mCreds <- for (es ^. esCredentials) initCredentials + e <- initIndex es mCreds galley c <- initDb cas runReindexIO e c reindexAllIfSameOrNewer - UpdateMapping esURI indexName galley -> do - e <- initIndex' esURI indexName galley + UpdateMapping esURI indexName mSecretPath galley -> do + mCreds <- for mSecretPath initCredentials + e <- initIndex' esURI indexName mCreds galley runIndexIO e updateMapping Migrate es cas galley -> do - migrate l es cas galley + mCreds <- for (es ^. esCredentials) initCredentials + migrate l mCreds es cas galley ReindexFromAnotherIndex reindexSettings -> do mgr <- newManager defaultManagerSettings - let bhEnv = initES (view reindexEsServer reindexSettings) mgr + mCreds <- for (view reindexCredentials reindexSettings) initCredentials + let bhEnv = initES (view reindexEsServer reindexSettings) mgr mCreds ES.runBH bhEnv $ do let src = view reindexSrcIndex reindexSettings dest = view reindexDestIndex reindexSettings @@ -85,22 +94,26 @@ runCommand l = \case waitForTaskToComplete @ES.ReindexResponse timeoutSeconds taskNodeId Log.info l $ Log.msg ("Finished reindexing" :: ByteString) where - initIndex es gly = - initIndex' (es ^. esServer) (es ^. esIndex) gly - initIndex' esURI indexName galleyEndpoint = do + initIndex es mCreds gly = + initIndex' (es ^. esServer) (es ^. esIndex) mCreds gly + + initIndex' esURI indexName mCreds galleyEndpoint = do mgr <- newManager defaultManagerSettings IndexEnv <$> Metrics.metrics <*> pure l - <*> pure (initES esURI mgr) + <*> pure (initES esURI mgr mCreds) <*> pure Nothing <*> pure indexName <*> pure Nothing <*> pure Nothing <*> pure galleyEndpoint <*> pure mgr - initES esURI mgr = - ES.mkBHEnv (toESServer esURI) mgr + + initES esURI mgr mCreds = + let env = ES.mkBHEnv (toESServer esURI) mgr + in maybe env (\(creds :: Credentials) -> env {ES.bhRequestHook = ES.basicAuthHook (ES.EsUsername creds.username) (ES.EsPassword creds.password)}) mCreds + initDb cas = defInitCassandra (toCassandraOpts cas) l waitForTaskToComplete :: forall a m. (ES.MonadBH m, MonadThrow m, FromJSON a) => Int -> ES.TaskNodeId -> m () diff --git a/services/brig/src/Brig/Index/Migrations.hs b/services/brig/src/Brig/Index/Migrations.hs index da7e78cc1a0..c0320fb7d5b 100644 --- a/services/brig/src/Brig/Index/Migrations.hs +++ b/services/brig/src/Brig/Index/Migrations.hs @@ -27,6 +27,7 @@ import Cassandra.Util (defInitCassandra) import Control.Lens (view, (^.)) import Control.Monad.Catch (MonadThrow, catchAll, finally, throwM) import Data.Aeson (Value, object, (.=)) +import Data.Credentials (Credentials (..)) import Data.Metrics qualified as Metrics import Data.Text qualified as Text import Database.Bloodhound qualified as ES @@ -37,9 +38,9 @@ import System.Logger.Class qualified as Log import System.Logger.Extended (runWithLogger) import Util.Options qualified as Options -migrate :: Logger -> Opts.ElasticSettings -> Opts.CassandraSettings -> Options.Endpoint -> IO () -migrate l es cas galleyEndpoint = do - env <- mkEnv l es cas galleyEndpoint +migrate :: Logger -> Maybe Credentials -> Opts.ElasticSettings -> Opts.CassandraSettings -> Options.Endpoint -> IO () +migrate l mCreds es cas galleyEndpoint = do + env <- mkEnv l mCreds es cas galleyEndpoint finally (go env `catchAll` logAndThrowAgain) (cleanup env) where go :: Env -> IO () @@ -74,10 +75,12 @@ indexMapping = ["migration_version" .= object ["index" .= True, "type" .= ("integer" :: Text)]] ] -mkEnv :: Logger -> Opts.ElasticSettings -> Opts.CassandraSettings -> Options.Endpoint -> IO Env -mkEnv l es cas galleyEndpoint = do +mkEnv :: Logger -> Maybe Credentials -> Opts.ElasticSettings -> Opts.CassandraSettings -> Options.Endpoint -> IO Env +mkEnv l mCreds es cas galleyEndpoint = do mgr <- HTTP.newManager HTTP.defaultManagerSettings - Env (ES.mkBHEnv (Opts.toESServer (es ^. Opts.esServer)) mgr) + let env = ES.mkBHEnv (Opts.toESServer (es ^. Opts.esServer)) mgr + let envWithAuth = maybe env (\(creds :: Credentials) -> env {ES.bhRequestHook = ES.basicAuthHook (ES.EsUsername creds.username) (ES.EsPassword creds.password)}) mCreds + Env envWithAuth <$> initCassandra <*> initLogger <*> Metrics.metrics diff --git a/services/brig/src/Brig/Index/Options.hs b/services/brig/src/Brig/Index/Options.hs index 89da5997cb4..c40dbb571a5 100644 --- a/services/brig/src/Brig/Index/Options.hs +++ b/services/brig/src/Brig/Index/Options.hs @@ -28,6 +28,7 @@ module Brig.Index.Options esIndexReplicas, esIndexRefreshInterval, esDeleteTemplate, + esCredentials, CassandraSettings, toCassandraOpts, cHost, @@ -44,6 +45,7 @@ module Brig.Index.Options reindexSrcIndex, reindexEsServer, reindexTimeoutSeconds, + reindexCredentials, ) where @@ -59,7 +61,7 @@ import Imports import Options.Applicative import URI.ByteString import URI.ByteString.QQ -import Util.Options (CassandraOpts (..), Endpoint (..)) +import Util.Options (CassandraOpts (..), Endpoint (..), FilePathSecrets) data Command = Create ElasticSettings Endpoint @@ -67,7 +69,7 @@ data Command | Reindex ElasticSettings CassandraSettings Endpoint | ReindexSameOrNewer ElasticSettings CassandraSettings Endpoint | -- | 'ElasticSettings' has shards and other settings that are not needed here. - UpdateMapping (URIRef Absolute) ES.IndexName Endpoint + UpdateMapping (URIRef Absolute) ES.IndexName (Maybe FilePathSecrets) Endpoint | Migrate ElasticSettings CassandraSettings Endpoint | ReindexFromAnotherIndex ReindexFromAnotherIndexSettings deriving (Show) @@ -78,7 +80,8 @@ data ElasticSettings = ElasticSettings _esIndexShardCount :: Int, _esIndexReplicas :: ES.ReplicaCount, _esIndexRefreshInterval :: NominalDiffTime, - _esDeleteTemplate :: Maybe ES.TemplateName + _esDeleteTemplate :: Maybe ES.TemplateName, + _esCredentials :: Maybe FilePathSecrets } deriving (Show) @@ -94,7 +97,8 @@ data ReindexFromAnotherIndexSettings = ReindexFromAnotherIndexSettings { _reindexEsServer :: URIRef Absolute, _reindexSrcIndex :: ES.IndexName, _reindexDestIndex :: ES.IndexName, - _reindexTimeoutSeconds :: Int + _reindexTimeoutSeconds :: Int, + _reindexCredentials :: Maybe FilePathSecrets } deriving (Show) @@ -130,7 +134,8 @@ localElasticSettings = _esIndexShardCount = 1, _esIndexReplicas = ES.ReplicaCount 1, _esIndexRefreshInterval = 1, - _esDeleteTemplate = Nothing + _esDeleteTemplate = Nothing, + _esCredentials = Nothing } localCassandraSettings :: CassandraSettings @@ -168,10 +173,12 @@ restrictedElasticSettingsParser = do <> value "directory" <> showDefault ) + mCreds <- credentialsPathParser pure $ localElasticSettings & esServer .~ server & esIndex .~ ES.IndexName (prefix <> "_test") + & esCredentials .~ mCreds indexNameParser :: Parser ES.IndexName indexNameParser = @@ -193,6 +200,7 @@ elasticSettingsParser = <*> indexReplicaCountParser <*> indexRefreshIntervalParser <*> templateParser + <*> credentialsPathParser where indexShardCountParser = option @@ -234,6 +242,16 @@ elasticSettingsParser = ) ) +credentialsPathParser :: Parser (Maybe FilePathSecrets) +credentialsPathParser = + optional + ( strOption + ( long "elasticsearch-credentials" + <> metavar "FILE" + <> help "Location of a file containing the Elasticsearch credentials" + ) + ) + cassandraSettingsParser :: Parser CassandraSettings cassandraSettingsParser = CassandraSettings @@ -293,6 +311,7 @@ reindexToAnotherIndexSettingsParser = <> value 600 <> showDefault ) + <*> credentialsPathParser galleyEndpointParser :: Parser Endpoint galleyEndpointParser = @@ -325,7 +344,7 @@ commandParser = <> command "update-mapping" ( info - (UpdateMapping <$> elasticServerParser <*> indexNameParser <*> galleyEndpointParser) + (UpdateMapping <$> elasticServerParser <*> indexNameParser <*> credentialsPathParser <*> galleyEndpointParser) (progDesc "Update mapping of the user index.") ) <> command diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 22a553228d8..3d21ee2533d 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -90,7 +90,11 @@ data ElasticSearchOpts = ElasticSearchOpts -- new instace of ES. It is necessary to provide 'additionalWriteIndex' for -- this to be used. If this is 'Nothing' and 'additionalWriteIndex' is -- configured, the 'url' field will be used. - additionalWriteIndexUrl :: !(Maybe Text) + additionalWriteIndexUrl :: !(Maybe Text), + -- | Elasticsearch credentials + credentials :: !(Maybe FilePathSecrets), + -- | Credentials for additional ES index (maily used for migrations) + additionalCredentials :: !(Maybe FilePathSecrets) } deriving (Show, Generic) diff --git a/services/brig/test/integration/API/Search.hs b/services/brig/test/integration/API/Search.hs index ef5a530ba61..5c1b341bec1 100644 --- a/services/brig/test/integration/API/Search.hs +++ b/services/brig/test/integration/API/Search.hs @@ -766,10 +766,12 @@ deleteIndex opts name = do let indexName = ES.IndexName name void $ runBH opts $ ES.deleteIndex indexName -runBH :: MonadIO m => Opt.Opts -> ES.BH IO a -> m a -runBH opts = +runBH :: MonadIO m => Opt.Opts -> ES.BH m a -> m a +runBH opts action = do let esURL = opts ^. Opt.elasticsearchL . Opt.urlL - in liftIO . ES.withBH HTTP.defaultManagerSettings (ES.Server esURL) + mgr <- liftIO $ HTTP.newManager HTTP.defaultManagerSettings + let bEnv = mkBHEnv esURL mgr + ES.runBH bEnv action -- | This was copied from at Brig.User.Search.Index at commit 3242aa26 analysisSettings :: ES.Analysis diff --git a/services/brig/test/integration/API/Search/Util.hs b/services/brig/test/integration/API/Search/Util.hs index 8be0145a09c..10e738a0eab 100644 --- a/services/brig/test/integration/API/Search/Util.hs +++ b/services/brig/test/integration/API/Search/Util.hs @@ -27,7 +27,9 @@ import Data.Id import Data.Qualified (Qualified (..)) import Data.Range (Range) import Data.Text.Encoding (encodeUtf8) +import Database.Bloodhound qualified as ES import Imports +import Network.HTTP.Client qualified as HTTP import Test.Tasty.HUnit import Util import Wire.API.User @@ -147,3 +149,7 @@ executeTeamUserSearchWithMaybeState brig teamid self mbSearchText mRoleFilter mS HTTP.Manager -> ES.BHEnv +mkBHEnv url mgr = do + (ES.mkBHEnv (ES.Server url) mgr) {ES.bhRequestHook = ES.basicAuthHook (ES.EsUsername "elastic") (ES.EsPassword "changeme")} diff --git a/services/brig/test/integration/Index/Create.hs b/services/brig/test/integration/Index/Create.hs index b31dd74725e..398e2126806 100644 --- a/services/brig/test/integration/Index/Create.hs +++ b/services/brig/test/integration/Index/Create.hs @@ -17,6 +17,7 @@ module Index.Create where +import API.Search.Util (mkBHEnv) import Brig.Index.Eval qualified as IndexEval import Brig.Index.Options qualified as IndexOpts import Brig.Options (Opts (galley)) @@ -48,6 +49,7 @@ spec brigOpts = testCreateIndexWhenNotPresent :: BrigOpts.Opts -> Assertion testCreateIndexWhenNotPresent brigOpts = do let esURL = brigOpts ^. BrigOpts.elasticsearchL . BrigOpts.urlL + let mCreds = BrigOpts.credentials . BrigOpts.elasticsearch $ brigOpts case parseURI strictURIParserOptions (Text.encodeUtf8 esURL) of Left e -> fail $ "Invalid ES URL: " <> show esURL <> "\nerror: " <> show e Right esURI -> do @@ -62,9 +64,12 @@ testCreateIndexWhenNotPresent brigOpts = do & IndexOpts.esIndexReplicas .~ ES.ReplicaCount replicas & IndexOpts.esIndexShardCount .~ shards & IndexOpts.esIndexRefreshInterval .~ refreshInterval + & IndexOpts.esCredentials .~ mCreds devNullLogger <- Log.create (Log.Path "/dev/null") IndexEval.runCommand devNullLogger (IndexOpts.Create esSettings (galley brigOpts)) - ES.withBH HTTP.defaultManagerSettings (ES.Server esURL) $ do + mgr <- liftIO $ HTTP.newManager HTTP.defaultManagerSettings + let bEnv = (mkBHEnv esURL mgr) {ES.bhRequestHook = ES.basicAuthHook (ES.EsUsername "elastic") (ES.EsPassword "changeme")} + ES.runBH bEnv $ do indexExists <- ES.indexExists indexName lift $ assertBool "Index should exist" indexExists @@ -75,16 +80,19 @@ testCreateIndexWhenNotPresent brigOpts = do Right indexSettings -> do assertEqual "Shard count should be set" (ES.ShardCount replicas) (ES.indexShards . ES.sSummaryFixedSettings $ indexSettings) assertEqual "Replica count should be set" (ES.ReplicaCount replicas) (ES.indexReplicas . ES.sSummaryFixedSettings $ indexSettings) - assertEqual "Refresh internval should be set" [ES.RefreshInterval refreshInterval] (ES.sSummaryUpdateable indexSettings) + assertEqual "Refresh interval should be set" [ES.RefreshInterval refreshInterval] (ES.sSummaryUpdateable indexSettings) testCreateIndexWhenPresent :: BrigOpts.Opts -> Assertion testCreateIndexWhenPresent brigOpts = do let esURL = brigOpts ^. BrigOpts.elasticsearchL . BrigOpts.urlL + let mCreds = BrigOpts.credentials . BrigOpts.elasticsearch $ brigOpts case parseURI strictURIParserOptions (Text.encodeUtf8 esURL) of Left e -> fail $ "Invalid ES URL: " <> show esURL <> "\nerror: " <> show e Right esURI -> do indexName <- ES.IndexName . Text.pack <$> replicateM 20 (Random.randomRIO ('a', 'z')) - ES.withBH HTTP.defaultManagerSettings (ES.Server esURL) $ do + mgr <- liftIO $ HTTP.newManager HTTP.defaultManagerSettings + let bEnv = (mkBHEnv esURL mgr) {ES.bhRequestHook = ES.basicAuthHook (ES.EsUsername "elastic") (ES.EsPassword "changeme")} + ES.runBH bEnv $ do _ <- ES.createIndex (ES.IndexSettings (ES.ShardCount 1) (ES.ReplicaCount 1)) indexName indexExists <- ES.indexExists indexName lift $ @@ -99,9 +107,10 @@ testCreateIndexWhenPresent brigOpts = do & IndexOpts.esIndexReplicas .~ ES.ReplicaCount replicas & IndexOpts.esIndexShardCount .~ shards & IndexOpts.esIndexRefreshInterval .~ refreshInterval + & IndexOpts.esCredentials .~ mCreds devNullLogger <- Log.create (Log.Path "/dev/null") IndexEval.runCommand devNullLogger (IndexOpts.Create esSettings (galley brigOpts)) - ES.withBH HTTP.defaultManagerSettings (ES.Server esURL) $ do + ES.runBH bEnv $ do indexExists <- ES.indexExists indexName lift $ assertBool "Index should still exist" indexExists @@ -112,4 +121,4 @@ testCreateIndexWhenPresent brigOpts = do Right indexSettings -> do assertEqual "Shard count should not be updated" (ES.ShardCount 1) (ES.indexShards . ES.sSummaryFixedSettings $ indexSettings) assertEqual "Replica count should not be updated" (ES.ReplicaCount 1) (ES.indexReplicas . ES.sSummaryFixedSettings $ indexSettings) - assertEqual "Refresh internval should not be updated" [] (ES.sSummaryUpdateable indexSettings) + assertEqual "Refresh interval should not be updated" [] (ES.sSummaryUpdateable indexSettings) diff --git a/services/brig/test/resources/elasticsearch-credentials.yaml b/services/brig/test/resources/elasticsearch-credentials.yaml new file mode 100644 index 00000000000..c1865766f56 --- /dev/null +++ b/services/brig/test/resources/elasticsearch-credentials.yaml @@ -0,0 +1,2 @@ +username: "elastic" +password: "changeme" From c7880097d1588fcb8025e26681545ea28505c366 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Tue, 26 Mar 2024 11:15:59 +0100 Subject: [PATCH 066/117] Re-arrange internal routing table types in brig to make more sense. (#3970) --- .../src/Wire/API/Routes/Internal/Brig.hs | 39 ++++++++----------- services/brig/src/Brig/API/Internal.hs | 14 +++---- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs index 7e9e76ff581..42af0b8ca40 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -21,7 +21,6 @@ module Wire.API.Routes.Internal.Brig brigInternalClient, runBrigInternalClient, IStatusAPI, - EJPD_API, AccountAPI, MLSAPI, TeamsAPI, @@ -156,27 +155,23 @@ type GetAllConnections = :> ReqBody '[Servant.JSON] ConnectionsStatusRequestV2 :> Post '[Servant.JSON] [ConnectionStatusV2] -type EJPD_API = - ( EJPDRequest - :<|> Named "get-account-conference-calling-config" GetAccountConferenceCallingConfig - :<|> PutAccountConferenceCallingConfig - :<|> DeleteAccountConferenceCallingConfig - :<|> GetAllConnectionsUnqualified - :<|> GetAllConnections - ) - type AccountAPI = - -- This endpoint can lead to the following events being sent: - -- - UserActivated event to created user, if it is a team invitation or user has an SSO ID - -- - UserIdentityUpdated event to created user, if email or phone get activated - Named - "createUserNoVerify" - ( "users" - :> MakesFederatedCall 'Brig "on-user-deleted-connections" - :> MakesFederatedCall 'Brig "send-connection-action" - :> ReqBody '[Servant.JSON] NewUser - :> MultiVerb 'POST '[Servant.JSON] RegisterInternalResponses (Either RegisterError SelfProfile) - ) + Named "get-account-conference-calling-config" GetAccountConferenceCallingConfig + :<|> PutAccountConferenceCallingConfig + :<|> DeleteAccountConferenceCallingConfig + :<|> GetAllConnectionsUnqualified + :<|> GetAllConnections + :<|> Named + "createUserNoVerify" + -- This endpoint can lead to the following events being sent: + -- - UserActivated event to created user, if it is a team invitation or user has an SSO ID + -- - UserIdentityUpdated event to created user, if email or phone get activated + ( "users" + :> MakesFederatedCall 'Brig "on-user-deleted-connections" + :> MakesFederatedCall 'Brig "send-connection-action" + :> ReqBody '[Servant.JSON] NewUser + :> MultiVerb 'POST '[Servant.JSON] RegisterInternalResponses (Either RegisterError SelfProfile) + ) :<|> Named "createUserNoVerifySpar" ( "users" @@ -532,7 +527,7 @@ type GetVerificationCode = type API = "i" :> ( IStatusAPI - :<|> EJPD_API + :<|> EJPDRequest :<|> AccountAPI :<|> MLSAPI :<|> GetVerificationCode diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 315ad9ae22f..75c4d6a3976 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -148,14 +148,9 @@ ejpdAPI :: Member NotificationSubsystem r, Member Rpc r ) => - ServerT BrigIRoutes.EJPD_API (Handler r) + ServerT BrigIRoutes.EJPDRequest (Handler r) ejpdAPI = Brig.User.EJPD.ejpdRequest - :<|> Named @"get-account-conference-calling-config" getAccountConferenceCallingConfig - :<|> putAccountConferenceCallingConfig - :<|> deleteAccountConferenceCallingConfig - :<|> getConnectionsStatusUnqualified - :<|> getConnectionsStatus mlsAPI :: ServerT BrigIRoutes.MLSAPI (Handler r) mlsAPI = getMLSClients @@ -176,7 +171,12 @@ accountAPI :: ) => ServerT BrigIRoutes.AccountAPI (Handler r) accountAPI = - Named @"createUserNoVerify" (callsFed (exposeAnnotations createUserNoVerify)) + Named @"get-account-conference-calling-config" getAccountConferenceCallingConfig + :<|> putAccountConferenceCallingConfig + :<|> deleteAccountConferenceCallingConfig + :<|> getConnectionsStatusUnqualified + :<|> getConnectionsStatus + :<|> Named @"createUserNoVerify" (callsFed (exposeAnnotations createUserNoVerify)) :<|> Named @"createUserNoVerifySpar" (callsFed (exposeAnnotations createUserNoVerifySpar)) :<|> Named @"putSelfEmail" changeSelfEmailMaybeSendH :<|> Named @"iDeleteUser" deleteUserNoAuthH From c17c7c93777b85838511ae488cb436699682148b Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 2 Apr 2024 12:15:58 +0200 Subject: [PATCH 067/117] gundeck: Support authenticating to redis (#3971) * dockerephemeral: Start redis in master mode for testing redis migrations locally This seems to have been deleted by mistake in https://github.com/wireapp/wire-server/pull/3719 * dockerephemeral: Start redis with creds * nix: Pin hedis to our fork which does auth correctly * gundeck: Log uncaught errors in redis connection * gundeck: Use redis creds from environment when provided * hack/helmfile: Spin up extra redis for testing redis migration * hack: Run redis with auth * changelog * docs/config-options: Wrap text for ES basic auth section * docs/config-options: Document setting creds for redis --- .envrc | 7 +++- changelog.d/2-features/redis-creds | 1 + charts/gundeck/templates/deployment.yaml | 28 +++++++++++++ charts/gundeck/templates/secret.yaml | 18 ++++++++- charts/gundeck/templates/tests/configmap.yaml | 2 +- .../templates/tests/gundeck-integration.yaml | 28 +++++++++++++ charts/gundeck/templates/tests/secret.yaml | 13 +++++- .../templates/integration-integration.yaml | 14 +++++++ charts/integration/templates/secret.yaml | 10 +++++ deploy/dockerephemeral/docker-compose.yaml | 13 +++++- .../docker/redis-master-mode.conf | 1 + .../dockerephemeral/docker/redis-node-1.conf | 2 + .../dockerephemeral/docker/redis-node-2.conf | 2 + .../dockerephemeral/docker/redis-node-3.conf | 2 + .../dockerephemeral/docker/redis-node-4.conf | 2 + .../dockerephemeral/docker/redis-node-5.conf | 2 + .../dockerephemeral/docker/redis-node-6.conf | 2 + .../src/developer/reference/config-options.md | 40 ++++++++++++++++++- .../redis-cluster/values.yaml.gotmpl | 1 + hack/helm_vars/wire-server/values.yaml.gotmpl | 17 +++++--- hack/helmfile.yaml | 21 ++++++++++ nix/haskell-pins.nix | 9 +++++ nix/manual-overrides.nix | 3 ++ services/gundeck/src/Gundeck/Env.hs | 20 +++++++--- services/gundeck/src/Gundeck/Presence/Data.hs | 7 ++-- services/gundeck/src/Gundeck/Redis.hs | 8 ++-- services/gundeck/test/integration/API.hs | 9 ++++- services/gundeck/test/integration/Util.hs | 14 +++++++ 28 files changed, 271 insertions(+), 25 deletions(-) create mode 100644 changelog.d/2-features/redis-creds create mode 100644 deploy/dockerephemeral/docker/redis-master-mode.conf diff --git a/.envrc b/.envrc index b7b3f2c35fd..6fd34b70988 100644 --- a/.envrc +++ b/.envrc @@ -49,6 +49,11 @@ export LANG=en_US.UTF-8 export RABBITMQ_USERNAME=guest export RABBITMQ_PASSWORD=alpaca-grapefruit +# Redis + +export REDIS_PASSWORD=very-secure-redis-cluster-password +export REDIS_ADDITIONAL_WRITE_PASSWORD=very-secure-redis-master-password + # Integration tests export INTEGRATION_DYNAMIC_BACKENDS_POOLSIZE=3 @@ -58,7 +63,7 @@ export AWS_REGION="eu-west-1" export AWS_ACCESS_KEY_ID="dummykey" export AWS_SECRET_ACCESS_KEY="dummysecret" -# integration test suite timeout +# integration test suite timeout export TEST_TIMEOUT_SECONDS=2 # allow local .envrc overrides diff --git a/changelog.d/2-features/redis-creds b/changelog.d/2-features/redis-creds new file mode 100644 index 00000000000..0ced29e8b42 --- /dev/null +++ b/changelog.d/2-features/redis-creds @@ -0,0 +1 @@ +Support authenticating to redis \ No newline at end of file diff --git a/charts/gundeck/templates/deployment.yaml b/charts/gundeck/templates/deployment.yaml index 20ca7988245..ec1e064ccc2 100644 --- a/charts/gundeck/templates/deployment.yaml +++ b/charts/gundeck/templates/deployment.yaml @@ -65,6 +65,34 @@ spec: name: gundeck key: awsSecretKey {{- end }} + {{- if hasKey .Values.secrets "redisUsername" }} + - name: REDIS_USERNAME + valueFrom: + secretKeyRef: + name: gundeck + key: redisUsername + {{- end }} + {{- if hasKey .Values.secrets "redisPassword" }} + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: gundeck + key: redisPassword + {{- end }} + {{- if hasKey .Values.secrets "redisAdditionalWriteUsername" }} + - name: REDIS_ADDITIONAL_WRITE_USERNAME + valueFrom: + secretKeyRef: + name: gundeck + key: redisAdditionalWriteUsername + {{- end }} + {{- if hasKey .Values.secrets "redisAdditionalWritePassword" }} + - name: REDIS_ADDITIONAL_WRITE_PASSWORD + valueFrom: + secretKeyRef: + name: gundeck + key: redisAdditionalWritePassword + {{- end }} - name: AWS_REGION value: "{{ .Values.config.aws.region }}" {{- with .Values.config.proxy }} diff --git a/charts/gundeck/templates/secret.yaml b/charts/gundeck/templates/secret.yaml index 459ab0f24f4..eae9c4ab33d 100644 --- a/charts/gundeck/templates/secret.yaml +++ b/charts/gundeck/templates/secret.yaml @@ -1,4 +1,4 @@ -{{- if hasKey .Values.secrets "awsKeyId" }} +{{- if not (empty .Values.secrets) }} apiVersion: v1 kind: Secret metadata: @@ -11,7 +11,23 @@ metadata: type: Opaque data: {{- with .Values.secrets }} + {{- if hasKey . "awsKeyId" }} awsKeyId: {{ .awsKeyId | b64enc | quote }} + {{- end }} + {{- if hasKey . "awsSecretKey" }} awsSecretKey: {{ .awsSecretKey | b64enc | quote }} {{- end }} + {{- if hasKey . "redisUsername" }} + redisUsername: {{ .redisUsername | b64enc | quote }} + {{- end }} + {{- if hasKey . "redisPassword" }} + redisPassword: {{ .redisPassword | b64enc | quote }} + {{- end }} + {{- if hasKey . "redisAdditionalWriteUsername" }} + redisAdditionalWriteUsername: {{ .redisAdditionalWriteUsername | b64enc | quote }} + {{- end }} + {{- if hasKey . "redisAdditionalWritePassword" }} + redisAdditionalWritePassword: {{ .redisAdditionalWritePassword | b64enc | quote }} + {{- end }} + {{- end }} {{- end }} diff --git a/charts/gundeck/templates/tests/configmap.yaml b/charts/gundeck/templates/tests/configmap.yaml index e2051925c11..b3e1423acf6 100644 --- a/charts/gundeck/templates/tests/configmap.yaml +++ b/charts/gundeck/templates/tests/configmap.yaml @@ -41,6 +41,6 @@ data: # a "redis migration" test in gundeck makes use of a second (distinct) redis redis2: - host: redis-ephemeral-master + host: redis-ephemeral-2-master port: 6379 connectionMode: master diff --git a/charts/gundeck/templates/tests/gundeck-integration.yaml b/charts/gundeck/templates/tests/gundeck-integration.yaml index 8b00f2c9865..088ed679bdb 100644 --- a/charts/gundeck/templates/tests/gundeck-integration.yaml +++ b/charts/gundeck/templates/tests/gundeck-integration.yaml @@ -73,6 +73,34 @@ spec: value: "eu-west-1" - name: TEST_XML value: /tmp/result.xml + {{- if hasKey .Values.secrets "redisUsername" }} + - name: REDIS_USERNAME + valueFrom: + secretKeyRef: + name: gundeck + key: redisUsername + {{- end }} + {{- if hasKey .Values.secrets "redisPassword" }} + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: gundeck + key: redisPassword + {{- end }} + {{- if and (hasKey .Values.tests "secrets") (hasKey .Values.tests.secrets "redisAdditionalWriteUsername") }} + - name: REDIS_ADDITIONAL_WRITE_USERNAME + valueFrom: + secretKeyRef: + name: gundeck-integration + key: redisAdditionalWriteUsername + {{- end }} + {{- if and (hasKey .Values.tests "secrets") (hasKey .Values.tests.secrets "redisAdditionalWritePassword") }} + - name: REDIS_ADDITIONAL_WRITE_PASSWORD + valueFrom: + secretKeyRef: + name: gundeck-integration + key: redisAdditionalWritePassword + {{- end }} {{- if .Values.tests.config.uploadXml }} - name: UPLOAD_XML_S3_BASE_URL value: {{ .Values.tests.config.uploadXml.baseUrl }} diff --git a/charts/gundeck/templates/tests/secret.yaml b/charts/gundeck/templates/tests/secret.yaml index 1af8959e4c3..ff5712545c8 100644 --- a/charts/gundeck/templates/tests/secret.yaml +++ b/charts/gundeck/templates/tests/secret.yaml @@ -1,3 +1,4 @@ +{{- if not (empty .Values.tests.secrets) }} apiVersion: v1 kind: Secret metadata: @@ -10,7 +11,17 @@ metadata: type: Opaque data: {{- with .Values.tests.secrets }} + {{- if hasKey . "uploadXmlAwsAccessKeyId" }} uploadXmlAwsAccessKeyId: {{ .uploadXmlAwsAccessKeyId | b64enc | quote }} + {{- end }} + {{- if hasKey . "uploadXmlAwsSecretAccessKey" }} uploadXmlAwsSecretAccessKey: {{ .uploadXmlAwsSecretAccessKey | b64enc | quote }} {{- end }} - + {{- if hasKey . "redisAdditionalWriteUsername" }} + redisAdditionalWriteUsername: {{ .redisAdditionalWriteUsername | b64enc | quote }} + {{- end }} + {{- if hasKey . "redisAdditionalWritePassword" }} + redisAdditionalWritePassword: {{ .redisAdditionalWritePassword | b64enc | quote }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/integration/templates/integration-integration.yaml b/charts/integration/templates/integration-integration.yaml index 5f6a98f7595..e0d72e62a87 100644 --- a/charts/integration/templates/integration-integration.yaml +++ b/charts/integration/templates/integration-integration.yaml @@ -262,6 +262,20 @@ spec: secretKeyRef: name: brig key: rabbitmqPassword + {{- if hasKey .Values.secrets "redisUsername" }} + - name: REDIS_USERNAME + valueFrom: + secretKeyRef: + name: integration + key: redisUsername + {{- end }} + {{- if hasKey .Values.secrets "redisPassword" }} + - name: REDIS_PASSWORD + valueFrom: + secretKeyRef: + name: integration + key: redisPassword + {{- end }} - name: TEST_XML value: /tmp/result.xml {{- if .Values.config.uploadXml }} diff --git a/charts/integration/templates/secret.yaml b/charts/integration/templates/secret.yaml index 52c3199b5f0..32f6085176e 100644 --- a/charts/integration/templates/secret.yaml +++ b/charts/integration/templates/secret.yaml @@ -10,6 +10,16 @@ metadata: type: Opaque data: {{- with .Values.secrets }} + {{- if hasKey . "uploadXmlAwsAccessKeyId" }} uploadXmlAwsAccessKeyId: {{ .uploadXmlAwsAccessKeyId | b64enc | quote }} + {{- end }} + {{- if hasKey . "uploadXmlAwsSecretAccessKey" }} uploadXmlAwsSecretAccessKey: {{ .uploadXmlAwsSecretAccessKey | b64enc | quote }} {{- end }} + {{- if hasKey . "redisUsername" }} + redisUsername: {{ .redisUsername | b64enc | quote }} + {{- end }} + {{- if hasKey . "redisPassword" }} + redisPassword: {{ .redisPassword | b64enc | quote }} + {{- end }} + {{- end }} diff --git a/deploy/dockerephemeral/docker-compose.yaml b/deploy/dockerephemeral/docker-compose.yaml index 2ad299a6d19..d49e141dfb1 100644 --- a/deploy/dockerephemeral/docker-compose.yaml +++ b/deploy/dockerephemeral/docker-compose.yaml @@ -77,9 +77,20 @@ services: networks: - demo_wire + redis-master: + container_name: demo_wire_redis + image: redis:6.0-alpine + command: redis-server /usr/local/etc/redis/redis.conf + ports: + - "127.0.0.1:6379:6379" + volumes: + - ./docker/redis-master-mode.conf:/usr/local/etc/redis/redis.conf + networks: + - demo_wire + redis-cluster: image: 'redis:6.0-alpine' - command: redis-cli --cluster create 172.20.0.31:6373 172.20.0.32:6374 172.20.0.33:6375 172.20.0.34:6376 172.20.0.35:6377 172.20.0.36:6378 --cluster-replicas 1 --cluster-yes + command: redis-cli --cluster create 172.20.0.31:6373 172.20.0.32:6374 172.20.0.33:6375 172.20.0.34:6376 172.20.0.35:6377 172.20.0.36:6378 --cluster-replicas 1 --cluster-yes -a very-secure-redis-cluster-password networks: redis: ipv4_address: 172.20.0.30 diff --git a/deploy/dockerephemeral/docker/redis-master-mode.conf b/deploy/dockerephemeral/docker/redis-master-mode.conf new file mode 100644 index 00000000000..d71dbc51c97 --- /dev/null +++ b/deploy/dockerephemeral/docker/redis-master-mode.conf @@ -0,0 +1 @@ +requirepass very-secure-redis-master-password \ No newline at end of file diff --git a/deploy/dockerephemeral/docker/redis-node-1.conf b/deploy/dockerephemeral/docker/redis-node-1.conf index 3f7f7d69ff4..011df166cda 100644 --- a/deploy/dockerephemeral/docker/redis-node-1.conf +++ b/deploy/dockerephemeral/docker/redis-node-1.conf @@ -3,3 +3,5 @@ cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 appendonly yes +requirepass very-secure-redis-cluster-password +masterauth very-secure-redis-cluster-password \ No newline at end of file diff --git a/deploy/dockerephemeral/docker/redis-node-2.conf b/deploy/dockerephemeral/docker/redis-node-2.conf index c81ccd43ffa..fa2850e9234 100644 --- a/deploy/dockerephemeral/docker/redis-node-2.conf +++ b/deploy/dockerephemeral/docker/redis-node-2.conf @@ -3,3 +3,5 @@ cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 appendonly yes +requirepass very-secure-redis-cluster-password +masterauth very-secure-redis-cluster-password \ No newline at end of file diff --git a/deploy/dockerephemeral/docker/redis-node-3.conf b/deploy/dockerephemeral/docker/redis-node-3.conf index 6ae5804185a..81d01b5421f 100644 --- a/deploy/dockerephemeral/docker/redis-node-3.conf +++ b/deploy/dockerephemeral/docker/redis-node-3.conf @@ -3,3 +3,5 @@ cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 appendonly yes +requirepass very-secure-redis-cluster-password +masterauth very-secure-redis-cluster-password diff --git a/deploy/dockerephemeral/docker/redis-node-4.conf b/deploy/dockerephemeral/docker/redis-node-4.conf index 1c3464629ef..50361d22810 100644 --- a/deploy/dockerephemeral/docker/redis-node-4.conf +++ b/deploy/dockerephemeral/docker/redis-node-4.conf @@ -3,3 +3,5 @@ cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 appendonly yes +requirepass very-secure-redis-cluster-password +masterauth very-secure-redis-cluster-password diff --git a/deploy/dockerephemeral/docker/redis-node-5.conf b/deploy/dockerephemeral/docker/redis-node-5.conf index e28f7909b5a..68885b25b43 100644 --- a/deploy/dockerephemeral/docker/redis-node-5.conf +++ b/deploy/dockerephemeral/docker/redis-node-5.conf @@ -3,3 +3,5 @@ cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 appendonly yes +requirepass very-secure-redis-cluster-password +masterauth very-secure-redis-cluster-password diff --git a/deploy/dockerephemeral/docker/redis-node-6.conf b/deploy/dockerephemeral/docker/redis-node-6.conf index b77c8e19c91..07da6325790 100644 --- a/deploy/dockerephemeral/docker/redis-node-6.conf +++ b/deploy/dockerephemeral/docker/redis-node-6.conf @@ -3,3 +3,5 @@ cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000 appendonly yes +requirepass very-secure-redis-cluster-password +masterauth very-secure-redis-cluster-password diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index 857dee00806..d45c805dc65 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -856,7 +856,11 @@ The corresponding Cassandra options are described in Cassandra's documentation: ## Configure Elasticsearch basic authentication -When the Wire backend is configured to work against a custom Elasticsearch instance, it may be desired to enable basic authentication for the internal communication between the Wire backend and the ES instance. To do so the Elasticsearch credentials can be set in wire-server's secrets for `brig` and `elasticsearch-index` as follows: +When the Wire backend is configured to work against a custom Elasticsearch +instance, it may be desired to enable basic authentication for the internal +communication between the Wire backend and the ES instance. To do so the +Elasticsearch credentials can be set in wire-server's secrets for `brig` and +`elasticsearch-index` as follows: ```yaml brig: @@ -872,7 +876,9 @@ elasticsearch-index: password: changeme ``` -In some cases an additional Elasticsearch instance is needed (e.g. for index migrations). To configure credentials for the additional ES instance add the secret as follows: +In some cases an additional Elasticsearch instance is needed (e.g. for index +migrations). To configure credentials for the additional ES instance add the +secret as follows: ```yaml brig: @@ -881,3 +887,33 @@ brig: username: elastic password: changeme ``` + +## Configure Redis authentication + +If the redis used needs authentication with either username and password or just +password (legacy auth), it can be configured like this: + +```yaml +gundeck: + secrets: + redisUsername: + redisPassword: +``` + +**NOTE**: When using redis < 6, the `redisUsername` must not be set at all (not +even set to `null` or empty string, the key must be absent from the config). +When using redis >= 6 and using legacy auth, the `redisUsername` must either be +not set at all or set to `"default"`. + +While doing migrations to another redis instance, the credentials for the +addtional redis can be set as follows: + +```yaml +gundeck: + secrets: + redisAdditionalWriteUsername: # Do not set this at all when using legacy auth + redisAdditionalWritePassword: +``` + +**NOTE**: `redisAddtiionalWriteUsername` follows same restrictions as +`redisUsername` when using legacy auth. diff --git a/hack/helm_vars/redis-cluster/values.yaml.gotmpl b/hack/helm_vars/redis-cluster/values.yaml.gotmpl index 658cb795566..9d81712a59d 100644 --- a/hack/helm_vars/redis-cluster/values.yaml.gotmpl +++ b/hack/helm_vars/redis-cluster/values.yaml.gotmpl @@ -6,3 +6,4 @@ redis-cluster: size: 100Mi volumePermissions: enabled: true + password: very-secure-redis-cluster-password diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index d215f8efd6a..1547d6f846a 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -313,15 +313,19 @@ gundeck: secrets: awsKeyId: dummykey awsSecretKey: dummysecret + redisPassword: very-secure-redis-master-password tests: {{- if .Values.uploadXml }} config: uploadXml: baseUrl: {{ .Values.uploadXml.baseUrl }} + {{- end }} secrets: + {{- if .Values.uploadXml }} uploadXmlAwsAccessKeyId: {{ .Values.uploadXml.awsAccessKeyId }} uploadXmlAwsSecretAccessKey: {{ .Values.uploadXml.awsSecretAccessKey }} - {{- end }} + {{- end }} + redisAdditionalWritePassword: very-secure-redis-master-password-2 nginz: replicaCount: 1 @@ -444,18 +448,21 @@ integration: host: {{ .Values.cassandraHost }} port: 9042 replicationFactor: 1 - {{- if .Values.useK8ssandraSSL.enabled }} + {{- if .Values.useK8ssandraSSL.enabled }} tlsCaSecretRef: name: cassandra-jks-keystore key: ca.crt - {{- end }} - {{- if .Values.uploadXml }} + {{- end }} + {{- if .Values.uploadXml }} uploadXml: baseUrl: {{ .Values.uploadXml.baseUrl }} + {{- end }} secrets: + {{- if .Values.uploadXml }} uploadXmlAwsAccessKeyId: {{ .Values.uploadXml.awsAccessKeyId }} uploadXmlAwsSecretAccessKey: {{ .Values.uploadXml.awsSecretAccessKey }} - {{- end }} + {{- end }} + redisPassword: very-secure-redis-master-password tls: caNamespace: wire-federation-v0 diff --git a/hack/helmfile.yaml b/hack/helmfile.yaml index 78634f17b25..0d34c252954 100644 --- a/hack/helmfile.yaml +++ b/hack/helmfile.yaml @@ -67,13 +67,34 @@ releases: chart: '../.local/charts/fake-aws' values: - './helm_vars/fake-aws/values.yaml' + - name: 'databases-ephemeral' namespace: '{{ .Values.namespace1 }}' chart: '../.local/charts/databases-ephemeral' + values: + - redis-ephemeral: + redis-ephemeral: + usePassword: true + password: very-secure-redis-master-password + + # Required for testing redis migration + - name: 'redis-ephemeral-2' + namespace: '{{ .Values.namespace1 }}' + chart: '../.local/charts/redis-ephemeral' + values: + - redis-ephemeral: + nameOverride: redis-ephemeral-2 + usePassword: true + password: very-secure-redis-master-password-2 - name: 'databases-ephemeral' namespace: '{{ .Values.namespace2 }}' chart: '../.local/charts/databases-ephemeral' + values: + - redis-ephemeral: + redis-ephemeral: + usePassword: true + password: very-secure-redis-master-password - name: k8ssandra-test-cluster chart: '../.local/charts/k8ssandra-test-cluster' diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 1edd473eade..831fe39eff0 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -116,6 +116,15 @@ let }; }; + # PR: https://github.com/informatikr/hedis/pull/224 + hedis = { + src = fetchgit { + url = "https://github.com/wireapp/hedis"; + rev = "81cdd8a2350b96168a06662c2601a41141a19f2d"; + sha256 = "sha256-0g6x9UOUq7s5ClnxMXvjYR2AsWNA6ymv1tYlQC44hGs="; + }; + }; + # Our fork because we need to a few special things http-client = { src = fetchgit { diff --git a/nix/manual-overrides.nix b/nix/manual-overrides.nix index 51d0f437aea..2a70e83728d 100644 --- a/nix/manual-overrides.nix +++ b/nix/manual-overrides.nix @@ -23,6 +23,9 @@ hself: hsuper: { transitive-anns = hlib.dontCheck hsuper.transitive-anns; warp = hlib.dontCheck hsuper.warp; + # Tests require a running redis + hedis = hlib.dontCheck hsuper.hedis; + # --------------------- # need to be jailbroken # (these need to be fixed upstream eventually) diff --git a/services/gundeck/src/Gundeck/Env.hs b/services/gundeck/src/Gundeck/Env.hs index fdc67f2f223..c9e8a4d286b 100644 --- a/services/gundeck/src/Gundeck/Env.hs +++ b/services/gundeck/src/Gundeck/Env.hs @@ -26,6 +26,7 @@ import Control.AutoUpdate import Control.Concurrent.Async (Async) import Control.Lens (makeLenses, (^.)) import Control.Retry (capDelay, exponentialBackoff) +import Data.ByteString.Char8 qualified as BSChar8 import Data.Metrics.Middleware (Metrics) import Data.Misc (Milliseconds (..)) import Data.Text (unpack) @@ -74,12 +75,16 @@ createEnv m o = do managerResponseTimeout = responseTimeoutMicro 5000000 } - (rThread, r) <- createRedisPool l (o ^. redis) "main-redis" + redisUsername <- BSChar8.pack <$$> lookupEnv "REDIS_USERNAME" + redisPassword <- BSChar8.pack <$$> lookupEnv "REDIS_PASSWORD" + (rThread, r) <- createRedisPool l (o ^. redis) redisUsername redisPassword "main-redis" (rAdditionalThreads, rAdditional) <- case o ^. redisAdditionalWrite of Nothing -> pure ([], Nothing) Just additionalRedis -> do - (rAddThread, rAdd) <- createRedisPool l additionalRedis "additional-write-redis" + additionalRedisUsername <- BSChar8.pack <$$> lookupEnv "REDIS_ADDITIONAL_WRITE_USERNAME" + addtionalRedisPassword <- BSChar8.pack <$$> lookupEnv "REDIS_ADDITIONAL_WRITE_PASSWORD" + (rAddThread, rAdd) <- createRedisPool l additionalRedis additionalRedisUsername addtionalRedisPassword "additional-write-redis" pure ([rAddThread], Just rAdd) p <- @@ -103,12 +108,14 @@ reqIdMsg :: RequestId -> Logger.Msg -> Logger.Msg reqIdMsg = ("request" Logger..=) . unRequestId {-# INLINE reqIdMsg #-} -createRedisPool :: Logger.Logger -> RedisEndpoint -> ByteString -> IO (Async (), Redis.RobustConnection) -createRedisPool l ep identifier = do +createRedisPool :: Logger.Logger -> RedisEndpoint -> Maybe ByteString -> Maybe ByteString -> ByteString -> IO (Async (), Redis.RobustConnection) +createRedisPool l ep username password identifier = do let redisConnInfo = Redis.defaultConnectInfo { Redis.connectHost = unpack $ ep ^. O.host, Redis.connectPort = Redis.PortNumber (fromIntegral $ ep ^. O.port), + Redis.connectUsername = username, + Redis.connectAuth = password, Redis.connectTimeout = Just (secondsToNominalDiffTime 5), Redis.connectMaxConnections = 100 } @@ -116,10 +123,13 @@ createRedisPool l ep identifier = do Log.info l $ Log.msg (Log.val $ "starting connection to " <> identifier <> "...") . Log.field "connectionMode" (show $ ep ^. O.connectionMode) - . Log.field "connInfo" (show redisConnInfo) + . Log.field "connInfo" (safeShowConnInfo redisConnInfo) let connectWithRetry = Redis.connectRobust l (capDelay 1000000 (exponentialBackoff 50000)) r <- case ep ^. O.connectionMode of Master -> connectWithRetry $ Redis.checkedConnect redisConnInfo Cluster -> connectWithRetry $ Redis.checkedConnectCluster redisConnInfo Log.info l $ Log.msg (Log.val $ "Established connection to " <> identifier <> ".") pure r + +safeShowConnInfo :: Redis.ConnectInfo -> String +safeShowConnInfo connInfo = show $ connInfo {Redis.connectAuth = "[REDACTED]" <$ Redis.connectAuth connInfo} diff --git a/services/gundeck/src/Gundeck/Presence/Data.hs b/services/gundeck/src/Gundeck/Presence/Data.hs index 1536dab9ea6..158e8982217 100644 --- a/services/gundeck/src/Gundeck/Presence/Data.hs +++ b/services/gundeck/src/Gundeck/Presence/Data.hs @@ -25,13 +25,14 @@ where import Control.Monad.Catch import Control.Monad.Except -import Data.Aeson +import Data.Aeson as Aeson import Data.ByteString qualified as Strict import Data.ByteString.Builder (byteString) import Data.ByteString.Char8 qualified as StrictChars import Data.ByteString.Conversion hiding (fromList) import Data.ByteString.Lazy qualified as Lazy import Data.Id +import Data.List.NonEmpty qualified as NonEmpty import Data.Misc (Milliseconds) import Database.Redis import Gundeck.Monad (Gundeck, posixTime, runWithAdditionalRedis) @@ -61,10 +62,10 @@ add p = do now <- posixTime let k = toKey (userId p) let v = toField (connId p) - let d = Lazy.toStrict $ encode $ PresenceData (resource p) (clientId p) now + let d = Lazy.toStrict $ Aeson.encode $ PresenceData p.resource p.clientId now runWithAdditionalRedis . retry x3 $ do void . fromTxResult <=< (liftRedis . multiExec) $ do - void $ hset k v d + void $ hset k (NonEmpty.singleton (v, d)) -- nb. All presences of a user are expired 'maxIdleTime' after the -- last presence was registered. A client who keeps a presence -- (i.e. websocket) connected for longer than 'maxIdleTime' will be diff --git a/services/gundeck/src/Gundeck/Redis.hs b/services/gundeck/src/Gundeck/Redis.hs index 721106bdbd1..a4784349db2 100644 --- a/services/gundeck/src/Gundeck/Redis.hs +++ b/services/gundeck/src/Gundeck/Redis.hs @@ -61,7 +61,7 @@ connectRobust :: connectRobust l retryStrategy connectLowLevel = do robustConnection <- newEmptyMVar @IO @Connection thread <- - async $ safeForever $ do + async $ safeForever l $ do Log.info l $ Log.msg (Log.val "connecting to Redis") conn <- retry connectLowLevel Log.info l $ Log.msg (Log.val "successfully connected to Redis") @@ -117,9 +117,11 @@ instance Exception PingException safeForever :: forall m. (MonadUnliftIO m) => + Logger -> m () -> m () -safeForever action = +safeForever l action = forever $ - action `catchAny` \_ -> do + action `catchAny` \e -> do + Log.err l $ Log.msg (Log.val "Uncaught exception while connecting to redis") . Log.field "error" (displayException e) threadDelay 1e6 -- pause to keep worst-case noise in logs manageable diff --git a/services/gundeck/test/integration/API.hs b/services/gundeck/test/integration/API.hs index 4ef4bd327b8..0d9f128b4ae 100644 --- a/services/gundeck/test/integration/API.hs +++ b/services/gundeck/test/integration/API.hs @@ -64,7 +64,7 @@ import System.Timeout (timeout) import Test.Tasty import Test.Tasty.HUnit import TestSetup -import Util (runRedisProxy, withSettingsOverrides) +import Util (runRedisProxy, withEnvOverrides, withSettingsOverrides) import Wire.API.Internal.Notification import Prelude qualified @@ -921,7 +921,12 @@ testRedisMigration = do map resource . decodePresence <$> (getPresence g (toByteString' uid) lookupEnv "REDIS_ADDITIONAL_WRITE_USERNAME" + password <- ("REDIS_PASSWORD",) <$$> lookupEnv "REDIS_ADDITIONAL_WRITE_PASSWORD" + pure $ catMaybes [username, password] + + withEnvOverrides redis2CredsAsRedis1Creds $ withSettingsOverrides (redis .~ redis2) $ do g <- view tsGundeck retrievedPresence <- map resource . decodePresence <$> (getPresence g (toByteString' uid) [(String, String)] -> m a -> m a +withEnvOverrides envOverrides action = do + bracket (setEnvVars envOverrides) (resetEnvVars) $ const action + where + setEnvVars :: [(String, String)] -> m [(String, Maybe String)] + setEnvVars newVars = liftIO $ do + oldVars <- mapM (\(k, _) -> (k,) <$> lookupEnv k) newVars + mapM_ (uncurry setEnv) newVars + pure oldVars + + resetEnvVars :: [(String, Maybe String)] -> m () + resetEnvVars = + mapM_ (\(k, mV) -> maybe (unsetEnv k) (setEnv k) mV) + runRedisProxy :: Text -> Word16 -> Word16 -> IO () runRedisProxy redisHost redisPort proxyPort = do (servAddr : _) <- getAddrInfo Nothing (Just $ Text.unpack redisHost) (Just $ show redisPort) From 7b195ed21a61029ede747fbb3cdf38dbc2d126d7 Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Tue, 2 Apr 2024 13:30:50 +0200 Subject: [PATCH 068/117] [WPB-5990] Wire.API.User.User: Remove userId field (#3976) * Wire.API.User.User: Remove userId field The field is redundant given there is also userQualifiedId, this causes scope for inconsistencies, which show up in Arbitrary instances. * tools/db/inconsistencies: Fix name shadowing Also enable -Werror in cabal.project --- cabal.project | 2 + .../test/unit/Test/Brig/Types/User.hs | 4 +- libs/wire-api/src/Wire/API/User.hs | 13 ++-- .../API/Golden/Generated/SelfProfile_user.hs | 3 +- .../Wire/API/Golden/Generated/User_user.hs | 15 ++-- .../Test/Wire/API/Golden/Manual/UserEvent.hs | 20 +++--- .../golden/testObject_SelfProfile_user_1.json | 2 +- .../test/golden/testObject_User_user_1.json | 2 +- .../test/golden/testObject_User_user_2.json | 2 +- .../test/golden/testObject_User_user_3.json | 2 +- .../test/golden/testObject_User_user_4.json | 2 +- .../test/golden/testObject_User_user_5.json | 2 +- services/brig/src/Brig/API/Client.hs | 4 +- services/brig/src/Brig/API/Public.hs | 7 +- services/brig/src/Brig/Data/User.hs | 5 +- services/brig/src/Brig/IO/Journal.hs | 2 +- services/brig/src/Brig/Provider/API.hs | 2 +- .../brig/test/integration/API/Federation.hs | 14 ++-- services/brig/test/integration/API/OAuth.hs | 66 ++++++++--------- .../brig/test/integration/API/Provider.hs | 26 +++---- services/brig/test/integration/API/Search.hs | 20 +++--- .../test/integration/API/TeamUserSearch.hs | 2 +- .../brig/test/integration/API/User/Auth.hs | 20 +++--- .../test/integration/API/User/Connection.hs | 72 +++++++++---------- services/galley/test/integration/API.hs | 2 +- services/galley/test/integration/API/Util.hs | 12 ++-- .../test-integration/Test/Spar/APISpec.hs | 28 ++++---- .../Test/Spar/Scim/UserSpec.hs | 16 ++--- .../inconsistencies/src/DanglingUserKeys.hs | 28 ++++---- .../db/inconsistencies/src/EmailLessUsers.hs | 10 +-- tools/stern/test/integration/API.hs | 8 +-- tools/stern/test/integration/Util.hs | 15 ++-- 32 files changed, 210 insertions(+), 218 deletions(-) diff --git a/cabal.project b/cabal.project index 471d12e874a..932f0f55399 100644 --- a/cabal.project +++ b/cabal.project @@ -103,6 +103,8 @@ package hscim ghc-options: -Werror package http2-manager ghc-options: -Werror +package inconsistencies + ghc-options: -Werror package integration ghc-options: -Werror package imports diff --git a/libs/brig-types/test/unit/Test/Brig/Types/User.hs b/libs/brig-types/test/unit/Test/Brig/Types/User.hs index da85beb6253..dee80388143 100644 --- a/libs/brig-types/test/unit/Test/Brig/Types/User.hs +++ b/libs/brig-types/test/unit/Test/Brig/Types/User.hs @@ -65,7 +65,7 @@ testCaseUserAccount = testCase "UserAcccount" $ do assertEqual "2" (Just json2) (encode <$> decode @UserAccount json2) where json1 :: LByteString - json1 = "{\"accent_id\":1,\"assets\":[],\"deleted\":true,\"expires_at\":\"1864-05-09T17:20:22.192Z\",\"handle\":\"-ve\",\"id\":\"00000001-0000-0000-0000-000000000001\",\"locale\":\"lu\",\"managed_by\":\"wire\",\"name\":\"bla\",\"phone\":\"+433017355611929\",\"picture\":[],\"qualified_id\":{\"domain\":\"4-o60.j7-i\",\"id\":\"00000000-0000-0001-0000-000100000000\"},\"service\":{\"id\":\"00000000-0000-0001-0000-000000000001\",\"provider\":\"00000001-0000-0001-0000-000000000001\"},\"status\":\"suspended\",\"supported_protocols\":[\"proteus\"],\"team\":\"00000000-0000-0001-0000-000100000001\"}" + json1 = "{\"accent_id\":1,\"assets\":[],\"deleted\":true,\"expires_at\":\"1864-05-09T17:20:22.192Z\",\"handle\":\"-ve\",\"id\":\"00000000-0000-0001-0000-000100000000\",\"locale\":\"lu\",\"managed_by\":\"wire\",\"name\":\"bla\",\"phone\":\"+433017355611929\",\"picture\":[],\"qualified_id\":{\"domain\":\"4-o60.j7-i\",\"id\":\"00000000-0000-0001-0000-000100000000\"},\"service\":{\"id\":\"00000000-0000-0001-0000-000000000001\",\"provider\":\"00000001-0000-0001-0000-000000000001\"},\"status\":\"suspended\",\"supported_protocols\":[\"proteus\"],\"team\":\"00000000-0000-0001-0000-000100000001\"}" json2 :: LByteString - json2 = "{\"accent_id\":0,\"assets\":[{\"key\":\"3-4-00000000-0000-0001-0000-000000000000\",\"size\":\"preview\",\"type\":\"image\"}],\"email\":\"@\",\"expires_at\":\"1864-05-10T22:45:44.823Z\",\"handle\":\"b8m\",\"id\":\"00000001-0000-0000-0000-000100000000\",\"locale\":\"tk-KZ\",\"managed_by\":\"wire\",\"name\":\"name2\",\"picture\":[],\"qualified_id\":{\"domain\":\"1-8wq0.b22k1.w5\",\"id\":\"00000000-0000-0000-0000-000000000001\"},\"service\":{\"id\":\"00000000-0000-0001-0000-000000000001\",\"provider\":\"00000001-0000-0001-0000-000100000000\"},\"status\":\"pending-invitation\",\"supported_protocols\":[\"proteus\"],\"team\":\"00000000-0000-0001-0000-000000000001\"}" + json2 = "{\"accent_id\":0,\"assets\":[{\"key\":\"3-4-00000000-0000-0001-0000-000000000000\",\"size\":\"preview\",\"type\":\"image\"}],\"email\":\"@\",\"expires_at\":\"1864-05-10T22:45:44.823Z\",\"handle\":\"b8m\",\"id\":\"00000000-0000-0000-0000-000000000001\",\"locale\":\"tk-KZ\",\"managed_by\":\"wire\",\"name\":\"name2\",\"picture\":[],\"qualified_id\":{\"domain\":\"1-8wq0.b22k1.w5\",\"id\":\"00000000-0000-0000-0000-000000000001\"},\"service\":{\"id\":\"00000000-0000-0001-0000-000000000001\",\"provider\":\"00000001-0000-0001-0000-000100000000\"},\"status\":\"pending-invitation\",\"supported_protocols\":[\"proteus\"],\"team\":\"00000000-0000-0001-0000-000000000001\"}" diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index d7f9da9ae09..e605fd9ec84 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -35,6 +35,7 @@ module Wire.API.User SelfProfile (..), -- User (should not be here) User (..), + userId, userEmail, userPhone, userSSOId, @@ -665,8 +666,7 @@ instance FromJSON SelfProfile where -- | The data of an existing user. data User = User - { userId :: UserId, - userQualifiedId :: Qualified UserId, + { userQualifiedId :: Qualified UserId, -- | User identity. For endpoints like @/self@, it will be present in the response iff -- the user is activated, and the email/phone contained in it will be guaranteedly -- verified. {#RefActivation} @@ -697,6 +697,9 @@ data User = User deriving (Arbitrary) via (GenericUniform User) deriving (ToJSON, FromJSON, S.ToSchema) via (Schema User) +userId :: User -> UserId +userId = qUnqualified . userQualifiedId + -- -- FUTUREWORK: -- -- disentangle json serializations for 'User', 'NewUser', 'UserIdentity', 'NewUserOrigin'. instance ToSchema User where @@ -705,10 +708,10 @@ instance ToSchema User where userObjectSchema :: ObjectSchema SwaggerDoc User userObjectSchema = User - <$> userId - .= field "id" schema - <*> userQualifiedId + <$> userQualifiedId .= field "qualified_id" schema + <* userId + .= optional (field "id" (deprecatedSchema "qualified_id" schema)) <*> userIdentity .= maybeUserIdentityObjectSchema <*> userDisplayName diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SelfProfile_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SelfProfile_user.hs index 119b936cb89..8347f901b60 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SelfProfile_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SelfProfile_user.hs @@ -36,8 +36,7 @@ testObject_SelfProfile_user_1 = SelfProfile { selfUser = User - { userId = Id (fromJust (UUID.fromString "00000002-0000-0002-0000-000200000002")), - userQualifiedId = + { userQualifiedId = Qualified { qUnqualified = Id (fromJust (UUID.fromString "00000001-0000-0000-0000-000000000002")), qDomain = Domain {_domainText = "n0-994.m-226.f91.vg9p-mj-j2"} diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/User_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/User_user.hs index 439e3c220d3..c744ea8f57a 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/User_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/User_user.hs @@ -49,8 +49,7 @@ import Wire.API.User testObject_User_user_1 :: User testObject_User_user_1 = User - { userId = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000100000000")), - userQualifiedId = + { userQualifiedId = Qualified { qUnqualified = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000200000002")), qDomain = Domain {_domainText = "s-f4.s"} @@ -73,8 +72,7 @@ testObject_User_user_1 = testObject_User_user_2 :: User testObject_User_user_2 = User - { userId = Id (fromJust (UUID.fromString "00000000-0000-0000-0000-000100000001")), - userQualifiedId = + { userQualifiedId = Qualified { qUnqualified = Id (fromJust (UUID.fromString "00000000-0000-0001-0000-000200000001")), qDomain = Domain {_domainText = "k.vbg.p"} @@ -111,8 +109,7 @@ testObject_User_user_2 = testObject_User_user_3 :: User testObject_User_user_3 = User - { userId = Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000100000000")), - userQualifiedId = + { userQualifiedId = Qualified { qUnqualified = Id (fromJust (UUID.fromString "00000002-0000-0000-0000-000100000002")), qDomain = Domain {_domainText = "dt.n"} @@ -142,8 +139,7 @@ testObject_User_user_3 = testObject_User_user_4 :: User testObject_User_user_4 = User - { userId = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000100000002")), - userQualifiedId = + { userQualifiedId = Qualified { qUnqualified = Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000200000002")), qDomain = Domain {_domainText = "28b.cqb"} @@ -183,8 +179,7 @@ testObject_User_user_4 = testObject_User_user_5 :: User testObject_User_user_5 = User - { userId = Id (fromJust (UUID.fromString "00000002-0000-0001-0000-000100000002")), - userQualifiedId = + { userQualifiedId = Qualified { qUnqualified = Id (fromJust (UUID.fromString "00000000-0000-0002-0000-000200000002")), qDomain = Domain {_domainText = "28b.cqb"} diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs index 3c063f6fde5..7ae86628304 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Manual/UserEvent.hs @@ -87,7 +87,7 @@ testObject_UserEvent_6 = UserEvent ( UserUpdated ( UserUpdatedData - alice.userId + (userId alice) (Just alice.userDisplayName) (Just alice.userPict) (Just alice.userAccentId) @@ -106,7 +106,7 @@ testObject_UserEvent_7 = UserEvent ( UserIdentityUpdated ( UserIdentityUpdatedData - alice.userId + (userId alice) (Just (Email "alice" "foo.example.com")) Nothing ) @@ -117,24 +117,24 @@ testObject_UserEvent_8 = UserEvent ( UserIdentityRemoved ( UserIdentityRemovedData - alice.userId + (userId alice) (Just (Email "alice" "foo.example.com")) Nothing ) ) testObject_UserEvent_9 :: Event -testObject_UserEvent_9 = UserEvent (UserLegalHoldDisabled alice.userId) +testObject_UserEvent_9 = UserEvent (UserLegalHoldDisabled (userId alice)) testObject_UserEvent_10 :: Event -testObject_UserEvent_10 = UserEvent (UserLegalHoldEnabled alice.userId) +testObject_UserEvent_10 = UserEvent (UserLegalHoldEnabled (userId alice)) testObject_UserEvent_11 :: Event testObject_UserEvent_11 = UserEvent ( LegalHoldClientRequested ( LegalHoldClientRequestedData - alice.userId + (userId alice) (lastPrekey "foo") (ClientId 3728) ) @@ -145,7 +145,7 @@ testObject_UserEvent_12 = ConnectionEvent ( ConnectionUpdated ( UserConnection - bob.userId + (userId bob) bob.userQualifiedId Accepted (fromJust (readUTCTimeMillis "2007-02-03T10:51:17.329Z")) @@ -195,8 +195,7 @@ testObject_UserEvent_17 = ClientEvent (ClientRemoved (ClientId 2839)) alice :: User alice = User - { userId = Id (fromJust (UUID.fromString "539d9183-32a5-4fc4-ba5c-4634454e7585")), - userQualifiedId = + { userQualifiedId = Qualified { qUnqualified = Id (fromJust (UUID.fromString "539d9183-32a5-4fc4-ba5c-4634454e7585")), qDomain = Domain {_domainText = "foo.example.com"} @@ -223,8 +222,7 @@ alice = bob :: User bob = User - { userId = Id (fromJust (UUID.fromString "284d1c86-5117-4c58-aa18-c0068f3f7d8c")), - userQualifiedId = + { userQualifiedId = Qualified { qUnqualified = Id (fromJust (UUID.fromString "284d1c86-5117-4c58-aa18-c0068f3f7d8c")), qDomain = Domain {_domainText = "baz.example.com"} diff --git a/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json b/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json index df030f6b257..111b4170f0e 100644 --- a/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json +++ b/libs/wire-api/test/golden/testObject_SelfProfile_user_1.json @@ -4,7 +4,7 @@ "email": "\u0007@", "expires_at": "1864-05-07T21:09:29.342Z", "handle": "do9-5", - "id": "00000002-0000-0002-0000-000200000002", + "id": "00000001-0000-0000-0000-000000000002", "locale": "gl-PA", "managed_by": "scim", "name": "@ֱਦ𐋂\u001f􍱇l+𡡖6󳒏^𧦣Mu\t", diff --git a/libs/wire-api/test/golden/testObject_User_user_1.json b/libs/wire-api/test/golden/testObject_User_user_1.json index 0e06a5f2c45..b3fbc638960 100644 --- a/libs/wire-api/test/golden/testObject_User_user_1.json +++ b/libs/wire-api/test/golden/testObject_User_user_1.json @@ -2,7 +2,7 @@ "accent_id": 1, "assets": [], "deleted": true, - "id": "00000000-0000-0001-0000-000100000000", + "id": "00000002-0000-0001-0000-000200000002", "locale": "tn-SB", "managed_by": "wire", "name": "\u0000uv󳊼su渱lRi", diff --git a/libs/wire-api/test/golden/testObject_User_user_2.json b/libs/wire-api/test/golden/testObject_User_user_2.json index d34488ab3c2..1a9f918989f 100644 --- a/libs/wire-api/test/golden/testObject_User_user_2.json +++ b/libs/wire-api/test/golden/testObject_User_user_2.json @@ -18,7 +18,7 @@ ], "deleted": true, "expires_at": "1864-05-11T17:06:58.936Z", - "id": "00000000-0000-0000-0000-000100000001", + "id": "00000000-0000-0001-0000-000200000001", "locale": "da-TN", "managed_by": "wire", "name": "4􄢻7\u0006\u0012\u0012\u0017bp\u0001麙0Yr\\󰘣vKRg󿽓)󽼺S󰇌􂏦:3B\u0006\u0013\u0003T", diff --git a/libs/wire-api/test/golden/testObject_User_user_3.json b/libs/wire-api/test/golden/testObject_User_user_3.json index ba235a89721..fb25c3feb83 100644 --- a/libs/wire-api/test/golden/testObject_User_user_3.json +++ b/libs/wire-api/test/golden/testObject_User_user_3.json @@ -5,7 +5,7 @@ "email": "f@𔒫", "expires_at": "1864-05-09T20:12:05.821Z", "handle": "1c", - "id": "00000002-0000-0000-0000-000100000000", + "id": "00000002-0000-0000-0000-000100000002", "locale": "tg-UA", "managed_by": "wire", "name": ",r\u0019XEg0$𗾋\u001e\u000f'uS\u0003/󶙆`äV.J{\u000cgE(\rK!\u000ep8s9gXO唲Xj\u0002\u001e\u0012", diff --git a/libs/wire-api/test/golden/testObject_User_user_4.json b/libs/wire-api/test/golden/testObject_User_user_4.json index 612526358e8..9e5243bc311 100644 --- a/libs/wire-api/test/golden/testObject_User_user_4.json +++ b/libs/wire-api/test/golden/testObject_User_user_4.json @@ -4,7 +4,7 @@ "email": "@", "expires_at": "1864-05-09T14:25:26.089Z", "handle": "iw2-.udd2l7-7yg3dfg.wzn4vx3hjhch8.--5t6uyjqk93twv-a2pce8p1xjh7387nztzu.q", - "id": "00000002-0000-0001-0000-000100000002", + "id": "00000000-0000-0002-0000-000200000002", "locale": "bi-MQ", "managed_by": "scim", "name": "^󺝨F􈝼=&o>f<7\u000eq|6\u0011\u0019󳟧􁗄\u001bf󷯶𩣇\u0013bnVAj`^L\u000c󿮁\u001fLI\u0005!􃈈\u0017`󾒁\u0003e曉\u001aK|", diff --git a/libs/wire-api/test/golden/testObject_User_user_5.json b/libs/wire-api/test/golden/testObject_User_user_5.json index a11342eca05..2f9c0c23c40 100644 --- a/libs/wire-api/test/golden/testObject_User_user_5.json +++ b/libs/wire-api/test/golden/testObject_User_user_5.json @@ -4,7 +4,7 @@ "email": "@", "expires_at": "1864-05-09T14:25:26.089Z", "handle": "iw2-.udd2l7-7yg3dfg.wzn4vx3hjhch8.--5t6uyjqk93twv-a2pce8p1xjh7387nztzu.q", - "id": "00000002-0000-0001-0000-000100000002", + "id": "00000000-0000-0002-0000-000200000002", "locale": "bi-MQ", "managed_by": "scim", "name": "^󺝨F􈝼=&o>f<7\u000eq|6\u0011\u0019󳟧􁗄\u001bf󷯶𩣇\u0013bnVAj`^L\u000c󿮁\u001fLI\u0005!􃈈\u0017`󾒁\u0003e曉\u001aK|", diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 17071f56ef3..65bb1ab8b77 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -225,10 +225,10 @@ addClientWithReAuthPolicy policy u con new = do Maybe Code.Value -> UserId -> ExceptT ClientError (AppT r) () - verifyCode mbCode userId = + verifyCode mbCode uid = -- this only happens inside the login flow (in particular, when logging in from a new device) -- the code obtained for logging in is used a second time for adding the device - UserAuth.verifyCode mbCode Code.Login userId `catchE` \case + UserAuth.verifyCode mbCode Code.Login uid `catchE` \case VerificationCodeRequired -> throwE ClientCodeAuthenticationRequired VerificationCodeNoPendingCode -> throwE ClientCodeAuthenticationFailed VerificationCodeNoEmail -> throwE ClientCodeAuthenticationFailed diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 0630ee43602..f15bd59953d 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -731,9 +731,10 @@ createUser (Public.NewUserPublic new) = lift . runExceptT $ do ) lift . Log.info $ context . Log.msg @Text "Sucessfully created user" - let Public.User {userLocale, userDisplayName, userId} = usr - let userEmail = Public.userEmail usr - let userPhone = Public.userPhone usr + let Public.User {userLocale, userDisplayName} = usr + userEmail = Public.userEmail usr + userPhone = Public.userPhone usr + userId = Public.userId usr lift $ do for_ (liftM2 (,) userEmail epair) $ \(e, p) -> sendActivationEmail e userDisplayName p (Just userLocale) newUserTeam diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 64638d9af57..053e472a997 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -159,7 +159,7 @@ newAccount u inv tid mbHandle = do locale defLoc = fromMaybe defLoc (newUserLocale u) managedBy = fromMaybe defaultManagedBy (newUserManagedBy u) prots = fromMaybe defSupportedProtocols (newUserSupportedProtocols u) - user uid domain l e = User uid (Qualified uid domain) ident name pict assets colour False l Nothing mbHandle e tid managedBy prots + user uid domain l e = User (Qualified uid domain) ident name pict assets colour False l Nothing mbHandle e tid managedBy prots newAccountInviteViaScim :: MonadReader Env m => UserId -> TeamId -> Maybe Locale -> Name -> Email -> m UserAccount newAccountInviteViaScim uid tid locale name email = do @@ -170,7 +170,6 @@ newAccountInviteViaScim uid tid locale name email = do where user domain loc = User - uid (Qualified uid domain) (Just $ EmailIdentity email) name @@ -721,7 +720,6 @@ toUserAccount svc = newServiceRef <$> sid <*> pid in UserAccount ( User - uid (Qualified uid domain) ident name @@ -797,7 +795,6 @@ toUsers domain defaultLocale havePendingInvitations = fmap mk . filter fp loc = toLocale defaultLocale (lan, con) svc = newServiceRef <$> sid <*> pid in User - uid (Qualified uid domain) ident name diff --git a/services/brig/src/Brig/IO/Journal.hs b/services/brig/src/Brig/IO/Journal.hs index 054792ae50c..d5faf53332c 100644 --- a/services/brig/src/Brig/IO/Journal.hs +++ b/services/brig/src/Brig/IO/Journal.hs @@ -48,7 +48,7 @@ import Wire.API.User -- without journaling arguments for user updates userActivate :: (MonadReader Env m, MonadIO m) => User -> m () -userActivate u@User {..} = journalEvent UserEvent'USER_ACTIVATE userId (userEmail u) (Just userLocale) userTeam (Just userDisplayName) +userActivate u@User {..} = journalEvent UserEvent'USER_ACTIVATE (userId u) (userEmail u) (Just userLocale) userTeam (Just userDisplayName) userUpdate :: (MonadReader Env m, MonadIO m) => UserId -> Maybe Email -> Maybe Locale -> Maybe Name -> m () userUpdate uid em loc = journalEvent UserEvent'USER_UPDATE uid em loc Nothing diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 0bc3963beeb..5f198876d38 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -693,7 +693,7 @@ addBot zuid zcon cid add = do let colour = fromMaybe defaultAccentId (Ext.rsNewBotColour rs) let pict = Pict [] -- Legacy let sref = newServiceRef sid pid - let usr = User (botUserId bid) (Qualified (botUserId bid) domain) Nothing name pict assets colour False locale (Just sref) Nothing Nothing Nothing ManagedByWire defSupportedProtocols + let usr = User (Qualified (botUserId bid) domain) Nothing name pict assets colour False locale (Just sref) Nothing Nothing Nothing ManagedByWire defSupportedProtocols let newClt = (newClient PermanentClientType (Ext.rsNewBotLastPrekey rs)) { newClientPrekeys = Ext.rsNewBotPrekeys rs diff --git a/services/brig/test/integration/API/Federation.hs b/services/brig/test/integration/API/Federation.hs index 3b5829f1c5a..9f416d5b252 100644 --- a/services/brig/test/integration/API/Federation.hs +++ b/services/brig/test/integration/API/Federation.hs @@ -44,7 +44,7 @@ import Wire.API.Federation.API.Brig qualified as S import Wire.API.Federation.Component import Wire.API.Federation.Version import Wire.API.Routes.FederationDomainConfig as FD -import Wire.API.User +import Wire.API.User as User import Wire.API.User.Client import Wire.API.User.Client.Prekey import Wire.API.User.Search @@ -126,7 +126,7 @@ testFulltextSearchMultipleUsers opts brig = do update'' :: UserUpdate <- liftIO $ generate arbitrary let update' = update'' {uupName = Just (Name (fromHandle handle))} update = RequestBodyLBS . encode $ update' - put (brig . path "/self" . contentJson . zUser identityThief.userId . zConn "c" . body update) !!! const 200 === statusCode + put (brig . path "/self" . contentJson . zUser (User.userId identityThief) . zConn "c" . body update) !!! const 200 === statusCode refreshIndex brig @@ -272,9 +272,9 @@ testGetUsersByIdsSuccess :: Brig -> FedClient 'Brig -> Http () testGetUsersByIdsSuccess brig fedBrigClient = do user1 <- randomUser brig user2 <- randomUser brig - let uid1 = user1.userId + let uid1 = User.userId user1 quid1 = userQualifiedId user1 - uid2 = user2.userId + uid2 = User.userId user2 quid2 = userQualifiedId user2 profiles <- runFedClient @"get-users-by-ids" fedBrigClient (Domain "example.com") [uid1, uid2] liftIO $ do @@ -287,7 +287,7 @@ testGetUsersByIdsPartial brig fedBrigClient = do absentUserId :: UserId <- Id <$> lift UUIDv4.nextRandom profiles <- runFedClient @"get-users-by-ids" fedBrigClient (Domain "example.com") $ - [presentUser.userId, absentUserId] + [User.userId presentUser, absentUserId] liftIO $ assertEqual "should return the present user and skip the absent ones" [userQualifiedId presentUser] (profileQualifiedId <$> profiles) @@ -302,7 +302,7 @@ testGetUsersByIdsFederationRestrictionAllowAllFound fedBrigClient = do testClaimPrekeySuccess :: Brig -> FedClient 'Brig -> Http () testClaimPrekeySuccess brig fedBrigClient = do user <- randomUser brig - let uid = user.userId + let uid = User.userId user let new = defNewClient PermanentClientType [head somePrekeys] (head someLastPrekeys) c <- responseJsonError =<< addClient brig uid new mkey <- runFedClient @"claim-prekey" fedBrigClient (Domain "example.com") (uid, clientId c) @@ -351,7 +351,7 @@ addTestClients brig uid idxs = testGetUserClients :: Brig -> FedClient 'Brig -> Http () testGetUserClients brig fedBrigClient = do - uid1 <- (.userId) <$> randomUser brig + uid1 <- User.userId <$> randomUser brig clients :: [Client] <- addTestClients brig uid1 [0, 1, 2] UserMap userClients <- runFedClient @"get-user-clients" fedBrigClient (Domain "example.com") (GetUserClients [uid1]) liftIO $ diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 757815af2c0..162a920b431 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -58,7 +58,7 @@ import Wire.API.Conversation.Code (CreateConversationCodeRequest (CreateConversa import Wire.API.Conversation.Role qualified as Role import Wire.API.OAuth import Wire.API.Routes.Bearer (Bearer (Bearer, unBearer)) -import Wire.API.User +import Wire.API.User as User import Wire.API.User.Auth (CookieType (PersistentCookie)) import Wire.Sem.Jwk (readJwk) @@ -185,7 +185,7 @@ testCreateAccessTokenSuccess opts brig = do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.singleton ReadSelf - (cid, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest -- authorization code should be deleted and can only be used once @@ -203,7 +203,7 @@ testCreateAccessTokenSuccess opts brig = do claims.scope @?= scopes (view claimIss $ claims) @?= (expectedDomain ^? stringOrUri @Text) (view claimAud $ claims) @?= (Audience . (: []) <$> expectedDomain ^? stringOrUri @Text) - (view claimSub $ claims) @?= (idToText user.userId ^? stringOrUri) + (view claimSub $ claims) @?= (idToText (User.userId user) ^? stringOrUri) let expTime = (\(NumericDate x) -> x) . fromMaybe (error "exp claim missing") . view claimExp $ claims diffUTCTime expTime now > 0 @?= True let issuingTime = (\(NumericDate x) -> x) . fromMaybe (error "iat claim missing") . view claimIat $ claims @@ -361,7 +361,7 @@ testAccessResourceSuccessNginz brig nginz = do -- with Authorization header containing an OAuth bearer token let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest self' <- responseJsonError =<< get (nginz . paths ["self"] . authHeader resp.accessToken) fromMaybe (error "invalid key") @@ -483,7 +483,7 @@ testRefreshTokenRetrieveAccessToken brig nginz = do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest get (nginz . paths ["self"] . authHeader (resp.accessToken)) !!! const 200 === statusCode @@ -498,7 +498,7 @@ testRefreshTokenWrongSignature opts brig = do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest key <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") @@ -515,7 +515,7 @@ testRefreshTokenNoTokenId opts brig = do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, _) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + (cid, _) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl key <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") badRefreshToken <- liftIO $ OAuthToken <$> signRefreshToken key emptyClaimsSet let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid badRefreshToken @@ -528,7 +528,7 @@ testRefreshTokenNonExistingId opts brig = do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest key <- liftIO $ readJwk (fromMaybe "path to jwk not set" (Opt.setOAuthJwkKeyPair $ Opt.optSettings opts)) <&> fromMaybe (error "invalid key") @@ -550,7 +550,7 @@ testRefreshTokenWrongClientId brig = do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest badCid <- randomId @@ -564,7 +564,7 @@ testRefreshTokenWrongGrantType brig = do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeAuthorizationCode cid resp.refreshToken @@ -579,7 +579,7 @@ testRefreshTokenExpiredToken opts brig = user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid resp.refreshToken @@ -593,7 +593,7 @@ testRefreshTokenRevokedToken brig = do user <- createUser "alice" brig let redirectUrl = mkUrl "https://example.com" let scopes = OAuthScopes $ Set.fromList [ReadSelf] - (cid, code) <- generateOAuthClientAndAuthorizationCode brig user.userId scopes redirectUrl + (cid, code) <- generateOAuthClientAndAuthorizationCode brig (User.userId user) scopes redirectUrl let accessTokenRequest = OAuthAccessTokenRequest OAuthGrantTypeAuthorizationCode cid verifier code redirectUrl resp <- createOAuthAccessToken brig accessTokenRequest let refreshAccessTokenRequest = OAuthRefreshAccessTokenRequest OAuthGrantTypeRefreshToken cid resp.refreshToken @@ -607,45 +607,45 @@ testListApplicationsWithAccountAccess brig = do alice <- createUser "alice" brig bob <- createUser "bob" brig do - apps <- listOAuthApplications brig alice.userId + apps <- listOAuthApplications brig (User.userId alice) liftIO $ assertEqual "apps" 0 (length apps) - void $ createOAuthApplicationWithAccountAccess brig alice.userId - void $ createOAuthApplicationWithAccountAccess brig alice.userId + void $ createOAuthApplicationWithAccountAccess brig (User.userId alice) + void $ createOAuthApplicationWithAccountAccess brig (User.userId alice) do - aliceApps <- listOAuthApplications brig alice.userId + aliceApps <- listOAuthApplications brig (User.userId alice) liftIO $ assertEqual "apps" 2 (length aliceApps) - bobsApps <- listOAuthApplications brig bob.userId + bobsApps <- listOAuthApplications brig (User.userId bob) liftIO $ assertEqual "apps" 0 (length bobsApps) - void $ createOAuthApplicationWithAccountAccess brig alice.userId - void $ createOAuthApplicationWithAccountAccess brig bob.userId + void $ createOAuthApplicationWithAccountAccess brig (User.userId alice) + void $ createOAuthApplicationWithAccountAccess brig (User.userId bob) do - aliceApps <- listOAuthApplications brig alice.userId + aliceApps <- listOAuthApplications brig (User.userId alice) liftIO $ assertEqual "apps" 3 (length aliceApps) - bobsApps <- listOAuthApplications brig bob.userId + bobsApps <- listOAuthApplications brig (User.userId bob) liftIO $ assertEqual "apps" 1 (length bobsApps) testRevokeApplicationAccountAccess :: Brig -> Http () testRevokeApplicationAccountAccess brig = do user <- createUser "alice" brig do - apps <- listOAuthApplications brig user.userId + apps <- listOAuthApplications brig (User.userId user) liftIO $ assertEqual "apps" 0 (length apps) - for_ [1 .. 3 :: Int] $ const $ createOAuthApplicationWithAccountAccess brig user.userId - cids <- fmap applicationId <$> listOAuthApplications brig user.userId + for_ [1 .. 3 :: Int] $ const $ createOAuthApplicationWithAccountAccess brig (User.userId user) + cids <- fmap applicationId <$> listOAuthApplications brig (User.userId user) liftIO $ assertEqual "apps" 3 (length cids) case cids of [cid1, cid2, cid3] -> do - revokeOAuthApplicationAccess brig user.userId cid1 + revokeOAuthApplicationAccess brig (User.userId user) cid1 do - apps <- listOAuthApplications brig user.userId + apps <- listOAuthApplications brig (User.userId user) liftIO $ assertEqual "apps" 2 (length apps) - revokeOAuthApplicationAccess brig user.userId cid2 + revokeOAuthApplicationAccess brig (User.userId user) cid2 do - apps <- listOAuthApplications brig user.userId + apps <- listOAuthApplications brig (User.userId user) liftIO $ assertEqual "apps" 1 (length apps) - revokeOAuthApplicationAccess brig user.userId cid3 + revokeOAuthApplicationAccess brig (User.userId user) cid3 do - apps <- listOAuthApplications brig user.userId + apps <- listOAuthApplications brig (User.userId user) liftIO $ assertEqual "apps" 0 (length apps) _ -> liftIO $ assertFailure "unexpected number of apps" diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index 0de2cdbb67a..a2479df44e8 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -574,22 +574,22 @@ testClaimUserPrekeys :: Config -> DB.ClientState -> Brig -> Galley -> Http () testClaimUserPrekeys config db brig galley = withTestService config db brig defServiceApp $ \sref _ -> do (pid, sid, u1, _u2, _h) <- prepareUsers sref brig cid <- do - rs <- createConv galley u1.userId [] DB.ClientState -> Brig -> Galley -> Http () testListUserProfiles config db brig galley = withTestService config db brig defServiceApp $ \sref _ -> do (pid, sid, u1, u2, _h) <- prepareUsers sref brig cid <- do - rs <- createConv galley u1.userId [] DB.ClientState -> Brig -> Galley -> Http () testGetUserClients config db brig galley = withTestService config db brig defServiceApp $ \sref _ -> do (pid, sid, u1, _u2, _h) <- prepareUsers sref brig cid <- do - rs <- createConv galley u1.userId [] DB.ClientState -> Brig -> Galley -> Http () diff --git a/services/brig/test/integration/API/Search.hs b/services/brig/test/integration/API/Search.hs index 5c1b341bec1..4a1e359149d 100644 --- a/services/brig/test/integration/API/Search.hs +++ b/services/brig/test/integration/API/Search.hs @@ -66,7 +66,7 @@ import Util import Wire.API.Federation.API.Brig (SearchResponse (SearchResponse)) import Wire.API.Team.Feature import Wire.API.Team.SearchVisibility -import Wire.API.User +import Wire.API.User as User import Wire.API.User.Search import Wire.API.User.Search qualified as Search @@ -236,23 +236,23 @@ testSearchCJK brig = do user' <- createUser' True "さおり" brig user'' <- createUser' True "ジョン" brig refreshIndex brig - assertCanFind brig searcher.userId user.userQualifiedId "藤崎詩織" + assertCanFind brig (User.userId searcher) user.userQualifiedId "藤崎詩織" - assertCanFind brig searcher.userId user'.userQualifiedId "saori" - assertCanFind brig searcher.userId user'.userQualifiedId "さおり" - assertCanFind brig searcher.userId user'.userQualifiedId "サオリ" + assertCanFind brig (User.userId searcher) user'.userQualifiedId "saori" + assertCanFind brig (User.userId searcher) user'.userQualifiedId "さおり" + assertCanFind brig (User.userId searcher) user'.userQualifiedId "サオリ" - assertCanFind brig searcher.userId user''.userQualifiedId "jon" - assertCanFind brig searcher.userId user''.userQualifiedId "ジョン" - assertCanFind brig searcher.userId user''.userQualifiedId "じょん" + assertCanFind brig (User.userId searcher) user''.userQualifiedId "jon" + assertCanFind brig (User.userId searcher) user''.userQualifiedId "ジョン" + assertCanFind brig (User.userId searcher) user''.userQualifiedId "じょん" testSearchWithUmlaut :: TestConstraints m => Brig -> m () testSearchWithUmlaut brig = do searcher <- randomUser brig user <- createUser' True "Özi Müller" brig refreshIndex brig - assertCanFind brig searcher.userId user.userQualifiedId "ozi muller" - assertCanFind brig searcher.userId user.userQualifiedId "Özi Müller" + assertCanFind brig (User.userId searcher) user.userQualifiedId "ozi muller" + assertCanFind brig (User.userId searcher) user.userQualifiedId "Özi Müller" testSearchByHandle :: TestConstraints m => Brig -> m () testSearchByHandle brig = do diff --git a/services/brig/test/integration/API/TeamUserSearch.hs b/services/brig/test/integration/API/TeamUserSearch.hs index add51662ff9..c939dc5d16c 100644 --- a/services/brig/test/integration/API/TeamUserSearch.hs +++ b/services/brig/test/integration/API/TeamUserSearch.hs @@ -33,7 +33,7 @@ import System.Random.Shuffle (shuffleM) import Test.Tasty (TestTree, testGroup) import Test.Tasty.HUnit (assertBool, assertEqual, (@?=)) import Util (Brig, Galley, randomEmail, test, withSettingsOverrides) -import Wire.API.User (User (..), userEmail) +import Wire.API.User (User (..), userEmail, userId) import Wire.API.User.Identity import Wire.API.User.Search diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index 06a10761028..fcb4ad5b9ca 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -68,10 +68,8 @@ import Util import Wire.API.Conversation (Conversation (..)) import Wire.API.Password (Password, mkSafePassword) import Wire.API.Team.Feature qualified as Public -import Wire.API.User -import Wire.API.User qualified as Public -import Wire.API.User.Auth -import Wire.API.User.Auth qualified as Auth +import Wire.API.User as Public +import Wire.API.User.Auth as Auth import Wire.API.User.Auth.LegalHold import Wire.API.User.Auth.ReAuth import Wire.API.User.Auth.Sso @@ -390,7 +388,7 @@ testPhoneLogin brig = do testHandleLogin :: Brig -> Http () testHandleLogin brig = do - usr <- (.userId) <$> randomUser brig + usr <- Public.userId <$> randomUser brig hdl <- randomHandle let update = RequestBodyLBS . encode $ HandleUpdate hdl put (brig . path "/self/handle" . contentJson . zUser usr . zConn "c" . Http.body update) @@ -703,7 +701,7 @@ testLimitRetries conf brig = do testRegularUserLegalHoldLogin :: Brig -> Http () testRegularUserLegalHoldLogin brig = do -- Create a regular user - uid <- (.userId) <$> randomUser brig + uid <- Public.userId <$> randomUser brig -- fail if user is not a team user legalHoldLogin brig (LegalHoldLogin uid (Just defPassword) Nothing) PersistentCookie !!! do const 403 === statusCode @@ -788,7 +786,7 @@ testLegalHoldLogout brig galley = do testEmailSsoLogin :: Brig -> Http () testEmailSsoLogin brig = do -- Create a user - uid <- (.userId) <$> randomUser brig + uid <- Public.userId <$> randomUser brig now <- liftIO getCurrentTime -- Login and do some checks _rs <- @@ -803,7 +801,7 @@ testEmailSsoLogin brig = do testSuspendedSsoLogin :: Brig -> Http () testSuspendedSsoLogin brig = do -- Create a user and immediately suspend them - uid <- (.userId) <$> randomUser brig + uid <- Public.userId <$> randomUser brig setStatus brig uid Suspended -- Try to login and see if we fail ssoLogin brig (SsoLogin uid Nothing) PersistentCookie !!! do @@ -833,7 +831,7 @@ testInvalidCookie z b = do const 403 === statusCode const (Just "Invalid user token") =~= responseBody -- Expired - user <- (.userId) <$> randomUser b + user <- Public.userId <$> randomUser b let f = set (ZAuth.userTTL (Proxy @u)) 0 t <- toByteString' <$> runZAuth z (ZAuth.localSettings f (ZAuth.newUserToken @u user Nothing)) liftIO $ threadDelay 1000000 @@ -845,7 +843,7 @@ testInvalidCookie z b = do testInvalidToken :: ZAuth.Env -> Brig -> Http () testInvalidToken z b = do - user <- (.userId) <$> randomUser b + user <- Public.userId <$> randomUser b t <- toByteString' <$> runZAuth z (ZAuth.newUserToken @ZAuth.User user Nothing) -- Syntactically invalid @@ -1421,7 +1419,7 @@ testLogout b = do testReauthentication :: Brig -> Http () testReauthentication b = do - u <- (.userId) <$> randomUser b + u <- Public.userId <$> randomUser b let js = Http.body . RequestBodyLBS . encode $ object ["foo" .= ("bar" :: Text)] get (b . paths ["/i/users", toByteString' u, "reauthenticate"] . contentJson . js) !!! do const 403 === statusCode diff --git a/services/brig/test/integration/API/User/Connection.hs b/services/brig/test/integration/API/User/Connection.hs index 90559a710a1..7dbb61f7bcb 100644 --- a/services/brig/test/integration/API/User/Connection.hs +++ b/services/brig/test/integration/API/User/Connection.hs @@ -47,7 +47,7 @@ import Wire.API.Federation.API.Brig import Wire.API.Federation.Component import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.MultiTablePaging -import Wire.API.User +import Wire.API.User as User tests :: ConnectionLimit -> @@ -101,7 +101,7 @@ tests cl _at p b _c g fedBrigClient db = testCreateConnectionInvalidUser :: Brig -> Http () testCreateConnectionInvalidUser brig = do - uid1 <- (.userId) <$> randomUser brig + uid1 <- User.userId <$> randomUser brig -- user does not exist uid2 <- Id <$> liftIO UUID.nextRandom postConnection brig uid1 uid2 !!! do @@ -130,14 +130,14 @@ testCreateConnectionInvalidUserQualified brig = do testCreateManualConnections :: Brig -> Http () testCreateManualConnections brig = do - uid1 <- (.userId) <$> randomUser brig - uid2 <- (.userId) <$> randomUser brig + uid1 <- User.userId <$> randomUser brig + uid2 <- User.userId <$> randomUser brig postConnection brig uid1 uid2 !!! const 201 === statusCode assertConnections brig uid1 [ConnectionStatus uid1 uid2 Sent] assertConnections brig uid2 [ConnectionStatus uid2 uid1 Pending] -- Test that no connections to anonymous users can be created, -- as well as that anonymous users cannot create connections. - uid3 <- (.userId) <$> createAnonUser "foo3" brig + uid3 <- User.userId <$> createAnonUser "foo3" brig postConnection brig uid1 uid3 !!! const 400 === statusCode postConnection brig uid3 uid1 !!! const 403 === statusCode @@ -156,8 +156,8 @@ testCreateManualConnectionsQualified brig = do testCreateMutualConnections :: Brig -> Galley -> Http () testCreateMutualConnections brig galley = do - uid1 <- (.userId) <$> randomUser brig - uid2 <- (.userId) <$> randomUser brig + uid1 <- User.userId <$> randomUser brig + uid2 <- User.userId <$> randomUser brig postConnection brig uid1 uid2 !!! const 201 === statusCode assertConnections brig uid1 [ConnectionStatus uid1 uid2 Sent] assertConnections brig uid2 [ConnectionStatus uid2 uid1 Pending] @@ -198,8 +198,8 @@ testCreateMutualConnectionsQualified brig galley = do testAcceptConnection :: Brig -> Http () testAcceptConnection brig = do - uid1 <- (.userId) <$> randomUser brig - uid2 <- (.userId) <$> randomUser brig + uid1 <- User.userId <$> randomUser brig + uid2 <- User.userId <$> randomUser brig -- Initiate a new connection (A -> B) postConnection brig uid1 uid2 !!! const 201 === statusCode -- B accepts @@ -207,7 +207,7 @@ testAcceptConnection brig = do assertConnections brig uid1 [ConnectionStatus uid1 uid2 Accepted] assertConnections brig uid2 [ConnectionStatus uid2 uid1 Accepted] -- Mutual connection request with a user C - uid3 <- (.userId) <$> randomUser brig + uid3 <- User.userId <$> randomUser brig postConnection brig uid1 uid3 !!! const 201 === statusCode postConnection brig uid3 uid1 !!! const 200 === statusCode assertConnections brig uid1 [ConnectionStatus uid1 uid3 Accepted] @@ -226,8 +226,8 @@ testAcceptConnectionQualified brig = do testIgnoreConnection :: Brig -> Http () testIgnoreConnection brig = do - uid1 <- (.userId) <$> randomUser brig - uid2 <- (.userId) <$> randomUser brig + uid1 <- User.userId <$> randomUser brig + uid2 <- User.userId <$> randomUser brig -- Initiate a new connection (A -> B) postConnection brig uid1 uid2 !!! const 201 === statusCode -- B ignores A @@ -255,8 +255,8 @@ testIgnoreConnectionQualified brig = do testCancelConnection :: Brig -> Http () testCancelConnection brig = do - uid1 <- (.userId) <$> randomUser brig - uid2 <- (.userId) <$> randomUser brig + uid1 <- User.userId <$> randomUser brig + uid2 <- User.userId <$> randomUser brig -- Initiate a new connection (A -> B) postConnection brig uid1 uid2 !!! const 201 === statusCode -- A cancels the request @@ -284,8 +284,8 @@ testCancelConnectionQualified brig = do testCancelConnection2 :: Brig -> Galley -> Http () testCancelConnection2 brig galley = do - uid1 <- (.userId) <$> randomUser brig - uid2 <- (.userId) <$> randomUser brig + uid1 <- User.userId <$> randomUser brig + uid2 <- User.userId <$> randomUser brig -- Initiate a new connection (A -> B) postConnection brig uid1 uid2 !!! const 201 === statusCode -- A cancels the request @@ -361,8 +361,8 @@ testBlockConnection :: Brig -> Http () testBlockConnection brig = do u1 <- randomUser brig u2 <- randomUser brig - let uid1 = u1.userId - let uid2 = u2.userId + let uid1 = User.userId u1 + let uid2 = User.userId u2 -- Initiate a new connection (A -> B) postConnection brig uid1 uid2 !!! const 201 === statusCode -- Even connected users cannot see each other's email @@ -406,8 +406,8 @@ testBlockConnectionQualified :: Brig -> Http () testBlockConnectionQualified brig = do u1 <- randomUser brig u2 <- randomUser brig - let uid1 = u1.userId - uid2 = u2.userId + let uid1 = User.userId u1 + uid2 = User.userId u2 quid1 = userQualifiedId u1 quid2 = userQualifiedId u2 -- Initiate a new connection (A -> B) @@ -453,8 +453,8 @@ testBlockAndResendConnection :: Brig -> Galley -> Http () testBlockAndResendConnection brig galley = do u1 <- randomUser brig u2 <- randomUser brig - let uid1 = u1.userId - let uid2 = u2.userId + let uid1 = User.userId u1 + let uid2 = User.userId u2 -- Initiate a new connection (A -> B) postConnection brig uid1 uid2 !!! const 201 === statusCode -- B blocks A @@ -504,8 +504,8 @@ testBlockAndResendConnectionQualified brig galley = do testUnblockPendingConnection :: Brig -> Http () testUnblockPendingConnection brig = do - u1 <- (.userId) <$> randomUser brig - u2 <- (.userId) <$> randomUser brig + u1 <- User.userId <$> randomUser brig + u2 <- User.userId <$> randomUser brig postConnection brig u1 u2 !!! const 201 === statusCode putConnection brig u1 u2 Blocked !!! const 200 === statusCode assertConnections brig u1 [ConnectionStatus u1 u2 Blocked] @@ -527,8 +527,8 @@ testUnblockPendingConnectionQualified brig = do testAcceptWhileBlocked :: Brig -> Http () testAcceptWhileBlocked brig = do - u1 <- (.userId) <$> randomUser brig - u2 <- (.userId) <$> randomUser brig + u1 <- User.userId <$> randomUser brig + u2 <- User.userId <$> randomUser brig postConnection brig u1 u2 !!! const 201 === statusCode putConnection brig u1 u2 Blocked !!! const 200 === statusCode assertConnections brig u1 [ConnectionStatus u1 u2 Blocked] @@ -564,8 +564,8 @@ testUpdateConnectionNoopQualified brig = do testBadUpdateConnection :: Brig -> Http () testBadUpdateConnection brig = do - uid1 <- (.userId) <$> randomUser brig - uid2 <- (.userId) <$> randomUser brig + uid1 <- User.userId <$> randomUser brig + uid2 <- User.userId <$> randomUser brig postConnection brig uid1 uid2 !!! const 201 === statusCode assertBadUpdate uid1 uid2 Pending assertBadUpdate uid1 uid2 Ignored @@ -594,9 +594,9 @@ testBadUpdateConnectionQualified brig = do testLocalConnectionsPaging :: Brig -> Http () testLocalConnectionsPaging b = do - u <- (.userId) <$> randomUser b + u <- User.userId <$> randomUser b replicateM_ total $ do - u2 <- (.userId) <$> randomUser b + u2 <- User.userId <$> randomUser b postConnection b u u2 !!! const 201 === statusCode foldM_ (next u 2) (0, Nothing) [2, 2, 1, 0] foldM_ (next u total) (0, Nothing) [total, 0] @@ -660,21 +660,21 @@ testAllConnectionsPaging b db = do testConnectionLimit :: Brig -> ConnectionLimit -> Http () testConnectionLimit brig (ConnectionLimit l) = do - uid1 <- (.userId) <$> randomUser brig + uid1 <- User.userId <$> randomUser brig (uid2 : _) <- replicateM (fromIntegral l) (newConn uid1) - uidX <- (.userId) <$> randomUser brig + uidX <- User.userId <$> randomUser brig postConnection brig uid1 uidX !!! assertLimited -- blocked connections do not count towards the limit putConnection brig uid1 uid2 Blocked !!! const 200 === statusCode postConnection brig uid1 uidX !!! const 201 === statusCode -- the next send/accept hits the limit again - uidY <- (.userId) <$> randomUser brig + uidY <- User.userId <$> randomUser brig postConnection brig uid1 uidY !!! assertLimited -- (re-)sending an already accepted connection does not affect the limit postConnection brig uid1 uidX !!! const 200 === statusCode where newConn from = do - to <- (.userId) <$> randomUser brig + to <- User.userId <$> randomUser brig postConnection brig from to !!! const 201 === statusCode pure to assertLimited = do @@ -725,7 +725,7 @@ testConnectOK brig galley fedBrigClient = do testConnectWithAnon :: Brig -> FedClient 'Brig -> Http () testConnectWithAnon brig fedBrigClient = do fromUser <- randomId - toUser <- (.userId) <$> createAnonUser "anon1234" brig + toUser <- User.userId <$> createAnonUser "anon1234" brig res <- runFedClient @"send-connection-action" fedBrigClient (Domain "far-away.example.com") $ NewConnectionRequest fromUser Nothing toUser RemoteConnect @@ -734,7 +734,7 @@ testConnectWithAnon brig fedBrigClient = do testConnectFromAnon :: Brig -> Http () testConnectFromAnon brig = do - anonUser <- (.userId) <$> createAnonUser "anon1234" brig + anonUser <- User.userId <$> createAnonUser "anon1234" brig remoteUser <- fakeRemoteUser postConnectionQualified brig anonUser remoteUser !!! const 403 === statusCode diff --git a/services/galley/test/integration/API.hs b/services/galley/test/integration/API.hs index ccff779599f..62475a4684c 100644 --- a/services/galley/test/integration/API.hs +++ b/services/galley/test/integration/API.hs @@ -628,7 +628,7 @@ postMessageRejectIfMissingClients = do checkSendWitMissingClientsShouldFail where mkMsg :: ByteString -> (UserId, ClientId) -> (UserId, ClientId, Text) - mkMsg text (userId, clientId) = (userId, clientId, toBase64Text text) + mkMsg text (uid, clientId) = (uid, clientId, toBase64Text text) -- @END diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 4de8b00e8df..c9cd393dc9c 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -145,7 +145,7 @@ import Wire.API.Team.Member hiding (userId) import Wire.API.Team.Member qualified as Team import Wire.API.Team.Permission hiding (self) import Wire.API.Team.Role -import Wire.API.User hiding (AccountStatus (..)) +import Wire.API.User as User hiding (AccountStatus (..)) import Wire.API.User.Auth hiding (Access) import Wire.API.User.Client import Wire.API.User.Client qualified as Client @@ -194,12 +194,12 @@ symmPermissions p = let s = Set.fromList p in fromJust (newPermissions s s) createBindingTeam :: HasCallStack => TestM (UserId, TeamId) createBindingTeam = do - first Wire.API.User.userId <$> createBindingTeam' + first User.userId <$> createBindingTeam' createBindingTeam' :: HasCallStack => TestM (User, TeamId) createBindingTeam' = do owner <- randomTeamCreator' - teams <- getTeams owner.userId [] + teams <- getTeams (User.userId owner) [] team <- assertOne $ view teamListTeams teams let tid = view teamId team SQS.assertTeamActivate "create team" tid @@ -457,7 +457,7 @@ addUserToTeamWithRole :: HasCallStack => Maybe Role -> UserId -> TeamId -> TestM addUserToTeamWithRole role inviter tid = do (inv, rsp2) <- addUserToTeamWithRole' role inviter tid let invitee :: User = responseJsonUnsafe rsp2 - inviteeId = invitee.userId + inviteeId = User.userId invitee let invmeta = Just (inviter, inCreatedAt inv) mem <- getTeamMember inviter tid inviteeId liftIO $ assertEqual "Member has no/wrong invitation metadata" invmeta (mem ^. Team.invitation) @@ -485,7 +485,7 @@ addUserToTeamWithRole' role inviter tid = do addUserToTeamWithSSO :: HasCallStack => Bool -> TeamId -> TestM TeamMember addUserToTeamWithSSO hasEmail tid = do let ssoid = UserSSOId mkSimpleSampleUref - uid <- fmap (\(u :: User) -> u.userId) $ responseJsonError =<< postSSOUser "SSO User" hasEmail ssoid tid + uid <- fmap (\(u :: User) -> User.userId u) $ responseJsonError =<< postSSOUser "SSO User" hasEmail ssoid tid getTeamMember uid tid uid makeOwner :: HasCallStack => UserId -> TeamMember -> TeamId -> TestM () @@ -2175,7 +2175,7 @@ ephemeralUser = do let p = object ["name" .= name] r <- post (b . path "/register" . json p) UserId -> LastPrekey -> TestM ClientId randomClient uid lk = randomClientWithCaps uid lk Nothing diff --git a/services/spar/test-integration/Test/Spar/APISpec.hs b/services/spar/test-integration/Test/Spar/APISpec.hs index 1e2ea1ab5d9..a0f2ba1b706 100644 --- a/services/spar/test-integration/Test/Spar/APISpec.hs +++ b/services/spar/test-integration/Test/Spar/APISpec.hs @@ -1413,16 +1413,16 @@ specProvisionScimAndSAMLUserWithRole = do scimUser <- do u <- ScimT.randomScimUser pure $ u {Scim.roles = [cs $ toByteString $ role]} - userId <- ScimT.scimUserId <$> ScimT.createUser tok scimUser - ScimT.checkTeamMembersRole tid owner userId role + uid <- ScimT.scimUserId <$> ScimT.createUser tok scimUser + ScimT.checkTeamMembersRole tid owner uid role mapM_ testCreateUserWithRole [minBound .. maxBound] it "create user - default to member if no role given" $ do (tok, (owner, tid, _idp, (_, _privcreds))) <- ScimT.registerIdPAndScimTokenWithMeta scimUser <- do u <- ScimT.randomScimUser pure $ u {Scim.roles = []} - userId <- ScimT.scimUserId <$> ScimT.createUser tok scimUser - ScimT.checkTeamMembersRole tid owner userId RoleMember + uid <- ScimT.scimUserId <$> ScimT.createUser tok scimUser + ScimT.checkTeamMembersRole tid owner uid RoleMember it "create user - fail if more than one role given" $ do (tok, _) <- ScimT.registerIdPAndScimTokenWithMeta scimUser <- do @@ -1442,11 +1442,11 @@ specProvisionScimAndSAMLUserWithRole = do it "update user" $ do (tok, (owner, tid, _idp, (_, _privcreds))) <- ScimT.registerIdPAndScimTokenWithMeta scimUserWithDefaultRole <- ScimT.randomScimUser - userId <- ScimT.scimUserId <$> ScimT.createUser tok scimUserWithDefaultRole + uid <- ScimT.scimUserId <$> ScimT.createUser tok scimUserWithDefaultRole let testUpdateUserWithRole role = do let scimUserWithRole = scimUserWithDefaultRole {Scim.roles = [cs $ toByteString $ role]} - _ <- ScimT.updateUser tok userId scimUserWithRole - ScimT.checkTeamMembersRole tid owner userId role + _ <- ScimT.updateUser tok uid scimUserWithRole + ScimT.checkTeamMembersRole tid owner uid role mapM_ testUpdateUserWithRole [minBound .. maxBound] it "update user - do not change current role if no role given" $ do (tok, (owner, tid, _idp, (_, _privcreds))) <- ScimT.registerIdPAndScimTokenWithMeta @@ -1455,22 +1455,22 @@ specProvisionScimAndSAMLUserWithRole = do scimUser <- do u <- ScimT.randomScimUser pure $ u {Scim.roles = [cs $ toByteString $ role]} - userId <- ScimT.scimUserId <$> ScimT.createUser tok scimUser - _ <- ScimT.updateUser tok userId (scimUser {Scim.roles = []}) - ScimT.checkTeamMembersRole tid owner userId role + uid <- ScimT.scimUserId <$> ScimT.createUser tok scimUser + _ <- ScimT.updateUser tok uid (scimUser {Scim.roles = []}) + ScimT.checkTeamMembersRole tid owner uid role mapM_ testUpdateUserWithDefaultRole [minBound .. maxBound] it "updated user - fail if more than one role given" $ do (tok, _) <- ScimT.registerIdPAndScimTokenWithMeta scimUser <- ScimT.randomScimUser - userId <- ScimT.scimUserId <$> ScimT.createUser tok scimUser - ScimT.updateUser' tok userId (scimUser {Scim.roles = ["admin", "member"]}) !!! do + uid <- ScimT.scimUserId <$> ScimT.createUser tok scimUser + ScimT.updateUser' tok uid (scimUser {Scim.roles = ["admin", "member"]}) !!! do const 400 === statusCode const (Just "A user cannot have more than one role.") =~= responseBody it "updated user - fail if role name cannot be parsed correctly" $ do (tok, _) <- ScimT.registerIdPAndScimTokenWithMeta scimUser <- ScimT.randomScimUser - userId <- ScimT.scimUserId <$> ScimT.createUser tok scimUser - ScimT.updateUser' tok userId (scimUser {Scim.roles = ["hamlet"]}) !!! do + uid <- ScimT.scimUserId <$> ScimT.createUser tok scimUser + ScimT.updateUser' tok uid (scimUser {Scim.roles = ["hamlet"]}) !!! do const 400 === statusCode const (Just "The role 'hamlet' is not valid. Valid roles are owner, admin, member, partner.") =~= responseBody diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index 55c928e0678..e3ec56d2f92 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -2000,17 +2000,17 @@ testPatchIvalidInput patchOp = do let galley = env ^. teGalley (owner, tid) <- call $ createUserWithTeam brig galley tok <- registerScimToken tid Nothing - userId <- createScimUserWithRole brig tid owner tok defaultRole + uid <- createScimUserWithRole brig tid owner tok defaultRole let patchWithInvalidRole = PatchOp.Operation PatchOp.Replace (Just (PatchOp.NormalPath (Filter.topLevelAttrPath "roles"))) (Just $ Data.Aeson.Array $ V.singleton $ Data.Aeson.String "invalid-role") - patchUser' tok userId (PatchOp.PatchOp [patchWithInvalidRole]) !!! do + patchUser' tok uid (PatchOp.PatchOp [patchWithInvalidRole]) !!! do const 400 === statusCode const (Just "The role 'invalid-role' is not valid. Valid roles are owner, admin, member, partner.") =~= responseBody let patchWithTooManyRoles = patchOp "roles" [defaultRole, defaultRole] - patchUser' tok userId (PatchOp.PatchOp [patchWithTooManyRoles]) !!! do + patchUser' tok uid (PatchOp.PatchOp [patchWithTooManyRoles]) !!! do const 400 === statusCode const (Just "A user cannot have more than one role.") =~= responseBody @@ -2028,13 +2028,13 @@ testPatchRole replaceOrAdd = do where testCreateUserWithInitialRoleAndPatchToTargetRole :: BrigReq -> TeamId -> UserId -> ScimToken -> Role -> Maybe Role -> TestSpar () testCreateUserWithInitialRoleAndPatchToTargetRole brig tid owner tok initialRole mTargetRole = do - userId <- createScimUserWithRole brig tid owner tok initialRole - void $ patchUser tok userId $ PatchOp.PatchOp [replaceOrAdd "roles" (maybeToList mTargetRole)] - checkTeamMembersRole tid owner userId (fromMaybe initialRole mTargetRole) + uid <- createScimUserWithRole brig tid owner tok initialRole + void $ patchUser tok uid $ PatchOp.PatchOp [replaceOrAdd "roles" (maybeToList mTargetRole)] + checkTeamMembersRole tid owner uid (fromMaybe initialRole mTargetRole) -- also check if remove works let removeAttrib name = PatchOp.Operation PatchOp.Remove (Just (PatchOp.NormalPath (Filter.topLevelAttrPath name))) Nothing - void $ patchUser tok userId $ PatchOp.PatchOp [removeAttrib "roles"] - checkTeamMembersRole tid owner userId (fromMaybe initialRole mTargetRole) + void $ patchUser tok uid $ PatchOp.PatchOp [removeAttrib "roles"] + checkTeamMembersRole tid owner uid (fromMaybe initialRole mTargetRole) createScimUserWithRole :: BrigReq -> TeamId -> UserId -> ScimToken -> Role -> TestSpar UserId createScimUserWithRole brig tid owner tok initialRole = do diff --git a/tools/db/inconsistencies/src/DanglingUserKeys.hs b/tools/db/inconsistencies/src/DanglingUserKeys.hs index 5378cfa534a..7d04d8348f3 100644 --- a/tools/db/inconsistencies/src/DanglingUserKeys.hs +++ b/tools/db/inconsistencies/src/DanglingUserKeys.hs @@ -54,7 +54,7 @@ runCommand l brig inconsistenciesFile = do Log.info l (Log.field "keys" (show ((i - 1) * pageSize + fromIntegral (length userKeys)))) pure userKeys ) - .| C.mapM (liftIO . pooledMapConcurrentlyN 48 (\(key, userId, claimTime) -> checkUser l brig key userId claimTime False)) + .| C.mapM (liftIO . pooledMapConcurrentlyN 48 (\(key, uid, claimTime) -> checkUser l brig key uid claimTime False)) .| C.map ((<> "\n") . BS.intercalate "\n" . map (cs . Aeson.encode) . catMaybes) .| sinkFile inconsistenciesFile @@ -176,8 +176,8 @@ insertKey l u k = do -- 3.c this user's email, when searched for does not exist in user_keys. Do nothing, let this be handled by the other module EmailLessUsers.hs -- 4. user has an email in user_keys but no email inside user table -> do nothing. How to resolve? checkUser :: Logger -> ClientState -> UserKey -> UserId -> Writetime UserId -> Bool -> IO (Maybe Inconsistency) -checkUser l brig key userId time repairData = do - maybeDetails <- runClient brig $ getUserDetails userId +checkUser l brig key uid time repairData = do + maybeDetails <- runClient brig $ getUserDetails uid case maybeDetails of Nothing -> do let status = Nothing @@ -187,7 +187,7 @@ checkUser l brig key userId time repairData = do when repairData $ -- case 2. runClient brig $ freeUserKey l key - pure . Just $ Inconsistency {..} + pure . Just $ Inconsistency {userId = uid, ..} Just (mStatus, mStatusWriteTime, mEmail, mEmailWriteTime, mPhone, mPhoneWriteTime) -> do let status = WithWritetime <$> mStatus <*> mStatusWriteTime userEmail = WithWritetime <$> mEmail <*> mEmailWriteTime @@ -203,20 +203,20 @@ checkUser l brig key userId time repairData = do then do let inconsistencyCase = "1." when repairData $ runClient brig (freeUserKey l key) - pure . Just $ Inconsistency {..} + pure . Just $ Inconsistency {userId = uid, ..} else if keyError then do case mEmail of Nothing -> do - Log.warn l (Log.msg (Log.val "Subcase 4: user has no email") . Log.field "userId" (show userId)) + Log.warn l (Log.msg (Log.val "Subcase 4: user has no email") . Log.field "userId" (show uid)) let inconsistencyCase = "4." - pure . Just $ Inconsistency {..} + pure . Just $ Inconsistency {userId = uid, ..} Just email -> do validKeysEntry <- runClient brig $ getKey (userEmailKey email) case validKeysEntry of - Just (uid, _) -> - if uid == userId + Just (keyEntryUserId, _) -> + if keyEntryUserId == uid then do -- there is a valid matching user_key entry for a user in the user table; just *also* an extra entry that can be cleaned up (case 3.a.) Log.warn l (Log.msg (Log.val "Subcase 3a: entry can be repaired by removing entry") . Log.field "key" (keyText key)) @@ -224,13 +224,13 @@ checkUser l brig key userId time repairData = do when repairData $ runClient brig $ freeUserKey l key - pure . Just $ Inconsistency {..} + pure . Just $ Inconsistency {userId = uid, ..} else do let inconsistencyCase = "3.b." - Log.warn l (Log.msg (Log.val "Subcase 3b: double mismatch entry in user_keys") . Log.field "userId" (show userId)) - pure . Just $ Inconsistency {..} + Log.warn l (Log.msg (Log.val "Subcase 3b: double mismatch entry in user_keys") . Log.field "userId" (show uid)) + pure . Just $ Inconsistency {userId = uid, ..} Nothing -> do let inconsistencyCase = "3.c." - Log.warn l (Log.msg (Log.val "Subcase 3c: missing entry in user_keys") . Log.field "userId" (show userId)) - pure . Just $ Inconsistency {..} + Log.warn l (Log.msg (Log.val "Subcase 3c: missing entry in user_keys") . Log.field "userId" (show uid)) + pure . Just $ Inconsistency {userId = uid, ..} else pure Nothing diff --git a/tools/db/inconsistencies/src/EmailLessUsers.hs b/tools/db/inconsistencies/src/EmailLessUsers.hs index 331f17646eb..5ac4cb87389 100644 --- a/tools/db/inconsistencies/src/EmailLessUsers.hs +++ b/tools/db/inconsistencies/src/EmailLessUsers.hs @@ -136,7 +136,7 @@ repairUser l brig repairData uid = do Just x -> checkUser l brig repairData x checkUser :: Logger -> ClientState -> Bool -> (UserId, AccountStatus, Writetime AccountStatus, Email, Writetime Email) -> IO (Maybe EmailInfo) -checkUser l brig repairData (userId, statusValue, statusWritetime, userEmailValue, userEmailWriteTime) = do +checkUser l brig repairData (uid, statusValue, statusWritetime, userEmailValue, userEmailWriteTime) = do let status = WithWritetime statusValue statusWritetime userEmail = WithWritetime userEmailValue userEmailWriteTime mKeyDetails <- runClient brig $ K.getKey (userEmailKey userEmailValue) @@ -145,11 +145,11 @@ checkUser l brig repairData (userId, statusValue, statusWritetime, userEmailValu let emailKey = Nothing inconsistencyCase = if statusValue == Active then "1-missing-email" else "2-missing-email-but-not-active" when (repairData && (statusValue == Active)) $ do - insertMissingEmail l brig userEmailValue userId - pure . Just $ EmailInfo {..} + insertMissingEmail l brig userEmailValue uid + pure . Just $ EmailInfo {userId = uid, ..} Just (emailKeyValue, emailClaimTime) -> do let emailKey = Just $ WithWritetime emailKeyValue emailClaimTime let inconsistencyCase = "3-wrong-email" - if emailKeyValue == userId + if emailKeyValue == uid then pure Nothing - else pure . Just $ EmailInfo {..} + else pure . Just $ EmailInfo {userId = uid, ..} diff --git a/tools/stern/test/integration/API.hs b/tools/stern/test/integration/API.hs index 12602fbfc24..72322dc4380 100644 --- a/tools/stern/test/integration/API.hs +++ b/tools/stern/test/integration/API.hs @@ -402,19 +402,19 @@ testGetUsersByHandles = do h <- randomHandle void $ setHandle uid h [ua] <- getUsersByHandles h - liftIO $ ua.accountUser.userId @?= uid + liftIO $ userId ua.accountUser @?= uid testGetUsersByPhone :: TestM () testGetUsersByPhone = do (uid, phone) <- randomPhoneUser [ua] <- getUsersByPhone phone - liftIO $ ua.accountUser.userId @?= uid + liftIO $ userId ua.accountUser @?= uid testGetUsersByEmail :: TestM () testGetUsersByEmail = do (uid, email) <- randomEmailUser [ua] <- getUsersByEmail email - liftIO $ ua.accountUser.userId @?= uid + liftIO $ userId ua.accountUser @?= uid testUnsuspendUser :: TestM () testUnsuspendUser = do @@ -448,7 +448,7 @@ testGetUsersByIds = do uas <- getUsersByIds [uid1, uid2] liftIO $ do length uas @?= 2 - Set.fromList ((.accountUser.userId) <$> uas) @?= Set.fromList [uid1, uid2] + Set.fromList (userId . (.accountUser) <$> uas) @?= Set.fromList [uid1, uid2] testGetTeamInfo :: TestM () testGetTeamInfo = do diff --git a/tools/stern/test/integration/Util.hs b/tools/stern/test/integration/Util.hs index 983d9875132..ebb65c85e3c 100644 --- a/tools/stern/test/integration/Util.hs +++ b/tools/stern/test/integration/Util.hs @@ -47,10 +47,9 @@ import UnliftIO.Retry (limitRetries, recoverAll) import Web.Cookie import Wire.API.Team import Wire.API.Team.Invitation -import Wire.API.Team.Member -import Wire.API.Team.Member qualified as Team +import Wire.API.Team.Member as Team import Wire.API.Team.Role -import Wire.API.User +import Wire.API.User as User eventually :: (MonadIO m, MonadMask m, MonadUnliftIO m) => m a -> m a eventually = recoverAll (limitRetries 7 <> exponentialBackoff 50000) . const @@ -65,7 +64,7 @@ createTeamWithNMembers n = do createBindingTeam :: HasCallStack => TestM (UserId, TeamId) createBindingTeam = do - first (.userId) <$> createBindingTeam' + first User.userId <$> createBindingTeam' createBindingTeam' :: HasCallStack => TestM (User, TeamId) createBindingTeam' = do @@ -109,13 +108,13 @@ randomPhone = liftIO $ do pure $ fromMaybe (error "Invalid random phone#") phone randomEmailUser :: HasCallStack => TestM (UserId, Email) -randomEmailUser = randomUserProfile'' False False True <&> bimap ((.userId) . selfUser) fst +randomEmailUser = randomUserProfile'' False False True <&> bimap (User.userId . selfUser) fst randomPhoneUser :: HasCallStack => TestM (UserId, Phone) -randomPhoneUser = randomUserProfile'' False False True <&> bimap ((.userId) . selfUser) snd +randomPhoneUser = randomUserProfile'' False False True <&> bimap (User.userId . selfUser) snd randomEmailPhoneUser :: HasCallStack => TestM (UserId, (Email, Phone)) -randomEmailPhoneUser = randomUserProfile'' False False True <&> first ((.userId) . selfUser) +randomEmailPhoneUser = randomUserProfile'' False False True <&> first (User.userId . selfUser) defPassword :: PlainTextPassword8 defPassword = plainTextPassword8Unsafe "topsecretdefaultpassword" @@ -153,7 +152,7 @@ addUserToTeamWithRole :: HasCallStack => Maybe Role -> UserId -> TeamId -> TestM addUserToTeamWithRole role inviter tid = do (inv, rsp2) <- addUserToTeamWithRole' role inviter tid let invitee :: User = responseJsonUnsafe rsp2 - inviteeId = invitee.userId + inviteeId = User.userId invitee let invmeta = Just (inviter, inCreatedAt inv) mem <- getTeamMember inviter tid inviteeId liftIO $ assertEqual "Member has no/wrong invitation metadata" invmeta (mem ^. Team.invitation) From 1f9f16455fa457a275332ef85c7104275d3f196d Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Tue, 2 Apr 2024 15:52:34 +0200 Subject: [PATCH 069/117] integration: Fail with logs when a service times out to come up (#3929) * Duplicate console output of services from log noise to error msg. * integration: Store stdout and stderr chans in the ServiceInstance (Also ensure logs are kept for nginz) * Stronger signal to backend processes when terminating. (The timeout before this change wasn't effective; waitForProcess blocks beyond the timeout if the process doesn't terminate.) * Make default log level in ModServices easier to change. * shouldMatch argument order matters for error message! * Use lifted-base (instead of re-inventing it). * Use timestats library to dig into where the timeouts are caused. Co-authored-by: Akshay Mankar --- .../wpb6985-better-integration-test-logs | 1 + integration/default.nix | 4 + integration/integration.cabal | 3 + integration/test/Test/Cargohold/API.hs | 2 +- integration/test/Test/Demo.hs | 40 +++++ integration/test/Testlib/ModService.hs | 161 ++++++------------ .../Testlib/ModService/ServiceInstance.hs | 159 +++++++++++++++++ integration/test/Testlib/Run.hs | 4 + 8 files changed, 268 insertions(+), 106 deletions(-) create mode 100644 changelog.d/5-internal/wpb6985-better-integration-test-logs create mode 100644 integration/test/Testlib/ModService/ServiceInstance.hs diff --git a/changelog.d/5-internal/wpb6985-better-integration-test-logs b/changelog.d/5-internal/wpb6985-better-integration-test-logs new file mode 100644 index 00000000000..05f85f68860 --- /dev/null +++ b/changelog.d/5-internal/wpb6985-better-integration-test-logs @@ -0,0 +1 @@ +integration: Fail with logs when a service times out to come up \ No newline at end of file diff --git a/integration/default.nix b/integration/default.nix index a259708844e..e076020df31 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -38,6 +38,7 @@ , lens , lens-aeson , lib +, lifted-base , memory , mime , monad-control @@ -62,6 +63,7 @@ , temporary , text , time +, timestats , transformers , transformers-base , unix @@ -123,6 +125,7 @@ mkDerivation { kan-extensions lens lens-aeson + lifted-base memory mime monad-control @@ -147,6 +150,7 @@ mkDerivation { temporary text time + timestats transformers transformers-base unix diff --git a/integration/integration.cabal b/integration/integration.cabal index bcafb9ff147..e77d918f5c9 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -152,6 +152,7 @@ library Testlib.Mock Testlib.MockIntegrationService Testlib.ModService + Testlib.ModService.ServiceInstance Testlib.One2One Testlib.Options Testlib.Ports @@ -197,6 +198,7 @@ library , kan-extensions , lens , lens-aeson + , lifted-base , memory , mime , monad-control @@ -221,6 +223,7 @@ library , temporary , text , time + , timestats , transformers , transformers-base , unix diff --git a/integration/test/Test/Cargohold/API.hs b/integration/test/Test/Cargohold/API.hs index 25f3c4956d9..33e5893199c 100644 --- a/integration/test/Test/Cargohold/API.hs +++ b/integration/test/Test/Cargohold/API.hs @@ -236,7 +236,7 @@ testDownloadURLOverride = do downloadURLRes.status `shouldMatchInt` 302 cs @_ @String downloadURLRes.body `shouldMatch` "" downloadURL <- parseUrlThrow (C8.unpack (getHeader' (mk $ cs "Location") downloadURLRes)) - downloadEndpoint `shouldMatch` cs @_ @String (HTTP.host downloadURL) + cs @_ @String (HTTP.host downloadURL) `shouldMatch` downloadEndpoint HTTP.port downloadURL `shouldMatchInt` 443 True `shouldMatch` (HTTP.secure downloadURL) diff --git a/integration/test/Test/Demo.hs b/integration/test/Test/Demo.hs index 824af5a7d2c..95c12dd3cd1 100644 --- a/integration/test/Test/Demo.hs +++ b/integration/test/Test/Demo.hs @@ -7,10 +7,15 @@ import qualified API.Brig as BrigP import qualified API.BrigInternal as BrigI import qualified API.GalleyInternal as GalleyI import qualified API.Nginz as Nginz +import Control.Concurrent import Control.Monad.Cont +import Data.Function +import Data.Maybe import GHC.Stack import SetupHelpers +import Testlib.ModService.ServiceInstance import Testlib.Prelude +import UnliftIO.Directory -- | Deleting unknown clients should fail with 404. testDeleteUnknownClient :: HasCallStack => App () @@ -207,3 +212,38 @@ testFedV0Federation = do bob' <- BrigP.getUser alice bob >>= getJSON 200 bob' %. "qualified_id" `shouldMatch` (bob %. "qualified_id") + +testServiceHandles :: App () +testServiceHandles = do + -- The name was generated with a roll of a fair dice + let exe = "/tmp/tmp-42956614-e50a-11ee-8c4b-6b596d54b36b" + execName = "test-exec" + dom = "test-domain" + + writeFile + exe + "#!/usr/bin/env bash\n\ + \echo errmsg >&2\n\ + \for i in `seq 0 100`; do\n\ + \ echo $i\n\ + \ sleep 0.1\n\ + \done\n" + perms <- getPermissions exe + setPermissions exe (setOwnerExecutable True perms) + serviceInstance <- liftIO $ startServiceInstance exe [] Nothing exe execName dom + liftIO $ threadDelay 1_000_000 + cleanupServiceInstance serviceInstance + processState <- liftIO $ flushServiceInstanceOutput serviceInstance + processState + `shouldContainString` "=== stdout: ============================================\n\ + \[test-exec@test-domain] 0\n\ + \[test-exec@test-domain] 1\n\ + \[test-exec@test-domain] 2\n\ + \[test-exec@test-domain] 3\n\ + \[test-exec@test-domain] 4\n\ + \[test-exec@test-domain] 5\n\ + \[test-exec@test-domain] 6\n\ + \[test-exec@test-domain] 7\n" + processState + `shouldContainString` "=== stderr: ============================================\n\ + \[test-exec@test-domain] errmsg\n" diff --git a/integration/test/Testlib/ModService.hs b/integration/test/Testlib/ModService.hs index f4390d7286f..845b7ba2bd0 100644 --- a/integration/test/Testlib/ModService.hs +++ b/integration/test/Testlib/ModService.hs @@ -29,25 +29,25 @@ import qualified Data.Text as Text import qualified Data.Text.IO as Text import Data.Traversable import qualified Data.Yaml as Yaml +import Debug.TimeStats (measureM) import GHC.Stack import qualified Network.HTTP.Client as HTTP -import System.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExist, doesFileExist, listDirectory, removeDirectoryRecursive, removeFile) +import System.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExist, doesFileExist, listDirectory, removeFile) import System.FilePath import System.IO import System.IO.Temp (createTempDirectory, writeTempFile) -import System.Posix (keyboardSignal, killProcess, signalProcess) -import System.Process import Testlib.App import Testlib.HTTP import Testlib.JSON -import Testlib.Printing +import Testlib.ModService.ServiceInstance import Testlib.ResourcePool import Testlib.Types import Text.RawString.QQ +import qualified UnliftIO import Prelude withModifiedBackend :: HasCallStack => ServiceOverrides -> (HasCallStack => String -> App a) -> App a -withModifiedBackend overrides k = +withModifiedBackend overrides k = measureM "withModifiedBackend" $ do startDynamicBackends [overrides] (\domains -> k (head domains)) copyDirectoryRecursively :: FilePath -> FilePath -> IO () @@ -118,7 +118,7 @@ traverseConcurrentlyCodensity f args = do pure result startDynamicBackends :: [ServiceOverrides] -> ([String] -> App a) -> App a -startDynamicBackends beOverrides k = +startDynamicBackends beOverrides k = measureM "startDynamicBackends" do runCodensity do when (Prelude.length beOverrides > 3) $ lift $ failApp "Too many backends. Currently only 3 are supported." @@ -203,17 +203,22 @@ startDynamicBackend resource beOverrides = do setLogLevel :: ServiceOverrides setLogLevel = def - { sparCfg = setField "saml.logLevel" ("Warn" :: String), - brigCfg = setField "logLevel" ("Warn" :: String), - cannonCfg = setField "logLevel" ("Warn" :: String), - cargoholdCfg = setField "logLevel" ("Warn" :: String), - galleyCfg = setField "logLevel" ("Warn" :: String), - gundeckCfg = setField "logLevel" ("Warn" :: String), - nginzCfg = setField "logLevel" ("Warn" :: String), - backgroundWorkerCfg = setField "logLevel" ("Warn" :: String), - sternCfg = setField "logLevel" ("Warn" :: String), - federatorInternalCfg = setField "logLevel" ("Warn" :: String) + { -- NOTE: if you want to set logLevel to "Debug", consider doing it for the service + -- you're interested in only. it's *very* noisy! + sparCfg = setField "saml.logLevel" logLevel, + brigCfg = setField "logLevel" logLevel, + cannonCfg = setField "logLevel" logLevel, + cargoholdCfg = setField "logLevel" logLevel, + galleyCfg = setField "logLevel" logLevel, + gundeckCfg = setField "logLevel" logLevel, + nginzCfg = setField "logLevel" logLevel, + backgroundWorkerCfg = setField "logLevel" logLevel, + sternCfg = setField "logLevel" logLevel, + federatorInternalCfg = setField "logLevel" logLevel } + where + logLevel :: String + logLevel = "Warn" updateServiceMapInConfig :: BackendResource -> Service -> Value -> App Value updateServiceMapInConfig resource forSrv config = @@ -246,12 +251,12 @@ startBackend :: BackendResource -> ServiceOverrides -> Codensity App () -startBackend resource overrides = do +startBackend resource overrides = measureM "startBackend" do traverseConcurrentlyCodensity (withProcess resource overrides) allServices lift $ ensureBackendReachable resource.berDomain ensureBackendReachable :: String -> App () -ensureBackendReachable domain = do +ensureBackendReachable domain = measureM "ensureBackendReachable" do env <- ask let checkServiceIsUpReq = do req <- @@ -280,54 +285,21 @@ ensureBackendReachable domain = do pure $ either (\(_e :: HTTP.HttpException) -> False) id eith when ((domain /= env.domain1) && (domain /= env.domain2)) $ do - retryRequestUntil checkServiceIsUpReq "Federator ingress" - -processColors :: [(String, String -> String)] -processColors = - [ ("brig", colored green), - ("galley", colored yellow), - ("gundeck", colored blue), - ("cannon", colored orange), - ("cargohold", colored purpleish), - ("spar", colored orange), - ("federator", colored blue), - ("background-worker", colored blue), - ("nginx", colored purpleish) - ] - -data ServiceInstance = ServiceInstance - { handle :: ProcessHandle, - config :: FilePath - } - -timeout :: Int -> IO a -> IO (Maybe a) -timeout usecs action = either (const Nothing) Just <$> race (threadDelay usecs) action - -cleanupService :: ServiceInstance -> IO () -cleanupService inst = do - let ignoreExceptions action = E.catch action $ \(_ :: E.SomeException) -> pure () - ignoreExceptions $ do - mPid <- getPid inst.handle - for_ mPid (signalProcess keyboardSignal) - timeout 50000 (waitForProcess inst.handle) >>= \case - Just _ -> pure () - Nothing -> do - for_ mPid (signalProcess killProcess) - void $ waitForProcess inst.handle - whenM (doesFileExist inst.config) $ removeFile inst.config - whenM (doesDirectoryExist inst.config) $ removeDirectoryRecursive inst.config + retryRequestUntil checkServiceIsUpReq "Federator ingress" domain Nothing -- | Wait for a service to come up. -waitUntilServiceIsUp :: String -> Service -> App () -waitUntilServiceIsUp domain srv = +waitUntilServiceIsUp :: String -> Service -> ServiceInstance -> App () +waitUntilServiceIsUp domain srv serviceInstance = measureM "waitUntilServiceUp" do retryRequestUntil (checkServiceIsUp domain srv) (show srv) + domain + (Just serviceInstance) -- | Check if a service is up and running. checkServiceIsUp :: String -> Service -> App Bool checkServiceIsUp _ Nginz = pure True -checkServiceIsUp domain srv = do +checkServiceIsUp domain srv = measureM "checkServiceIsUp" do req <- baseRequest domain srv Unversioned "/i/status" checkStatus <- appToIO $ do res <- submit "GET" req @@ -353,44 +325,34 @@ withProcess resource overrides service = do startNginzLocalIO <- lift $ appToIO $ startNginzLocal resource - let initProcess = case (service, cwd) of + let initProcess = liftIO $ case (service, cwd) of (Nginz, Nothing) -> startNginzK8s domain sm (Nginz, Just _) -> startNginzLocalIO _ -> do config <- getConfig tempFile <- writeTempFile "/tmp" (execName <> "-" <> domain <> "-" <> ".yaml") (cs $ Yaml.encode config) - (_, Just stdoutHdl, Just stderrHdl, ph) <- createProcess (proc exe ["-c", tempFile]) {cwd = cwd, std_out = CreatePipe, std_err = CreatePipe} - let prefix = "[" <> execName <> "@" <> domain <> "] " - let colorize = fromMaybe id (lookup execName processColors) - void $ forkIO $ logToConsole colorize prefix stdoutHdl - void $ forkIO $ logToConsole colorize prefix stderrHdl - pure $ ServiceInstance ph tempFile + startServiceInstance exe ["-c", tempFile] cwd tempFile execName domain void $ Codensity $ \k -> do - iok <- appToIOKleisli k - liftIO $ E.bracket initProcess cleanupService iok - - lift $ waitUntilServiceIsUp domain service - -logToConsole :: (String -> String) -> String -> Handle -> IO () -logToConsole colorize prefix hdl = do - let go = - do - line <- hGetLine hdl - putStrLn (colorize (prefix <> line)) - go - `E.catch` (\(_ :: E.IOException) -> pure ()) - go - -retryRequestUntil :: HasCallStack => App Bool -> String -> App () -retryRequestUntil reqAction err = do + UnliftIO.bracket initProcess cleanupServiceInstance $ \serviceInstance -> do + waitUntilServiceIsUp domain service serviceInstance + k serviceInstance + +retryRequestUntil :: HasCallStack => App Bool -> String -> String -> Maybe ServiceInstance -> App () +retryRequestUntil reqAction execName domain mServiceInstance = measureM "retryRequestUntil" do isUp <- retrying (limitRetriesByCumulativeDelay (4 * 1000 * 1000) (fibonacciBackoff (200 * 1000))) (\_ isUp -> pure (not isUp)) (const reqAction) - unless isUp $ - failApp ("Timed out waiting for service " <> err <> " to come up") + unless isUp $ do + errDetails <- liftIO $ do + case mServiceInstance of + Nothing -> pure "" + Just serviceInstance -> do + outStr <- flushServiceInstanceOutput serviceInstance + pure $ unlines [":", outStr] + failApp ("Timed out waiting for service " <> execName <> "@" <> domain <> " to come up" <> errDetails) startNginzK8s :: String -> ServiceMap -> IO ServiceInstance startNginzK8s domain sm = do @@ -412,8 +374,7 @@ startNginzK8s domain sm = do & Text.replace ("/etc/wire/nginz/upstreams/upstreams.conf") (cs upstreamsCfg) ) createUpstreamsCfg upstreamsCfg sm - ph <- startNginz domain nginxConfFile "/" - pure $ ServiceInstance ph tmpDir + startNginz domain nginxConfFile tmpDir startNginzLocal :: BackendResource -> App ServiceInstance startNginzLocal resource = do @@ -486,10 +447,7 @@ server 127.0.0.1:{port} max_fails=3 weight=1; writeFile pidConfigFile (cs $ "pid " <> pid <> ";") -- start service - ph <- liftIO $ startNginz domain nginxConfFile tmpDir - - -- return handle and nginx tmp dir path - pure $ ServiceInstance ph tmpDir + liftIO $ startNginz domain nginxConfFile tmpDir createUpstreamsCfg :: String -> ServiceMap -> IO () createUpstreamsCfg upstreamsCfg sm = do @@ -520,19 +478,12 @@ server 127.0.0.1:{port} max_fails=3 weight=1; & Text.replace "{port}" (cs $ show p) liftIO $ appendFile upstreamsCfg (cs upstream) -startNginz :: String -> FilePath -> FilePath -> IO ProcessHandle -startNginz domain conf workingDir = do - (_, Just stdoutHdl, Just stderrHdl, ph) <- - createProcess - (proc "nginx" ["-c", conf, "-g", "daemon off;", "-e", "/dev/stdout"]) - { cwd = Just workingDir, - std_out = CreatePipe, - std_err = CreatePipe - } - - let prefix = "[" <> "nginz" <> "@" <> domain <> "] " - let colorize = fromMaybe id (lookup "nginx" processColors) - void $ forkIO $ logToConsole colorize prefix stdoutHdl - void $ forkIO $ logToConsole colorize prefix stderrHdl - - pure ph +startNginz :: String -> FilePath -> FilePath -> IO ServiceInstance +startNginz domain conf configDir = do + startServiceInstance + "nginx" + ["-c", conf, "-g", "daemon off;", "-e", "/dev/stdout"] + (Just configDir) + configDir + "nginz" + domain diff --git a/integration/test/Testlib/ModService/ServiceInstance.hs b/integration/test/Testlib/ModService/ServiceInstance.hs new file mode 100644 index 00000000000..efc4389fff0 --- /dev/null +++ b/integration/test/Testlib/ModService/ServiceInstance.hs @@ -0,0 +1,159 @@ +module Testlib.ModService.ServiceInstance + ( ServiceInstance, + startServiceInstance, + cleanupServiceInstance, + flushServiceInstanceOutput, + ) +where + +import Control.Concurrent +import qualified Control.Exception as E +import Control.Monad.Extra +import Control.Monad.IO.Class +import Data.Foldable +import Data.Function +import Data.Functor +import Data.Maybe +import Data.Monoid +import Data.String +import Debug.TimeStats +import System.Directory +import System.IO +import qualified System.IO.Error as E +import System.Posix +import System.Process +import Testlib.Printing +import Testlib.Types +import Prelude + +data ServiceInstance = ServiceInstance + { name :: String, + domain :: String, + processHandle :: ProcessHandle, + stdoutChan :: Chan LineOrEOF, + stderrChan :: Chan LineOrEOF, + cleanupPath :: FilePath + } + +startServiceInstance :: FilePath -> [String] -> Maybe FilePath -> FilePath -> String -> String -> IO ServiceInstance +startServiceInstance exe args workingDir pathToCleanup execName execDomain = measureM "startServiceInstance" do + (_, Just stdoutHdl, Just stderrHdl, ph) <- + createProcess + (proc exe args) + { cwd = workingDir, + std_out = CreatePipe, + std_err = CreatePipe + } + (out1, out2) <- mkChans stdoutHdl + (err1, err2) <- mkChans stderrHdl + void $ forkIO $ logChanToConsole execName execDomain out1 + void $ forkIO $ logChanToConsole execName execDomain err1 + pure $ + ServiceInstance + { name = execName, + domain = execDomain, + processHandle = ph, + stdoutChan = out2, + stderrChan = err2, + cleanupPath = pathToCleanup + } + +cleanupServiceInstance :: ServiceInstance -> App () +cleanupServiceInstance inst = measureM "cleanupService" . liftIO $ do + let ignoreExceptions action = E.catch action $ \(_ :: E.SomeException) -> pure () + ignoreExceptions $ do + mPid <- getPid inst.processHandle + for_ mPid (signalProcess killProcess) + void $ waitForProcess inst.processHandle + whenM (doesFileExist inst.cleanupPath) $ removeFile inst.cleanupPath + whenM (doesDirectoryExist inst.cleanupPath) $ removeDirectoryRecursive inst.cleanupPath + +flushServiceInstanceOutput :: ServiceInstance -> IO String +flushServiceInstanceOutput serviceInstance = measureM "flushProcessState" do + outStr <- flushChan serviceInstance.name serviceInstance.domain serviceInstance.stdoutChan + errStr <- flushChan serviceInstance.name serviceInstance.domain serviceInstance.stderrChan + statusStr <- getPid serviceInstance.processHandle <&> maybe "(already closed)" show + pure $ + unlines + [ "=== process pid: =======================================", + statusStr, + "\n\n=== stdout: ============================================", + outStr, + "\n\n=== stderr: ============================================", + errStr + ] + +data LineOrEOF = Line String | EOF + deriving (Eq, Show) + +logChanToConsole :: String -> String -> Chan LineOrEOF -> IO () +logChanToConsole execName domain chan = go + where + go = + readChan chan >>= \case + Line line -> do + putStrLn (decorateLine execName domain line) + go + EOF -> pure () + +-- | Read everything from a channel and return it as a decorated multi-line String. +flushChan :: String -> String -> Chan LineOrEOF -> IO String +flushChan execName domain chan = measureM "flushChan" do + let go lns = + readChan chan >>= \case + Line ln -> go (ln : lns) + EOF -> pure (reverse lns) + (unlines . fmap (decorateLine execName domain)) <$> go [] + +-- | Run a thread that feeds output from a 'Handle' into two channels. +-- +-- (We could also duplicate the posic handle, not the chan. might save a few LOC.) +mkChans :: Handle -> IO (Chan LineOrEOF, Chan LineOrEOF) +mkChans hdl = do + chn1 <- newChan + chn2 <- dupChan chn1 + let go = do + packet <- catchEOF (hGetLine hdl) + writeList2Chan chn1 packet + unless (EOF `elem` packet) go + void $ forkIO go + pure (chn1, chn2) + +-- | If 'SomeException' is thrown, show it, split up in lines, and feed it to the output +-- followed be '[EOF]'. (But if the exception is 'EOF', do not add it to the output.) +catchEOF :: IO String -> IO [LineOrEOF] +catchEOF feed = + (((: []) . Line) <$> feed) + `E.catch` handleEOF + `E.catch` handleEverythingElse + where + handleEOF :: E.IOException -> IO [LineOrEOF] + handleEOF e = + if E.isEOFError e + then pure [EOF] + else renderErr e + + handleEverythingElse :: E.SomeException -> IO [LineOrEOF] + handleEverythingElse e = renderErr e + + renderErr :: E.Exception e => e -> IO [LineOrEOF] + renderErr e = pure $ (Line <$> lines (show e)) <> [EOF] + +decorateLine :: String -> String -> String -> String +decorateLine execName domain = colorize . (prefix <>) + where + prefix = "[" <> execName <> "@" <> domain <> "] " + colorize = fromMaybe id (lookup execName processColors) + +processColors :: [(String, String -> String)] +processColors = + [ ("brig", colored green), + ("galley", colored yellow), + ("gundeck", colored blue), + ("cannon", colored orange), + ("cargohold", colored purpleish), + ("spar", colored orange), + ("federator", colored blue), + ("background-worker", colored blue), + ("nginx", colored purpleish) + ] diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index 0a50c6429a1..454ff0a9e8b 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -16,6 +16,7 @@ import Data.Functor import Data.List import Data.PEM import Data.Time.Clock +import Debug.TimeStats (printTimeStats) import RunAllTests import System.Directory import System.Environment @@ -107,6 +108,9 @@ main = do if opts.listTests then doListTests tests else runTests tests opts.xmlReport cfg + putStrLn "output from timestats library: (use `DEBUG_TIMESTATS_ENABLE=1` to enable)" + printTimeStats + createGlobalEnv :: FilePath -> Codensity IO GlobalEnv createGlobalEnv cfg = do genv0 <- mkGlobalEnv cfg From 560d8a20c83069e1bd756669363456fc874f117f Mon Sep 17 00:00:00 2001 From: Akshay Mankar Date: Wed, 3 Apr 2024 09:28:55 +0200 Subject: [PATCH 070/117] [WPB-5990] Consolidate logic of building a user profile (#3978) * wire-api: Move HardTruncationLimit to its own module To avoid dependency from Wire.API.Error.Galley to Wire.API.Team.Member In preparation for moving code from galley-types to wire-api * Move code for checking roles and permissions from galley-types to wire-api * Wire.API.User: Delte connectedProfile, publicProfile to only have mkUserProfile `connectedProfile` and `publicProfile` did the same thing no matter what, then the logic to display email or not was part of Brig itself. It makes more sense if we make the email logic part of a single function which makes the profile. This way we could write tests for it. Not having these two thing removes the need to get relations between requesting user and the viewed users, which saves 1 DB query per viewed user. --- libs/galley-types/default.nix | 4 - libs/galley-types/galley-types.cabal | 4 - libs/galley-types/src/Galley/Types/Teams.hs | 175 +---------------- libs/galley-types/test/unit/Main.hs | 9 +- .../test/unit/Test/Galley/Permissions.hs | 35 ---- .../test/unit/Test/Galley/Types.hs | 49 +---- libs/wire-api/src/Wire/API/Error/Galley.hs | 2 +- .../src/Wire/API/Team/HardTruncationLimit.hs | 10 + libs/wire-api/src/Wire/API/Team/Member.hs | 185 +++++++++++++++++- libs/wire-api/src/Wire/API/Team/Permission.hs | 3 +- libs/wire-api/src/Wire/API/User.hs | 110 +++++------ .../test/unit/Test/Wire/API/Team/Member.hs | 71 ++++++- libs/wire-api/test/unit/Test/Wire/API/User.hs | 131 ++++++++++--- libs/wire-api/wire-api.cabal | 2 + services/brig/src/Brig/API/Public.hs | 2 +- services/brig/src/Brig/API/User.hs | 59 +----- .../src/Brig/Effects/GalleyProvider/RPC.hs | 10 +- services/brig/src/Brig/Options.hs | 32 +-- services/brig/src/Brig/Provider/API.hs | 4 +- services/brig/src/Brig/Team/API.hs | 3 +- services/brig/src/Brig/Team/Util.hs | 1 - services/brig/src/Brig/User/API/Search.hs | 2 +- .../brig/test/integration/API/Settings.hs | 25 +-- services/brig/test/integration/API/Team.hs | 9 +- .../integration/Test/Federator/IngressSpec.hs | 2 +- .../integration/Test/Federator/InwardSpec.hs | 2 +- services/galley/galley.cabal | 1 - .../migrate-data/src/V3_BackfillTeamAdmins.hs | 2 +- services/galley/src/Galley/API/Action.hs | 1 - .../API/Teams/LegalHold/DisabledByDefault.hs | 1 - services/galley/test/integration/API/Util.hs | 1 - services/spar/default.nix | 3 - services/spar/spar.cabal | 2 - services/spar/src/Spar/API.hs | 2 +- services/spar/src/Spar/Intra/BrigApp.hs | 2 +- services/spar/src/Spar/Intra/Galley.hs | 1 - services/spar/src/Spar/Scim/User.hs | 3 +- services/spar/src/Spar/Sem/GalleyAccess.hs | 1 - .../test-integration/Test/Spar/APISpec.hs | 7 +- .../Test/Spar/Scim/AuthSpec.hs | 4 +- services/spar/test-integration/Util/Core.hs | 6 +- services/spar/test-integration/Util/Scim.hs | 3 +- tools/stern/default.nix | 2 - tools/stern/src/Stern/Types.hs | 1 - tools/stern/stern.cabal | 1 - 45 files changed, 468 insertions(+), 517 deletions(-) delete mode 100644 libs/galley-types/test/unit/Test/Galley/Permissions.hs create mode 100644 libs/wire-api/src/Wire/API/Team/HardTruncationLimit.hs diff --git a/libs/galley-types/default.nix b/libs/galley-types/default.nix index 9fb082913eb..2cbe283392e 100644 --- a/libs/galley-types/default.nix +++ b/libs/galley-types/default.nix @@ -18,7 +18,6 @@ , QuickCheck , schema-profunctor , tasty -, tasty-hunit , tasty-quickcheck , text , types-common @@ -50,12 +49,9 @@ mkDerivation { testHaskellDepends = [ aeson base - containers imports - lens QuickCheck tasty - tasty-hunit tasty-quickcheck wire-api ]; diff --git a/libs/galley-types/galley-types.cabal b/libs/galley-types/galley-types.cabal index 97c786cf57f..947ca36cd70 100644 --- a/libs/galley-types/galley-types.cabal +++ b/libs/galley-types/galley-types.cabal @@ -97,7 +97,6 @@ test-suite galley-types-tests -- cabal-fmt: expand test other-modules: Paths_galley_types - Test.Galley.Permissions Test.Galley.Roundtrip Test.Galley.Types @@ -153,13 +152,10 @@ test-suite galley-types-tests build-depends: aeson , base - , containers , galley-types , imports - , lens , QuickCheck , tasty - , tasty-hunit , tasty-quickcheck , wire-api diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index ef54abdd5dc..07a2d755b4c 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -56,95 +56,20 @@ module Galley.Types.Teams isTeamMember, isTeamOwner, canSeePermsOf, - rolePermissions, - roleHiddenPermissions, - permissionsRole, - isAdminOrOwner, - HiddenPerm (..), - IsPerm (..), ) where -import Control.Lens (makeLenses, view, (^.)) +import Control.Lens (makeLenses, view) import Data.Aeson import Data.Aeson.Types qualified as A import Data.Id (UserId) -import Data.Maybe qualified as Maybe import Data.Schema qualified as Schema import Data.Set qualified as Set import Imports import Test.QuickCheck (Arbitrary) -import Wire.API.Error.Galley import Wire.API.Team.Feature import Wire.API.Team.Member import Wire.API.Team.Permission -import Wire.API.Team.Role - -rolePermissions :: Role -> Permissions -rolePermissions role = Permissions p p where p = rolePerms role - -permissionsRole :: Permissions -> Maybe Role -permissionsRole (Permissions p p') = - if p /= p' - then do - -- we never did use @p /= p'@ for anything, fingers crossed that it doesn't occur anywhere - -- in the wild. but if it does, this implementation prevents privilege escalation. - let p'' = Set.intersection p p' - in permissionsRole (Permissions p'' p'') - else permsRole p - where - permsRole :: Set Perm -> Maybe Role - permsRole perms = - Maybe.listToMaybe - [ role - | role <- [minBound ..], - -- if a there is a role that is strictly less permissive than the perms set that - -- we encounter, we downgrade. this shouldn't happen in real life, but it has - -- happened to very old users on a staging environment, where a user (probably) - -- was create before the current publicly visible permissions had been stabilized. - rolePerms role `Set.isSubsetOf` perms - ] - -isAdminOrOwner :: Permissions -> Bool -isAdminOrOwner perms = - case permissionsRole perms of - Just RoleOwner -> True - Just RoleAdmin -> True - Just RoleMember -> False - Just RoleExternalPartner -> False - Nothing -> False - --- | Internal function for 'rolePermissions'. (It works iff the two sets in 'Permissions' are --- identical for every 'Role', otherwise it'll need to be specialized for the resp. sides.) -rolePerms :: Role -> Set Perm -rolePerms RoleOwner = - rolePerms RoleAdmin - <> Set.fromList - [ GetBilling, - SetBilling, - DeleteTeam - ] -rolePerms RoleAdmin = - rolePerms RoleMember - <> Set.fromList - [ AddTeamMember, - RemoveTeamMember, - SetTeamData, - SetMemberPermissions - ] -rolePerms RoleMember = - rolePerms RoleExternalPartner - <> Set.fromList - [ DeleteConversation, - AddRemoveConvMember, - ModifyConvName, - GetMemberPermissions - ] -rolePerms RoleExternalPartner = - Set.fromList - [ CreateConversation, - GetTeamConversations - ] -- This is the cassandra timestamp of writetime(binding) newtype TeamCreationTime = TeamCreationTime @@ -304,104 +229,6 @@ makeLenses ''TeamCreationTime makeLenses ''FeatureFlags makeLenses ''Defaults --- Note [hidden team roles] --- --- The problem: the mapping between 'Role' and 'Permissions' is fixed by external contracts: --- client apps treat permission bit matrices as opaque role identifiers, so if we add new --- permission flags, things will break there. --- --- "Hidden" in "HiddenPerm", therefore, refers to a permission hidden from --- clients, thereby making it internal to the backend. --- --- The solution: add new permission bits to 'HiddenPerm', 'HiddenPermissions', and make --- 'hasPermission', 'mayGrantPermission' polymorphic. Now you can check both for the hidden --- permission bits and the old ones that we share with the client apps. - --- | See Note [hidden team roles] -data HiddenPerm - = ChangeLegalHoldTeamSettings - | ChangeLegalHoldUserSettings - | ViewLegalHoldUserSettings - | ChangeTeamFeature - | ChangeTeamSearchVisibility - | ViewTeamSearchVisibility - | ViewSameTeamEmails - | ReadIdp - | CreateUpdateDeleteIdp - | CreateReadDeleteScimToken - | -- | this has its own permission because we're not sure how - -- efficient this end-point is. better not let all team members - -- play with it unless we have to. - DownloadTeamMembersCsv - | ChangeTeamMemberProfiles - | SearchContacts - deriving (Eq, Ord, Show) - --- | See Note [hidden team roles] -data HiddenPermissions = HiddenPermissions - { _hself :: Set HiddenPerm, - _hcopy :: Set HiddenPerm - } - deriving (Eq, Ord, Show) - -makeLenses ''HiddenPermissions - -roleHiddenPermissions :: Role -> HiddenPermissions -roleHiddenPermissions role = HiddenPermissions p p - where - p = roleHiddenPerms role - roleHiddenPerms :: Role -> Set HiddenPerm - roleHiddenPerms RoleOwner = roleHiddenPerms RoleAdmin - roleHiddenPerms RoleAdmin = - (roleHiddenPerms RoleMember <>) $ - Set.fromList - [ ChangeLegalHoldTeamSettings, - ChangeLegalHoldUserSettings, - ChangeTeamSearchVisibility, - ChangeTeamFeature, - ChangeTeamMemberProfiles, - ReadIdp, - CreateUpdateDeleteIdp, - CreateReadDeleteScimToken, - DownloadTeamMembersCsv - ] - roleHiddenPerms RoleMember = - (roleHiddenPerms RoleExternalPartner <>) $ - Set.fromList - [ ViewSameTeamEmails, - SearchContacts - ] - roleHiddenPerms RoleExternalPartner = - Set.fromList - [ ViewLegalHoldUserSettings, - ViewTeamSearchVisibility - ] - --- | See Note [hidden team roles] -class IsPerm perm where - type PermError (e :: perm) :: GalleyError - - roleHasPerm :: Role -> perm -> Bool - roleGrantsPerm :: Role -> perm -> Bool - hasPermission :: TeamMember -> perm -> Bool - hasPermission tm perm = maybe False (`roleHasPerm` perm) . permissionsRole $ tm ^. permissions - mayGrantPermission :: TeamMember -> perm -> Bool - mayGrantPermission tm perm = maybe False (`roleGrantsPerm` perm) . permissionsRole $ tm ^. permissions - -instance IsPerm Perm where - type PermError p = 'MissingPermission ('Just p) - - roleHasPerm r p = p `Set.member` (rolePermissions r ^. self) - roleGrantsPerm r p = p `Set.member` (rolePermissions r ^. copy) - hasPermission tm p = p `Set.member` (tm ^. permissions . self) - mayGrantPermission tm p = p `Set.member` (tm ^. permissions . copy) - -instance IsPerm HiddenPerm where - type PermError p = OperationDenied - - roleHasPerm r p = p `Set.member` (roleHiddenPermissions r ^. hself) - roleGrantsPerm r p = p `Set.member` (roleHiddenPermissions r ^. hcopy) - notTeamMember :: [UserId] -> [TeamMember] -> [UserId] notTeamMember uids tmms = Set.toList $ diff --git a/libs/galley-types/test/unit/Main.hs b/libs/galley-types/test/unit/Main.hs index c45ccb60b3d..90b692813d3 100644 --- a/libs/galley-types/test/unit/Main.hs +++ b/libs/galley-types/test/unit/Main.hs @@ -21,15 +21,8 @@ module Main where import Imports -import Test.Galley.Permissions qualified import Test.Galley.Types qualified import Test.Tasty main :: IO () -main = - defaultMain $ - testGroup - "Tests" - [ Test.Galley.Types.tests, - Test.Galley.Permissions.tests - ] +main = defaultMain $ testGroup "Tests" [Test.Galley.Types.tests] diff --git a/libs/galley-types/test/unit/Test/Galley/Permissions.hs b/libs/galley-types/test/unit/Test/Galley/Permissions.hs deleted file mode 100644 index fa23c1a278e..00000000000 --- a/libs/galley-types/test/unit/Test/Galley/Permissions.hs +++ /dev/null @@ -1,35 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Test.Galley.Permissions where - -import Galley.Types.Teams -import Imports -import Test.Tasty -import Test.Tasty.HUnit -import Wire.API.Team.Permission -import Wire.API.Team.Role - -tests :: TestTree -tests = - testGroup - "permsToInt / rolePermissions / serialization of `Role`s" - [ testCase "partner" $ assertEqual "" (permsToInt . _self $ rolePermissions RoleExternalPartner) 1025, - testCase "member" $ assertEqual "" (permsToInt . _self $ rolePermissions RoleMember) 1587, - testCase "admin" $ assertEqual "" (permsToInt . _self $ rolePermissions RoleAdmin) 5951, - testCase "owner" $ assertEqual "" (permsToInt . _self $ rolePermissions RoleOwner) 8191 - ] diff --git a/libs/galley-types/test/unit/Test/Galley/Types.hs b/libs/galley-types/test/unit/Test/Galley/Types.hs index 3381fe49ef5..aa2c03a1411 100644 --- a/libs/galley-types/test/unit/Test/Galley/Types.hs +++ b/libs/galley-types/test/unit/Test/Galley/Types.hs @@ -20,63 +20,16 @@ module Test.Galley.Types where -import Control.Lens -import Data.Set hiding (drop) -import Data.Set qualified as Set import Galley.Types.Teams import Imports import Test.Galley.Roundtrip (testRoundTrip) import Test.QuickCheck qualified as QC import Test.Tasty -import Test.Tasty.HUnit import Test.Tasty.QuickCheck import Wire.API.Team.Feature as Public -import Wire.API.Team.Permission -import Wire.API.Team.Role tests :: TestTree -tests = - testGroup - "Tests" - [ testCase "owner has all permissions" $ - rolePermissions RoleOwner @=? fullPermissions, - testCase "smaller roles (further to the left/top in the type def) are strictly more powerful" $ - -- we may not want to maintain this property in the future when adding more roles, but for - -- now it's true, and it's nice to have that written down somewhere. - forM_ [(r1, r2) | r1 <- [minBound ..], r2 <- drop 1 [r1 ..]] $ - \(r1, r2) -> do - assertBool "owner.self" ((rolePermissions r2 ^. self) `isSubsetOf` (rolePermissions r1 ^. self)) - assertBool "owner.copy" ((rolePermissions r2 ^. copy) `isSubsetOf` (rolePermissions r1 ^. copy)), - testRoundTrip @FeatureFlags, - testGroup - "permissionsRole, rolePermissions" - [ testCase "'Role' maps to expected permissions" $ do - assertEqual "role type changed" [minBound ..] [RoleOwner, RoleAdmin, RoleMember, RoleExternalPartner] - assertEqual "owner" (permissionsRole =<< newPermissions (intToPerms 8191) (intToPerms 8191)) (Just RoleOwner) - assertEqual "admin" (permissionsRole =<< newPermissions (intToPerms 5951) (intToPerms 5951)) (Just RoleAdmin) - assertEqual "member" (permissionsRole =<< newPermissions (intToPerms 1587) (intToPerms 1587)) (Just RoleMember) - assertEqual "external partner" (permissionsRole =<< newPermissions (intToPerms 1025) (intToPerms 1025)) (Just RoleExternalPartner), - testCase "Role <-> Permissions roundtrip" $ do - assertEqual "admin" (permissionsRole . rolePermissions <$> [minBound ..]) (Just <$> [minBound ..]), - testProperty "Random, incoherent 'Permission' values gracefully translate to subsets." $ - let fakeSort (w, w') = (w `Set.union` w', w') - in \(fakeSort -> (w, w')) -> do - let Just perms = newPermissions w w' - case permissionsRole perms of - Just role -> do - let perms' = rolePermissions role - assertEqual "eq" (perms' ^. self) (perms' ^. copy) - assertBool "self" ((perms' ^. self) `Set.isSubsetOf` (perms ^. self)) - assertBool "copy" ((perms' ^. copy) `Set.isSubsetOf` (perms ^. copy)) - Nothing -> do - let leastPermissions = rolePermissions maxBound - assertBool "no role for perms, but strictly more perms than max role" $ - not - ( (leastPermissions ^. self) `Set.isSubsetOf` w - && (leastPermissions ^. copy) `Set.isSubsetOf` w' - ) - ] - ] +tests = testGroup "Tests" [testRoundTrip @FeatureFlags] instance Arbitrary FeatureFlags where arbitrary = diff --git a/libs/wire-api/src/Wire/API/Error/Galley.hs b/libs/wire-api/src/Wire/API/Error/Galley.hs index 57f76ef1d65..cf48534dff7 100644 --- a/libs/wire-api/src/Wire/API/Error/Galley.hs +++ b/libs/wire-api/src/Wire/API/Error/Galley.hs @@ -57,7 +57,7 @@ import Wire.API.Conversation.Role import Wire.API.Error import Wire.API.Error.Brig qualified as BrigError import Wire.API.Routes.API -import Wire.API.Team.Member +import Wire.API.Team.HardTruncationLimit import Wire.API.Team.Permission import Wire.API.Unreachable import Wire.API.Util.Aeson (CustomEncoded (..)) diff --git a/libs/wire-api/src/Wire/API/Team/HardTruncationLimit.hs b/libs/wire-api/src/Wire/API/Team/HardTruncationLimit.hs new file mode 100644 index 00000000000..0ec378cc1bd --- /dev/null +++ b/libs/wire-api/src/Wire/API/Team/HardTruncationLimit.hs @@ -0,0 +1,10 @@ +module Wire.API.Team.HardTruncationLimit where + +import Data.Proxy +import GHC.TypeLits +import Imports + +type HardTruncationLimit = (2000 :: Nat) + +hardTruncationLimit :: Integral a => a +hardTruncationLimit = fromIntegral $ natVal (Proxy @HardTruncationLimit) diff --git a/libs/wire-api/src/Wire/API/Team/Member.hs b/libs/wire-api/src/Wire/API/Team/Member.hs index 91e790aa66e..28c72a3808b 100644 --- a/libs/wire-api/src/Wire/API/Team/Member.hs +++ b/libs/wire-api/src/Wire/API/Team/Member.hs @@ -61,12 +61,19 @@ module Wire.API.Team.Member TeamMemberDeleteData, newTeamMemberDeleteData, tmdAuthPassword, + + -- * Permissions + isAdminOrOwner, + permissionsRole, + rolePermissions, + IsPerm (..), + HiddenPerm (..), ) where import Cassandra (PageWithState (..)) import Cassandra qualified as C -import Control.Lens (Lens, Lens', makeLenses, (%~), (?~)) +import Control.Lens (Lens, Lens', makeLenses, (%~), (?~), (^.)) import Data.Aeson (FromJSON (..), ToJSON (..), Value (..)) import Data.ByteString.Lazy qualified as LBS import Data.Id (UserId) @@ -78,11 +85,14 @@ import Data.OpenApi (ToParamSchema (..)) import Data.OpenApi.Schema qualified as S import Data.Proxy import Data.Schema -import GHC.TypeLits +import Data.Set qualified as Set import Imports +import Wire.API.Error.Galley import Wire.API.Routes.MultiTablePaging (MultiTablePage (..)) import Wire.API.Routes.MultiTablePaging.State -import Wire.API.Team.Permission (Permissions) +import Wire.API.Team.HardTruncationLimit +import Wire.API.Team.Permission +import Wire.API.Team.Role import Wire.Arbitrary (Arbitrary, GenericUniform (..)) data PermissionTag = Required | Optional @@ -271,11 +281,6 @@ instance ToSchema (TeamMember' tag) => ToSchema (TeamMemberList' tag) where <*> _teamMemberListType .= fieldWithDocModifier "hasMore" (description ?~ "true if 'members' doesn't contain all team members") schema -type HardTruncationLimit = (2000 :: Nat) - -hardTruncationLimit :: Integral a => a -hardTruncationLimit = fromIntegral $ natVal (Proxy @HardTruncationLimit) - -- | Like 'ListType', but without backwards-compatible and boolean-blind json serialization. data NewListType = NewListComplete @@ -430,3 +435,167 @@ setOptionalPerms withPerms m = m & permissions %~ setPerm (withPerms m) setOptionalPermsMany :: (TeamMember -> Bool) -> TeamMemberList -> TeamMemberList' 'Optional setOptionalPermsMany withPerms l = l {_teamMembers = map (setOptionalPerms withPerms) (_teamMembers l)} + +-- Note [hidden team roles] +-- +-- The problem: the mapping between 'Role' and 'Permissions' is fixed by external contracts: +-- client apps treat permission bit matrices as opaque role identifiers, so if we add new +-- permission flags, things will break there. +-- +-- "Hidden" in "HiddenPerm", therefore, refers to a permission hidden from +-- clients, thereby making it internal to the backend. +-- +-- The solution: add new permission bits to 'HiddenPerm', 'HiddenPermissions', and make +-- 'hasPermission', 'mayGrantPermission' polymorphic. Now you can check both for the hidden +-- permission bits and the old ones that we share with the client apps. + +-- | See Note [hidden team roles] +data HiddenPerm + = ChangeLegalHoldTeamSettings + | ChangeLegalHoldUserSettings + | ViewLegalHoldUserSettings + | ChangeTeamFeature + | ChangeTeamSearchVisibility + | ViewTeamSearchVisibility + | ViewSameTeamEmails + | ReadIdp + | CreateUpdateDeleteIdp + | CreateReadDeleteScimToken + | -- | this has its own permission because we're not sure how + -- efficient this end-point is. better not let all team members + -- play with it unless we have to. + DownloadTeamMembersCsv + | ChangeTeamMemberProfiles + | SearchContacts + deriving (Eq, Ord, Show) + +-- | See Note [hidden team roles] +data HiddenPermissions = HiddenPermissions + { _hself :: Set HiddenPerm, + _hcopy :: Set HiddenPerm + } + deriving (Eq, Ord, Show) + +makeLenses ''HiddenPermissions + +rolePermissions :: Role -> Permissions +rolePermissions role = Permissions p p where p = rolePerms role + +permissionsRole :: Permissions -> Maybe Role +permissionsRole (Permissions p p') = + if p /= p' + then do + -- we never did use @p /= p'@ for anything, fingers crossed that it doesn't occur anywhere + -- in the wild. but if it does, this implementation prevents privilege escalation. + let p'' = Set.intersection p p' + in permissionsRole (Permissions p'' p'') + else permsRole p + where + permsRole :: Set Perm -> Maybe Role + permsRole perms = + listToMaybe + [ role + | role <- [minBound ..], + -- if a there is a role that is strictly less permissive than the perms set that + -- we encounter, we downgrade. this shouldn't happen in real life, but it has + -- happened to very old users on a staging environment, where a user (probably) + -- was create before the current publicly visible permissions had been stabilized. + rolePerms role `Set.isSubsetOf` perms + ] + +-- | Internal function for 'rolePermissions'. (It works iff the two sets in 'Permissions' are +-- identical for every 'Role', otherwise it'll need to be specialized for the resp. sides.) +rolePerms :: Role -> Set Perm +rolePerms RoleOwner = + rolePerms RoleAdmin + <> Set.fromList + [ GetBilling, + SetBilling, + DeleteTeam + ] +rolePerms RoleAdmin = + rolePerms RoleMember + <> Set.fromList + [ AddTeamMember, + RemoveTeamMember, + SetTeamData, + SetMemberPermissions + ] +rolePerms RoleMember = + rolePerms RoleExternalPartner + <> Set.fromList + [ DeleteConversation, + AddRemoveConvMember, + ModifyConvName, + GetMemberPermissions + ] +rolePerms RoleExternalPartner = + Set.fromList + [ CreateConversation, + GetTeamConversations + ] + +roleHiddenPermissions :: Role -> HiddenPermissions +roleHiddenPermissions role = HiddenPermissions p p + where + p = roleHiddenPerms role + roleHiddenPerms :: Role -> Set HiddenPerm + roleHiddenPerms RoleOwner = roleHiddenPerms RoleAdmin + roleHiddenPerms RoleAdmin = + (roleHiddenPerms RoleMember <>) $ + Set.fromList + [ ChangeLegalHoldTeamSettings, + ChangeLegalHoldUserSettings, + ChangeTeamSearchVisibility, + ChangeTeamFeature, + ChangeTeamMemberProfiles, + ReadIdp, + CreateUpdateDeleteIdp, + CreateReadDeleteScimToken, + DownloadTeamMembersCsv + ] + roleHiddenPerms RoleMember = + (roleHiddenPerms RoleExternalPartner <>) $ + Set.fromList + [ ViewSameTeamEmails, + SearchContacts + ] + roleHiddenPerms RoleExternalPartner = + Set.fromList + [ ViewLegalHoldUserSettings, + ViewTeamSearchVisibility + ] + +isAdminOrOwner :: Permissions -> Bool +isAdminOrOwner perms = + case permissionsRole perms of + Just RoleOwner -> True + Just RoleAdmin -> True + Just RoleMember -> False + Just RoleExternalPartner -> False + Nothing -> False + +-- | See Note [hidden team roles] +class IsPerm perm where + type PermError (e :: perm) :: GalleyError + + roleHasPerm :: Role -> perm -> Bool + roleGrantsPerm :: Role -> perm -> Bool + hasPermission :: TeamMember -> perm -> Bool + hasPermission tm perm = maybe False (`roleHasPerm` perm) . permissionsRole $ tm ^. permissions + mayGrantPermission :: TeamMember -> perm -> Bool + mayGrantPermission tm perm = maybe False (`roleGrantsPerm` perm) . permissionsRole $ tm ^. permissions + +instance IsPerm Perm where + type PermError p = 'MissingPermission ('Just p) + + roleHasPerm r p = p `Set.member` (rolePermissions r ^. self) + roleGrantsPerm r p = p `Set.member` (rolePermissions r ^. copy) + hasPermission tm p = p `Set.member` (tm ^. permissions . self) + mayGrantPermission tm p = p `Set.member` (tm ^. permissions . copy) + +instance IsPerm HiddenPerm where + type PermError p = OperationDenied + + roleHasPerm r p = p `Set.member` (roleHiddenPermissions r ^. hself) + roleGrantsPerm r p = p `Set.member` (roleHiddenPermissions r ^. hcopy) diff --git a/libs/wire-api/src/Wire/API/Team/Permission.hs b/libs/wire-api/src/Wire/API/Team/Permission.hs index 3b108391eda..b4ac0d90455 100644 --- a/libs/wire-api/src/Wire/API/Team/Permission.hs +++ b/libs/wire-api/src/Wire/API/Team/Permission.hs @@ -53,6 +53,7 @@ import Data.Schema import Data.Set qualified as Set import Data.Singletons.Base.TH import Imports +import Test.QuickCheck (oneof) import Wire.API.Util.Aeson (CustomEncoded (..)) import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..)) @@ -89,7 +90,7 @@ instance ToSchema Permissions where instance Arbitrary Permissions where arbitrary = maybe (error "instance Arbitrary Permissions") pure =<< do - selfperms <- arbitrary + selfperms <- oneof $ map (pure . intToPerms) [1025, 1587, 5951, 8191] copyperms <- Set.intersection selfperms <$> arbitrary pure $ newPermissions selfperms copyperms diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index e605fd9ec84..a55509da237 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -43,8 +43,7 @@ module Wire.API.User userSCIMExternalId, scimExternalId, ssoIssuerAndNameId, - connectedProfile, - publicProfile, + mkUserProfile, userObjectSchema, -- * NewUser @@ -138,6 +137,9 @@ module Wire.API.User UpdateSSOIdResponse (..), CheckHandleResponse (..), UpdateConnectionsInternal (..), + EmailVisibility (..), + EmailVisibilityConfig, + EmailVisibilityConfigWithViewer, -- * re-exports module Wire.API.User.Identity, @@ -161,7 +163,7 @@ import Control.Applicative import Control.Arrow ((&&&)) import Control.Error.Safe (rightMay) import Control.Lens (makePrisms, over, view, (.~), (?~), (^.)) -import Data.Aeson (FromJSON (..), ToJSON (..)) +import Data.Aeson (FromJSON (..), ToJSON (..), withText) import Data.Aeson.Types qualified as A import Data.Attoparsec.ByteString qualified as Parser import Data.Attoparsec.Text qualified as TParser @@ -211,6 +213,8 @@ import Wire.API.Error.Brig qualified as E import Wire.API.Provider.Service (ServiceRef) import Wire.API.Routes.MultiVerb import Wire.API.Team (BindingNewTeam, bindingNewTeamObjectSchema) +import Wire.API.Team.Member (TeamMember) +import Wire.API.Team.Member qualified as TeamMember import Wire.API.Team.Role import Wire.API.User.Activation (ActivationCode, ActivationKey) import Wire.API.User.Auth (CookieLabel) @@ -770,60 +774,54 @@ userIssuer user = userSSOId user >>= fromSSOId fromSSOId (UserSSOId (SAML.UserRef issuer _)) = Just issuer fromSSOId _ = Nothing -connectedProfile :: User -> UserLegalHoldStatus -> UserProfile -connectedProfile u legalHoldStatus = - UserProfile - { profileQualifiedId = userQualifiedId u, - profileHandle = userHandle u, - profileName = userDisplayName u, - profilePict = userPict u, - profileAssets = userAssets u, - profileAccentId = userAccentId u, - profileService = userService u, - profileDeleted = userDeleted u, - profileExpire = userExpire u, - profileTeam = userTeam u, - -- We don't want to show the email by default; - -- However we do allow adding it back in intentionally later. - profileEmail = Nothing, - profileLegalholdStatus = legalHoldStatus, - profileSupportedProtocols = userSupportedProtocols u - } +-- | Configurations for whether to show a user's email to others. +data EmailVisibility a + = -- | Anyone can see the email of someone who is on ANY team. + -- This may sound strange; but certain on-premise hosters have many different teams + -- and still want them to see each-other's emails. + EmailVisibleIfOnTeam + | -- | Anyone on your team with at least 'Member' privileges can see your email address. + EmailVisibleIfOnSameTeam a + | -- | Show your email only to yourself + EmailVisibleToSelf + deriving (Eq, Show) --- FUTUREWORK: should public and conect profile be separate types? -publicProfile :: User -> UserLegalHoldStatus -> UserProfile -publicProfile u legalHoldStatus = - -- Note that we explicitly unpack and repack the types here rather than using - -- RecordWildCards or something similar because we want changes to the public profile - -- to be EXPLICIT and INTENTIONAL so we don't accidentally leak sensitive data. - let UserProfile - { profileQualifiedId, - profileHandle, - profileName, - profilePict, - profileAssets, - profileAccentId, - profileService, - profileDeleted, - profileExpire, - profileTeam, - profileLegalholdStatus, - profileSupportedProtocols - } = connectedProfile u legalHoldStatus - in UserProfile - { profileEmail = Nothing, - profileQualifiedId, - profileHandle, - profileName, - profilePict, - profileAssets, - profileAccentId, - profileService, - profileDeleted, - profileExpire, - profileTeam, - profileLegalholdStatus, - profileSupportedProtocols +type EmailVisibilityConfig = EmailVisibility () + +type EmailVisibilityConfigWithViewer = EmailVisibility (Maybe (TeamId, TeamMember)) + +instance FromJSON (EmailVisibility ()) where + parseJSON = withText "EmailVisibility" $ \case + "visible_if_on_team" -> pure EmailVisibleIfOnTeam + "visible_if_on_same_team" -> pure $ EmailVisibleIfOnSameTeam () + "visible_to_self" -> pure EmailVisibleToSelf + _ -> fail "unexpected value for EmailVisibility settings" + +mkUserProfile :: EmailVisibilityConfigWithViewer -> User -> UserLegalHoldStatus -> UserProfile +mkUserProfile emailVisibilityConfigAndViewer u legalHoldStatus = + let isEmailVisible = case emailVisibilityConfigAndViewer of + EmailVisibleToSelf -> False + EmailVisibleIfOnTeam -> isJust (userTeam u) + EmailVisibleIfOnSameTeam Nothing -> False + EmailVisibleIfOnSameTeam (Just (viewerTeamId, viewerMembership)) -> + Just viewerTeamId == userTeam u + && TeamMember.hasPermission viewerMembership TeamMember.ViewSameTeamEmails + in -- This profile would be visible to any other user. When a new field is + -- added, please make sure it is OK for other users to have access to it. + UserProfile + { profileQualifiedId = userQualifiedId u, + profileHandle = userHandle u, + profileName = userDisplayName u, + profilePict = userPict u, + profileAssets = userAssets u, + profileAccentId = userAccentId u, + profileService = userService u, + profileDeleted = userDeleted u, + profileExpire = userExpire u, + profileTeam = userTeam u, + profileEmail = if isEmailVisible then userEmail u else Nothing, + profileLegalholdStatus = legalHoldStatus, + profileSupportedProtocols = userSupportedProtocols u } -------------------------------------------------------------------------------- diff --git a/libs/wire-api/test/unit/Test/Wire/API/Team/Member.hs b/libs/wire-api/test/unit/Test/Wire/API/Team/Member.hs index a0d6404b1ac..8a44da25f23 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Team/Member.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Team/Member.hs @@ -1,4 +1,5 @@ {-# LANGUAGE ScopedTypeVariables #-} +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} {-# OPTIONS_GHC -Wno-orphans #-} -- This file is part of the Wire Server implementation. @@ -18,20 +19,82 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Test.Wire.API.Team.Member where +module Test.Wire.API.Team.Member (tests) where +import Control.Lens ((^.)) import Data.Aeson +import Data.Set (isSubsetOf) +import Data.Set qualified as Set import Imports import Test.Tasty import Test.Tasty.HUnit -import Wire.API.Team.Member qualified as Team.Member +import Test.Tasty.QuickCheck +import Wire.API.Team.Member +import Wire.API.Team.Permission +import Wire.API.Team.Role -- NB: validateEveryToJSON from servant-swagger doesn't render these tests unnecessary! tests :: TestTree -tests = +tests = testGroup "Wire.API.Team.Member" [commonTests, permissionTests, permissionConversionTests] + +commonTests :: TestTree +commonTests = testGroup "Common (types vs. aeson)" [ testCase "{} is a valid TeamMemberDeleteData" $ do - assertBool "{}" (isRight (eitherDecode @Team.Member.TeamMemberDeleteData "{}")) + assertBool "{}" (isRight (eitherDecode @TeamMemberDeleteData "{}")) + ] + +permissionTests :: TestTree +permissionTests = + testGroup + "Permissions" + [ testCase "owner has all permissions" $ + rolePermissions RoleOwner @=? fullPermissions, + testCase "smaller roles (further to the left/top in the type def) are strictly more powerful" $ + -- we may not want to maintain this property in the future when adding more roles, but for + -- now it's true, and it's nice to have that written down somewhere. + forM_ [(r1, r2) | r1 <- [minBound ..], r2 <- drop 1 [r1 ..]] $ + \(r1, r2) -> do + assertBool "owner.self" ((rolePermissions r2 ^. self) `isSubsetOf` (rolePermissions r1 ^. self)) + assertBool "owner.copy" ((rolePermissions r2 ^. copy) `isSubsetOf` (rolePermissions r1 ^. copy)), + testGroup + "permissionsRole, rolePermissions" + [ testCase "'Role' maps to expected permissions" $ do + assertEqual "role type changed" [minBound ..] [RoleOwner, RoleAdmin, RoleMember, RoleExternalPartner] + assertEqual "owner" (permissionsRole =<< newPermissions (intToPerms 8191) (intToPerms 8191)) (Just RoleOwner) + assertEqual "admin" (permissionsRole =<< newPermissions (intToPerms 5951) (intToPerms 5951)) (Just RoleAdmin) + assertEqual "member" (permissionsRole =<< newPermissions (intToPerms 1587) (intToPerms 1587)) (Just RoleMember) + assertEqual "external partner" (permissionsRole =<< newPermissions (intToPerms 1025) (intToPerms 1025)) (Just RoleExternalPartner), + testCase "Role <-> Permissions roundtrip" $ do + assertEqual "admin" (permissionsRole . rolePermissions <$> [minBound ..]) (Just <$> [minBound ..]), + testProperty "Random, incoherent 'Permission' values gracefully translate to subsets." $ + let fakeSort (w, w') = (w `Set.union` w', w') + in \(fakeSort -> (w, w')) -> do + let Just perms = newPermissions w w' + case permissionsRole perms of + Just role -> do + let perms' = rolePermissions role + assertEqual "eq" (perms' ^. self) (perms' ^. copy) + assertBool "self" ((perms' ^. self) `Set.isSubsetOf` (perms ^. self)) + assertBool "copy" ((perms' ^. copy) `Set.isSubsetOf` (perms ^. copy)) + Nothing -> do + let leastPermissions = rolePermissions maxBound + assertBool "no role for perms, but strictly more perms than max role" $ + not + ( (leastPermissions ^. self) `Set.isSubsetOf` w + && (leastPermissions ^. copy) `Set.isSubsetOf` w' + ) + ] + ] + +permissionConversionTests :: TestTree +permissionConversionTests = + testGroup + "permsToInt / rolePermissions / serialization of `Role`s" + [ testCase "partner" $ assertEqual "" (permsToInt . _self $ rolePermissions RoleExternalPartner) 1025, + testCase "member" $ assertEqual "" (permsToInt . _self $ rolePermissions RoleMember) 1587, + testCase "admin" $ assertEqual "" (permsToInt . _self $ rolePermissions RoleAdmin) 5951, + testCase "owner" $ assertEqual "" (permsToInt . _self $ rolePermissions RoleOwner) 8191 ] diff --git a/libs/wire-api/test/unit/Test/Wire/API/User.hs b/libs/wire-api/test/unit/Test/Wire/API/User.hs index 98b7745b618..d8f9a115376 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/User.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/User.hs @@ -17,8 +17,9 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Test.Wire.API.User where +module Test.Wire.API.User (tests) where +import Control.Lens ((.~)) import Data.Aeson import Data.Aeson qualified as Aeson import Data.Aeson.Types as Aeson @@ -31,16 +32,85 @@ import Data.UUID.V4 qualified as UUID import Imports import Test.Tasty import Test.Tasty.HUnit +import Test.Tasty.QuickCheck +import Wire.API.Team.Member (TeamMember) +import Wire.API.Team.Member qualified as TeamMember +import Wire.API.Team.Role import Wire.API.User tests :: TestTree -tests = testGroup "User (types vs. aeson)" unitTests +tests = + testGroup + "User (types vs. aeson)" + [ parseIdentityTests, + jsonNullTests, + testMkUserProfile + ] -unitTests :: [TestTree] -unitTests = parseIdentityTests ++ jsonNullTests +jsonNullTests :: TestTree +jsonNullTests = testGroup "JSON null" [testCase "userProfile" testUserProfile] -jsonNullTests :: [TestTree] -jsonNullTests = [testGroup "JSON null" [testCase "userProfile" testUserProfile]] +testMkUserProfile :: TestTree +testMkUserProfile = + testGroup + "mkUserProfile" + [ testEmailVisibleToSelf, + testEmailVisibleIfOnTeam, + testEmailVisibleIfOnSameTeam + ] + +testEmailVisibleToSelf :: TestTree +testEmailVisibleToSelf = + testProperty "should not contain email when email visibility is EmailVisibleToSelf" $ + \user lhStatus -> + let profile = mkUserProfile EmailVisibleToSelf user lhStatus + in profileEmail profile === Nothing + .&&. profileLegalholdStatus profile === lhStatus + +testEmailVisibleIfOnTeam :: TestTree +testEmailVisibleIfOnTeam = + testProperty "should contain email only if the user has one and is part of a team when email visibility is EmailVisibleIfOnTeam" $ + \user lhStatus -> + let profile = mkUserProfile EmailVisibleIfOnTeam user lhStatus + in (profileEmail profile === (userTeam user *> userEmail user)) + .&&. profileLegalholdStatus profile === lhStatus + +testEmailVisibleIfOnSameTeam :: TestTree +testEmailVisibleIfOnSameTeam = + testGroup "when email visibility is EmailVisibleIfOnSameTeam" [testNoViewerTeam, testViewerDifferentTeam, testViewerSameTeamExternal, testViewerSameTeamNotExternal] + where + testNoViewerTeam = testProperty "should not contain email when viewer is not part of a team" $ + \user lhStatus -> + let profile = mkUserProfile (EmailVisibleIfOnSameTeam Nothing) user lhStatus + in (profileEmail profile === Nothing) + .&&. profileLegalholdStatus profile === lhStatus + + testViewerDifferentTeam = testProperty "should not contain email when viewer is not part of the same team" $ + \viewerTeamId viewerMembership user lhStatus -> + let profile = mkUserProfile (EmailVisibleIfOnSameTeam (Just (viewerTeamId, viewerMembership))) user lhStatus + in Just viewerTeamId /= userTeam user ==> + ( profileEmail profile === Nothing + .&&. profileLegalholdStatus profile === lhStatus + ) + + testViewerSameTeamExternal = testProperty "should not contain email when viewer is part of the same team and is an external member" $ + \viewerTeamId (viewerMembershipNoRole :: TeamMember) userNoTeam lhStatus -> + let user = userNoTeam {userTeam = Just viewerTeamId} + viewerMembership = viewerMembershipNoRole & TeamMember.permissions .~ TeamMember.rolePermissions RoleExternalPartner + profile = mkUserProfile (EmailVisibleIfOnSameTeam (Just (viewerTeamId, viewerMembership))) user lhStatus + in ( profileEmail profile === Nothing + .&&. profileLegalholdStatus profile === lhStatus + ) + + testViewerSameTeamNotExternal = testProperty "should contain email when viewer is part of the same team and is not an external member" $ + \viewerTeamId (viewerMembershipNoRole :: TeamMember) viewerRole userNoTeam lhStatus -> + let user = userNoTeam {userTeam = Just viewerTeamId} + viewerMembership = viewerMembershipNoRole & TeamMember.permissions .~ TeamMember.rolePermissions viewerRole + profile = mkUserProfile (EmailVisibleIfOnSameTeam (Just (viewerTeamId, viewerMembership))) user lhStatus + in viewerRole /= RoleExternalPartner ==> + ( profileEmail profile === userEmail user + .&&. profileLegalholdStatus profile === lhStatus + ) testUserProfile :: Assertion testUserProfile = do @@ -52,32 +122,31 @@ testUserProfile = do let msg = "toJSON encoding must not convert Nothing to null, but instead omit those json fields for backwards compatibility. UserProfileJSON:" <> profileJSONAsText assertBool msg (not $ "null" `isInfixOf` profileJSONAsText) -parseIdentityTests :: [TestTree] +parseIdentityTests :: TestTree parseIdentityTests = - [ let (=#=) :: Either String (Maybe UserIdentity) -> [Pair] -> Assertion - (=#=) uid (object -> Object obj) = assertEqual "=#=" uid (parseEither (schemaIn maybeUserIdentityObjectSchema) obj) - (=#=) _ bad = error $ "=#=: impossible: " <> show bad - in testGroup - "parseIdentity" - [ testCase "FullIdentity" $ - Right (Just (FullIdentity hemail hphone)) =#= [email, phone], - testCase "EmailIdentity" $ - Right (Just (EmailIdentity hemail)) =#= [email], - testCase "PhoneIdentity" $ - Right (Just (PhoneIdentity hphone)) =#= [phone], - testCase "SSOIdentity" $ do - Right (Just (SSOIdentity hssoid Nothing Nothing)) =#= [ssoid] - Right (Just (SSOIdentity hssoid Nothing (Just hphone))) =#= [ssoid, phone] - Right (Just (SSOIdentity hssoid (Just hemail) Nothing)) =#= [ssoid, email] - Right (Just (SSOIdentity hssoid (Just hemail) (Just hphone))) =#= [ssoid, email, phone], - testCase "Bad phone" $ - Left "Error in $.phone: Invalid phone number. Expected E.164 format." =#= [badphone], - testCase "Bad email" $ - Left "Error in $.email: Invalid email. Expected '@'." =#= [bademail], - testCase "Nothing" $ - Right Nothing =#= [("something_unrelated", "#")] - ] - ] + let (=#=) :: Either String (Maybe UserIdentity) -> [Pair] -> Assertion + (=#=) uid (object -> Object obj) = assertEqual "=#=" uid (parseEither (schemaIn maybeUserIdentityObjectSchema) obj) + (=#=) _ bad = error $ "=#=: impossible: " <> show bad + in testGroup + "parseIdentity" + [ testCase "FullIdentity" $ + Right (Just (FullIdentity hemail hphone)) =#= [email, phone], + testCase "EmailIdentity" $ + Right (Just (EmailIdentity hemail)) =#= [email], + testCase "PhoneIdentity" $ + Right (Just (PhoneIdentity hphone)) =#= [phone], + testCase "SSOIdentity" $ do + Right (Just (SSOIdentity hssoid Nothing Nothing)) =#= [ssoid] + Right (Just (SSOIdentity hssoid Nothing (Just hphone))) =#= [ssoid, phone] + Right (Just (SSOIdentity hssoid (Just hemail) Nothing)) =#= [ssoid, email] + Right (Just (SSOIdentity hssoid (Just hemail) (Just hphone))) =#= [ssoid, email, phone], + testCase "Bad phone" $ + Left "Error in $.phone: Invalid phone number. Expected E.164 format." =#= [badphone], + testCase "Bad email" $ + Left "Error in $.email: Invalid email. Expected '@'." =#= [bademail], + testCase "Nothing" $ + Right Nothing =#= [("something_unrelated", "#")] + ] where hemail = Email "me" "example.com" email = ("email", "me@example.com") diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index 625c8fec75e..a5ad166cf26 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -203,6 +203,7 @@ library Wire.API.Team.Conversation Wire.API.Team.Export Wire.API.Team.Feature + Wire.API.Team.HardTruncationLimit Wire.API.Team.Invitation Wire.API.Team.LegalHold Wire.API.Team.LegalHold.External @@ -677,6 +678,7 @@ test-suite wire-api-tests , hspec-wai , http-types , imports + , lens , memory , metrics-wai , openapi3 diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index f15bd59953d..b747d81dba5 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -98,7 +98,6 @@ import Data.Text.Lazy (pack) import Data.Time.Clock (UTCTime) import Data.ZAuth.Token qualified as ZAuth import FileEmbedLzma -import Galley.Types.Teams (HiddenPerm (..), hasPermission) import Imports hiding (head) import Network.Socket (PortNumber) import Network.Wai.Utilities as Utilities @@ -142,6 +141,7 @@ import Wire.API.SwaggerHelper (cleanupSwagger) import Wire.API.SystemSettings import Wire.API.Team qualified as Public import Wire.API.Team.LegalHold (LegalholdProtectee (..)) +import Wire.API.Team.Member (HiddenPerm (..), hasPermission) import Wire.API.User (RegisterError (RegisterErrorAllowlistError)) import Wire.API.User qualified as Public import Wire.API.User.Activation qualified as Public diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 4b565ce4509..6db13202f28 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -140,7 +140,6 @@ import Brig.User.Phone import Brig.User.Search.Index (reindex) import Brig.User.Search.TeamSize qualified as TeamSize import Cassandra hiding (Set) -import Control.Arrow ((&&&)) import Control.Error import Control.Lens (view, (^.)) import Control.Monad.Catch @@ -153,13 +152,11 @@ import Data.Json.Util import Data.LegalHold (UserLegalHoldStatus (..), defUserLegalHoldStatus) import Data.List.Extra import Data.List1 as List1 (List1, singleton) -import Data.Map.Strict qualified as Map import Data.Metrics qualified as Metrics import Data.Misc import Data.Qualified import Data.Time.Clock (UTCTime, addUTCTime, diffUTCTime) import Data.UUID.V4 (nextRandom) -import Galley.Types.Teams qualified as Team import Imports hiding (cs) import Network.Wai.Utilities import Polysemy @@ -174,7 +171,6 @@ import Wire.API.Error import Wire.API.Error.Brig qualified as E import Wire.API.Federation.Error import Wire.API.Password -import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Routes.Internal.Galley.TeamsIntra qualified as Team import Wire.API.Team hiding (newTeam) import Wire.API.Team.Feature @@ -1660,22 +1656,17 @@ lookupLocalProfiles :: AppT r [UserProfile] lookupLocalProfiles requestingUser others = do users <- wrapHttpClient $ Data.lookupUsers NoPendingInvitations others >>= mapM userGC - css <- case requestingUser of - Just localReqUser -> toMap <$> wrapHttpClient (Data.lookupConnectionStatus (map userId users) [localReqUser]) - Nothing -> pure mempty - emailVisibility' <- view (settings . emailVisibility) - emailVisibility'' <- case emailVisibility' of - EmailVisibleIfOnTeam -> pure EmailVisibleIfOnTeam' - EmailVisibleIfOnSameTeam -> case requestingUser of - Just localReqUser -> EmailVisibleIfOnSameTeam' <$> getSelfInfo localReqUser - Nothing -> pure EmailVisibleToSelf' - EmailVisibleToSelf -> pure EmailVisibleToSelf' + emailVisibilityConfig <- view (settings . emailVisibility) + emailVisibilityConfigWithViewer <- + case emailVisibilityConfig of + EmailVisibleIfOnTeam -> pure EmailVisibleIfOnTeam + EmailVisibleToSelf -> pure EmailVisibleToSelf + EmailVisibleIfOnSameTeam () -> + EmailVisibleIfOnSameTeam . join @Maybe + <$> traverse getSelfInfo requestingUser usersAndStatus <- liftSem $ for users $ \u -> (u,) <$> getLegalHoldStatus' u - pure $ map (toProfile emailVisibility'' css) usersAndStatus + pure $ map (uncurry $ mkUserProfile emailVisibilityConfigWithViewer) usersAndStatus where - toMap :: [ConnectionStatus] -> Map UserId Relation - toMap = Map.fromList . map (csFrom &&& csStatus) - getSelfInfo :: UserId -> AppT r (Maybe (TeamId, TeamMember)) getSelfInfo selfId = do -- FUTUREWORK: it is an internal error for the two lookups (for 'User' and 'TeamMember') @@ -1686,16 +1677,6 @@ lookupLocalProfiles requestingUser others = do Nothing -> pure Nothing Just tid -> (tid,) <$$> liftSem (GalleyProvider.getTeamMember selfId tid) - toProfile :: EmailVisibility' -> Map UserId Relation -> (User, UserLegalHoldStatus) -> UserProfile - toProfile emailVisibility'' css (u, userLegalHold) = - let cs = Map.lookup (userId u) css - profileEmail' = getEmailForProfile u emailVisibility'' - baseProfile = - if Just (userId u) == requestingUser || cs == Just Accepted || cs == Just Sent - then connectedProfile u userLegalHold - else publicProfile u userLegalHold - in baseProfile {profileEmail = profileEmail'} - getLegalHoldStatus :: Member GalleyProvider r => UserId -> @@ -1713,28 +1694,6 @@ getLegalHoldStatus' user = teamMember <- GalleyProvider.getTeamMember (userId user) tid pure $ maybe defUserLegalHoldStatus (^. legalHoldStatus) teamMember -data EmailVisibility' - = EmailVisibleIfOnTeam' - | EmailVisibleIfOnSameTeam' (Maybe (TeamId, TeamMember)) - | EmailVisibleToSelf' - --- | Gets the email if it's visible to the requester according to configured settings -getEmailForProfile :: - User -> - EmailVisibility' -> - Maybe Email -getEmailForProfile profileOwner EmailVisibleIfOnTeam' = - if isJust (userTeam profileOwner) - then userEmail profileOwner - else Nothing -getEmailForProfile profileOwner (EmailVisibleIfOnSameTeam' (Just (viewerTeamId, viewerTeamMember))) = - if Just viewerTeamId == userTeam profileOwner - && Team.hasPermission viewerTeamMember Team.ViewSameTeamEmails - then userEmail profileOwner - else Nothing -getEmailForProfile _ (EmailVisibleIfOnSameTeam' Nothing) = Nothing -getEmailForProfile _ EmailVisibleToSelf' = Nothing - -- | Find user accounts for a given identity, both activated and those -- currently pending activation. lookupAccountsByIdentity :: Either Email Phone -> Bool -> (AppT r) [UserAccount] diff --git a/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs b/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs index 447f8386b41..08759ee2b06 100644 --- a/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs +++ b/services/brig/src/Brig/Effects/GalleyProvider/RPC.hs @@ -33,7 +33,6 @@ import Data.Id import Data.Json.Util (UTCTimeMillis) import Data.Qualified import Data.Range -import Galley.Types.Teams qualified as Team import Imports import Network.HTTP.Client qualified as HTTP import Network.HTTP.Types qualified as HTTP @@ -53,8 +52,7 @@ import Wire.API.Routes.Version import Wire.API.Team import Wire.API.Team.Conversation qualified as Conv import Wire.API.Team.Feature -import Wire.API.Team.Member qualified as Member -import Wire.API.Team.Member qualified as Team +import Wire.API.Team.Member as Member import Wire.API.Team.Role import Wire.API.Team.SearchVisibility import Wire.Rpc @@ -250,7 +248,7 @@ addTeamMember u tid (minvmeta, role) = do 200 -> True _ -> False where - prm = Team.rolePermissions role + prm = Member.rolePermissions role bdy = Member.mkNewTeamMember u prm minvmeta req = method POST @@ -298,7 +296,7 @@ getTeamMember :: ) => UserId -> TeamId -> - Sem r (Maybe Team.TeamMember) + Sem r (Maybe TeamMember) getTeamMember u tid = do debug $ remote "galley" @@ -326,7 +324,7 @@ getTeamMembers :: Member TinyLog r ) => TeamId -> - Sem r Team.TeamMemberList + Sem r TeamMemberList getTeamMembers tid = do debug $ remote "galley" . msg (val "Get team members") galleyRequest req >>= decodeBodyOrThrow "galley" diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 3d21ee2533d..d38e4182677 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -30,7 +30,7 @@ import Brig.User.Auth.Cookie.Limit import Brig.ZAuth qualified as ZAuth import Control.Applicative import Control.Lens qualified as Lens -import Data.Aeson (defaultOptions, fieldLabelModifier, genericParseJSON, withText) +import Data.Aeson (defaultOptions, fieldLabelModifier, genericParseJSON) import Data.Aeson qualified as A import Data.Aeson qualified as Aeson import Data.Aeson.Types (typeMismatch) @@ -363,34 +363,6 @@ instance FromJSON TurnDnsOpts where <$> (asciiOnly =<< o .: "baseDomain") <*> o .:? "discoveryIntervalSeconds" --- | Configurations for whether to show a user's email to others. -data EmailVisibility - = -- | Anyone can see the email of someone who is on ANY team. - -- This may sound strange; but certain on-premise hosters have many different teams - -- and still want them to see each-other's emails. - EmailVisibleIfOnTeam - | -- | Anyone on your team with at least 'Member' privileges can see your email address. - EmailVisibleIfOnSameTeam - | -- | Show your email only to yourself - EmailVisibleToSelf - deriving (Eq, Show, Bounded, Enum) - -instance FromJSON EmailVisibility where - parseJSON = withText "EmailVisibility" $ \case - "visible_if_on_team" -> pure EmailVisibleIfOnTeam - "visible_if_on_same_team" -> pure EmailVisibleIfOnSameTeam - "visible_to_self" -> pure EmailVisibleToSelf - _ -> - fail $ - "unexpected value for EmailVisibility settings: " - <> "expected one of " - <> show (Aeson.encode <$> [(minBound :: EmailVisibility) ..]) - -instance ToJSON EmailVisibility where - toJSON EmailVisibleIfOnTeam = "visible_if_on_team" - toJSON EmailVisibleIfOnSameTeam = "visible_if_on_same_team" - toJSON EmailVisibleToSelf = "visible_to_self" - data ListAllSFTServers = ListAllSFTServers | HideAllSFTServers @@ -532,7 +504,7 @@ data Settings = Settings -- the given provider id setProviderSearchFilter :: !(Maybe ProviderId), -- | Whether to expose user emails and to whom - setEmailVisibility :: !EmailVisibility, + setEmailVisibility :: !EmailVisibilityConfig, setPropertyMaxKeyLen :: !(Maybe Int64), setPropertyMaxValueLen :: !(Maybe Int64), -- | How long, in milliseconds, to wait diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 5f198876d38..db1616d3c67 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -122,7 +122,7 @@ import Wire.API.Team.Feature qualified as Feature import Wire.API.Team.LegalHold (LegalholdProtectee (UnprotectedBot)) import Wire.API.Team.Permission import Wire.API.User hiding (cpNewPassword, cpOldPassword) -import Wire.API.User qualified as Public (UserProfile, publicProfile) +import Wire.API.User qualified as Public (UserProfile, mkUserProfile) import Wire.API.User.Auth import Wire.API.User.Client import Wire.API.User.Client qualified as Public (Client, ClientCapability (ClientSupportsLegalholdImplicitConsent), PubClient (..), UserClientPrekeyMap, UserClients, userClients) @@ -751,7 +751,7 @@ guardConvAdmin conv = do botGetSelf :: BotId -> (Handler r) Public.UserProfile botGetSelf bot = do p <- lift $ wrapClient $ User.lookupUser NoPendingInvitations (botUserId bot) - maybe (throwStd (errorToWai @'E.UserNotFound)) (pure . (`Public.publicProfile` UserLegalHoldNoConsent)) p + maybe (throwStd (errorToWai @'E.UserNotFound)) (\u -> pure $ Public.mkUserProfile EmailVisibleToSelf u UserLegalHoldNoConsent) p botGetClient :: Member GalleyProvider r => BotId -> (Handler r) (Maybe Public.Client) botGetClient bot = do diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index a98512e25d7..cb42bc6f010 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -57,7 +57,6 @@ import Data.List1 qualified as List1 import Data.Qualified (Local) import Data.Range import Data.Time.Clock (UTCTime) -import Galley.Types.Teams qualified as Team import Imports hiding (head) import Network.Wai.Utilities hiding (code, message) import Polysemy @@ -147,7 +146,7 @@ createInvitationPublic :: createInvitationPublic uid tid body = do let inviteeRole = fromMaybe defaultRole . irRole $ body inviter <- do - let inviteePerms = Team.rolePermissions inviteeRole + let inviteePerms = Teams.rolePermissions inviteeRole idt <- maybe (throwStd (errorToWai @'E.NoIdentity)) pure =<< lift (fetchUserIdentity uid) from <- maybe (throwStd (errorToWai @'E.NoEmail)) pure (emailIdentity idt) ensurePermissionToAddUser uid tid inviteePerms diff --git a/services/brig/src/Brig/Team/Util.hs b/services/brig/src/Brig/Team/Util.hs index eeec7fe8d9a..96669f4c9d6 100644 --- a/services/brig/src/Brig/Team/Util.hs +++ b/services/brig/src/Brig/Team/Util.hs @@ -27,7 +27,6 @@ import Control.Error import Control.Lens import Data.Id import Data.Set qualified as Set -import Galley.Types.Teams import Imports import Polysemy (Member) import Wire.API.Team.Member diff --git a/services/brig/src/Brig/User/API/Search.hs b/services/brig/src/Brig/User/API/Search.hs index 44a87a7dd50..14035f1e804 100644 --- a/services/brig/src/Brig/User/API/Search.hs +++ b/services/brig/src/Brig/User/API/Search.hs @@ -45,7 +45,6 @@ import Data.Domain (Domain) import Data.Handle (parseHandle) import Data.Id import Data.Range -import Galley.Types.Teams (HiddenPerm (SearchContacts)) import Imports import Network.Wai.Utilities ((!>>)) import Polysemy @@ -55,6 +54,7 @@ import System.Logger.Class qualified as Log import Wire.API.Federation.API.Brig qualified as FedBrig import Wire.API.Federation.API.Brig qualified as S import Wire.API.Routes.FederationDomainConfig +import Wire.API.Team.Member (HiddenPerm (SearchContacts)) import Wire.API.Team.Permission qualified as Public import Wire.API.Team.SearchVisibility (TeamSearchVisibility (..)) import Wire.API.User.Search diff --git a/services/brig/test/integration/API/Settings.hs b/services/brig/test/integration/API/Settings.hs index d3fb5add1aa..f600817a0e8 100644 --- a/services/brig/test/integration/API/Settings.hs +++ b/services/brig/test/integration/API/Settings.hs @@ -30,15 +30,18 @@ import Data.ByteString.Char8 qualified as C8 import Data.ByteString.Conversion import Data.Id import Data.Set qualified as Set -import Galley.Types.Teams qualified as Team import Imports import Test.Tasty hiding (Timeout) import Test.Tasty.HUnit import Util +import Wire.API.Team.Member (rolePermissions) import Wire.API.Team.Permission import Wire.API.Team.Role import Wire.API.User +allEmailVisibilities :: [EmailVisibilityConfig] +allEmailVisibilities = [EmailVisibleIfOnTeam, EmailVisibleIfOnSameTeam (), EmailVisibleToSelf] + tests :: Opts -> Manager -> Brig -> Galley -> IO TestTree tests defOpts manager brig galley = pure $ do testGroup @@ -47,14 +50,14 @@ tests defOpts manager brig galley = pure $ do "setEmailVisibility" [ testGroup "/users/" - $ ((,) <$> [minBound ..] <*> [minBound ..]) + $ ((,) <$> [minBound ..] <*> allEmailVisibilities) <&> \(viewingUserIs, visibility) -> do testCase (show (viewingUserIs, visibility)) . runHttpT manager $ testUsersEmailVisibleIffExpected defOpts brig galley viewingUserIs visibility, testGroup "/users/:uid" - $ ((,) <$> [minBound ..] <*> [minBound ..]) + $ ((,) <$> [minBound ..] <*> allEmailVisibilities) <&> \(viewingUserIs, visibility) -> do testCase (show (viewingUserIs, visibility)) . runHttpT manager @@ -70,13 +73,13 @@ data ViewedUserIs = SameTeam | DifferentTeam | NoTeam data ViewingUserIs = Creator | Member | Guest deriving (Eq, Show, Enum, Bounded) -expectEmailVisible :: Opt.EmailVisibility -> ViewingUserIs -> ViewedUserIs -> Bool -expectEmailVisible Opt.EmailVisibleIfOnTeam = \case +expectEmailVisible :: EmailVisibilityConfig -> ViewingUserIs -> ViewedUserIs -> Bool +expectEmailVisible EmailVisibleIfOnTeam = \case _ -> \case SameTeam -> True DifferentTeam -> True NoTeam -> False -expectEmailVisible Opt.EmailVisibleIfOnSameTeam = \case +expectEmailVisible (EmailVisibleIfOnSameTeam _) = \case Creator -> \case SameTeam -> True DifferentTeam -> False @@ -89,7 +92,7 @@ expectEmailVisible Opt.EmailVisibleIfOnSameTeam = \case SameTeam -> False DifferentTeam -> False NoTeam -> False -expectEmailVisible Opt.EmailVisibleToSelf = \case +expectEmailVisible EmailVisibleToSelf = \case _ -> \case SameTeam -> False DifferentTeam -> False @@ -98,7 +101,7 @@ expectEmailVisible Opt.EmailVisibleToSelf = \case jsonField :: FromJSON a => Key -> Value -> Maybe a jsonField f u = u ^? key f >>= maybeFromJSON -testUsersEmailVisibleIffExpected :: Opts -> Brig -> Galley -> ViewingUserIs -> Opt.EmailVisibility -> Http () +testUsersEmailVisibleIffExpected :: Opts -> Brig -> Galley -> ViewingUserIs -> EmailVisibilityConfig -> Http () testUsersEmailVisibleIffExpected opts brig galley viewingUserIs visibilitySetting = do (viewerId, userA, userB, nonTeamUser) <- setup brig galley viewingUserIs let uids = @@ -131,7 +134,7 @@ testUsersEmailVisibleIffExpected opts brig galley viewingUserIs visibilitySettin where result r = Set.fromList . map (jsonField "id" &&& jsonField "email") <$> responseJsonMaybe r -testGetUserEmailShowsEmailsIffExpected :: Opts -> Brig -> Galley -> ViewingUserIs -> Opt.EmailVisibility -> Http () +testGetUserEmailShowsEmailsIffExpected :: Opts -> Brig -> Galley -> ViewingUserIs -> EmailVisibilityConfig -> Http () testGetUserEmailShowsEmailsIffExpected opts brig galley viewingUserIs visibilitySetting = do (viewerId, userA, userB, nonTeamUser) <- setup brig galley viewingUserIs let expectations :: [(UserId, Maybe Email)] @@ -171,6 +174,6 @@ setup brig galley viewingUserIs = do nonTeamUser <- createUser "joe" brig viewerId <- case viewingUserIs of Creator -> pure creatorId - Member -> userId <$> createTeamMember brig galley creatorId tid (Team.rolePermissions RoleOwner) - Guest -> userId <$> createTeamMember brig galley creatorId tid (Team.rolePermissions RoleExternalPartner) + Member -> userId <$> createTeamMember brig galley creatorId tid (rolePermissions RoleOwner) + Guest -> userId <$> createTeamMember brig galley creatorId tid (rolePermissions RoleExternalPartner) pure (viewerId, userA, userB, nonTeamUser) diff --git a/services/brig/test/integration/API/Team.hs b/services/brig/test/integration/API/Team.hs index 946c4f7e1eb..350fb894d66 100644 --- a/services/brig/test/integration/API/Team.hs +++ b/services/brig/test/integration/API/Team.hs @@ -45,7 +45,6 @@ import Data.Text.Encoding (encodeUtf8) import Data.Time (addUTCTime, getCurrentTime) import Data.UUID qualified as UUID (fromString) import Data.UUID.V4 qualified as UUID -import Galley.Types.Teams qualified as Team import Imports import Network.HTTP.Types qualified as HTTP import Network.Wai qualified as Wai @@ -408,7 +407,7 @@ testInvitationRoles brig galley = do . zConn "c" . paths ["teams", toByteString' tid, "members", toByteString' uid] mem :: TeamMember <- responseJsonError =<< (get memreq creator') <- mkuser True updatePermissions creator tid (creator', fullPermissions) galley -- demote and delete creator, but cannot do it for second owner yet (as someone needs to demote them) - updatePermissions creator' tid (creator, Team.rolePermissions RoleMember) galley + updatePermissions creator' tid (creator, rolePermissions RoleMember) galley deleteUser creator (Just defPassword) brig !!! const 200 === statusCode -- create sso user without email, make an owner Just (userId -> user3) <- mkuser False @@ -969,7 +968,7 @@ testDeleteUserSSO brig galley = do -- can't delete herself, even without email deleteUser user3 (Just defPassword) brig !!! const 403 === statusCode -- delete second owner now, we don't enforce existence of emails in the backend - updatePermissions user3 tid (creator', Team.rolePermissions RoleMember) galley + updatePermissions user3 tid (creator', rolePermissions RoleMember) galley deleteUser creator' (Just defPassword) brig !!! const 200 === statusCode test2FaDisabledForSsoUser :: Brig -> Galley -> Http () diff --git a/services/federator/test/integration/Test/Federator/IngressSpec.hs b/services/federator/test/integration/Test/Federator/IngressSpec.hs index 38c75dbfb68..e8eb7151c7e 100644 --- a/services/federator/test/integration/Test/Federator/IngressSpec.hs +++ b/services/federator/test/integration/Test/Federator/IngressSpec.hs @@ -58,7 +58,7 @@ spec env = do brig <- view teBrig <$> ask user <- randomUser brig - let expectedProfile = publicProfile user UserLegalHoldNoConsent + let expectedProfile = mkUserProfile EmailVisibleToSelf user UserLegalHoldNoConsent runTestSem $ do resp <- liftToCodensity diff --git a/services/federator/test/integration/Test/Federator/InwardSpec.hs b/services/federator/test/integration/Test/Federator/InwardSpec.hs index 3b4cc55bd9b..b75beeb5a0e 100644 --- a/services/federator/test/integration/Test/Federator/InwardSpec.hs +++ b/services/federator/test/integration/Test/Federator/InwardSpec.hs @@ -71,7 +71,7 @@ spec env = brig <- view teBrig <$> ask user <- randomUser brig - let expectedProfile = publicProfile user UserLegalHoldNoConsent + let expectedProfile = mkUserProfile EmailVisibleToSelf user UserLegalHoldNoConsent bdy <- responseJsonError =<< inwardCall "/federation/brig/get-users-by-ids" (encode [userId user]) diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 4088a64b453..4866b41930d 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -575,7 +575,6 @@ executable galley-migrate-data , containers , exceptions , extended - , galley-types , imports , lens , optparse-applicative diff --git a/services/galley/migrate-data/src/V3_BackfillTeamAdmins.hs b/services/galley/migrate-data/src/V3_BackfillTeamAdmins.hs index d0eb1e2a071..6578b4e9631 100644 --- a/services/galley/migrate-data/src/V3_BackfillTeamAdmins.hs +++ b/services/galley/migrate-data/src/V3_BackfillTeamAdmins.hs @@ -23,9 +23,9 @@ import Data.Conduit.Internal (zipSources) import Data.Conduit.List qualified as C import Data.Id import Galley.DataMigration.Types -import Galley.Types.Teams import Imports import System.Logger.Class qualified as Log +import Wire.API.Team.Member import Wire.API.Team.Permission import Wire.API.Team.Role diff --git a/services/galley/src/Galley/API/Action.hs b/services/galley/src/Galley/API/Action.hs index 063b985fd52..46e549a6fc9 100644 --- a/services/galley/src/Galley/API/Action.hs +++ b/services/galley/src/Galley/API/Action.hs @@ -90,7 +90,6 @@ import Galley.Effects.TeamStore qualified as E import Galley.Env (Env) import Galley.Options import Galley.Types.Conversations.Members -import Galley.Types.Teams (IsPerm (hasPermission)) import Galley.Types.UserList import Galley.Validation import Gundeck.Types.Push.V2 qualified as PushV2 diff --git a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs index 5f85b3a1259..023da95ed90 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs @@ -47,7 +47,6 @@ import Galley.Cassandra.LegalHold import Galley.Cassandra.LegalHold qualified as LegalHoldData import Galley.Env qualified as Galley import Galley.Types.Clients qualified as Clients -import Galley.Types.Teams import Imports import Network.HTTP.Types.Status (status200, status404) import Network.Wai as Wai diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index c9cd393dc9c..936548d6363 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -81,7 +81,6 @@ import Galley.Intra.User (chunkify) import Galley.Options qualified as Opts import Galley.Run qualified as Run import Galley.Types.Conversations.One2One -import Galley.Types.Teams qualified as Team import Galley.Types.UserList import Imports import Network.HTTP.Client qualified as HTTP diff --git a/services/spar/default.nix b/services/spar/default.nix index daebe1a84f6..f31e52c6ff1 100644 --- a/services/spar/default.nix +++ b/services/spar/default.nix @@ -23,7 +23,6 @@ , email-validate , exceptions , extended -, galley-types , gitignoreSource , hscim , HsOpenSSL @@ -101,7 +100,6 @@ mkDerivation { crypton-x509 exceptions extended - galley-types hscim hspec http-types @@ -154,7 +152,6 @@ mkDerivation { email-validate exceptions extended - galley-types hscim HsOpenSSL hspec diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index 3b15efd6505..f86140dbb83 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -163,7 +163,6 @@ library , crypton-x509 , exceptions , extended - , galley-types , hscim , hspec , http-types @@ -347,7 +346,6 @@ executable spar-integration , email-validate , exceptions , extended - , galley-types , hscim , HsOpenSSL , hspec diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index 2060030f9e3..81146b5de06 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -56,7 +56,6 @@ import Data.Proxy import Data.Range import qualified Data.Set as Set import Data.Time -import Galley.Types.Teams (HiddenPerm (CreateUpdateDeleteIdp, ReadIdp)) import Imports import Polysemy import Polysemy.Error @@ -101,6 +100,7 @@ import System.Logger (Msg) import qualified URI.ByteString as URI import Wire.API.Routes.Internal.Spar import Wire.API.Routes.Public.Spar +import Wire.API.Team.Member (HiddenPerm (CreateUpdateDeleteIdp, ReadIdp)) import Wire.API.User import Wire.API.User.IdentityProvider import Wire.API.User.Saml diff --git a/services/spar/src/Spar/Intra/BrigApp.hs b/services/spar/src/Spar/Intra/BrigApp.hs index c8f3ddb14ab..9a125f34010 100644 --- a/services/spar/src/Spar/Intra/BrigApp.hs +++ b/services/spar/src/Spar/Intra/BrigApp.hs @@ -52,7 +52,6 @@ import Data.ByteString.Conversion import qualified Data.CaseInsensitive as CI import Data.Handle (Handle (Handle)) import Data.Id (TeamId, UserId) -import Galley.Types.Teams (HiddenPerm (CreateReadDeleteScimToken), IsPerm) import Imports import Polysemy import Polysemy.Error @@ -62,6 +61,7 @@ import Spar.Sem.BrigAccess (BrigAccess) import qualified Spar.Sem.BrigAccess as BrigAccess import Spar.Sem.GalleyAccess (GalleyAccess) import qualified Spar.Sem.GalleyAccess as GalleyAccess +import Wire.API.Team.Member (HiddenPerm (CreateReadDeleteScimToken), IsPerm) import Wire.API.User import Wire.API.User.Scim (ValidExternalId (..), runValidExternalIdEither) diff --git a/services/spar/src/Spar/Intra/Galley.hs b/services/spar/src/Spar/Intra/Galley.hs index 96fd70920b0..663de17f109 100644 --- a/services/spar/src/Spar/Intra/Galley.hs +++ b/services/spar/src/Spar/Intra/Galley.hs @@ -26,7 +26,6 @@ import Control.Lens import Control.Monad.Except import Data.ByteString.Conversion import Data.Id (TeamId, UserId) -import Galley.Types.Teams import Imports import Network.HTTP.Types.Method import Spar.Error diff --git a/services/spar/src/Spar/Scim/User.hs b/services/spar/src/Spar/Scim/User.hs index c8792a6e977..989f0d76603 100644 --- a/services/spar/src/Spar/Scim/User.hs +++ b/services/spar/src/Spar/Scim/User.hs @@ -57,7 +57,6 @@ import Data.Id (Id (..), TeamId, UserId, idToText) import Data.Json.Util (UTCTimeMillis, fromUTCTimeMillis, toUTCTimeMillis) import qualified Data.Text as Text import qualified Data.UUID as UUID -import qualified Galley.Types.Teams as Galley import Imports import Network.URI (URI, parseURI) import Polysemy @@ -991,7 +990,7 @@ synthesizeStoredUser usr veid = where getRole :: Sem r Role getRole = do - let tmRoleOrDefault m = fromMaybe defaultRole $ m >>= \member -> member ^. Member.permissions . to Galley.permissionsRole + let tmRoleOrDefault m = fromMaybe defaultRole $ m >>= \member -> member ^. Member.permissions . to Member.permissionsRole maybe (pure defaultRole) (\tid -> tmRoleOrDefault <$> GalleyAccess.getTeamMember tid (userId $ accountUser usr)) (userTeam $ accountUser usr) synthesizeStoredUser' :: diff --git a/services/spar/src/Spar/Sem/GalleyAccess.hs b/services/spar/src/Spar/Sem/GalleyAccess.hs index 47d8d6c159e..36f993df871 100644 --- a/services/spar/src/Spar/Sem/GalleyAccess.hs +++ b/services/spar/src/Spar/Sem/GalleyAccess.hs @@ -29,7 +29,6 @@ module Spar.Sem.GalleyAccess where import Data.Id (TeamId, UserId) -import Galley.Types.Teams (IsPerm) import Imports import Polysemy import Wire.API.Team.Member diff --git a/services/spar/test-integration/Test/Spar/APISpec.hs b/services/spar/test-integration/Test/Spar/APISpec.hs index a0f2ba1b706..2e4f8aa0b4c 100644 --- a/services/spar/test-integration/Test/Spar/APISpec.hs +++ b/services/spar/test-integration/Test/Spar/APISpec.hs @@ -44,7 +44,6 @@ import qualified Data.UUID as UUID hiding (fromByteString, null) import qualified Data.UUID.V4 as UUID (nextRandom) import qualified Data.Vector as Vec import qualified Data.ZAuth.Token as ZAuth -import qualified Galley.Types.Teams as Galley import Imports hiding (head) import Network.HTTP.Types (status200, status202) import SAML2.WebSSO @@ -90,7 +89,7 @@ import qualified Web.Scim.Class.User as Scim import qualified Web.Scim.Schema.Common as Scim import qualified Web.Scim.Schema.Meta as Scim import qualified Web.Scim.Schema.User as Scim -import Wire.API.Team.Member (newTeamMemberDeleteData) +import Wire.API.Team.Member (newTeamMemberDeleteData, rolePermissions) import Wire.API.Team.Permission hiding (self) import Wire.API.Team.Role import Wire.API.User @@ -657,7 +656,7 @@ specCRUDIdentityProvider = do (_, tid, (^. idpId) -> idpid) <- registerTestIdP let mkUser :: Role -> TestSpar UserId mkUser role = do - let perms = Galley.rolePermissions role + let perms = rolePermissions role call $ createTeamMember (env ^. teBrig) (env ^. teGalley) tid perms admin <- mkUser RoleAdmin member <- mkUser RoleMember @@ -1607,7 +1606,7 @@ specSparUserMigration = do let issuer = idp ^. SAML.idpMetadata . SAML.edIssuer pure (issuer, subj) - memberUid <- call $ createTeamMember (env ^. teBrig) (env ^. teGalley) tid (Galley.rolePermissions RoleMember) + memberUid <- call $ createTeamMember (env ^. teBrig) (env ^. teGalley) tid (rolePermissions RoleMember) do -- insert to legacy tale diff --git a/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs b/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs index f9e54ce015e..b087f03e158 100644 --- a/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs @@ -40,7 +40,6 @@ import Data.Range (unsafeRange) import Data.Text.Ascii (AsciiChars (validate)) import Data.Time (UTCTime) import Data.Time.Clock (getCurrentTime) -import qualified Galley.Types.Teams as Galley import Imports import OpenSSL.Random (randBytes) import qualified SAML2.WebSSO as SAML @@ -50,6 +49,7 @@ import Text.RawString.QQ (r) import Util import Wire.API.Team.Feature (featureNameBS) import qualified Wire.API.Team.Feature as Public +import Wire.API.Team.Member (rolePermissions) import Wire.API.Team.Role import Wire.API.User (userEmail) import qualified Wire.API.User as Public @@ -233,7 +233,7 @@ testCreateTokenAuthorizesOnlyAdmins = do (env ^. teBrig) (env ^. teGalley) teamId - (Galley.rolePermissions role) + (rolePermissions role) let createToken' uid = createToken_ diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index c8c2d042db1..16c48a576f1 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -168,8 +168,6 @@ import Data.UUID as UUID hiding (fromByteString, null) import Data.UUID.V4 as UUID (nextRandom) import qualified Data.Yaml as Yaml import GHC.TypeLits -import Galley.Types.Teams (rolePermissions) -import qualified Galley.Types.Teams as Teams import Imports hiding (head) import Network.HTTP.Client.MultipartFormData import qualified Network.Wai.Handler.Warp as Warp @@ -207,7 +205,7 @@ import Wire.API.Team (Icon (..)) import qualified Wire.API.Team as Galley import Wire.API.Team.Feature (FeatureStatus (..), FeatureTTL' (..), FeatureTrivialConfig (trivialConfig), SSOConfig, WithStatusNoLock (WithStatusNoLock)) import qualified Wire.API.Team.Invitation as TeamInvitation -import Wire.API.Team.Member (NewTeamMember, TeamMemberList) +import Wire.API.Team.Member (NewTeamMember, TeamMemberList, rolePermissions) import qualified Wire.API.Team.Member as Member import qualified Wire.API.Team.Member as Team import Wire.API.Team.Permission @@ -1374,7 +1372,7 @@ checkChangeRoleOfTeamMember :: TeamId -> UserId -> UserId -> TestSpar () checkChangeRoleOfTeamMember tid adminId targetId = forM_ [minBound ..] $ \role -> do updateTeamMemberRole tid adminId targetId role [member'] <- filter ((== targetId) . (^. Member.userId)) <$> getTeamMembers adminId tid - liftIO $ (member' ^. Member.permissions . to Teams.permissionsRole) `shouldBe` Just role + liftIO $ (member' ^. Member.permissions . to Member.permissionsRole) `shouldBe` Just role eventually :: HasCallStack => TestSpar a -> TestSpar a eventually = recoverAll (limitRetries 3 <> exponentialBackoff 100000) . const diff --git a/services/spar/test-integration/Util/Scim.hs b/services/spar/test-integration/Util/Scim.hs index ad79ef9d74e..9b2ab331a12 100644 --- a/services/spar/test-integration/Util/Scim.hs +++ b/services/spar/test-integration/Util/Scim.hs @@ -34,7 +34,6 @@ import qualified Data.Text.Lazy as Lazy import Data.Time import Data.UUID as UUID import Data.UUID.V4 as UUID -import qualified Galley.Types.Teams as Teams import Imports import qualified Network.Wai.Utilities as Error import Polysemy.Error (runError) @@ -747,4 +746,4 @@ getDefaultUserLocale = do checkTeamMembersRole :: HasCallStack => TeamId -> UserId -> UserId -> Role -> TestSpar () checkTeamMembersRole tid owner uid role = do [member] <- filter ((== uid) . (^. Member.userId)) <$> getTeamMembers owner tid - liftIO $ (member ^. Member.permissions . to Teams.permissionsRole) `shouldBe` Just role + liftIO $ (member ^. Member.permissions . to Member.permissionsRole) `shouldBe` Just role diff --git a/tools/stern/default.nix b/tools/stern/default.nix index 4359d6841b3..db33922fa3b 100644 --- a/tools/stern/default.nix +++ b/tools/stern/default.nix @@ -15,7 +15,6 @@ , exceptions , extended , extra -, galley-types , gitignoreSource , HsOpenSSL , http-client @@ -70,7 +69,6 @@ mkDerivation { errors exceptions extended - galley-types http-client http-types imports diff --git a/tools/stern/src/Stern/Types.hs b/tools/stern/src/Stern/Types.hs index f8ed807492a..24dde504caa 100644 --- a/tools/stern/src/Stern/Types.hs +++ b/tools/stern/src/Stern/Types.hs @@ -34,7 +34,6 @@ import Data.OpenApi qualified as Swagger import Data.Proxy import Data.Range import Data.Schema qualified as S -import Galley.Types.Teams import Imports import Servant.API import Wire.API.Properties diff --git a/tools/stern/stern.cabal b/tools/stern/stern.cabal index a05d95b93d3..479e9a07aef 100644 --- a/tools/stern/stern.cabal +++ b/tools/stern/stern.cabal @@ -83,7 +83,6 @@ library , errors >=1.4 , exceptions >=0.6 , extended - , galley-types >=0.81.0 , http-client >=0.7 , http-types >=0.8 , imports From 5e8dc5c4f3c2eb322826c3038be04ba7ff0dfc59 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Thu, 4 Apr 2024 09:20:57 +0200 Subject: [PATCH 071/117] Revert "integration: Fail with logs when a service times out to come up (#3929)" (#3979) This reverts commit 1f9f16455fa457a275332ef85c7104275d3f196d. --- .../wpb6985-better-integration-test-logs | 1 - integration/default.nix | 4 - integration/integration.cabal | 3 - integration/test/Test/Cargohold/API.hs | 2 +- integration/test/Test/Demo.hs | 40 ----- integration/test/Testlib/ModService.hs | 161 ++++++++++++------ .../Testlib/ModService/ServiceInstance.hs | 159 ----------------- integration/test/Testlib/Run.hs | 4 - 8 files changed, 106 insertions(+), 268 deletions(-) delete mode 100644 changelog.d/5-internal/wpb6985-better-integration-test-logs delete mode 100644 integration/test/Testlib/ModService/ServiceInstance.hs diff --git a/changelog.d/5-internal/wpb6985-better-integration-test-logs b/changelog.d/5-internal/wpb6985-better-integration-test-logs deleted file mode 100644 index 05f85f68860..00000000000 --- a/changelog.d/5-internal/wpb6985-better-integration-test-logs +++ /dev/null @@ -1 +0,0 @@ -integration: Fail with logs when a service times out to come up \ No newline at end of file diff --git a/integration/default.nix b/integration/default.nix index e076020df31..a259708844e 100644 --- a/integration/default.nix +++ b/integration/default.nix @@ -38,7 +38,6 @@ , lens , lens-aeson , lib -, lifted-base , memory , mime , monad-control @@ -63,7 +62,6 @@ , temporary , text , time -, timestats , transformers , transformers-base , unix @@ -125,7 +123,6 @@ mkDerivation { kan-extensions lens lens-aeson - lifted-base memory mime monad-control @@ -150,7 +147,6 @@ mkDerivation { temporary text time - timestats transformers transformers-base unix diff --git a/integration/integration.cabal b/integration/integration.cabal index e77d918f5c9..bcafb9ff147 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -152,7 +152,6 @@ library Testlib.Mock Testlib.MockIntegrationService Testlib.ModService - Testlib.ModService.ServiceInstance Testlib.One2One Testlib.Options Testlib.Ports @@ -198,7 +197,6 @@ library , kan-extensions , lens , lens-aeson - , lifted-base , memory , mime , monad-control @@ -223,7 +221,6 @@ library , temporary , text , time - , timestats , transformers , transformers-base , unix diff --git a/integration/test/Test/Cargohold/API.hs b/integration/test/Test/Cargohold/API.hs index 33e5893199c..25f3c4956d9 100644 --- a/integration/test/Test/Cargohold/API.hs +++ b/integration/test/Test/Cargohold/API.hs @@ -236,7 +236,7 @@ testDownloadURLOverride = do downloadURLRes.status `shouldMatchInt` 302 cs @_ @String downloadURLRes.body `shouldMatch` "" downloadURL <- parseUrlThrow (C8.unpack (getHeader' (mk $ cs "Location") downloadURLRes)) - cs @_ @String (HTTP.host downloadURL) `shouldMatch` downloadEndpoint + downloadEndpoint `shouldMatch` cs @_ @String (HTTP.host downloadURL) HTTP.port downloadURL `shouldMatchInt` 443 True `shouldMatch` (HTTP.secure downloadURL) diff --git a/integration/test/Test/Demo.hs b/integration/test/Test/Demo.hs index 95c12dd3cd1..824af5a7d2c 100644 --- a/integration/test/Test/Demo.hs +++ b/integration/test/Test/Demo.hs @@ -7,15 +7,10 @@ import qualified API.Brig as BrigP import qualified API.BrigInternal as BrigI import qualified API.GalleyInternal as GalleyI import qualified API.Nginz as Nginz -import Control.Concurrent import Control.Monad.Cont -import Data.Function -import Data.Maybe import GHC.Stack import SetupHelpers -import Testlib.ModService.ServiceInstance import Testlib.Prelude -import UnliftIO.Directory -- | Deleting unknown clients should fail with 404. testDeleteUnknownClient :: HasCallStack => App () @@ -212,38 +207,3 @@ testFedV0Federation = do bob' <- BrigP.getUser alice bob >>= getJSON 200 bob' %. "qualified_id" `shouldMatch` (bob %. "qualified_id") - -testServiceHandles :: App () -testServiceHandles = do - -- The name was generated with a roll of a fair dice - let exe = "/tmp/tmp-42956614-e50a-11ee-8c4b-6b596d54b36b" - execName = "test-exec" - dom = "test-domain" - - writeFile - exe - "#!/usr/bin/env bash\n\ - \echo errmsg >&2\n\ - \for i in `seq 0 100`; do\n\ - \ echo $i\n\ - \ sleep 0.1\n\ - \done\n" - perms <- getPermissions exe - setPermissions exe (setOwnerExecutable True perms) - serviceInstance <- liftIO $ startServiceInstance exe [] Nothing exe execName dom - liftIO $ threadDelay 1_000_000 - cleanupServiceInstance serviceInstance - processState <- liftIO $ flushServiceInstanceOutput serviceInstance - processState - `shouldContainString` "=== stdout: ============================================\n\ - \[test-exec@test-domain] 0\n\ - \[test-exec@test-domain] 1\n\ - \[test-exec@test-domain] 2\n\ - \[test-exec@test-domain] 3\n\ - \[test-exec@test-domain] 4\n\ - \[test-exec@test-domain] 5\n\ - \[test-exec@test-domain] 6\n\ - \[test-exec@test-domain] 7\n" - processState - `shouldContainString` "=== stderr: ============================================\n\ - \[test-exec@test-domain] errmsg\n" diff --git a/integration/test/Testlib/ModService.hs b/integration/test/Testlib/ModService.hs index 845b7ba2bd0..f4390d7286f 100644 --- a/integration/test/Testlib/ModService.hs +++ b/integration/test/Testlib/ModService.hs @@ -29,25 +29,25 @@ import qualified Data.Text as Text import qualified Data.Text.IO as Text import Data.Traversable import qualified Data.Yaml as Yaml -import Debug.TimeStats (measureM) import GHC.Stack import qualified Network.HTTP.Client as HTTP -import System.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExist, doesFileExist, listDirectory, removeFile) +import System.Directory (copyFile, createDirectoryIfMissing, doesDirectoryExist, doesFileExist, listDirectory, removeDirectoryRecursive, removeFile) import System.FilePath import System.IO import System.IO.Temp (createTempDirectory, writeTempFile) +import System.Posix (keyboardSignal, killProcess, signalProcess) +import System.Process import Testlib.App import Testlib.HTTP import Testlib.JSON -import Testlib.ModService.ServiceInstance +import Testlib.Printing import Testlib.ResourcePool import Testlib.Types import Text.RawString.QQ -import qualified UnliftIO import Prelude withModifiedBackend :: HasCallStack => ServiceOverrides -> (HasCallStack => String -> App a) -> App a -withModifiedBackend overrides k = measureM "withModifiedBackend" $ do +withModifiedBackend overrides k = startDynamicBackends [overrides] (\domains -> k (head domains)) copyDirectoryRecursively :: FilePath -> FilePath -> IO () @@ -118,7 +118,7 @@ traverseConcurrentlyCodensity f args = do pure result startDynamicBackends :: [ServiceOverrides] -> ([String] -> App a) -> App a -startDynamicBackends beOverrides k = measureM "startDynamicBackends" do +startDynamicBackends beOverrides k = runCodensity do when (Prelude.length beOverrides > 3) $ lift $ failApp "Too many backends. Currently only 3 are supported." @@ -203,22 +203,17 @@ startDynamicBackend resource beOverrides = do setLogLevel :: ServiceOverrides setLogLevel = def - { -- NOTE: if you want to set logLevel to "Debug", consider doing it for the service - -- you're interested in only. it's *very* noisy! - sparCfg = setField "saml.logLevel" logLevel, - brigCfg = setField "logLevel" logLevel, - cannonCfg = setField "logLevel" logLevel, - cargoholdCfg = setField "logLevel" logLevel, - galleyCfg = setField "logLevel" logLevel, - gundeckCfg = setField "logLevel" logLevel, - nginzCfg = setField "logLevel" logLevel, - backgroundWorkerCfg = setField "logLevel" logLevel, - sternCfg = setField "logLevel" logLevel, - federatorInternalCfg = setField "logLevel" logLevel + { sparCfg = setField "saml.logLevel" ("Warn" :: String), + brigCfg = setField "logLevel" ("Warn" :: String), + cannonCfg = setField "logLevel" ("Warn" :: String), + cargoholdCfg = setField "logLevel" ("Warn" :: String), + galleyCfg = setField "logLevel" ("Warn" :: String), + gundeckCfg = setField "logLevel" ("Warn" :: String), + nginzCfg = setField "logLevel" ("Warn" :: String), + backgroundWorkerCfg = setField "logLevel" ("Warn" :: String), + sternCfg = setField "logLevel" ("Warn" :: String), + federatorInternalCfg = setField "logLevel" ("Warn" :: String) } - where - logLevel :: String - logLevel = "Warn" updateServiceMapInConfig :: BackendResource -> Service -> Value -> App Value updateServiceMapInConfig resource forSrv config = @@ -251,12 +246,12 @@ startBackend :: BackendResource -> ServiceOverrides -> Codensity App () -startBackend resource overrides = measureM "startBackend" do +startBackend resource overrides = do traverseConcurrentlyCodensity (withProcess resource overrides) allServices lift $ ensureBackendReachable resource.berDomain ensureBackendReachable :: String -> App () -ensureBackendReachable domain = measureM "ensureBackendReachable" do +ensureBackendReachable domain = do env <- ask let checkServiceIsUpReq = do req <- @@ -285,21 +280,54 @@ ensureBackendReachable domain = measureM "ensureBackendReachable" do pure $ either (\(_e :: HTTP.HttpException) -> False) id eith when ((domain /= env.domain1) && (domain /= env.domain2)) $ do - retryRequestUntil checkServiceIsUpReq "Federator ingress" domain Nothing + retryRequestUntil checkServiceIsUpReq "Federator ingress" + +processColors :: [(String, String -> String)] +processColors = + [ ("brig", colored green), + ("galley", colored yellow), + ("gundeck", colored blue), + ("cannon", colored orange), + ("cargohold", colored purpleish), + ("spar", colored orange), + ("federator", colored blue), + ("background-worker", colored blue), + ("nginx", colored purpleish) + ] + +data ServiceInstance = ServiceInstance + { handle :: ProcessHandle, + config :: FilePath + } + +timeout :: Int -> IO a -> IO (Maybe a) +timeout usecs action = either (const Nothing) Just <$> race (threadDelay usecs) action + +cleanupService :: ServiceInstance -> IO () +cleanupService inst = do + let ignoreExceptions action = E.catch action $ \(_ :: E.SomeException) -> pure () + ignoreExceptions $ do + mPid <- getPid inst.handle + for_ mPid (signalProcess keyboardSignal) + timeout 50000 (waitForProcess inst.handle) >>= \case + Just _ -> pure () + Nothing -> do + for_ mPid (signalProcess killProcess) + void $ waitForProcess inst.handle + whenM (doesFileExist inst.config) $ removeFile inst.config + whenM (doesDirectoryExist inst.config) $ removeDirectoryRecursive inst.config -- | Wait for a service to come up. -waitUntilServiceIsUp :: String -> Service -> ServiceInstance -> App () -waitUntilServiceIsUp domain srv serviceInstance = measureM "waitUntilServiceUp" do +waitUntilServiceIsUp :: String -> Service -> App () +waitUntilServiceIsUp domain srv = retryRequestUntil (checkServiceIsUp domain srv) (show srv) - domain - (Just serviceInstance) -- | Check if a service is up and running. checkServiceIsUp :: String -> Service -> App Bool checkServiceIsUp _ Nginz = pure True -checkServiceIsUp domain srv = measureM "checkServiceIsUp" do +checkServiceIsUp domain srv = do req <- baseRequest domain srv Unversioned "/i/status" checkStatus <- appToIO $ do res <- submit "GET" req @@ -325,34 +353,44 @@ withProcess resource overrides service = do startNginzLocalIO <- lift $ appToIO $ startNginzLocal resource - let initProcess = liftIO $ case (service, cwd) of + let initProcess = case (service, cwd) of (Nginz, Nothing) -> startNginzK8s domain sm (Nginz, Just _) -> startNginzLocalIO _ -> do config <- getConfig tempFile <- writeTempFile "/tmp" (execName <> "-" <> domain <> "-" <> ".yaml") (cs $ Yaml.encode config) - startServiceInstance exe ["-c", tempFile] cwd tempFile execName domain + (_, Just stdoutHdl, Just stderrHdl, ph) <- createProcess (proc exe ["-c", tempFile]) {cwd = cwd, std_out = CreatePipe, std_err = CreatePipe} + let prefix = "[" <> execName <> "@" <> domain <> "] " + let colorize = fromMaybe id (lookup execName processColors) + void $ forkIO $ logToConsole colorize prefix stdoutHdl + void $ forkIO $ logToConsole colorize prefix stderrHdl + pure $ ServiceInstance ph tempFile void $ Codensity $ \k -> do - UnliftIO.bracket initProcess cleanupServiceInstance $ \serviceInstance -> do - waitUntilServiceIsUp domain service serviceInstance - k serviceInstance - -retryRequestUntil :: HasCallStack => App Bool -> String -> String -> Maybe ServiceInstance -> App () -retryRequestUntil reqAction execName domain mServiceInstance = measureM "retryRequestUntil" do + iok <- appToIOKleisli k + liftIO $ E.bracket initProcess cleanupService iok + + lift $ waitUntilServiceIsUp domain service + +logToConsole :: (String -> String) -> String -> Handle -> IO () +logToConsole colorize prefix hdl = do + let go = + do + line <- hGetLine hdl + putStrLn (colorize (prefix <> line)) + go + `E.catch` (\(_ :: E.IOException) -> pure ()) + go + +retryRequestUntil :: HasCallStack => App Bool -> String -> App () +retryRequestUntil reqAction err = do isUp <- retrying (limitRetriesByCumulativeDelay (4 * 1000 * 1000) (fibonacciBackoff (200 * 1000))) (\_ isUp -> pure (not isUp)) (const reqAction) - unless isUp $ do - errDetails <- liftIO $ do - case mServiceInstance of - Nothing -> pure "" - Just serviceInstance -> do - outStr <- flushServiceInstanceOutput serviceInstance - pure $ unlines [":", outStr] - failApp ("Timed out waiting for service " <> execName <> "@" <> domain <> " to come up" <> errDetails) + unless isUp $ + failApp ("Timed out waiting for service " <> err <> " to come up") startNginzK8s :: String -> ServiceMap -> IO ServiceInstance startNginzK8s domain sm = do @@ -374,7 +412,8 @@ startNginzK8s domain sm = do & Text.replace ("/etc/wire/nginz/upstreams/upstreams.conf") (cs upstreamsCfg) ) createUpstreamsCfg upstreamsCfg sm - startNginz domain nginxConfFile tmpDir + ph <- startNginz domain nginxConfFile "/" + pure $ ServiceInstance ph tmpDir startNginzLocal :: BackendResource -> App ServiceInstance startNginzLocal resource = do @@ -447,7 +486,10 @@ server 127.0.0.1:{port} max_fails=3 weight=1; writeFile pidConfigFile (cs $ "pid " <> pid <> ";") -- start service - liftIO $ startNginz domain nginxConfFile tmpDir + ph <- liftIO $ startNginz domain nginxConfFile tmpDir + + -- return handle and nginx tmp dir path + pure $ ServiceInstance ph tmpDir createUpstreamsCfg :: String -> ServiceMap -> IO () createUpstreamsCfg upstreamsCfg sm = do @@ -478,12 +520,19 @@ server 127.0.0.1:{port} max_fails=3 weight=1; & Text.replace "{port}" (cs $ show p) liftIO $ appendFile upstreamsCfg (cs upstream) -startNginz :: String -> FilePath -> FilePath -> IO ServiceInstance -startNginz domain conf configDir = do - startServiceInstance - "nginx" - ["-c", conf, "-g", "daemon off;", "-e", "/dev/stdout"] - (Just configDir) - configDir - "nginz" - domain +startNginz :: String -> FilePath -> FilePath -> IO ProcessHandle +startNginz domain conf workingDir = do + (_, Just stdoutHdl, Just stderrHdl, ph) <- + createProcess + (proc "nginx" ["-c", conf, "-g", "daemon off;", "-e", "/dev/stdout"]) + { cwd = Just workingDir, + std_out = CreatePipe, + std_err = CreatePipe + } + + let prefix = "[" <> "nginz" <> "@" <> domain <> "] " + let colorize = fromMaybe id (lookup "nginx" processColors) + void $ forkIO $ logToConsole colorize prefix stdoutHdl + void $ forkIO $ logToConsole colorize prefix stderrHdl + + pure ph diff --git a/integration/test/Testlib/ModService/ServiceInstance.hs b/integration/test/Testlib/ModService/ServiceInstance.hs deleted file mode 100644 index efc4389fff0..00000000000 --- a/integration/test/Testlib/ModService/ServiceInstance.hs +++ /dev/null @@ -1,159 +0,0 @@ -module Testlib.ModService.ServiceInstance - ( ServiceInstance, - startServiceInstance, - cleanupServiceInstance, - flushServiceInstanceOutput, - ) -where - -import Control.Concurrent -import qualified Control.Exception as E -import Control.Monad.Extra -import Control.Monad.IO.Class -import Data.Foldable -import Data.Function -import Data.Functor -import Data.Maybe -import Data.Monoid -import Data.String -import Debug.TimeStats -import System.Directory -import System.IO -import qualified System.IO.Error as E -import System.Posix -import System.Process -import Testlib.Printing -import Testlib.Types -import Prelude - -data ServiceInstance = ServiceInstance - { name :: String, - domain :: String, - processHandle :: ProcessHandle, - stdoutChan :: Chan LineOrEOF, - stderrChan :: Chan LineOrEOF, - cleanupPath :: FilePath - } - -startServiceInstance :: FilePath -> [String] -> Maybe FilePath -> FilePath -> String -> String -> IO ServiceInstance -startServiceInstance exe args workingDir pathToCleanup execName execDomain = measureM "startServiceInstance" do - (_, Just stdoutHdl, Just stderrHdl, ph) <- - createProcess - (proc exe args) - { cwd = workingDir, - std_out = CreatePipe, - std_err = CreatePipe - } - (out1, out2) <- mkChans stdoutHdl - (err1, err2) <- mkChans stderrHdl - void $ forkIO $ logChanToConsole execName execDomain out1 - void $ forkIO $ logChanToConsole execName execDomain err1 - pure $ - ServiceInstance - { name = execName, - domain = execDomain, - processHandle = ph, - stdoutChan = out2, - stderrChan = err2, - cleanupPath = pathToCleanup - } - -cleanupServiceInstance :: ServiceInstance -> App () -cleanupServiceInstance inst = measureM "cleanupService" . liftIO $ do - let ignoreExceptions action = E.catch action $ \(_ :: E.SomeException) -> pure () - ignoreExceptions $ do - mPid <- getPid inst.processHandle - for_ mPid (signalProcess killProcess) - void $ waitForProcess inst.processHandle - whenM (doesFileExist inst.cleanupPath) $ removeFile inst.cleanupPath - whenM (doesDirectoryExist inst.cleanupPath) $ removeDirectoryRecursive inst.cleanupPath - -flushServiceInstanceOutput :: ServiceInstance -> IO String -flushServiceInstanceOutput serviceInstance = measureM "flushProcessState" do - outStr <- flushChan serviceInstance.name serviceInstance.domain serviceInstance.stdoutChan - errStr <- flushChan serviceInstance.name serviceInstance.domain serviceInstance.stderrChan - statusStr <- getPid serviceInstance.processHandle <&> maybe "(already closed)" show - pure $ - unlines - [ "=== process pid: =======================================", - statusStr, - "\n\n=== stdout: ============================================", - outStr, - "\n\n=== stderr: ============================================", - errStr - ] - -data LineOrEOF = Line String | EOF - deriving (Eq, Show) - -logChanToConsole :: String -> String -> Chan LineOrEOF -> IO () -logChanToConsole execName domain chan = go - where - go = - readChan chan >>= \case - Line line -> do - putStrLn (decorateLine execName domain line) - go - EOF -> pure () - --- | Read everything from a channel and return it as a decorated multi-line String. -flushChan :: String -> String -> Chan LineOrEOF -> IO String -flushChan execName domain chan = measureM "flushChan" do - let go lns = - readChan chan >>= \case - Line ln -> go (ln : lns) - EOF -> pure (reverse lns) - (unlines . fmap (decorateLine execName domain)) <$> go [] - --- | Run a thread that feeds output from a 'Handle' into two channels. --- --- (We could also duplicate the posic handle, not the chan. might save a few LOC.) -mkChans :: Handle -> IO (Chan LineOrEOF, Chan LineOrEOF) -mkChans hdl = do - chn1 <- newChan - chn2 <- dupChan chn1 - let go = do - packet <- catchEOF (hGetLine hdl) - writeList2Chan chn1 packet - unless (EOF `elem` packet) go - void $ forkIO go - pure (chn1, chn2) - --- | If 'SomeException' is thrown, show it, split up in lines, and feed it to the output --- followed be '[EOF]'. (But if the exception is 'EOF', do not add it to the output.) -catchEOF :: IO String -> IO [LineOrEOF] -catchEOF feed = - (((: []) . Line) <$> feed) - `E.catch` handleEOF - `E.catch` handleEverythingElse - where - handleEOF :: E.IOException -> IO [LineOrEOF] - handleEOF e = - if E.isEOFError e - then pure [EOF] - else renderErr e - - handleEverythingElse :: E.SomeException -> IO [LineOrEOF] - handleEverythingElse e = renderErr e - - renderErr :: E.Exception e => e -> IO [LineOrEOF] - renderErr e = pure $ (Line <$> lines (show e)) <> [EOF] - -decorateLine :: String -> String -> String -> String -decorateLine execName domain = colorize . (prefix <>) - where - prefix = "[" <> execName <> "@" <> domain <> "] " - colorize = fromMaybe id (lookup execName processColors) - -processColors :: [(String, String -> String)] -processColors = - [ ("brig", colored green), - ("galley", colored yellow), - ("gundeck", colored blue), - ("cannon", colored orange), - ("cargohold", colored purpleish), - ("spar", colored orange), - ("federator", colored blue), - ("background-worker", colored blue), - ("nginx", colored purpleish) - ] diff --git a/integration/test/Testlib/Run.hs b/integration/test/Testlib/Run.hs index 454ff0a9e8b..0a50c6429a1 100644 --- a/integration/test/Testlib/Run.hs +++ b/integration/test/Testlib/Run.hs @@ -16,7 +16,6 @@ import Data.Functor import Data.List import Data.PEM import Data.Time.Clock -import Debug.TimeStats (printTimeStats) import RunAllTests import System.Directory import System.Environment @@ -108,9 +107,6 @@ main = do if opts.listTests then doListTests tests else runTests tests opts.xmlReport cfg - putStrLn "output from timestats library: (use `DEBUG_TIMESTATS_ENABLE=1` to enable)" - printTimeStats - createGlobalEnv :: FilePath -> Codensity IO GlobalEnv createGlobalEnv cfg = do genv0 <- mkGlobalEnv cfg From 6e2d54f12e14f4e8c9d047bee66a95e07f6bf231 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Thu, 4 Apr 2024 09:51:06 +0200 Subject: [PATCH 072/117] Update integration docs (#3980) --- integration/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/integration/README.md b/integration/README.md index b725474d691..3a501089e2c 100644 --- a/integration/README.md +++ b/integration/README.md @@ -3,7 +3,8 @@ To develop a new test in a fast-loading ghci session: 1. Run `make cr` to build the whole project and start all services OR run `make cr package=galley` to build galley and start all services OR run `./dist/run-services` to just start all services without rebuilding -2.`TEST_INCLUDE=testFederationDomain make devtest` to start a ghcid session that re-runs the test after each successful build of the test suite +2. `TEST_INCLUDE=testFederationDomain,testFederationFoo make devtest` to start a ghcid session that re-runs the test after each successful build of the test suite. + This should provide faster feedback loops when you are only developing tests, e.g. when migrating tests. Note that `devtest` doesn't spawn static backends, so you need to run `make cr` prior in a separate terminal. Original design guidelines / goals: From 8338956f7edab77543157a00c1ec5cc75b31656b Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Thu, 4 Apr 2024 16:20:57 +0200 Subject: [PATCH 073/117] remove geoDb option from Brig (#3975) --- changelog.d/0-release-notes/geodb | 1 + services/brig/src/Brig/Options.hs | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 changelog.d/0-release-notes/geodb diff --git a/changelog.d/0-release-notes/geodb b/changelog.d/0-release-notes/geodb new file mode 100644 index 00000000000..f7a3b4ffb25 --- /dev/null +++ b/changelog.d/0-release-notes/geodb @@ -0,0 +1 @@ +Removed the deprecated and unused field `geoDb` from Brig's config. diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index d38e4182677..027e1a1ec3e 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -416,8 +416,6 @@ data Opts = Opts -- | Disco URL discoUrl :: !(Maybe Text), - -- | GeoDB file path - geoDb :: !(Maybe FilePath), -- | Event queue for -- Brig-generated events (e.g. -- user deletion) From 10a3a548318558af9821e15b3600204750bfdb3a Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Thu, 4 Apr 2024 16:44:35 +0200 Subject: [PATCH 074/117] add API change documentation to PR guidlines (#3981) --- docs/src/developer/developer/pr-guidelines.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/developer/developer/pr-guidelines.md b/docs/src/developer/developer/pr-guidelines.md index 09b7e37fd22..06f0897f049 100644 --- a/docs/src/developer/developer/pr-guidelines.md +++ b/docs/src/developer/developer/pr-guidelines.md @@ -40,6 +40,7 @@ The following needs to be done, as part of a PR adding endpoints or changing end - [ ] Update nginz config in helm: `charts/nginz/values.yaml` - [ ] Update nginz config for the local integration tests: `services/nginz/integration-test/conf/nginz/nginx.conf` + - [ ] Update the API change documentation on Confluece for the correct version, e.g., [v5 -> v6](https://wearezeta.atlassian.net/wiki/spaces/ENGINEERIN/pages/1035632650/API+changes+v5+v6) ### Helm configuration From 5589540ccfb6d023312a2f49b46dca34d27ec443 Mon Sep 17 00:00:00 2001 From: Paolo Capriotti Date: Fri, 5 Apr 2024 10:16:57 +0200 Subject: [PATCH 075/117] Serialisation of client capabilities (take 2) (#3909) * Use Multiverb in add-client endpoint * Add versioned Client schema * Add v5 version of more client endpoints * Version client list * Update golden files * Add CHANGELOG entry * Use old format in client-add event * Add note about migration plan Co-authored-by: Matthias Fischmann * Revert cosmetic change * Fix assertion in brig integration test --------- Co-authored-by: Matthias Fischmann --- .../0-release-notes/client-internal-api | 1 + changelog.d/1-api-changes/client-capabilities | 1 + integration/test/Test/Notifications.hs | 19 +++++ .../src/Wire/API/Routes/Public/Brig.hs | 79 +++++++++++++++++-- .../src/Wire/API/Routes/Public/Brig/Bot.hs | 33 ++++++-- libs/wire-api/src/Wire/API/User/Client.hs | 56 +++++++++---- libs/wire-api/src/Wire/API/UserEvent.hs | 3 +- .../golden/Test/Wire/API/Golden/Generated.hs | 2 + .../golden/testObject_ClientV5_user_1.json | 12 +++ .../golden/testObject_ClientV5_user_10.json | 13 +++ .../golden/testObject_ClientV5_user_11.json | 13 +++ .../golden/testObject_ClientV5_user_12.json | 12 +++ .../golden/testObject_ClientV5_user_13.json | 13 +++ .../golden/testObject_ClientV5_user_14.json | 12 +++ .../golden/testObject_ClientV5_user_15.json | 12 +++ .../golden/testObject_ClientV5_user_16.json | 13 +++ .../golden/testObject_ClientV5_user_17.json | 12 +++ .../golden/testObject_ClientV5_user_18.json | 13 +++ .../golden/testObject_ClientV5_user_19.json | 12 +++ .../golden/testObject_ClientV5_user_2.json | 10 +++ .../golden/testObject_ClientV5_user_20.json | 14 ++++ .../golden/testObject_ClientV5_user_3.json | 13 +++ .../golden/testObject_ClientV5_user_4.json | 12 +++ .../golden/testObject_ClientV5_user_5.json | 12 +++ .../golden/testObject_ClientV5_user_6.json | 13 +++ .../golden/testObject_ClientV5_user_7.json | 12 +++ .../golden/testObject_ClientV5_user_8.json | 13 +++ .../golden/testObject_ClientV5_user_9.json | 13 +++ .../test/golden/testObject_Client_user_1.json | 4 +- .../golden/testObject_Client_user_10.json | 4 +- .../golden/testObject_Client_user_11.json | 4 +- .../golden/testObject_Client_user_12.json | 4 +- .../golden/testObject_Client_user_13.json | 4 +- .../golden/testObject_Client_user_14.json | 4 +- .../golden/testObject_Client_user_15.json | 4 +- .../golden/testObject_Client_user_16.json | 4 +- .../golden/testObject_Client_user_17.json | 4 +- .../golden/testObject_Client_user_18.json | 4 +- .../golden/testObject_Client_user_19.json | 4 +- .../test/golden/testObject_Client_user_2.json | 4 +- .../golden/testObject_Client_user_20.json | 8 +- .../test/golden/testObject_Client_user_3.json | 4 +- .../test/golden/testObject_Client_user_4.json | 4 +- .../test/golden/testObject_Client_user_5.json | 4 +- .../test/golden/testObject_Client_user_6.json | 4 +- .../test/golden/testObject_Client_user_7.json | 4 +- .../test/golden/testObject_Client_user_8.json | 4 +- .../test/golden/testObject_Client_user_9.json | 4 +- services/brig/src/Brig/API/Public.hs | 15 ++-- services/brig/src/Brig/Provider/API.hs | 1 + services/brig/src/Brig/Run.hs | 2 +- .../brig/test/integration/API/User/Client.hs | 4 +- 52 files changed, 449 insertions(+), 100 deletions(-) create mode 100644 changelog.d/0-release-notes/client-internal-api create mode 100644 changelog.d/1-api-changes/client-capabilities create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_1.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_10.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_11.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_12.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_13.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_14.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_15.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_16.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_17.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_18.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_19.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_2.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_20.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_3.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_4.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_5.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_6.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_7.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_8.json create mode 100644 libs/wire-api/test/golden/testObject_ClientV5_user_9.json diff --git a/changelog.d/0-release-notes/client-internal-api b/changelog.d/0-release-notes/client-internal-api new file mode 100644 index 00000000000..d0d44c9b703 --- /dev/null +++ b/changelog.d/0-release-notes/client-internal-api @@ -0,0 +1 @@ +The "addClient" internal endpoint of galley has been changed. This can cause temporary failures during upgrades if brig attempts to use this endpoint on a different version of galley. diff --git a/changelog.d/1-api-changes/client-capabilities b/changelog.d/1-api-changes/client-capabilities new file mode 100644 index 00000000000..7bd98638b78 --- /dev/null +++ b/changelog.d/1-api-changes/client-capabilities @@ -0,0 +1 @@ +Create version 6 of client-related endpoints, fixing an oddity in the serialisation of capabilities. diff --git a/integration/test/Test/Notifications.hs b/integration/test/Test/Notifications.hs index 741d9b10b0a..14078b5b56e 100644 --- a/integration/test/Test/Notifications.hs +++ b/integration/test/Test/Notifications.hs @@ -1,9 +1,11 @@ {-# OPTIONS -Wno-ambiguous-fields #-} module Test.Notifications where +import API.Brig import API.Common import API.Gundeck import API.GundeckInternal +import Notifications import SetupHelpers import Testlib.Prelude @@ -89,3 +91,20 @@ testInvalidNotification = do void $ getNotifications user def {since = Just notifId} >>= getJSON 404 + +-- | Check that client-add notifications use the V5 format: +-- @ +-- "capabilities": { "capabilities": [..] } +-- @ +-- +-- Migration plan: clients must be able to parse both old and new schema starting from V6. Once V5 is deprecated, the backend can start sending notifications in the new form. +testAddClientNotification :: HasCallStack => App () +testAddClientNotification = do + alice <- randomUser OwnDomain def + + e <- withWebSocket alice $ \ws -> do + void $ addClient alice def + n <- awaitMatch isUserClientAddNotif ws + nPayload n + + void $ e %. "client.capabilities.capabilities" & asList diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs index 15b07451e10..0cd23b3c3e3 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig.hs @@ -65,6 +65,7 @@ import Wire.API.Routes.Public.Brig.Services (ServicesAPI) import Wire.API.Routes.Public.Util import Wire.API.Routes.QualifiedCapture import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.API.SystemSettings import Wire.API.Team.Invitation import Wire.API.Team.Size @@ -126,8 +127,6 @@ type QualifiedCaptureUserId name = QualifiedCapture' '[Description "User Id"] na type CaptureClientId name = Capture' '[Description "ClientId"] name ClientId -type NewClientResponse = Headers '[Header "Location" ClientId] Client - type DeleteSelfResponses = '[ RespondEmpty 200 "Deletion is initiated.", RespondWithDeletionCodeTimeout @@ -730,15 +729,18 @@ type PrekeyAPI = :> Post '[JSON] QualifiedUserClientPrekeyMapV4 ) -type UserClientAPI = - -- User Client API ---------------------------------------------------- +-- User Client API ---------------------------------------------------- + +type ClientHeaders = '[DescHeader "Location" "Client ID" ClientId] +type UserClientAPI = -- This endpoint can lead to the following events being sent: -- - ClientAdded event to self -- - ClientRemoved event to self, if removing old clients due to max number Named - "add-client" + "add-client-v5" ( Summary "Register a new client" + :> Until 'V6 :> MakesFederatedCall 'Brig "send-connection-action" :> CanThrow 'TooManyClients :> CanThrow 'MissingAuth @@ -749,8 +751,38 @@ type UserClientAPI = :> ZConn :> "clients" :> ReqBody '[JSON] NewClient - :> Verb 'POST 201 '[JSON] NewClientResponse + :> MultiVerb1 + 'POST + '[JSON] + ( WithHeaders + ClientHeaders + Client + (VersionedRespond 'V5 201 "Client registered" Client) + ) ) + :<|> Named + "add-client" + ( Summary "Register a new client" + :> From 'V6 + :> MakesFederatedCall 'Brig "send-connection-action" + :> CanThrow 'TooManyClients + :> CanThrow 'MissingAuth + :> CanThrow 'MalformedPrekeys + :> CanThrow 'CodeAuthenticationFailed + :> CanThrow 'CodeAuthenticationRequired + :> ZUser + :> ZConn + :> "clients" + :> ReqBody '[JSON] NewClient + :> MultiVerb1 + 'POST + '[JSON] + ( WithHeaders + ClientHeaders + Client + (Respond 201 "Client registered" Client) + ) + ) :<|> Named "update-client" ( Summary "Update a registered client" @@ -774,16 +806,49 @@ type UserClientAPI = :> ReqBody '[JSON] RmClient :> MultiVerb 'DELETE '[JSON] '[RespondEmpty 200 "Client deleted"] () ) + :<|> Named + "list-clients-v5" + ( Summary "List the registered clients" + :> Until 'V6 + :> ZUser + :> "clients" + :> MultiVerb1 + 'GET + '[JSON] + ( VersionedRespond 'V5 200 "List of clients" [Client] + ) + ) :<|> Named "list-clients" ( Summary "List the registered clients" + :> From 'V6 + :> ZUser + :> "clients" + :> MultiVerb1 + 'GET + '[JSON] + ( Respond 200 "List of clients" [Client] + ) + ) + :<|> Named + "get-client-v5" + ( Summary "Get a registered client by ID" + :> Until 'V6 :> ZUser :> "clients" - :> Get '[JSON] [Client] + :> CaptureClientId "client" + :> MultiVerb + 'GET + '[JSON] + '[ EmptyErrorForLegacyReasons 404 "Client not found", + VersionedRespond 'V5 200 "Client found" Client + ] + (Maybe Client) ) :<|> Named "get-client" ( Summary "Get a registered client by ID" + :> From 'V6 :> ZUser :> "clients" :> CaptureClientId "client" diff --git a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs index 4d544b64a53..635e6711c4a 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public/Brig/Bot.hs @@ -30,6 +30,8 @@ import Wire.API.Provider.Bot (BotUserView) import Wire.API.Routes.MultiVerb import Wire.API.Routes.Named (Named) import Wire.API.Routes.Public +import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.API.User import Wire.API.User.Client import Wire.API.User.Client.Prekey (PrekeyId) @@ -39,11 +41,6 @@ type DeleteResponses = Respond 200 "User found" RemoveBotResponse ] -type GetClientResponses = - '[ ErrorResponse 'ClientNotFound, - Respond 200 "Client found" Client - ] - type BotAPI = Named "add-bot" @@ -116,15 +113,39 @@ type BotAPI = :> ReqBody '[JSON] UpdateBotPrekeys :> MultiVerb1 'POST '[JSON] (RespondEmpty 200 "") ) + :<|> Named + "bot-get-client-v5" + ( Summary "Get client for bot" + :> Until 'V6 + :> CanThrow 'AccessDenied + :> CanThrow 'ClientNotFound + :> ZBot + :> "bot" + :> "client" + :> MultiVerb + 'GET + '[JSON] + '[ ErrorResponse 'ClientNotFound, + VersionedRespond 'V5 200 "Client found" Client + ] + (Maybe Client) + ) :<|> Named "bot-get-client" ( Summary "Get client for bot" + :> From 'V6 :> CanThrow 'AccessDenied :> CanThrow 'ClientNotFound :> ZBot :> "bot" :> "client" - :> MultiVerb 'GET '[JSON] GetClientResponses (Maybe Client) + :> MultiVerb + 'GET + '[JSON] + '[ ErrorResponse 'ClientNotFound, + Respond 200 "Client found" Client + ] + (Maybe Client) ) :<|> Named "bot-claim-users-prekeys" diff --git a/libs/wire-api/src/Wire/API/User/Client.hs b/libs/wire-api/src/Wire/API/User/Client.hs index d900d8b830a..c26b7c5b9b0 100644 --- a/libs/wire-api/src/Wire/API/User/Client.hs +++ b/libs/wire-api/src/Wire/API/User/Client.hs @@ -46,6 +46,7 @@ module Wire.API.User.Client -- * Client Client (..), + clientSchema, PubClient (..), ClientType (..), ClientClass (..), @@ -85,9 +86,11 @@ import Data.Misc (Latitude (..), Longitude (..), PlainTextPassword6) import Data.OpenApi hiding (Schema, ToSchema, nullable, schema) import Data.OpenApi qualified as Swagger hiding (nullable) import Data.Qualified +import Data.SOP hiding (fn) import Data.Schema import Data.Set qualified as Set -import Data.Text.Encoding qualified as Text.E +import Data.Text qualified as T +import Data.Text.Encoding qualified as T import Data.Time.Clock import Data.UUID (toASCIIBytes) import Deriving.Swagger @@ -98,6 +101,9 @@ import Deriving.Swagger ) import Imports import Wire.API.MLS.CipherSuite +import Wire.API.Routes.MultiVerb +import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.API.User.Auth import Wire.API.User.Client.Prekey as Prekey import Wire.Arbitrary (Arbitrary (arbitrary), GenericUniform (..), generateExample, mapOf', setOf') @@ -376,7 +382,7 @@ instance ToJSON UserClientsFull where toJSON . Map.foldrWithKey' fn Map.empty . userClientsFull where fn u c m = - let k = Text.E.decodeLatin1 (toASCIIBytes (toUUID u)) + let k = T.decodeLatin1 (toASCIIBytes (toUUID u)) in Map.insert k c m instance FromJSON UserClientsFull where @@ -498,24 +504,46 @@ mlsPublicKeysSchema = mapSchema :: ValueSchema SwaggerDoc MLSPublicKeys mapSchema = map_ base64Schema +clientSchema :: Maybe Version -> ValueSchema NamedSwaggerDoc Client +clientSchema mv = + object ("Client" <> T.pack (foldMap show mv)) $ + Client + <$> clientId .= field "id" schema + <*> clientType .= field "type" schema + <*> clientTime .= field "time" schema + <*> clientClass .= maybe_ (optField "class" schema) + <*> clientLabel .= maybe_ (optField "label" schema) + <*> clientCookie .= maybe_ (optField "cookie" schema) + <*> clientModel .= maybe_ (optField "model" schema) + <*> clientCapabilities .= (fromMaybe mempty <$> caps) + <*> clientMLSPublicKeys .= mlsPublicKeysFieldSchema + <*> clientLastActive .= maybe_ (optField "last_active" utcTimeSchema) + where + caps :: ObjectSchemaP SwaggerDoc ClientCapabilityList (Maybe ClientCapabilityList) + caps = case mv of + -- broken capability serialisation for backwards compatibility + Just v | v <= V5 -> optField "capabilities" schema + _ -> fmap ClientCapabilityList <$> fromClientCapabilityList .= capabilitiesFieldSchema + instance ToSchema Client where + schema = clientSchema Nothing + +instance ToSchema (Versioned 'V5 Client) where + schema = Versioned <$> unVersioned .= clientSchema (Just V5) + +instance {-# OVERLAPPING #-} ToSchema (Versioned 'V5 [Client]) where schema = - object "Client" $ - Client - <$> clientId .= field "id" schema - <*> clientType .= field "type" schema - <*> clientTime .= field "time" schema - <*> clientClass .= maybe_ (optField "class" schema) - <*> clientLabel .= maybe_ (optField "label" schema) - <*> clientCookie .= maybe_ (optField "cookie" schema) - <*> clientModel .= maybe_ (optField "model" schema) - <*> clientCapabilities .= (fromMaybe mempty <$> optField "capabilities" schema) - <*> clientMLSPublicKeys .= mlsPublicKeysFieldSchema - <*> clientLastActive .= maybe_ (optField "last_active" utcTimeSchema) + Versioned + <$> unVersioned + .= named "ClientList" (array (clientSchema (Just V5))) mlsPublicKeysFieldSchema :: ObjectSchema SwaggerDoc MLSPublicKeys mlsPublicKeysFieldSchema = fromMaybe mempty <$> optField "mls_public_keys" mlsPublicKeysSchema +instance AsHeaders '[ClientId] Client Client where + toHeaders c = (I (clientId c) :* Nil, c) + fromHeaders = snd + -------------------------------------------------------------------------------- -- ClientList diff --git a/libs/wire-api/src/Wire/API/UserEvent.hs b/libs/wire-api/src/Wire/API/UserEvent.hs index 6ed42e3690c..59e5ff91502 100644 --- a/libs/wire-api/src/Wire/API/UserEvent.hs +++ b/libs/wire-api/src/Wire/API/UserEvent.hs @@ -33,6 +33,7 @@ import Imports import System.Logger.Message hiding (field, (.=)) import Wire.API.Connection import Wire.API.Properties +import Wire.API.Routes.Version import Wire.API.User import Wire.API.User.Client import Wire.API.User.Client.Prekey @@ -380,7 +381,7 @@ eventObjectSchema = _ClientEvent ( tag _ClientAdded - (field "client" schema) + (field "client" (clientSchema (Just V5))) ) EventTypeClientRemoved -> tag diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs index 52cde0922bd..88433fe1f78 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs @@ -1004,6 +1004,8 @@ tests = testObjects [(Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_1, "testObject_ClientClass_user_1.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_2, "testObject_ClientClass_user_2.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_3, "testObject_ClientClass_user_3.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_4, "testObject_ClientClass_user_4.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_5, "testObject_ClientClass_user_5.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_6, "testObject_ClientClass_user_6.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_7, "testObject_ClientClass_user_7.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_8, "testObject_ClientClass_user_8.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_9, "testObject_ClientClass_user_9.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_10, "testObject_ClientClass_user_10.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_11, "testObject_ClientClass_user_11.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_12, "testObject_ClientClass_user_12.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_13, "testObject_ClientClass_user_13.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_14, "testObject_ClientClass_user_14.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_15, "testObject_ClientClass_user_15.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_16, "testObject_ClientClass_user_16.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_17, "testObject_ClientClass_user_17.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_18, "testObject_ClientClass_user_18.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_19, "testObject_ClientClass_user_19.json"), (Test.Wire.API.Golden.Generated.ClientClass_user.testObject_ClientClass_user_20, "testObject_ClientClass_user_20.json")], testGroup "Golden: PubClient_user" $ testObjects [(Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_1, "testObject_PubClient_user_1.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_2, "testObject_PubClient_user_2.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_3, "testObject_PubClient_user_3.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_4, "testObject_PubClient_user_4.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_5, "testObject_PubClient_user_5.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_6, "testObject_PubClient_user_6.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_7, "testObject_PubClient_user_7.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_8, "testObject_PubClient_user_8.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_9, "testObject_PubClient_user_9.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_10, "testObject_PubClient_user_10.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_11, "testObject_PubClient_user_11.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_12, "testObject_PubClient_user_12.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_13, "testObject_PubClient_user_13.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_14, "testObject_PubClient_user_14.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_15, "testObject_PubClient_user_15.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_16, "testObject_PubClient_user_16.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_17, "testObject_PubClient_user_17.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_18, "testObject_PubClient_user_18.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_19, "testObject_PubClient_user_19.json"), (Test.Wire.API.Golden.Generated.PubClient_user.testObject_PubClient_user_20, "testObject_PubClient_user_20.json")], + testGroup "Golden: ClientV5_user" $ + testObjects [(Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_1, "testObject_ClientV5_user_1.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_2, "testObject_ClientV5_user_2.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_3, "testObject_ClientV5_user_3.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_4, "testObject_ClientV5_user_4.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_5, "testObject_ClientV5_user_5.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_6, "testObject_ClientV5_user_6.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_7, "testObject_ClientV5_user_7.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_8, "testObject_ClientV5_user_8.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_9, "testObject_ClientV5_user_9.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_10, "testObject_ClientV5_user_10.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_11, "testObject_ClientV5_user_11.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_12, "testObject_ClientV5_user_12.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_13, "testObject_ClientV5_user_13.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_14, "testObject_ClientV5_user_14.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_15, "testObject_ClientV5_user_15.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_16, "testObject_ClientV5_user_16.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_17, "testObject_ClientV5_user_17.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_18, "testObject_ClientV5_user_18.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_19, "testObject_ClientV5_user_19.json"), (Versioned @'V5 Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_20, "testObject_ClientV5_user_20.json")], testGroup "Golden: Client_user" $ testObjects [(Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_1, "testObject_Client_user_1.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_2, "testObject_Client_user_2.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_3, "testObject_Client_user_3.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_4, "testObject_Client_user_4.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_5, "testObject_Client_user_5.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_6, "testObject_Client_user_6.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_7, "testObject_Client_user_7.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_8, "testObject_Client_user_8.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_9, "testObject_Client_user_9.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_10, "testObject_Client_user_10.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_11, "testObject_Client_user_11.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_12, "testObject_Client_user_12.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_13, "testObject_Client_user_13.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_14, "testObject_Client_user_14.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_15, "testObject_Client_user_15.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_16, "testObject_Client_user_16.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_17, "testObject_Client_user_17.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_18, "testObject_Client_user_18.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_19, "testObject_Client_user_19.json"), (Test.Wire.API.Golden.Generated.Client_user.testObject_Client_user_20, "testObject_Client_user_20.json")], testGroup "Golden: NewClient_user" $ diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_1.json b/libs/wire-api/test/golden/testObject_ClientV5_user_1.json new file mode 100644 index 00000000000..9fc8b644e4a --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_1.json @@ -0,0 +1,12 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "desktop", + "id": "2", + "label": "%*", + "mls_public_keys": {}, + "model": "󳇚;􇻫", + "time": "1864-05-06T19:39:12.770Z", + "type": "permanent" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_10.json b/libs/wire-api/test/golden/testObject_ClientV5_user_10.json new file mode 100644 index 00000000000..1d08a33cfd0 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_10.json @@ -0,0 +1,13 @@ +{ + "capabilities": { + "capabilities": [] + }, + "cookie": "L", + "id": "0", + "mls_public_keys": { + "ed25519": "Wm1GclpTQndkV0pzYVdNZ2EyVjU=" + }, + "model": "\u0018", + "time": "1864-05-10T18:42:04.137Z", + "type": "permanent" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_11.json b/libs/wire-api/test/golden/testObject_ClientV5_user_11.json new file mode 100644 index 00000000000..6e4c38b8dc9 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_11.json @@ -0,0 +1,13 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "legalhold", + "cookie": "5", + "id": "3", + "label": "\u001fb", + "mls_public_keys": {}, + "model": "ML", + "time": "1864-05-08T11:57:08.087Z", + "type": "temporary" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_12.json b/libs/wire-api/test/golden/testObject_ClientV5_user_12.json new file mode 100644 index 00000000000..644db85ecbf --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_12.json @@ -0,0 +1,12 @@ +{ + "capabilities": { + "capabilities": [] + }, + "cookie": "0", + "id": "2", + "label": "", + "mls_public_keys": {}, + "model": "", + "time": "1864-05-08T18:44:00.378Z", + "type": "permanent" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_13.json b/libs/wire-api/test/golden/testObject_ClientV5_user_13.json new file mode 100644 index 00000000000..9034bcbc4ab --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_13.json @@ -0,0 +1,13 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "phone", + "cookie": "\u000c^󷋏", + "id": "2", + "label": "􃱽", + "mls_public_keys": {}, + "model": "\u0017𐲤", + "time": "1864-05-07T01:09:04.597Z", + "type": "permanent" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_14.json b/libs/wire-api/test/golden/testObject_ClientV5_user_14.json new file mode 100644 index 00000000000..a4d61fe168c --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_14.json @@ -0,0 +1,12 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "tablet", + "id": "2", + "label": "x\u000e", + "mls_public_keys": {}, + "model": "􀸏\r󠁨", + "time": "1864-05-12T11:00:10.449Z", + "type": "temporary" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_15.json b/libs/wire-api/test/golden/testObject_ClientV5_user_15.json new file mode 100644 index 00000000000..626f76201cd --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_15.json @@ -0,0 +1,12 @@ +{ + "capabilities": { + "capabilities": [] + }, + "cookie": "􌨷N", + "id": "3", + "label": "\u0004G", + "mls_public_keys": {}, + "model": "zAI", + "time": "1864-05-08T11:28:27.778Z", + "type": "temporary" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_16.json b/libs/wire-api/test/golden/testObject_ClientV5_user_16.json new file mode 100644 index 00000000000..7216da58868 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_16.json @@ -0,0 +1,13 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "legalhold", + "cookie": "U", + "id": "2", + "label": "=E", + "mls_public_keys": {}, + "model": "", + "time": "1864-05-12T11:31:10.072Z", + "type": "temporary" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_17.json b/libs/wire-api/test/golden/testObject_ClientV5_user_17.json new file mode 100644 index 00000000000..9f0f36f96a3 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_17.json @@ -0,0 +1,12 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "desktop", + "cookie": "", + "id": "4", + "mls_public_keys": {}, + "model": "", + "time": "1864-05-12T02:25:34.770Z", + "type": "temporary" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_18.json b/libs/wire-api/test/golden/testObject_ClientV5_user_18.json new file mode 100644 index 00000000000..80dad343c4e --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_18.json @@ -0,0 +1,13 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "legalhold", + "cookie": "PG:", + "id": "1", + "label": "󳔺", + "mls_public_keys": {}, + "model": "􅩹", + "time": "1864-05-07T17:21:05.930Z", + "type": "temporary" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_19.json b/libs/wire-api/test/golden/testObject_ClientV5_user_19.json new file mode 100644 index 00000000000..db061827756 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_19.json @@ -0,0 +1,12 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "desktop", + "id": "2", + "label": "􌇰l", + "mls_public_keys": {}, + "model": "", + "time": "1864-05-12T07:49:27.999Z", + "type": "permanent" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_2.json b/libs/wire-api/test/golden/testObject_ClientV5_user_2.json new file mode 100644 index 00000000000..08dd2786531 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_2.json @@ -0,0 +1,10 @@ +{ + "capabilities": { + "capabilities": [] + }, + "cookie": "􏬺c􄂩", + "id": "1", + "mls_public_keys": {}, + "time": "1864-05-07T08:48:22.537Z", + "type": "legalhold" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_20.json b/libs/wire-api/test/golden/testObject_ClientV5_user_20.json new file mode 100644 index 00000000000..253cd8c3952 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_20.json @@ -0,0 +1,14 @@ +{ + "capabilities": { + "capabilities": [ + "legalhold-implicit-consent" + ] + }, + "class": "phone", + "cookie": "", + "id": "1", + "label": "-󼊣v", + "mls_public_keys": {}, + "time": "1864-05-06T18:43:52.483Z", + "type": "legalhold" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_3.json b/libs/wire-api/test/golden/testObject_ClientV5_user_3.json new file mode 100644 index 00000000000..8c5026d2cb7 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_3.json @@ -0,0 +1,13 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "legalhold", + "cookie": "", + "id": "1", + "label": "pi", + "last_active": "2023-07-04T09:35:32Z", + "mls_public_keys": {}, + "time": "1864-05-07T00:38:22.384Z", + "type": "temporary" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_4.json b/libs/wire-api/test/golden/testObject_ClientV5_user_4.json new file mode 100644 index 00000000000..25e8c8860bd --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_4.json @@ -0,0 +1,12 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "legalhold", + "cookie": "j", + "id": "3", + "mls_public_keys": {}, + "model": "", + "time": "1864-05-06T09:13:45.902Z", + "type": "permanent" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_5.json b/libs/wire-api/test/golden/testObject_ClientV5_user_5.json new file mode 100644 index 00000000000..0af93523dc2 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_5.json @@ -0,0 +1,12 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "desktop", + "cookie": "", + "id": "0", + "mls_public_keys": {}, + "model": "⌷o", + "time": "1864-05-07T09:07:14.559Z", + "type": "temporary" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_6.json b/libs/wire-api/test/golden/testObject_ClientV5_user_6.json new file mode 100644 index 00000000000..90a2b0ea16e --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_6.json @@ -0,0 +1,13 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "tablet", + "cookie": "l\u0002", + "id": "4", + "last_active": "2021-09-15T22:00:21Z", + "mls_public_keys": {}, + "model": "", + "time": "1864-05-08T22:37:53.030Z", + "type": "permanent" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_7.json b/libs/wire-api/test/golden/testObject_ClientV5_user_7.json new file mode 100644 index 00000000000..41253b1fb0a --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_7.json @@ -0,0 +1,12 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "phone", + "id": "4", + "label": "", + "mls_public_keys": {}, + "model": "", + "time": "1864-05-07T04:35:34.201Z", + "type": "permanent" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_8.json b/libs/wire-api/test/golden/testObject_ClientV5_user_8.json new file mode 100644 index 00000000000..fafbbc7e6e5 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_8.json @@ -0,0 +1,13 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "phone", + "cookie": "\u0015p`", + "id": "4", + "label": "", + "mls_public_keys": {}, + "model": "􏽉", + "time": "1864-05-11T06:32:01.921Z", + "type": "legalhold" +} diff --git a/libs/wire-api/test/golden/testObject_ClientV5_user_9.json b/libs/wire-api/test/golden/testObject_ClientV5_user_9.json new file mode 100644 index 00000000000..ed4e67747ca --- /dev/null +++ b/libs/wire-api/test/golden/testObject_ClientV5_user_9.json @@ -0,0 +1,13 @@ +{ + "capabilities": { + "capabilities": [] + }, + "class": "legalhold", + "cookie": "G", + "id": "1", + "label": "v", + "mls_public_keys": {}, + "model": "㌀m", + "time": "1864-05-08T03:54:56.526Z", + "type": "legalhold" +} diff --git a/libs/wire-api/test/golden/testObject_Client_user_1.json b/libs/wire-api/test/golden/testObject_Client_user_1.json index 9fc8b644e4a..3ae58f75402 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_1.json +++ b/libs/wire-api/test/golden/testObject_Client_user_1.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "desktop", "id": "2", "label": "%*", diff --git a/libs/wire-api/test/golden/testObject_Client_user_10.json b/libs/wire-api/test/golden/testObject_Client_user_10.json index 1d08a33cfd0..35ad363f074 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_10.json +++ b/libs/wire-api/test/golden/testObject_Client_user_10.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "cookie": "L", "id": "0", "mls_public_keys": { diff --git a/libs/wire-api/test/golden/testObject_Client_user_11.json b/libs/wire-api/test/golden/testObject_Client_user_11.json index 6e4c38b8dc9..8d6af47dc49 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_11.json +++ b/libs/wire-api/test/golden/testObject_Client_user_11.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "legalhold", "cookie": "5", "id": "3", diff --git a/libs/wire-api/test/golden/testObject_Client_user_12.json b/libs/wire-api/test/golden/testObject_Client_user_12.json index 644db85ecbf..63ca4553dee 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_12.json +++ b/libs/wire-api/test/golden/testObject_Client_user_12.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "cookie": "0", "id": "2", "label": "", diff --git a/libs/wire-api/test/golden/testObject_Client_user_13.json b/libs/wire-api/test/golden/testObject_Client_user_13.json index 9034bcbc4ab..9b2552d9086 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_13.json +++ b/libs/wire-api/test/golden/testObject_Client_user_13.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "phone", "cookie": "\u000c^󷋏", "id": "2", diff --git a/libs/wire-api/test/golden/testObject_Client_user_14.json b/libs/wire-api/test/golden/testObject_Client_user_14.json index a4d61fe168c..c95b927805a 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_14.json +++ b/libs/wire-api/test/golden/testObject_Client_user_14.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "tablet", "id": "2", "label": "x\u000e", diff --git a/libs/wire-api/test/golden/testObject_Client_user_15.json b/libs/wire-api/test/golden/testObject_Client_user_15.json index 626f76201cd..7050d356278 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_15.json +++ b/libs/wire-api/test/golden/testObject_Client_user_15.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "cookie": "􌨷N", "id": "3", "label": "\u0004G", diff --git a/libs/wire-api/test/golden/testObject_Client_user_16.json b/libs/wire-api/test/golden/testObject_Client_user_16.json index 7216da58868..e70257998b5 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_16.json +++ b/libs/wire-api/test/golden/testObject_Client_user_16.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "legalhold", "cookie": "U", "id": "2", diff --git a/libs/wire-api/test/golden/testObject_Client_user_17.json b/libs/wire-api/test/golden/testObject_Client_user_17.json index 9f0f36f96a3..485f822a3d2 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_17.json +++ b/libs/wire-api/test/golden/testObject_Client_user_17.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "desktop", "cookie": "", "id": "4", diff --git a/libs/wire-api/test/golden/testObject_Client_user_18.json b/libs/wire-api/test/golden/testObject_Client_user_18.json index 80dad343c4e..5f1ba1bf5b8 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_18.json +++ b/libs/wire-api/test/golden/testObject_Client_user_18.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "legalhold", "cookie": "PG:", "id": "1", diff --git a/libs/wire-api/test/golden/testObject_Client_user_19.json b/libs/wire-api/test/golden/testObject_Client_user_19.json index db061827756..f6263f00203 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_19.json +++ b/libs/wire-api/test/golden/testObject_Client_user_19.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "desktop", "id": "2", "label": "􌇰l", diff --git a/libs/wire-api/test/golden/testObject_Client_user_2.json b/libs/wire-api/test/golden/testObject_Client_user_2.json index 08dd2786531..802de9bd21f 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_2.json +++ b/libs/wire-api/test/golden/testObject_Client_user_2.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "cookie": "􏬺c􄂩", "id": "1", "mls_public_keys": {}, diff --git a/libs/wire-api/test/golden/testObject_Client_user_20.json b/libs/wire-api/test/golden/testObject_Client_user_20.json index 253cd8c3952..c9f3ae4459b 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_20.json +++ b/libs/wire-api/test/golden/testObject_Client_user_20.json @@ -1,9 +1,7 @@ { - "capabilities": { - "capabilities": [ - "legalhold-implicit-consent" - ] - }, + "capabilities": [ + "legalhold-implicit-consent" + ], "class": "phone", "cookie": "", "id": "1", diff --git a/libs/wire-api/test/golden/testObject_Client_user_3.json b/libs/wire-api/test/golden/testObject_Client_user_3.json index 8c5026d2cb7..b6cb51e0fbf 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_3.json +++ b/libs/wire-api/test/golden/testObject_Client_user_3.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "legalhold", "cookie": "", "id": "1", diff --git a/libs/wire-api/test/golden/testObject_Client_user_4.json b/libs/wire-api/test/golden/testObject_Client_user_4.json index 25e8c8860bd..4a8398a2e9b 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_4.json +++ b/libs/wire-api/test/golden/testObject_Client_user_4.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "legalhold", "cookie": "j", "id": "3", diff --git a/libs/wire-api/test/golden/testObject_Client_user_5.json b/libs/wire-api/test/golden/testObject_Client_user_5.json index 0af93523dc2..e1967bb1bcf 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_5.json +++ b/libs/wire-api/test/golden/testObject_Client_user_5.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "desktop", "cookie": "", "id": "0", diff --git a/libs/wire-api/test/golden/testObject_Client_user_6.json b/libs/wire-api/test/golden/testObject_Client_user_6.json index 90a2b0ea16e..929f3132496 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_6.json +++ b/libs/wire-api/test/golden/testObject_Client_user_6.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "tablet", "cookie": "l\u0002", "id": "4", diff --git a/libs/wire-api/test/golden/testObject_Client_user_7.json b/libs/wire-api/test/golden/testObject_Client_user_7.json index 41253b1fb0a..8ca4dc49b6a 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_7.json +++ b/libs/wire-api/test/golden/testObject_Client_user_7.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "phone", "id": "4", "label": "", diff --git a/libs/wire-api/test/golden/testObject_Client_user_8.json b/libs/wire-api/test/golden/testObject_Client_user_8.json index fafbbc7e6e5..35f568dd53c 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_8.json +++ b/libs/wire-api/test/golden/testObject_Client_user_8.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "phone", "cookie": "\u0015p`", "id": "4", diff --git a/libs/wire-api/test/golden/testObject_Client_user_9.json b/libs/wire-api/test/golden/testObject_Client_user_9.json index ed4e67747ca..cfda4f2768a 100644 --- a/libs/wire-api/test/golden/testObject_Client_user_9.json +++ b/libs/wire-api/test/golden/testObject_Client_user_9.json @@ -1,7 +1,5 @@ { - "capabilities": { - "capabilities": [] - }, + "capabilities": [], "class": "legalhold", "cookie": "G", "id": "1", diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index b747d81dba5..0a4137e24b7 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -369,10 +369,13 @@ servantSitemap = userClientAPI :: ServerT UserClientAPI (Handler r) userClientAPI = - Named @"add-client" (callsFed (exposeAnnotations addClient)) + Named @"add-client-v5" (callsFed (exposeAnnotations addClient)) + :<|> Named @"add-client" (callsFed (exposeAnnotations addClient)) :<|> Named @"update-client" updateClient :<|> Named @"delete-client" deleteClient + :<|> Named @"list-clients-v5" listClients :<|> Named @"list-clients" listClients + :<|> Named @"get-client-v5" getClient :<|> Named @"get-client" getClient :<|> Named @"get-client-capabilities" getClientCapabilities :<|> Named @"get-client-prekeys" getClientPrekeys @@ -578,17 +581,13 @@ addClient :: UserId -> ConnId -> Public.NewClient -> - (Handler r) NewClientResponse + Handler r Public.Client addClient usr con new = do -- Users can't add legal hold clients when (Public.newClientType new == Public.LegalHoldClientType) $ throwE (clientError ClientLegalHoldCannotBeAdded) - clientResponse - <$> API.addClient usr (Just con) new - !>> clientError - where - clientResponse :: Public.Client -> NewClientResponse - clientResponse client = Servant.addHeader (Public.clientId client) client + API.addClient usr (Just con) new + !>> clientError deleteClient :: UserId -> ConnId -> ClientId -> Public.RmClient -> (Handler r) () deleteClient usr con clt body = diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index db1616d3c67..535ad5c9750 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -142,6 +142,7 @@ botAPI = :<|> Named @"bot-delete-self" botDeleteSelf :<|> Named @"bot-list-prekeys" botListPrekeys :<|> Named @"bot-update-prekeys" botUpdatePrekeys + :<|> Named @"bot-get-client-v5" botGetClient :<|> Named @"bot-get-client" botGetClient :<|> Named @"bot-claim-users-prekeys" botClaimUsersPrekeys :<|> Named @"bot-list-users" botListUserProfiles diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index 553a773366f..b74e58081c2 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -26,7 +26,7 @@ import Brig.API (sitemap) import Brig.API.Federation import Brig.API.Handler import Brig.API.Internal qualified as IAPI -import Brig.API.Public (DocsAPI, docsAPI, servantSitemap) +import Brig.API.Public import Brig.API.User qualified as API import Brig.AWS (amazonkaEnv, sesQueue) import Brig.AWS qualified as AWS diff --git a/services/brig/test/integration/API/User/Client.hs b/services/brig/test/integration/API/User/Client.hs index ec3e9d35052..3372c1b3e56 100644 --- a/services/brig/test/integration/API/User/Client.hs +++ b/services/brig/test/integration/API/User/Client.hs @@ -71,6 +71,8 @@ import UnliftIO (mapConcurrently) import Util import Wire.API.Internal.Notification import Wire.API.MLS.CipherSuite +import Wire.API.Routes.Version +import Wire.API.Routes.Versioned import Wire.API.Team.Feature qualified as Public import Wire.API.User import Wire.API.User qualified as Public @@ -257,7 +259,7 @@ testAddGetClient params brig cannon = do let etype = j ^? key "type" . _String let eclient = j ^? key "client" etype @?= Just "user.client-add" - fmap fromJSON eclient @?= Just (Success c) + fmap fromJSON eclient @?= Just (Success (Versioned @'V5 c)) pure c liftIO $ clientMLSPublicKeys c @?= keys getClient brig uid (clientId c) !!! do From b23f4db354c90528a4203e16614b4b3fed0f9d41 Mon Sep 17 00:00:00 2001 From: Stefan Matting Date: Mon, 8 Apr 2024 14:26:48 +0200 Subject: [PATCH 076/117] wire-server chart: Disable integration chart by default (#3682) --- changelog.d/5-internal/disable-integration-chart | 1 + charts/wire-server/values.yaml | 1 + hack/helm_vars/wire-server/values.yaml.gotmpl | 1 + 3 files changed, 3 insertions(+) create mode 100644 changelog.d/5-internal/disable-integration-chart diff --git a/changelog.d/5-internal/disable-integration-chart b/changelog.d/5-internal/disable-integration-chart new file mode 100644 index 00000000000..8a222420e7a --- /dev/null +++ b/changelog.d/5-internal/disable-integration-chart @@ -0,0 +1 @@ +Disable `integration` subchart of `wire-server` by default \ No newline at end of file diff --git a/charts/wire-server/values.yaml b/charts/wire-server/values.yaml index 3a0a3f1f525..7e41eca7838 100644 --- a/charts/wire-server/values.yaml +++ b/charts/wire-server/values.yaml @@ -13,3 +13,4 @@ tags: sftd: false backoffice: false mlsstats: false + integration: false diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index 1547d6f846a..dec2183e9c5 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -14,6 +14,7 @@ tags: account-pages: false legalhold: false sftd: false + integration: true cassandra-migrations: imagePullPolicy: {{ .Values.imagePullPolicy }} From f1c1ea339ff9de1e2ddb8a64fa97b95df35a4ffb Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Tue, 9 Apr 2024 11:12:02 +0200 Subject: [PATCH 077/117] early exit when deleting already gone client (#3985) - in Galley's internal `DELETE /i/client/:clientid` --- changelog.d/5-internal/WPB-7416 | 2 + services/galley/galley.cabal | 1 + services/galley/src/Galley/API/Clients.hs | 21 ++- services/galley/src/Galley/App.hs | 4 +- .../galley/src/Galley/Cassandra/Client.hs | 25 ++- services/galley/src/Galley/Cassandra/Code.hs | 26 ++- .../src/Galley/Cassandra/Conversation.hs | 97 ++++++++--- .../Galley/Cassandra/Conversation/Members.hs | 90 +++++++--- .../src/Galley/Cassandra/ConversationList.hs | 23 ++- .../src/Galley/Cassandra/CustomBackend.hs | 17 +- .../galley/src/Galley/Cassandra/LegalHold.hs | 55 ++++-- .../galley/src/Galley/Cassandra/Proposal.hs | 36 ++-- .../src/Galley/Cassandra/SearchVisibility.hs | 17 +- .../galley/src/Galley/Cassandra/Services.hs | 17 +- .../src/Galley/Cassandra/SubConversation.hs | 42 +++-- services/galley/src/Galley/Cassandra/Team.hs | 164 +++++++++++++----- .../src/Galley/Cassandra/TeamFeatures.hs | 25 ++- .../src/Galley/Cassandra/TeamNotifications.hs | 17 +- services/galley/src/Galley/Cassandra/Util.hs | 27 +++ services/galley/src/Galley/External.hs | 17 +- .../Galley/Intra/BackendNotificationQueue.hs | 8 +- services/galley/src/Galley/Intra/Effects.hs | 95 +++++++--- services/galley/src/Galley/Intra/Federator.hs | 33 ++-- 23 files changed, 635 insertions(+), 224 deletions(-) create mode 100644 changelog.d/5-internal/WPB-7416 create mode 100644 services/galley/src/Galley/Cassandra/Util.hs diff --git a/changelog.d/5-internal/WPB-7416 b/changelog.d/5-internal/WPB-7416 new file mode 100644 index 00000000000..9a4acd3e7c5 --- /dev/null +++ b/changelog.d/5-internal/WPB-7416 @@ -0,0 +1,2 @@ +Galley's internal `DELETE /i/client/:clientID` now early-exits before visiting all conversations if the client is already gone. +Galley now reports debug logs for every call to Cassandra. diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 4866b41930d..b15592ed296 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -149,6 +149,7 @@ library Galley.Cassandra.Team Galley.Cassandra.TeamFeatures Galley.Cassandra.TeamNotifications + Galley.Cassandra.Util Galley.Data.Conversation Galley.Data.Conversation.Types Galley.Data.Scope diff --git a/services/galley/src/Galley/API/Clients.hs b/services/galley/src/Galley/API/Clients.hs index cfb18cd320a..080cb3b975a 100644 --- a/services/galley/src/Galley/API/Clients.hs +++ b/services/galley/src/Galley/API/Clients.hs @@ -47,6 +47,7 @@ import Polysemy import Polysemy.Error import Polysemy.Input import Polysemy.TinyLog qualified as P +import System.Logger.Message import Wire.API.Conversation hiding (Member) import Wire.API.Federation.API import Wire.API.Federation.API.Galley @@ -112,12 +113,20 @@ rmClientH :: UserId ::: ClientId -> Sem r Response rmClientH (usr ::: cid) = do - lusr <- qualifyLocal usr - let nRange1000 = toRange (Proxy @1000) :: Range 1 1000 Int32 - firstConvIds <- Query.conversationIdsPageFrom lusr (GetPaginatedConversationIds Nothing nRange1000) - goConvs nRange1000 firstConvIds lusr - - E.deleteClient usr cid + clients <- E.getClients [usr] + if (cid `elem` clientIds usr clients) + then do + lusr <- qualifyLocal usr + let nRange1000 = toRange (Proxy @1000) :: Range 1 1000 Int32 + firstConvIds <- Query.conversationIdsPageFrom lusr (GetPaginatedConversationIds Nothing nRange1000) + goConvs nRange1000 firstConvIds lusr + E.deleteClient usr cid + else + P.debug + ( field "user" (idToText usr) + . field "client" (clientToText cid) + . msg (val "rmClientH: client already gone") + ) pure empty where goConvs :: Range 1 1000 Int32 -> ConvIdsPage -> Local UserId -> Sem r () diff --git a/services/galley/src/Galley/App.hs b/services/galley/src/Galley/App.hs index a4f780bdb78..53413f9bf33 100644 --- a/services/galley/src/Galley/App.hs +++ b/services/galley/src/Galley/App.hs @@ -66,12 +66,12 @@ import Galley.Cassandra.LegalHold import Galley.Cassandra.Proposal import Galley.Cassandra.SearchVisibility import Galley.Cassandra.Services -import Galley.Cassandra.SubConversation (interpretSubConversationStoreToCassandra) +import Galley.Cassandra.SubConversation import Galley.Cassandra.Team import Galley.Cassandra.TeamFeatures import Galley.Cassandra.TeamNotifications import Galley.Effects -import Galley.Effects.FireAndForget (interpretFireAndForget) +import Galley.Effects.FireAndForget import Galley.Effects.WaiRoutes.IO import Galley.Env import Galley.External diff --git a/services/galley/src/Galley/Cassandra/Client.hs b/services/galley/src/Galley/Cassandra/Client.hs index 419feef79e6..bc37fece531 100644 --- a/services/galley/src/Galley/Cassandra/Client.hs +++ b/services/galley/src/Galley/Cassandra/Client.hs @@ -28,6 +28,7 @@ import Data.Id import Data.List.Split (chunksOf) import Galley.Cassandra.Queries qualified as Cql import Galley.Cassandra.Store +import Galley.Cassandra.Util import Galley.Effects.ClientStore (ClientStore (..)) import Galley.Env import Galley.Monad @@ -37,6 +38,7 @@ import Galley.Types.Clients qualified as Clients import Imports import Polysemy import Polysemy.Input +import Polysemy.TinyLog import UnliftIO qualified updateClient :: Bool -> UserId -> ClientId -> Client () @@ -60,13 +62,24 @@ eraseClients user = retry x5 (write Cql.rmClients (params LocalQuorum (Identity interpretClientStoreToCassandra :: ( Member (Embed IO) r, Member (Input ClientState) r, - Member (Input Env) r + Member (Input Env) r, + Member TinyLog r ) => Sem (ClientStore ': r) a -> Sem r a interpretClientStoreToCassandra = interpret $ \case - GetClients uids -> embedClient $ lookupClients uids - CreateClient uid cid -> embedClient $ updateClient True uid cid - DeleteClient uid cid -> embedClient $ updateClient False uid cid - DeleteClients uid -> embedClient $ eraseClients uid - UseIntraClientListing -> embedApp . view $ options . settings . intraListing + GetClients uids -> do + logEffect "ClientStore.GetClients" + embedClient $ lookupClients uids + CreateClient uid cid -> do + logEffect "ClientStore.CreateClient" + embedClient $ updateClient True uid cid + DeleteClient uid cid -> do + logEffect "ClientStore.DeleteClient" + embedClient $ updateClient False uid cid + DeleteClients uid -> do + logEffect "ClientStore.DeleteClients" + embedClient $ eraseClients uid + UseIntraClientListing -> do + logEffect "ClientStore.UseIntraClientListing" + embedApp . view $ options . settings . intraListing diff --git a/services/galley/src/Galley/Cassandra/Code.hs b/services/galley/src/Galley/Cassandra/Code.hs index f5b2770b38a..407e6ceedea 100644 --- a/services/galley/src/Galley/Cassandra/Code.hs +++ b/services/galley/src/Galley/Cassandra/Code.hs @@ -26,6 +26,7 @@ import Data.Code import Data.Map qualified as Map import Galley.Cassandra.Queries qualified as Cql import Galley.Cassandra.Store +import Galley.Cassandra.Util import Galley.Data.Types import Galley.Data.Types qualified as Code import Galley.Effects.CodeStore (CodeStore (..)) @@ -33,22 +34,35 @@ import Galley.Env import Imports import Polysemy import Polysemy.Input +import Polysemy.TinyLog import Wire.API.Password interpretCodeStoreToCassandra :: ( Member (Embed IO) r, Member (Input ClientState) r, - Member (Input Env) r + Member (Input Env) r, + Member TinyLog r ) => Sem (CodeStore ': r) a -> Sem r a interpretCodeStoreToCassandra = interpret $ \case - GetCode k s -> embedClient $ lookupCode k s - CreateCode code mPw -> embedClient $ insertCode code mPw - DeleteCode k s -> embedClient $ deleteCode k s - MakeKey cid -> Code.mkKey cid - GenerateCode cid s t -> Code.generate cid s t + GetCode k s -> do + logEffect "CodeStore.GetCode" + embedClient $ lookupCode k s + CreateCode code mPw -> do + logEffect "CodeStore.CreateCode" + embedClient $ insertCode code mPw + DeleteCode k s -> do + logEffect "CodeStore.DeleteCode" + embedClient $ deleteCode k s + MakeKey cid -> do + logEffect "CodeStore.MakeKey" + Code.mkKey cid + GenerateCode cid s t -> do + logEffect "CodeStore.GenerateCode" + Code.generate cid s t GetConversationCodeURI mbHost -> do + logEffect "CodeStore.GetConversationCodeURI" env <- input case env ^. convCodeURI of Left uri -> pure (Just uri) diff --git a/services/galley/src/Galley/Cassandra/Conversation.hs b/services/galley/src/Galley/Cassandra/Conversation.hs index 098a4771cac..286fd4b3264 100644 --- a/services/galley/src/Galley/Cassandra/Conversation.hs +++ b/services/galley/src/Galley/Cassandra/Conversation.hs @@ -41,6 +41,7 @@ import Galley.Cassandra.Conversation.MLS import Galley.Cassandra.Conversation.Members import Galley.Cassandra.Queries qualified as Cql import Galley.Cassandra.Store +import Galley.Cassandra.Util import Galley.Data.Conversation import Galley.Data.Conversation.Types import Galley.Effects.ConversationStore (ConversationStore (..)) @@ -450,27 +451,75 @@ interpretConversationStoreToCassandra :: Sem (ConversationStore ': r) a -> Sem r a interpretConversationStoreToCassandra = interpret $ \case - CreateConversationId -> Id <$> embed nextRandom - CreateConversation loc nc -> embedClient $ createConversation loc nc - CreateMLSSelfConversation lusr -> embedClient $ createMLSSelfConversation lusr - GetConversation cid -> embedClient $ getConversation cid - GetConversationEpoch cid -> embedClient $ getConvEpoch cid - GetConversations cids -> localConversations cids - GetConversationMetadata cid -> embedClient $ conversationMeta cid - GetGroupInfo cid -> embedClient $ getGroupInfo cid - IsConversationAlive cid -> embedClient $ isConvAlive cid - SelectConversations uid cids -> embedClient $ localConversationIdsOf uid cids - GetRemoteConversationStatus uid cids -> embedClient $ remoteConversationStatus uid cids - SetConversationType cid ty -> embedClient $ updateConvType cid ty - SetConversationName cid value -> embedClient $ updateConvName cid value - SetConversationAccess cid value -> embedClient $ updateConvAccess cid value - SetConversationReceiptMode cid value -> embedClient $ updateConvReceiptMode cid value - SetConversationMessageTimer cid value -> embedClient $ updateConvMessageTimer cid value - SetConversationEpoch cid epoch -> embedClient $ updateConvEpoch cid epoch - SetConversationCipherSuite cid cs -> embedClient $ updateConvCipherSuite cid cs - DeleteConversation cid -> embedClient $ deleteConversation cid - SetGroupInfo cid gib -> embedClient $ setGroupInfo cid gib - AcquireCommitLock gId epoch ttl -> embedClient $ acquireCommitLock gId epoch ttl - ReleaseCommitLock gId epoch -> embedClient $ releaseCommitLock gId epoch - UpdateToMixedProtocol cid ct cs -> updateToMixedProtocol cid ct cs - UpdateToMLSProtocol cid -> updateToMLSProtocol cid + CreateConversationId -> do + logEffect "ConversationStore.CreateConversationId" + Id <$> embed nextRandom + CreateConversation loc nc -> do + logEffect "ConversationStore.CreateConversation" + embedClient $ createConversation loc nc + CreateMLSSelfConversation lusr -> do + logEffect "ConversationStore.CreateMLSSelfConversation" + embedClient $ createMLSSelfConversation lusr + GetConversation cid -> do + logEffect "ConversationStore.GetConversation" + embedClient $ getConversation cid + GetConversationEpoch cid -> do + logEffect "ConversationStore.GetConversationEpoch" + embedClient $ getConvEpoch cid + GetConversations cids -> do + logEffect "ConversationStore.GetConversations" + localConversations cids + GetConversationMetadata cid -> do + logEffect "ConversationStore.GetConversationMetadata" + embedClient $ conversationMeta cid + GetGroupInfo cid -> do + logEffect "ConversationStore.GetGroupInfo" + embedClient $ getGroupInfo cid + IsConversationAlive cid -> do + logEffect "ConversationStore.IsConversationAlive" + embedClient $ isConvAlive cid + SelectConversations uid cids -> do + logEffect "ConversationStore.SelectConversations" + embedClient $ localConversationIdsOf uid cids + GetRemoteConversationStatus uid cids -> do + logEffect "ConversationStore.GetRemoteConversationStatus" + embedClient $ remoteConversationStatus uid cids + SetConversationType cid ty -> do + logEffect "ConversationStore.SetConversationType" + embedClient $ updateConvType cid ty + SetConversationName cid value -> do + logEffect "ConversationStore.SetConversationName" + embedClient $ updateConvName cid value + SetConversationAccess cid value -> do + logEffect "ConversationStore.SetConversationAccess" + embedClient $ updateConvAccess cid value + SetConversationReceiptMode cid value -> do + logEffect "ConversationStore.SetConversationReceiptMode" + embedClient $ updateConvReceiptMode cid value + SetConversationMessageTimer cid value -> do + logEffect "ConversationStore.SetConversationMessageTimer" + embedClient $ updateConvMessageTimer cid value + SetConversationEpoch cid epoch -> do + logEffect "ConversationStore.SetConversationEpoch" + embedClient $ updateConvEpoch cid epoch + SetConversationCipherSuite cid cs -> do + logEffect "ConversationStore.SetConversationCipherSuite" + embedClient $ updateConvCipherSuite cid cs + DeleteConversation cid -> do + logEffect "ConversationStore.DeleteConversation" + embedClient $ deleteConversation cid + SetGroupInfo cid gib -> do + logEffect "ConversationStore.SetGroupInfo" + embedClient $ setGroupInfo cid gib + AcquireCommitLock gId epoch ttl -> do + logEffect "ConversationStore.AcquireCommitLock" + embedClient $ acquireCommitLock gId epoch ttl + ReleaseCommitLock gId epoch -> do + logEffect "ConversationStore.ReleaseCommitLock" + embedClient $ releaseCommitLock gId epoch + UpdateToMixedProtocol cid ct cs -> do + logEffect "ConversationStore.UpdateToMixedProtocol" + updateToMixedProtocol cid ct cs + UpdateToMLSProtocol cid -> do + logEffect "ConversationStore.UpdateToMLSProtocol" + updateToMLSProtocol cid diff --git a/services/galley/src/Galley/Cassandra/Conversation/Members.hs b/services/galley/src/Galley/Cassandra/Conversation/Members.hs index abd3a0139e6..2bda5331335 100644 --- a/services/galley/src/Galley/Cassandra/Conversation/Members.hs +++ b/services/galley/src/Galley/Cassandra/Conversation/Members.hs @@ -39,6 +39,7 @@ import Galley.Cassandra.Instances () import Galley.Cassandra.Queries qualified as Cql import Galley.Cassandra.Services import Galley.Cassandra.Store +import Galley.Cassandra.Util import Galley.Effects.MemberStore (MemberStore (..)) import Galley.Types.Conversations.Members import Galley.Types.ToUserRole @@ -46,6 +47,7 @@ import Galley.Types.UserList import Imports hiding (Set, cs) import Polysemy import Polysemy.Input +import Polysemy.TinyLog import UnliftIO qualified import Wire.API.Conversation.Member hiding (Member) import Wire.API.Conversation.Role @@ -390,34 +392,76 @@ removeAllMLSClients groupId = do interpretMemberStoreToCassandra :: ( Member (Embed IO) r, - Member (Input ClientState) r + Member (Input ClientState) r, + Member TinyLog r ) => Sem (MemberStore ': r) a -> Sem r a interpretMemberStoreToCassandra = interpret $ \case - CreateMembers cid ul -> embedClient $ addMembers cid ul - CreateMembersInRemoteConversation rcid uids -> + CreateMembers cid ul -> do + logEffect "MemberStore.CreateMembers" + embedClient $ addMembers cid ul + CreateMembersInRemoteConversation rcid uids -> do + logEffect "MemberStore.CreateMembersInRemoteConversation" embedClient $ addLocalMembersToRemoteConv rcid uids - CreateBotMember sr bid cid -> embedClient $ addBotMember sr bid cid - GetLocalMember cid uid -> embedClient $ member cid uid - GetLocalMembers cid -> embedClient $ members cid - GetAllLocalMembers -> embedClient allMembers - GetRemoteMember cid uid -> embedClient $ lookupRemoteMember cid (tDomain uid) (tUnqualified uid) - GetRemoteMembers rcid -> embedClient $ lookupRemoteMembers rcid - CheckLocalMemberRemoteConv uid rcnv -> fmap (not . null) $ embedClient $ lookupLocalMemberRemoteConv uid rcnv - SelectRemoteMembers uids rcnv -> embedClient $ filterRemoteConvMembers uids rcnv - SetSelfMember qcid luid upd -> embedClient $ updateSelfMember qcid luid upd - SetOtherMember lcid quid upd -> + CreateBotMember sr bid cid -> do + logEffect "MemberStore.CreateBotMember" + embedClient $ addBotMember sr bid cid + GetLocalMember cid uid -> do + logEffect "MemberStore.GetLocalMember" + embedClient $ member cid uid + GetLocalMembers cid -> do + logEffect "MemberStore.GetLocalMembers" + embedClient $ members cid + GetAllLocalMembers -> do + logEffect "MemberStore.GetAllLocalMembers" + embedClient allMembers + GetRemoteMember cid uid -> do + logEffect "MemberStore.GetRemoteMember" + embedClient $ lookupRemoteMember cid (tDomain uid) (tUnqualified uid) + GetRemoteMembers rcid -> do + logEffect "MemberStore.GetRemoteMembers" + embedClient $ lookupRemoteMembers rcid + CheckLocalMemberRemoteConv uid rcnv -> do + logEffect "MemberStore.CheckLocalMemberRemoteConv" + fmap (not . null) $ embedClient $ lookupLocalMemberRemoteConv uid rcnv + SelectRemoteMembers uids rcnv -> do + logEffect "MemberStore.SelectRemoteMembers" + embedClient $ filterRemoteConvMembers uids rcnv + SetSelfMember qcid luid upd -> do + logEffect "MemberStore.SetSelfMember" + embedClient $ updateSelfMember qcid luid upd + SetOtherMember lcid quid upd -> do + logEffect "MemberStore.SetOtherMember" embedClient $ updateOtherMemberLocalConv lcid quid upd - DeleteMembers cnv ul -> embedClient $ removeMembersFromLocalConv cnv ul - DeleteMembersInRemoteConversation rcnv uids -> + DeleteMembers cnv ul -> do + logEffect "MemberStore.DeleteMembers" + embedClient $ removeMembersFromLocalConv cnv ul + DeleteMembersInRemoteConversation rcnv uids -> do + logEffect "MemberStore.DeleteMembersInRemoteConversation" embedClient $ removeLocalMembersFromRemoteConv rcnv uids - AddMLSClients lcnv quid cs -> embedClient $ addMLSClients lcnv quid cs - PlanClientRemoval lcnv cids -> embedClient $ planMLSClientRemoval lcnv cids - RemoveMLSClients lcnv quid cs -> embedClient $ removeMLSClients lcnv quid cs - RemoveAllMLSClients gid -> embedClient $ removeAllMLSClients gid - LookupMLSClients lcnv -> embedClient $ lookupMLSClients lcnv - LookupMLSClientLeafIndices lcnv -> embedClient $ lookupMLSClientLeafIndices lcnv - GetRemoteMembersByDomain dom -> embedClient $ lookupRemoteMembersByDomain dom - GetLocalMembersByDomain dom -> embedClient $ lookupLocalMembersByDomain dom + AddMLSClients lcnv quid cs -> do + logEffect "MemberStore.AddMLSClients" + embedClient $ addMLSClients lcnv quid cs + PlanClientRemoval lcnv cids -> do + logEffect "MemberStore.PlanClientRemoval" + embedClient $ planMLSClientRemoval lcnv cids + RemoveMLSClients lcnv quid cs -> do + logEffect "MemberStore.RemoveMLSClients" + embedClient $ removeMLSClients lcnv quid cs + RemoveAllMLSClients gid -> do + logEffect "MemberStore.RemoveAllMLSClients" + embedClient $ removeAllMLSClients gid + LookupMLSClients lcnv -> do + logEffect "MemberStore.LookupMLSClients" + embedClient $ lookupMLSClients lcnv + LookupMLSClientLeafIndices lcnv -> do + logEffect "MemberStore.LookupMLSClientLeafIndices" + embedClient $ lookupMLSClientLeafIndices lcnv + GetRemoteMembersByDomain dom -> do + logEffect "MemberStore.GetRemoteMembersByDomain" + embedClient $ lookupRemoteMembersByDomain dom + GetLocalMembersByDomain dom -> do + logEffect "MemberStore.GetLocalMembersByDomain" + embedClient $ lookupLocalMembersByDomain dom diff --git a/services/galley/src/Galley/Cassandra/ConversationList.hs b/services/galley/src/Galley/Cassandra/ConversationList.hs index 623fd7e59b0..9c43da34793 100644 --- a/services/galley/src/Galley/Cassandra/ConversationList.hs +++ b/services/galley/src/Galley/Cassandra/ConversationList.hs @@ -29,10 +29,12 @@ import Data.Range import Galley.Cassandra.Instances () import Galley.Cassandra.Queries qualified as Cql import Galley.Cassandra.Store +import Galley.Cassandra.Util import Galley.Effects.ListItems import Imports hiding (max) import Polysemy import Polysemy.Input +import Polysemy.TinyLog import Wire.Sem.Paging.Cassandra -- | Deprecated, use 'localConversationIdsPageFrom' @@ -66,27 +68,36 @@ remoteConversationIdsPageFrom usr pagingState max = interpretConversationListToCassandra :: ( Member (Embed IO) r, - Member (Input ClientState) r + Member (Input ClientState) r, + Member TinyLog r ) => Sem (ListItems CassandraPaging ConvId ': r) a -> Sem r a interpretConversationListToCassandra = interpret $ \case - ListItems uid ps max -> embedClient $ localConversationIdsPageFrom uid ps max + ListItems uid ps max -> do + logEffect "ConversationList.ListItems" + embedClient $ localConversationIdsPageFrom uid ps max interpretRemoteConversationListToCassandra :: ( Member (Embed IO) r, - Member (Input ClientState) r + Member (Input ClientState) r, + Member TinyLog r ) => Sem (ListItems CassandraPaging (Remote ConvId) ': r) a -> Sem r a interpretRemoteConversationListToCassandra = interpret $ \case - ListItems uid ps max -> embedClient $ remoteConversationIdsPageFrom uid ps (fromRange max) + ListItems uid ps max -> do + logEffect "RemoteConversationList.ListItems" + embedClient $ remoteConversationIdsPageFrom uid ps (fromRange max) interpretLegacyConversationListToCassandra :: ( Member (Embed IO) r, - Member (Input ClientState) r + Member (Input ClientState) r, + Member TinyLog r ) => Sem (ListItems LegacyPaging ConvId ': r) a -> Sem r a interpretLegacyConversationListToCassandra = interpret $ \case - ListItems uid ps max -> embedClient $ conversationIdsFrom uid ps max + ListItems uid ps max -> do + logEffect "LegacyConversationList.ListItems" + embedClient $ conversationIdsFrom uid ps max diff --git a/services/galley/src/Galley/Cassandra/CustomBackend.hs b/services/galley/src/Galley/Cassandra/CustomBackend.hs index cabe4a3a43e..f06f8187ac9 100644 --- a/services/galley/src/Galley/Cassandra/CustomBackend.hs +++ b/services/galley/src/Galley/Cassandra/CustomBackend.hs @@ -24,22 +24,31 @@ import Data.Domain (Domain) import Galley.Cassandra.Instances () import Galley.Cassandra.Queries qualified as Cql import Galley.Cassandra.Store +import Galley.Cassandra.Util import Galley.Effects.CustomBackendStore (CustomBackendStore (..)) import Imports import Polysemy import Polysemy.Input +import Polysemy.TinyLog import Wire.API.CustomBackend interpretCustomBackendStoreToCassandra :: ( Member (Embed IO) r, - Member (Input ClientState) r + Member (Input ClientState) r, + Member TinyLog r ) => Sem (CustomBackendStore ': r) a -> Sem r a interpretCustomBackendStoreToCassandra = interpret $ \case - GetCustomBackend dom -> embedClient $ getCustomBackend dom - SetCustomBackend dom b -> embedClient $ setCustomBackend dom b - DeleteCustomBackend dom -> embedClient $ deleteCustomBackend dom + GetCustomBackend dom -> do + logEffect "CustomBackendStore.GetCustomBackend" + embedClient $ getCustomBackend dom + SetCustomBackend dom b -> do + logEffect "CustomBackendStore.SetCustomBackend" + embedClient $ setCustomBackend dom b + DeleteCustomBackend dom -> do + logEffect "CustomBackendStore.DeleteCustomBackend" + embedClient $ deleteCustomBackend dom getCustomBackend :: MonadClient m => Domain -> m (Maybe CustomBackend) getCustomBackend domain = diff --git a/services/galley/src/Galley/Cassandra/LegalHold.hs b/services/galley/src/Galley/Cassandra/LegalHold.hs index db37db2657f..ccc4b9c53f5 100644 --- a/services/galley/src/Galley/Cassandra/LegalHold.hs +++ b/services/galley/src/Galley/Cassandra/LegalHold.hs @@ -38,6 +38,7 @@ import Data.Misc import Galley.Cassandra.Instances () import Galley.Cassandra.Queries qualified as Q import Galley.Cassandra.Store +import Galley.Cassandra.Util import Galley.Effects.LegalHoldStore (LegalHoldStore (..)) import Galley.Env import Galley.External.LegalHoldService.Internal @@ -50,6 +51,7 @@ import OpenSSL.PEM qualified as SSL import OpenSSL.RSA qualified as SSL import Polysemy import Polysemy.Input +import Polysemy.TinyLog import Ssl.Util qualified as SSL import Wire.API.Provider.Service import Wire.API.User.Client.Prekey @@ -57,28 +59,53 @@ import Wire.API.User.Client.Prekey interpretLegalHoldStoreToCassandra :: ( Member (Embed IO) r, Member (Input ClientState) r, - Member (Input Env) r + Member (Input Env) r, + Member TinyLog r ) => FeatureLegalHold -> Sem (LegalHoldStore ': r) a -> Sem r a interpretLegalHoldStoreToCassandra lh = interpret $ \case - CreateSettings s -> embedClient $ createSettings s - GetSettings tid -> embedClient $ getSettings tid - RemoveSettings tid -> embedClient $ removeSettings tid - InsertPendingPrekeys uid pkeys -> embedClient $ insertPendingPrekeys uid pkeys - SelectPendingPrekeys uid -> embedClient $ selectPendingPrekeys uid - DropPendingPrekeys uid -> embedClient $ dropPendingPrekeys uid - SetUserLegalHoldStatus tid uid st -> embedClient $ setUserLegalHoldStatus tid uid st - SetTeamLegalholdWhitelisted tid -> embedClient $ setTeamLegalholdWhitelisted tid - UnsetTeamLegalholdWhitelisted tid -> embedClient $ unsetTeamLegalholdWhitelisted tid - IsTeamLegalholdWhitelisted tid -> embedClient $ isTeamLegalholdWhitelisted lh tid + CreateSettings s -> do + logEffect "LegalHoldStore.CreateSettings" + embedClient $ createSettings s + GetSettings tid -> do + logEffect "LegalHoldStore.GetSettings" + embedClient $ getSettings tid + RemoveSettings tid -> do + logEffect "LegalHoldStore.RemoveSettings" + embedClient $ removeSettings tid + InsertPendingPrekeys uid pkeys -> do + logEffect "LegalHoldStore.InsertPendingPrekeys" + embedClient $ insertPendingPrekeys uid pkeys + SelectPendingPrekeys uid -> do + logEffect "LegalHoldStore.SelectPendingPrekeys" + embedClient $ selectPendingPrekeys uid + DropPendingPrekeys uid -> do + logEffect "LegalHoldStore.DropPendingPrekeys" + embedClient $ dropPendingPrekeys uid + SetUserLegalHoldStatus tid uid st -> do + logEffect "LegalHoldStore.SetUserLegalHoldStatus" + embedClient $ setUserLegalHoldStatus tid uid st + SetTeamLegalholdWhitelisted tid -> do + logEffect "LegalHoldStore.SetTeamLegalholdWhitelisted" + embedClient $ setTeamLegalholdWhitelisted tid + UnsetTeamLegalholdWhitelisted tid -> do + logEffect "LegalHoldStore.UnsetTeamLegalholdWhitelisted" + embedClient $ unsetTeamLegalholdWhitelisted tid + IsTeamLegalholdWhitelisted tid -> do + logEffect "LegalHoldStore.IsTeamLegalholdWhitelisted" + embedClient $ isTeamLegalholdWhitelisted lh tid -- FUTUREWORK: should this action be part of a separate effect? - MakeVerifiedRequestFreshManager fpr url r -> + MakeVerifiedRequestFreshManager fpr url r -> do + logEffect "LegalHoldStore.MakeVerifiedRequestFreshManager" embedApp $ makeVerifiedRequestFreshManager fpr url r - MakeVerifiedRequest fpr url r -> + MakeVerifiedRequest fpr url r -> do + logEffect "LegalHoldStore.MakeVerifiedRequest" embedApp $ makeVerifiedRequest fpr url r - ValidateServiceKey sk -> embed @IO $ validateServiceKey sk + ValidateServiceKey sk -> do + logEffect "LegalHoldStore.ValidateServiceKey" + embed @IO $ validateServiceKey sk -- | Returns 'False' if legal hold is not enabled for this team -- The Caller is responsible for checking whether legal hold is enabled for this team diff --git a/services/galley/src/Galley/Cassandra/Proposal.hs b/services/galley/src/Galley/Cassandra/Proposal.hs index 04263a234c2..68aae2e0f07 100644 --- a/services/galley/src/Galley/Cassandra/Proposal.hs +++ b/services/galley/src/Galley/Cassandra/Proposal.hs @@ -25,10 +25,12 @@ import Cassandra import Data.Timeout import Galley.Cassandra.Instances () import Galley.Cassandra.Store +import Galley.Cassandra.Util import Galley.Effects.ProposalStore import Imports import Polysemy import Polysemy.Input +import Polysemy.TinyLog import Wire.API.MLS.Epoch import Wire.API.MLS.Group import Wire.API.MLS.Proposal @@ -40,24 +42,28 @@ defaultTTL = 28 # Day interpretProposalStoreToCassandra :: ( Member (Embed IO) r, - Member (Input ClientState) r + Member (Input ClientState) r, + Member TinyLog r ) => Sem (ProposalStore ': r) a -> Sem r a -interpretProposalStoreToCassandra = - interpret $ - embedClient . \case - StoreProposal groupId epoch ref origin raw -> - retry x5 $ - write (storeQuery defaultTTL) (params LocalQuorum (groupId, epoch, ref, origin, raw)) - GetProposal groupId epoch ref -> - runIdentity <$$> retry x1 (query1 getQuery (params LocalQuorum (groupId, epoch, ref))) - GetAllPendingProposalRefs groupId epoch -> - runIdentity <$$> retry x1 (query getAllPendingRef (params LocalQuorum (groupId, epoch))) - GetAllPendingProposals groupId epoch -> - retry x1 (query getAllPending (params LocalQuorum (groupId, epoch))) - DeleteAllProposals groupId -> - retry x5 (write deleteAllProposalsForGroup (params LocalQuorum (Identity groupId))) +interpretProposalStoreToCassandra = interpret $ \case + StoreProposal groupId epoch ref origin raw -> do + logEffect "ProposalStore.StoreProposal" + embedClient . retry x5 $ + write (storeQuery defaultTTL) (params LocalQuorum (groupId, epoch, ref, origin, raw)) + GetProposal groupId epoch ref -> do + logEffect "ProposalStore.GetProposal" + embedClient (runIdentity <$$> retry x1 (query1 getQuery (params LocalQuorum (groupId, epoch, ref)))) + GetAllPendingProposalRefs groupId epoch -> do + logEffect "ProposalStore.GetAllPendingProposalRefs" + embedClient (runIdentity <$$> retry x1 (query getAllPendingRef (params LocalQuorum (groupId, epoch)))) + GetAllPendingProposals groupId epoch -> do + logEffect "ProposalStore.GetAllPendingProposals" + embedClient $ retry x1 (query getAllPending (params LocalQuorum (groupId, epoch))) + DeleteAllProposals groupId -> do + logEffect "ProposalStore.DeleteAllProposals" + embedClient $ retry x5 (write deleteAllProposalsForGroup (params LocalQuorum (Identity groupId))) storeQuery :: Timeout -> PrepQuery W (GroupId, Epoch, ProposalRef, ProposalOrigin, RawMLS Proposal) () storeQuery ttl = diff --git a/services/galley/src/Galley/Cassandra/SearchVisibility.hs b/services/galley/src/Galley/Cassandra/SearchVisibility.hs index 5612739030f..84505b5809a 100644 --- a/services/galley/src/Galley/Cassandra/SearchVisibility.hs +++ b/services/galley/src/Galley/Cassandra/SearchVisibility.hs @@ -22,22 +22,31 @@ import Data.Id import Galley.Cassandra.Instances () import Galley.Cassandra.Queries import Galley.Cassandra.Store +import Galley.Cassandra.Util import Galley.Effects.SearchVisibilityStore (SearchVisibilityStore (..)) import Imports import Polysemy import Polysemy.Input +import Polysemy.TinyLog import Wire.API.Team.SearchVisibility interpretSearchVisibilityStoreToCassandra :: ( Member (Embed IO) r, - Member (Input ClientState) r + Member (Input ClientState) r, + Member TinyLog r ) => Sem (SearchVisibilityStore ': r) a -> Sem r a interpretSearchVisibilityStoreToCassandra = interpret $ \case - GetSearchVisibility tid -> embedClient $ getSearchVisibility tid - SetSearchVisibility tid value -> embedClient $ setSearchVisibility tid value - ResetSearchVisibility tid -> embedClient $ resetSearchVisibility tid + GetSearchVisibility tid -> do + logEffect "SearchVisibilityStore.GetSearchVisibility" + embedClient $ getSearchVisibility tid + SetSearchVisibility tid value -> do + logEffect "SearchVisibilityStore.SetSearchVisibility" + embedClient $ setSearchVisibility tid value + ResetSearchVisibility tid -> do + logEffect "SearchVisibilityStore.ResetSearchVisibility" + embedClient $ resetSearchVisibility tid -- | Return whether a given team is allowed to enable/disable sso getSearchVisibility :: MonadClient m => TeamId -> m TeamSearchVisibility diff --git a/services/galley/src/Galley/Cassandra/Services.hs b/services/galley/src/Galley/Cassandra/Services.hs index 17cc86f2cc6..47810380b34 100644 --- a/services/galley/src/Galley/Cassandra/Services.hs +++ b/services/galley/src/Galley/Cassandra/Services.hs @@ -22,6 +22,7 @@ import Control.Lens import Data.Id import Galley.Cassandra.Queries import Galley.Cassandra.Store +import Galley.Cassandra.Util import Galley.Data.Services import Galley.Effects.ServiceStore hiding (deleteService) import Galley.Types.Bot.Service qualified as Bot @@ -29,6 +30,7 @@ import Galley.Types.Conversations.Members (lmService, newMember) import Imports import Polysemy import Polysemy.Input +import Polysemy.TinyLog import Wire.API.Provider.Service hiding (DeleteService) -- FUTUREWORK: support adding bots to a remote conversation @@ -49,14 +51,21 @@ addBotMember s bot cnv = do interpretServiceStoreToCassandra :: ( Member (Embed IO) r, - Member (Input ClientState) r + Member (Input ClientState) r, + Member TinyLog r ) => Sem (ServiceStore ': r) a -> Sem r a interpretServiceStoreToCassandra = interpret $ \case - CreateService s -> embedClient $ insertService s - GetService sr -> embedClient $ lookupService sr - DeleteService sr -> embedClient $ deleteService sr + CreateService s -> do + logEffect "ServiceStore.CreateService" + embedClient $ insertService s + GetService sr -> do + logEffect "ServiceStore.GetService" + embedClient $ lookupService sr + DeleteService sr -> do + logEffect "ServiceStore.DeleteService" + embedClient $ deleteService sr insertService :: MonadClient m => Bot.Service -> m () insertService s = do diff --git a/services/galley/src/Galley/Cassandra/SubConversation.hs b/services/galley/src/Galley/Cassandra/SubConversation.hs index 5827435aaa3..687c904402a 100644 --- a/services/galley/src/Galley/Cassandra/SubConversation.hs +++ b/services/galley/src/Galley/Cassandra/SubConversation.hs @@ -31,10 +31,12 @@ import Galley.API.MLS.Types import Galley.Cassandra.Conversation.MLS import Galley.Cassandra.Queries qualified as Cql import Galley.Cassandra.Store (embedClient) +import Galley.Cassandra.Util import Galley.Effects.SubConversationStore (SubConversationStore (..)) import Imports hiding (cs) import Polysemy import Polysemy.Input +import Polysemy.TinyLog import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite import Wire.API.MLS.Group @@ -124,20 +126,40 @@ listSubConversations cid = do ) interpretSubConversationStoreToCassandra :: - Members '[Embed IO, Input ClientState] r => + ( Member (Embed IO) r, + Member (Input ClientState) r, + Member TinyLog r + ) => Sem (SubConversationStore ': r) a -> Sem r a interpretSubConversationStoreToCassandra = interpret $ \case - CreateSubConversation convId subConvId suite groupId -> + CreateSubConversation convId subConvId suite groupId -> do + logEffect "SubConversationStore.CreateSubConversation" embedClient (insertSubConversation convId subConvId suite groupId) - GetSubConversation convId subConvId -> embedClient (selectSubConversation convId subConvId) - GetSubConversationGroupInfo convId subConvId -> embedClient (selectSubConvGroupInfo convId subConvId) - GetSubConversationEpoch convId subConvId -> embedClient (selectSubConvEpoch convId subConvId) - SetSubConversationGroupInfo convId subConvId mPgs -> embedClient (updateSubConvGroupInfo convId subConvId mPgs) - SetSubConversationEpoch cid sconv epoch -> embedClient $ setEpochForSubConversation cid sconv epoch - SetSubConversationCipherSuite cid sconv cs -> embedClient $ setCipherSuiteForSubConversation cid sconv cs - ListSubConversations cid -> embedClient $ listSubConversations cid - DeleteSubConversation convId subConvId -> embedClient $ deleteSubConversation convId subConvId + GetSubConversation convId subConvId -> do + logEffect "SubConversationStore.GetSubConversation" + embedClient (selectSubConversation convId subConvId) + GetSubConversationGroupInfo convId subConvId -> do + logEffect "SubConversationStore.GetSubConversationGroupInfo" + embedClient (selectSubConvGroupInfo convId subConvId) + GetSubConversationEpoch convId subConvId -> do + logEffect "SubConversationStore.GetSubConversationEpoch" + embedClient (selectSubConvEpoch convId subConvId) + SetSubConversationGroupInfo convId subConvId mPgs -> do + logEffect "SubConversationStore.SetSubConversationGroupInfo" + embedClient (updateSubConvGroupInfo convId subConvId mPgs) + SetSubConversationEpoch cid sconv epoch -> do + logEffect "SubConversationStore.SetSubConversationEpoch" + embedClient (setEpochForSubConversation cid sconv epoch) + SetSubConversationCipherSuite cid sconv cs -> do + logEffect "SubConversationStore.SetSubConversationCipherSuite" + embedClient (setCipherSuiteForSubConversation cid sconv cs) + ListSubConversations cid -> do + logEffect "SubConversationStore.ListSubConversations" + embedClient (listSubConversations cid) + DeleteSubConversation convId subConvId -> do + logEffect "SubConversationStore.DeleteSubConversation" + embedClient (deleteSubConversation convId subConvId) -------------------------------------------------------------------------------- -- Utilities diff --git a/services/galley/src/Galley/Cassandra/Team.hs b/services/galley/src/Galley/Cassandra/Team.hs index f6322b1015e..84b5458d115 100644 --- a/services/galley/src/Galley/Cassandra/Team.hs +++ b/services/galley/src/Galley/Cassandra/Team.hs @@ -44,6 +44,7 @@ import Galley.Cassandra.Conversation qualified as C import Galley.Cassandra.LegalHold (isTeamLegalholdWhitelisted) import Galley.Cassandra.Queries qualified as Cql import Galley.Cassandra.Store +import Galley.Cassandra.Util import Galley.Effects.ListItems import Galley.Effects.TeamMemberStore import Galley.Effects.TeamStore (TeamStore (..)) @@ -54,6 +55,7 @@ import Galley.Types.Teams import Imports hiding (Set, max) import Polysemy import Polysemy.Input +import Polysemy.TinyLog import UnliftIO qualified import Wire.API.Routes.Internal.Galley.TeamsIntra import Wire.API.Team @@ -65,92 +67,160 @@ import Wire.Sem.Paging.Cassandra interpretTeamStoreToCassandra :: ( Member (Embed IO) r, Member (Input Env) r, - Member (Input ClientState) r + Member (Input ClientState) r, + Member TinyLog r ) => FeatureLegalHold -> Sem (TeamStore ': r) a -> Sem r a interpretTeamStoreToCassandra lh = interpret $ \case - CreateTeamMember tid mem -> embedClient $ addTeamMember tid mem - SetTeamMemberPermissions perm0 tid uid perm1 -> - embedClient $ updateTeamMember perm0 tid uid perm1 - CreateTeam t uid n i k b -> embedClient $ createTeam t uid n i k b - DeleteTeamMember tid uid -> embedClient $ removeTeamMember tid uid - GetBillingTeamMembers tid -> embedClient $ listBillingTeamMembers tid - GetTeamAdmins tid -> embedClient $ listTeamAdmins tid - GetTeam tid -> embedClient $ team tid - GetTeamName tid -> embedClient $ getTeamName tid - GetTeamConversation tid cid -> embedClient $ teamConversation tid cid - GetTeamConversations tid -> embedClient $ getTeamConversations tid - SelectTeams uid tids -> embedClient $ teamIdsOf uid tids - GetTeamMember tid uid -> embedClient $ teamMember lh tid uid - GetTeamMembersWithLimit tid n -> embedClient $ teamMembersWithLimit lh tid n - GetTeamMembers tid -> embedClient $ teamMembersCollectedWithPagination lh tid - SelectTeamMembers tid uids -> embedClient $ teamMembersLimited lh tid uids - GetUserTeams uid -> embedClient $ userTeams uid - GetUsersTeams uids -> embedClient $ usersTeams uids - GetOneUserTeam uid -> embedClient $ oneUserTeam uid - GetTeamsBindings tid -> embedClient $ getTeamsBindings tid - GetTeamBinding tid -> embedClient $ getTeamBinding tid - GetTeamCreationTime tid -> embedClient $ teamCreationTime tid - DeleteTeam tid -> embedClient $ deleteTeam tid - DeleteTeamConversation tid cid -> embedClient $ removeTeamConv tid cid - SetTeamData tid upd -> embedClient $ updateTeam tid upd - SetTeamStatus tid st -> embedClient $ updateTeamStatus tid st - FanoutLimit -> embedApp $ currentFanoutLimit <$> view options - GetLegalHoldFlag -> + CreateTeamMember tid mem -> do + logEffect "TeamStore.CreateTeamMember" + embedClient (addTeamMember tid mem) + SetTeamMemberPermissions perm0 tid uid perm1 -> do + logEffect "TeamStore.SetTeamMemberPermissions" + embedClient (updateTeamMember perm0 tid uid perm1) + CreateTeam t uid n i k b -> do + logEffect "TeamStore.CreateTeam" + embedClient (createTeam t uid n i k b) + DeleteTeamMember tid uid -> do + logEffect "TeamStore.DeleteTeamMember" + embedClient (removeTeamMember tid uid) + GetBillingTeamMembers tid -> do + logEffect "TeamStore.GetBillingTeamMembers" + embedClient (listBillingTeamMembers tid) + GetTeamAdmins tid -> do + logEffect "TeamStore.GetTeamAdmins" + embedClient (listTeamAdmins tid) + GetTeam tid -> do + logEffect "TeamStore.GetTeam" + embedClient (team tid) + GetTeamName tid -> do + logEffect "TeamStore.GetTeamName" + embedClient (getTeamName tid) + GetTeamConversation tid cid -> do + logEffect "TeamStore.GetTeamConversation" + embedClient (teamConversation tid cid) + GetTeamConversations tid -> do + logEffect "TeamStore.GetTeamConversations" + embedClient (getTeamConversations tid) + SelectTeams uid tids -> do + logEffect "TeamStore.SelectTeams" + embedClient (teamIdsOf uid tids) + GetTeamMember tid uid -> do + logEffect "TeamStore.GetTeamMember" + embedClient (teamMember lh tid uid) + GetTeamMembersWithLimit tid n -> do + logEffect "TeamStore.GetTeamMembersWithLimit" + embedClient (teamMembersWithLimit lh tid n) + GetTeamMembers tid -> do + logEffect "TeamStore.GetTeamMembers" + embedClient (teamMembersCollectedWithPagination lh tid) + SelectTeamMembers tid uids -> do + logEffect "TeamStore.SelectTeamMembers" + embedClient (teamMembersLimited lh tid uids) + GetUserTeams uid -> do + logEffect "TeamStore.GetUserTeams" + embedClient (userTeams uid) + GetUsersTeams uids -> do + logEffect "TeamStore.GetUsersTeams" + embedClient (usersTeams uids) + GetOneUserTeam uid -> do + logEffect "TeamStore.GetOneUserTeam" + embedClient (oneUserTeam uid) + GetTeamsBindings tid -> do + logEffect "TeamStore.GetTeamsBindings" + embedClient (getTeamsBindings tid) + GetTeamBinding tid -> do + logEffect "TeamStore.GetTeamBinding" + embedClient (getTeamBinding tid) + GetTeamCreationTime tid -> do + logEffect "TeamStore.GetTeamCreationTime" + embedClient (teamCreationTime tid) + DeleteTeam tid -> do + logEffect "TeamStore.DeleteTeam" + embedClient (deleteTeam tid) + DeleteTeamConversation tid cid -> do + logEffect "TeamStore.DeleteTeamConversation" + embedClient (removeTeamConv tid cid) + SetTeamData tid upd -> do + logEffect "TeamStore.SetTeamData" + embedClient (updateTeam tid upd) + SetTeamStatus tid st -> do + logEffect "TeamStore.SetTeamStatus" + embedClient (updateTeamStatus tid st) + FanoutLimit -> do + logEffect "TeamStore.FanoutLimit" + embedApp (currentFanoutLimit <$> view options) + GetLegalHoldFlag -> do + logEffect "TeamStore.GetLegalHoldFlag" view (options . settings . featureFlags . flagLegalHold) <$> input EnqueueTeamEvent e -> do + logEffect "TeamStore.EnqueueTeamEvent" menv <- inputs (view aEnv) for_ menv $ \env -> - embed @IO $ Aws.execute env (Aws.enqueue e) - SelectTeamMembersPaginated tid uids mps lim -> embedClient $ selectSomeTeamMembersPaginated lh tid uids mps lim + embed @IO (Aws.execute env (Aws.enqueue e)) + SelectTeamMembersPaginated tid uids mps lim -> do + logEffect "TeamStore.SelectTeamMembersPaginated" + embedClient (selectSomeTeamMembersPaginated lh tid uids mps lim) interpretTeamListToCassandra :: ( Member (Embed IO) r, - Member (Input ClientState) r + Member (Input ClientState) r, + Member TinyLog r ) => Sem (ListItems LegacyPaging TeamId ': r) a -> Sem r a interpretTeamListToCassandra = interpret $ \case - ListItems uid ps lim -> embedClient $ teamIdsFrom uid ps lim + ListItems uid ps lim -> do + logEffect "TeamList.ListItems" + embedClient $ teamIdsFrom uid ps lim interpretInternalTeamListToCassandra :: ( Member (Embed IO) r, - Member (Input ClientState) r + Member (Input ClientState) r, + Member TinyLog r ) => Sem (ListItems InternalPaging TeamId ': r) a -> Sem r a interpretInternalTeamListToCassandra = interpret $ \case - ListItems uid mps lim -> embedClient $ case mps of - Nothing -> do - page <- teamIdsForPagination uid Nothing lim - mkInternalPage page pure - Just ps -> ipNext ps + ListItems uid mps lim -> do + logEffect "InternalTeamList.ListItems" + embedClient $ case mps of + Nothing -> do + page <- teamIdsForPagination uid Nothing lim + mkInternalPage page pure + Just ps -> ipNext ps interpretTeamMemberStoreToCassandra :: ( Member (Embed IO) r, - Member (Input ClientState) r + Member (Input ClientState) r, + Member TinyLog r ) => FeatureLegalHold -> Sem (TeamMemberStore InternalPaging ': r) a -> Sem r a interpretTeamMemberStoreToCassandra lh = interpret $ \case - ListTeamMembers tid mps lim -> embedClient $ case mps of - Nothing -> do - page <- teamMembersForPagination tid Nothing lim - mkInternalPage page (newTeamMember' lh tid) - Just ps -> ipNext ps + ListTeamMembers tid mps lim -> do + logEffect "TeamMemberStore.ListTeamMembers" + embedClient $ case mps of + Nothing -> do + page <- teamMembersForPagination tid Nothing lim + mkInternalPage page (newTeamMember' lh tid) + Just ps -> ipNext ps interpretTeamMemberStoreToCassandraWithPaging :: ( Member (Embed IO) r, - Member (Input ClientState) r + Member (Input ClientState) r, + Member TinyLog r ) => FeatureLegalHold -> Sem (TeamMemberStore CassandraPaging ': r) a -> Sem r a interpretTeamMemberStoreToCassandraWithPaging lh = interpret $ \case - ListTeamMembers tid mps lim -> embedClient $ teamMembersPageFrom lh tid mps lim + ListTeamMembers tid mps lim -> do + logEffect "TeamMemberStore.ListTeamMembers" + embedClient $ teamMembersPageFrom lh tid mps lim createTeam :: Maybe TeamId -> diff --git a/services/galley/src/Galley/Cassandra/TeamFeatures.hs b/services/galley/src/Galley/Cassandra/TeamFeatures.hs index 8e33a267a02..8d415a918c4 100644 --- a/services/galley/src/Galley/Cassandra/TeamFeatures.hs +++ b/services/galley/src/Galley/Cassandra/TeamFeatures.hs @@ -29,10 +29,12 @@ import Data.Misc (HttpsUrl) import Data.Time import Galley.Cassandra.Instances () import Galley.Cassandra.Store +import Galley.Cassandra.Util import Galley.Effects.TeamFeatureStore qualified as TFS import Imports import Polysemy import Polysemy.Input +import Polysemy.TinyLog import UnliftIO.Async (pooledMapConcurrentlyN) import Wire.API.Conversation.Protocol (ProtocolTag) import Wire.API.MLS.CipherSuite @@ -40,16 +42,27 @@ import Wire.API.Team.Feature interpretTeamFeatureStoreToCassandra :: ( Member (Embed IO) r, - Member (Input ClientState) r + Member (Input ClientState) r, + Member TinyLog r ) => Sem (TFS.TeamFeatureStore ': r) a -> Sem r a interpretTeamFeatureStoreToCassandra = interpret $ \case - TFS.GetFeatureConfig sing tid -> embedClient $ getFeatureConfig sing tid - TFS.GetFeatureConfigMulti sing tids -> embedClient $ getFeatureConfigMulti sing tids - TFS.SetFeatureConfig sing tid wsnl -> embedClient $ setFeatureConfig sing tid wsnl - TFS.GetFeatureLockStatus sing tid -> embedClient $ getFeatureLockStatus sing tid - TFS.SetFeatureLockStatus sing tid ls -> embedClient $ setFeatureLockStatus sing tid ls + TFS.GetFeatureConfig sing tid -> do + logEffect "TeamFeatureStore.GetFeatureConfig" + embedClient $ getFeatureConfig sing tid + TFS.GetFeatureConfigMulti sing tids -> do + logEffect "TeamFeatureStore.GetFeatureConfigMulti" + embedClient $ getFeatureConfigMulti sing tids + TFS.SetFeatureConfig sing tid wsnl -> do + logEffect "TeamFeatureStore.SetFeatureConfig" + embedClient $ setFeatureConfig sing tid wsnl + TFS.GetFeatureLockStatus sing tid -> do + logEffect "TeamFeatureStore.GetFeatureLockStatus" + embedClient $ getFeatureLockStatus sing tid + TFS.SetFeatureLockStatus sing tid ls -> do + logEffect "TeamFeatureStore.SetFeatureLockStatus" + embedClient $ setFeatureLockStatus sing tid ls getFeatureConfig :: MonadClient m => FeatureSingleton cfg -> TeamId -> m (Maybe (WithStatusNoLock cfg)) getFeatureConfig FeatureSingletonLegalholdConfig tid = getTrivialConfigC "legalhold_status" tid diff --git a/services/galley/src/Galley/Cassandra/TeamNotifications.hs b/services/galley/src/Galley/Cassandra/TeamNotifications.hs index e34762d5458..2138cbd6812 100644 --- a/services/galley/src/Galley/Cassandra/TeamNotifications.hs +++ b/services/galley/src/Galley/Cassandra/TeamNotifications.hs @@ -38,6 +38,7 @@ import Data.Sequence qualified as Seq import Data.Time (nominalDay, nominalDiffTimeToSeconds) import Data.UUID.V1 qualified as UUID import Galley.Cassandra.Store +import Galley.Cassandra.Util import Galley.Data.TeamNotifications import Galley.Effects import Galley.Effects.TeamNotificationStore (TeamNotificationStore (..)) @@ -46,18 +47,26 @@ import Network.HTTP.Types import Network.Wai.Utilities hiding (Error) import Polysemy import Polysemy.Input +import Polysemy.TinyLog hiding (err) import Wire.API.Internal.Notification interpretTeamNotificationStoreToCassandra :: ( Member (Embed IO) r, - Member (Input ClientState) r + Member (Input ClientState) r, + Member TinyLog r ) => Sem (TeamNotificationStore ': r) a -> Sem r a interpretTeamNotificationStoreToCassandra = interpret $ \case - CreateTeamNotification tid nid objs -> embedClient $ add tid nid objs - GetTeamNotifications tid mnid lim -> embedClient $ fetch tid mnid lim - MkNotificationId -> embed mkNotificationId + CreateTeamNotification tid nid objs -> do + logEffect "TeamNotificationStore.CreateTeamNotification" + embedClient $ add tid nid objs + GetTeamNotifications tid mnid lim -> do + logEffect "TeamNotificationStore.GetTeamNotifications" + embedClient $ fetch tid mnid lim + MkNotificationId -> do + logEffect "TeamNotificationStore.MkNotificationId" + embed mkNotificationId -- | 'Data.UUID.V1.nextUUID' is sometimes unsuccessful, so we try a few times. mkNotificationId :: IO NotificationId diff --git a/services/galley/src/Galley/Cassandra/Util.hs b/services/galley/src/Galley/Cassandra/Util.hs new file mode 100644 index 00000000000..2e3169fb523 --- /dev/null +++ b/services/galley/src/Galley/Cassandra/Util.hs @@ -0,0 +1,27 @@ +-- This file is part of the Wire Server implementation. +-- +-- Copyright (C) 2022 Wire Swiss GmbH +-- +-- This program is free software: you can redistribute it and/or modify it under +-- the terms of the GNU Affero General Public License as published by the Free +-- Software Foundation, either version 3 of the License, or (at your option) any +-- later version. +-- +-- This program is distributed in the hope that it will be useful, but WITHOUT +-- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +-- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +-- details. +-- +-- You should have received a copy of the GNU Affero General Public License along +-- with this program. If not, see . + +module Galley.Cassandra.Util where + +import Data.ByteString +import Imports +import Polysemy +import Polysemy.TinyLog +import System.Logger.Message + +logEffect :: Member TinyLog r => ByteString -> Sem r () +logEffect = debug . msg . val diff --git a/services/galley/src/Galley/External.hs b/services/galley/src/Galley/External.hs index 605ac731d0e..c1bf5eecc48 100644 --- a/services/galley/src/Galley/External.hs +++ b/services/galley/src/Galley/External.hs @@ -26,6 +26,7 @@ import Data.ByteString.Conversion.To import Data.Id import Data.Misc import Galley.Cassandra.Services +import Galley.Cassandra.Util import Galley.Data.Services (BotMember, botMemId, botMemService) import Galley.Effects import Galley.Effects.ExternalAccess (ExternalAccess (..)) @@ -39,6 +40,7 @@ import Network.HTTP.Types.Method import Network.HTTP.Types.Status (status410) import Polysemy import Polysemy.Input +import Polysemy.TinyLog import Ssl.Util (withVerifiedSslConnection) import System.Logger.Class qualified as Log import System.Logger.Message (field, msg, val, (~~)) @@ -49,14 +51,21 @@ import Wire.API.Provider.Service (serviceRefId, serviceRefProvider) interpretExternalAccess :: ( Member (Embed IO) r, - Member (Input Env) r + Member (Input Env) r, + Member TinyLog r ) => Sem (ExternalAccess ': r) a -> Sem r a interpretExternalAccess = interpret $ \case - Deliver pp -> embedApp $ deliver (toList pp) - DeliverAsync pp -> embedApp $ deliverAsync (toList pp) - DeliverAndDeleteAsync cid pp -> embedApp $ deliverAndDeleteAsync cid (toList pp) + Deliver pp -> do + logEffect "ExternalAccess.Deliver" + embedApp $ deliver (toList pp) + DeliverAsync pp -> do + logEffect "ExternalAccess.DeliverAsync" + embedApp $ deliverAsync (toList pp) + DeliverAndDeleteAsync cid pp -> do + logEffect "ExternalAccess.DeliverAndDeleteAsync" + embedApp $ deliverAndDeleteAsync cid (toList pp) -- | Like deliver, but ignore orphaned bots and return immediately. -- diff --git a/services/galley/src/Galley/Intra/BackendNotificationQueue.hs b/services/galley/src/Galley/Intra/BackendNotificationQueue.hs index 0323a7dff45..756ce2379a6 100644 --- a/services/galley/src/Galley/Intra/BackendNotificationQueue.hs +++ b/services/galley/src/Galley/Intra/BackendNotificationQueue.hs @@ -8,6 +8,7 @@ import Control.Monad.Trans.Except import Control.Retry import Data.Domain import Data.Qualified +import Galley.Cassandra.Util import Galley.Effects.BackendNotificationQueueAccess (BackendNotificationQueueAccess (..)) import Galley.Env import Galley.Monad @@ -16,6 +17,7 @@ import Imports import Network.AMQP qualified as Q import Polysemy import Polysemy.Input +import Polysemy.TinyLog import System.Logger.Class qualified as Log import UnliftIO import Wire.API.Federation.BackendNotifications @@ -23,16 +25,20 @@ import Wire.API.Federation.Error interpretBackendNotificationQueueAccess :: ( Member (Embed IO) r, - Member (Input Env) r + Member (Input Env) r, + Member TinyLog r ) => Sem (BackendNotificationQueueAccess ': r) a -> Sem r a interpretBackendNotificationQueueAccess = interpret $ \case EnqueueNotification deliveryMode remote action -> do + logEffect "BackendNotificationQueueAccess.EnqueueNotification" embedApp . runExceptT $ enqueueNotification deliveryMode (tDomain remote) action EnqueueNotificationsConcurrently m xs rpc -> do + logEffect "BackendNotificationQueueAccess.EnqueueNotificationsConcurrently" embedApp . runExceptT $ enqueueNotificationsConcurrently m xs rpc EnqueueNotificationsConcurrentlyBuckets m xs rpc -> do + logEffect "BackendNotificationQueueAccess.EnqueueNotificationsConcurrentlyBuckets" embedApp . runExceptT $ enqueueNotificationsConcurrentlyBuckets m xs rpc getChannel :: ExceptT FederationError App (MVar Q.Channel) diff --git a/services/galley/src/Galley/Intra/Effects.hs b/services/galley/src/Galley/Intra/Effects.hs index 70a78b982a4..ef071400ab0 100644 --- a/services/galley/src/Galley/Intra/Effects.hs +++ b/services/galley/src/Galley/Intra/Effects.hs @@ -23,6 +23,7 @@ module Galley.Intra.Effects where import Galley.API.Error +import Galley.Cassandra.Util import Galley.Effects.BotAccess (BotAccess (..)) import Galley.Effects.BrigAccess (BrigAccess (..)) import Galley.Effects.SparAccess (SparAccess (..)) @@ -36,66 +37,106 @@ import Imports import Polysemy import Polysemy.Error import Polysemy.Input -import Polysemy.TinyLog qualified as P +import Polysemy.TinyLog import UnliftIO qualified interpretBrigAccess :: ( Member (Embed IO) r, Member (Error InternalError) r, - Member P.TinyLog r, + Member TinyLog r, Member (Input Env) r ) => Sem (BrigAccess ': r) a -> Sem r a interpretBrigAccess = interpret $ \case - GetConnectionsUnqualified uids muids mrel -> + GetConnectionsUnqualified uids muids mrel -> do + logEffect "BrigAccess.GetConnectionsUnqualified" embedApp $ getConnectionsUnqualified uids muids mrel - GetConnectionsUnqualifiedBidi uids1 uids2 mrel1 mrel2 -> + GetConnectionsUnqualifiedBidi uids1 uids2 mrel1 mrel2 -> do + logEffect "BrigAccess.GetConnectionsUnqualifiedBidi" embedApp $ UnliftIO.concurrently (getConnectionsUnqualified uids1 (Just uids2) mrel1) (getConnectionsUnqualified uids2 (Just uids1) mrel2) - GetConnections uids mquids mrel -> + GetConnections uids mquids mrel -> do + logEffect "BrigAccess.GetConnections" embedApp $ getConnections uids mquids mrel - PutConnectionInternal uc -> embedApp $ putConnectionInternal uc - ReauthUser uid reauth -> embedApp $ reAuthUser uid reauth - LookupActivatedUsers uids -> embedApp $ lookupActivatedUsers uids - GetUsers uids -> embedApp $ getUsers uids - DeleteUser uid -> embedApp $ deleteUser uid - GetContactList uid -> embedApp $ getContactList uid - GetRichInfoMultiUser uids -> embedApp $ getRichInfoMultiUser uids - GetSize tid -> embedApp $ getSize tid - LookupClients uids -> embedApp $ lookupClients uids - LookupClientsFull uids -> embedApp $ lookupClientsFull uids - NotifyClientsAboutLegalHoldRequest self other pk -> + PutConnectionInternal uc -> do + logEffect "BrigAccess.PutConnectionInternal" + embedApp $ putConnectionInternal uc + ReauthUser uid reauth -> do + logEffect "BrigAccess.ReauthUser" + embedApp $ reAuthUser uid reauth + LookupActivatedUsers uids -> do + logEffect "BrigAccess.LookupActivatedUsers" + embedApp $ lookupActivatedUsers uids + GetUsers uids -> do + logEffect "BrigAccess.GetUsers" + embedApp $ getUsers uids + DeleteUser uid -> do + logEffect "BrigAccess.DeleteUser" + embedApp $ deleteUser uid + GetContactList uid -> do + logEffect "BrigAccess.GetContactList" + embedApp $ getContactList uid + GetRichInfoMultiUser uids -> do + logEffect "BrigAccess.GetRichInfoMultiUser" + embedApp $ getRichInfoMultiUser uids + GetSize tid -> do + logEffect "BrigAccess.GetSize" + embedApp $ getSize tid + LookupClients uids -> do + logEffect "BrigAccess.LookupClients" + embedApp $ lookupClients uids + LookupClientsFull uids -> do + logEffect "BrigAccess.LookupClientsFull" + embedApp $ lookupClientsFull uids + NotifyClientsAboutLegalHoldRequest self other pk -> do + logEffect "BrigAccess.NotifyClientsAboutLegalHoldRequest" embedApp $ notifyClientsAboutLegalHoldRequest self other pk - GetLegalHoldAuthToken uid mpwd -> getLegalHoldAuthToken uid mpwd - AddLegalHoldClientToUserEither uid conn pks lpk -> + GetLegalHoldAuthToken uid mpwd -> do + logEffect "BrigAccess.GetLegalHoldAuthToken" + getLegalHoldAuthToken uid mpwd + AddLegalHoldClientToUserEither uid conn pks lpk -> do + logEffect "BrigAccess.AddLegalHoldClientToUserEither" embedApp $ addLegalHoldClientToUser uid conn pks lpk - RemoveLegalHoldClientFromUser uid -> + RemoveLegalHoldClientFromUser uid -> do + logEffect "BrigAccess.RemoveLegalHoldClientFromUser" embedApp $ removeLegalHoldClientFromUser uid - GetAccountConferenceCallingConfigClient uid -> + GetAccountConferenceCallingConfigClient uid -> do + logEffect "BrigAccess.GetAccountConferenceCallingConfigClient" embedApp $ getAccountConferenceCallingConfigClient uid - GetLocalMLSClients qusr ss -> embedApp $ getLocalMLSClients qusr ss - UpdateSearchVisibilityInbound status -> + GetLocalMLSClients qusr ss -> do + logEffect "BrigAccess.GetLocalMLSClients" + embedApp $ getLocalMLSClients qusr ss + UpdateSearchVisibilityInbound status -> do + logEffect "BrigAccess.UpdateSearchVisibilityInbound" embedApp $ updateSearchVisibilityInbound status interpretSparAccess :: ( Member (Embed IO) r, - Member (Input Env) r + Member (Input Env) r, + Member TinyLog r ) => Sem (SparAccess ': r) a -> Sem r a interpretSparAccess = interpret $ \case - DeleteTeam tid -> embedApp $ deleteTeam tid - LookupScimUserInfos uids -> embedApp $ lookupScimUserInfos uids + DeleteTeam tid -> do + logEffect "SparAccess.DeleteTeam" + embedApp $ deleteTeam tid + LookupScimUserInfos uids -> do + logEffect "SparAccess.LookupScimUserInfos" + embedApp $ lookupScimUserInfos uids interpretBotAccess :: ( Member (Embed IO) r, - Member (Input Env) r + Member (Input Env) r, + Member TinyLog r ) => Sem (BotAccess ': r) a -> Sem r a interpretBotAccess = interpret $ \case - DeleteBot cid bid -> embedApp $ deleteBot cid bid + DeleteBot cid bid -> do + logEffect "BotAccess.DeleteBot" + embedApp $ deleteBot cid bid diff --git a/services/galley/src/Galley/Intra/Federator.hs b/services/galley/src/Galley/Intra/Federator.hs index c1dd13bae16..565cd417d3e 100644 --- a/services/galley/src/Galley/Intra/Federator.hs +++ b/services/galley/src/Galley/Intra/Federator.hs @@ -21,6 +21,7 @@ import Control.Lens import Control.Monad.Except import Data.Bifunctor import Data.Qualified +import Galley.Cassandra.Util import Galley.Effects.FederatorAccess (FederatorAccess (..)) import Galley.Env import Galley.Env qualified as E @@ -29,27 +30,37 @@ import Galley.Options import Imports import Polysemy import Polysemy.Input +import Polysemy.TinyLog import UnliftIO import Wire.API.Federation.Client import Wire.API.Federation.Error interpretFederatorAccess :: ( Member (Embed IO) r, - Member (Input Env) r + Member (Input Env) r, + Member TinyLog r ) => Sem (FederatorAccess ': r) a -> Sem r a interpretFederatorAccess = interpret $ \case - RunFederated dom rpc -> embedApp $ runFederated dom rpc - RunFederatedEither dom rpc -> embedApp $ runFederatedEither dom rpc - RunFederatedConcurrently rs f -> embedApp $ runFederatedConcurrently rs f - RunFederatedConcurrentlyEither rs f -> - embedApp $ - runFederatedConcurrentlyEither rs f - RunFederatedConcurrentlyBucketsEither rs f -> - embedApp $ - runFederatedConcurrentlyBucketsEither rs f - IsFederationConfigured -> embedApp $ isJust <$> view E.federator + RunFederated dom rpc -> do + logEffect "FederatorAccess.RunFederated" + embedApp $ runFederated dom rpc + RunFederatedEither dom rpc -> do + logEffect "FederatorAccess.RunFederatedEither" + embedApp $ runFederatedEither dom rpc + RunFederatedConcurrently rs f -> do + logEffect "FederatorAccess.RunFederatedConcurrently" + embedApp $ runFederatedConcurrently rs f + RunFederatedConcurrentlyEither rs f -> do + logEffect "FederatorAccess.RunFederatedConcurrentlyEither" + embedApp $ runFederatedConcurrentlyEither rs f + RunFederatedConcurrentlyBucketsEither rs f -> do + logEffect "FederatorAccess.RunFederatedConcurrentlyBucketsEither" + embedApp $ runFederatedConcurrentlyBucketsEither rs f + IsFederationConfigured -> do + logEffect "FederatorAccess.IsFederationConfigured" + embedApp $ isJust <$> view E.federator runFederatedEither :: Remote x -> From 84d361b6b36cebe51809c168b6198921c8ab78c6 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 9 Apr 2024 15:04:42 +0200 Subject: [PATCH 078/117] WPB-7283 fix authentication issue of brig index migrate data to elastic search (#3984) --- changelog.d/2-features/WPB-6717 | 2 +- libs/types-common/default.nix | 2 ++ libs/types-common/src/Data/Credentials.hs | 6 ++++++ libs/types-common/types-common.cabal | 1 + services/brig/src/Brig/App.hs | 2 +- services/brig/src/Brig/Index/Eval.hs | 1 + services/brig/src/Brig/Index/Migrations.hs | 1 + services/brig/src/Brig/Index/Migrations/Types.hs | 4 +++- services/brig/src/Brig/User/Search/Index.hs | 12 ++++++++++-- 9 files changed, 26 insertions(+), 5 deletions(-) diff --git a/changelog.d/2-features/WPB-6717 b/changelog.d/2-features/WPB-6717 index 6720334b245..48626e42295 100644 --- a/changelog.d/2-features/WPB-6717 +++ b/changelog.d/2-features/WPB-6717 @@ -1 +1 @@ -Support for Elasticsearch password authentication +Support for Elasticsearch password authentication (#6717, #7283) diff --git a/libs/types-common/default.nix b/libs/types-common/default.nix index d13e256a89c..abf2ee2f27d 100644 --- a/libs/types-common/default.nix +++ b/libs/types-common/default.nix @@ -23,6 +23,7 @@ , gitignoreSource , hashable , http-api-data +, http-types , imports , iproute , iso3166-country-codes @@ -77,6 +78,7 @@ mkDerivation { generic-random hashable http-api-data + http-types imports iproute iso3166-country-codes diff --git a/libs/types-common/src/Data/Credentials.hs b/libs/types-common/src/Data/Credentials.hs index 5423b574e7a..52c632f9307 100644 --- a/libs/types-common/src/Data/Credentials.hs +++ b/libs/types-common/src/Data/Credentials.hs @@ -18,8 +18,11 @@ module Data.Credentials where import Data.Aeson (FromJSON) +import Data.ByteString.Base64 qualified as B64 import Data.Text +import Data.Text.Encoding qualified as TE import Imports +import Network.HTTP.Types.Header -- | Generic credentials for authenticating a user. Usually used for deserializing from a secret yaml file. data Credentials = Credentials @@ -29,3 +32,6 @@ data Credentials = Credentials deriving stock (Generic) instance FromJSON Credentials + +mkBasicAuthHeader :: Credentials -> Header +mkBasicAuthHeader (Credentials u p) = (hAuthorization, "Basic " <> B64.encode (TE.encodeUtf8 (u <> ":" <> p))) diff --git a/libs/types-common/types-common.cabal b/libs/types-common/types-common.cabal index dc15cfbc2e2..14cb8cb6a6a 100644 --- a/libs/types-common/types-common.cabal +++ b/libs/types-common/types-common.cabal @@ -109,6 +109,7 @@ library , generic-random >=1.4.0.0 , hashable >=1.2 , http-api-data + , http-types , imports , iproute >=1.5 , iso3166-country-codes >=0.20140203.8 diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index b9f5a099cfc..7c0f49a0cba 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -324,7 +324,7 @@ mkIndexEnv o lgr mgr mtr mCreds mAddCreds galleyEp = mainIndex = ES.IndexName $ Opt.index (Opt.elasticsearch o) additionalIndex = ES.IndexName <$> Opt.additionalWriteIndex (Opt.elasticsearch o) additionalBhe = flip mkBhe mAddCreds <$> Opt.additionalWriteIndexUrl (Opt.elasticsearch o) - in IndexEnv mtr lgr' (mkBhe (Opt.url (Opt.elasticsearch o)) mCreds) Nothing mainIndex additionalIndex additionalBhe galleyEp mgr + in IndexEnv mtr lgr' (mkBhe (Opt.url (Opt.elasticsearch o)) mCreds) Nothing mainIndex additionalIndex additionalBhe galleyEp mgr mCreds initZAuth :: Opts -> IO ZAuth.Env initZAuth o = do diff --git a/services/brig/src/Brig/Index/Eval.hs b/services/brig/src/Brig/Index/Eval.hs index 8cae3079b05..7d4ea3f5dd8 100644 --- a/services/brig/src/Brig/Index/Eval.hs +++ b/services/brig/src/Brig/Index/Eval.hs @@ -109,6 +109,7 @@ runCommand l = \case <*> pure Nothing <*> pure galleyEndpoint <*> pure mgr + <*> pure mCreds initES esURI mgr mCreds = let env = ES.mkBHEnv (toESServer esURI) mgr diff --git a/services/brig/src/Brig/Index/Migrations.hs b/services/brig/src/Brig/Index/Migrations.hs index c0320fb7d5b..0a8aacb1b60 100644 --- a/services/brig/src/Brig/Index/Migrations.hs +++ b/services/brig/src/Brig/Index/Migrations.hs @@ -85,6 +85,7 @@ mkEnv l mCreds es cas galleyEndpoint = do <*> initLogger <*> Metrics.metrics <*> pure (view Opts.esIndex es) + <*> pure mCreds <*> pure mgr <*> pure galleyEndpoint where diff --git a/services/brig/src/Brig/Index/Migrations/Types.hs b/services/brig/src/Brig/Index/Migrations/Types.hs index dd70c151fa0..853570ffb6f 100644 --- a/services/brig/src/Brig/Index/Migrations/Types.hs +++ b/services/brig/src/Brig/Index/Migrations/Types.hs @@ -24,6 +24,7 @@ import Brig.User.Search.Index qualified as Search import Cassandra qualified as C import Control.Monad.Catch (MonadThrow) import Data.Aeson (FromJSON (..), ToJSON (..), object, withObject, (.:), (.=)) +import Data.Credentials (Credentials) import Data.Metrics (Metrics) import Database.Bloodhound qualified as ES import Imports @@ -70,7 +71,7 @@ instance MonadIO m => MonadLogger (MigrationActionT m) where instance MonadIO m => Search.MonadIndexIO (MigrationActionT m) where liftIndexIO m = do Env {..} <- ask - let indexEnv = Search.IndexEnv metrics logger bhEnv Nothing searchIndex Nothing Nothing galleyEndpoint httpManager + let indexEnv = Search.IndexEnv metrics logger bhEnv Nothing searchIndex Nothing Nothing galleyEndpoint httpManager searchIndexCredentials Search.runIndexIO indexEnv m instance MonadIO m => ES.MonadBH (MigrationActionT m) where @@ -82,6 +83,7 @@ data Env = Env logger :: Logger.Logger, metrics :: Metrics, searchIndex :: ES.IndexName, + searchIndexCredentials :: Maybe Credentials, httpManager :: Manager, galleyEndpoint :: Endpoint } diff --git a/services/brig/src/Brig/User/Search/Index.hs b/services/brig/src/Brig/User/Search/Index.hs index 812842aef82..4428d8bdeed 100644 --- a/services/brig/src/Brig/User/Search/Index.hs +++ b/services/brig/src/Brig/User/Search/Index.hs @@ -74,6 +74,7 @@ import Data.ByteString.Builder (Builder, toLazyByteString) import Data.ByteString.Conversion (toByteString') import Data.ByteString.Conversion qualified as Bytes import Data.ByteString.Lazy qualified as BL +import Data.Credentials import Data.Handle (Handle) import Data.Id import Data.Map qualified as Map @@ -112,7 +113,9 @@ data IndexEnv = IndexEnv idxAdditionalName :: Maybe ES.IndexName, idxAdditionalElastic :: Maybe ES.BHEnv, idxGalley :: Endpoint, - idxHttpManager :: Manager + idxHttpManager :: Manager, + -- credentials for reindexing have to be passed via the env because bulk API requests are not supported by bloodhound + idxCredentials :: Maybe Credentials } newtype IndexIO a = IndexIO (ReaderT IndexEnv IO a) @@ -210,12 +213,13 @@ updateIndex (IndexUpdateUsers updateType ius) = liftIndexIO $ do let (ES.MappingName mpp) = mappingName let (ES.Server base) = ES.bhServer bhe req <- parseRequest (view unpacked $ base <> "/" <> idx <> "/" <> mpp <> "/_bulk") + authHeaders <- mkAuthHeaders res <- liftIO $ httpLbs req { method = "POST", - requestHeaders = [(hContentType, "application/x-ndjson")], -- sic + requestHeaders = [(hContentType, "application/x-ndjson")] <> authHeaders, -- sic requestBody = RequestBodyLBS (toLazyByteString (foldMap bulkEncode ius)) } (ES.bhManager bhe) @@ -229,6 +233,10 @@ updateIndex (IndexUpdateUsers updateType ius) = liftIndexIO $ do (path ("user.index.update.bulk.status." <> review builder (decimal s))) m where + mkAuthHeaders = do + creds <- asks idxCredentials + pure $ maybe [] ((: []) . mkBasicAuthHeader) creds + encodeJSONToString :: ToJSON a => a -> Builder encodeJSONToString = fromEncoding . toEncoding bulkEncode iu = From e4020b881a768f35db6c2051f296f5e1ec482b94 Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 9 Apr 2024 17:09:41 +0200 Subject: [PATCH 079/117] [chore] Port 2FA tests (#3986) --- changelog.d/5-internal/port-2fa-tests | 1 + integration/integration.cabal | 1 + integration/test/API/BrigInternal.hs | 8 + integration/test/API/GalleyInternal.hs | 18 ++ integration/test/API/Nginz.hs | 8 + integration/test/Test/Login.hs | 119 ++++++++++++ .../brig/test/integration/API/User/Auth.hs | 171 ------------------ 7 files changed, 155 insertions(+), 171 deletions(-) create mode 100644 changelog.d/5-internal/port-2fa-tests create mode 100644 integration/test/Test/Login.hs diff --git a/changelog.d/5-internal/port-2fa-tests b/changelog.d/5-internal/port-2fa-tests new file mode 100644 index 00000000000..19acd88523c --- /dev/null +++ b/changelog.d/5-internal/port-2fa-tests @@ -0,0 +1 @@ +Ported 2FA tests to the new integration test suite diff --git a/integration/integration.cabal b/integration/integration.cabal index bcafb9ff147..56bcb614de8 100644 --- a/integration/integration.cabal +++ b/integration/integration.cabal @@ -125,6 +125,7 @@ library Test.Federation Test.Federator Test.LegalHold + Test.Login Test.MessageTimer Test.MLS Test.MLS.KeyPackage diff --git a/integration/test/API/BrigInternal.hs b/integration/test/API/BrigInternal.hs index d538bb35561..71bde9877dd 100644 --- a/integration/test/API/BrigInternal.hs +++ b/integration/test/API/BrigInternal.hs @@ -253,3 +253,11 @@ getEJPDInfo dom handles mode = do "include_contacts" -> [("include_contacts", "true")] bad -> error $ show bad submit "POST" $ req & addJSONObject ["ejpd_request" .= handles] & addQueryParams query + +-- https://staging-nginz-https.zinfra.io/api-internal/swagger-ui/brig/#/brig/get_i_users__uid__verification_code__action_ +getVerificationCode :: (HasCallStack, MakesValue user) => user -> String -> App Response +getVerificationCode user action = do + uid <- objId user + domain <- objDomain user + req <- baseRequest domain Brig Unversioned $ joinHttpPath ["i", "users", uid, "verification-code", action] + submit "GET" req diff --git a/integration/test/API/GalleyInternal.hs b/integration/test/API/GalleyInternal.hs index 4b5ad4cc970..f3b2ef1a135 100644 --- a/integration/test/API/GalleyInternal.hs +++ b/integration/test/API/GalleyInternal.hs @@ -44,6 +44,13 @@ setTeamFeatureStatus domain team featureName status = do res <- submit "PATCH" $ req & addJSONObject ["status" .= status] res.status `shouldMatchInt` 200 +setTeamFeatureLockStatus :: (HasCallStack, MakesValue domain, MakesValue team) => domain -> team -> String -> String -> App () +setTeamFeatureLockStatus domain team featureName status = do + tid <- asString team + req <- baseRequest domain Galley Unversioned $ joinHttpPath ["i", "teams", tid, "features", featureName, status] + res <- submit "PUT" $ req + res.status `shouldMatchInt` 200 + getFederationStatus :: ( HasCallStack, MakesValue user @@ -79,3 +86,14 @@ legalholdIsEnabled tid uid = do tidStr <- asString tid baseRequest uid Galley Unversioned do joinHttpPath ["i", "teams", tidStr, "features", "legalhold"] >>= submit "GET" + +generateVerificationCode :: (HasCallStack, MakesValue domain, MakesValue email) => domain -> email -> App () +generateVerificationCode domain email = do + res <- generateVerificationCode' domain email + res.status `shouldMatchInt` 200 + +generateVerificationCode' :: (HasCallStack, MakesValue domain, MakesValue email) => domain -> email -> App Response +generateVerificationCode' domain email = do + req <- baseRequest domain Brig Versioned "/verification-code/send" + emailStr <- asString email + submit "POST" $ req & addJSONObject ["email" .= emailStr, "action" .= "login"] diff --git a/integration/test/API/Nginz.hs b/integration/test/API/Nginz.hs index 4c34ef639d3..b4c2f08db5b 100644 --- a/integration/test/API/Nginz.hs +++ b/integration/test/API/Nginz.hs @@ -14,6 +14,14 @@ login domain email pw = do pwStr <- make pw >>= asString submit "POST" (req & addJSONObject ["email" .= emailStr, "password" .= pwStr, "label" .= "auth"]) +loginWith2ndFactor :: (HasCallStack, MakesValue domain, MakesValue email, MakesValue password, MakesValue sndFactor) => domain -> email -> password -> sndFactor -> App Response +loginWith2ndFactor domain email pw sf = do + req <- rawBaseRequest domain Nginz Unversioned "/login" + emailStr <- make email >>= asString + pwStr <- make pw >>= asString + sfStr <- make sf >>= asString + submit "POST" (req & addJSONObject ["email" .= emailStr, "password" .= pwStr, "label" .= "auth", "verification_code" .= sfStr]) + access :: (HasCallStack, MakesValue domain, MakesValue cookie) => domain -> cookie -> App Response access domain cookie = do req <- rawBaseRequest domain Nginz Unversioned "/access" diff --git a/integration/test/Test/Login.hs b/integration/test/Test/Login.hs new file mode 100644 index 00000000000..28a23b54d39 --- /dev/null +++ b/integration/test/Test/Login.hs @@ -0,0 +1,119 @@ +{-# OPTIONS_GHC -Wno-ambiguous-fields #-} + +module Test.Login where + +import API.BrigInternal (getVerificationCode) +import API.Common (defPassword) +import API.GalleyInternal +import API.Nginz (login, loginWith2ndFactor) +import Control.Concurrent (threadDelay) +import qualified Data.Aeson as Aeson +import SetupHelpers +import Testlib.Prelude +import Text.Printf (printf) + +testLoginVerify6DigitEmailCodeSuccess :: HasCallStack => App () +testLoginVerify6DigitEmailCodeSuccess = do + (owner, team, []) <- createTeam OwnDomain 0 + email <- owner %. "email" + setTeamFeatureLockStatus owner team "sndFactorPasswordChallenge" "unlocked" + setTeamFeatureStatus owner team "sndFactorPasswordChallenge" "enabled" + generateVerificationCode owner email + code <- getVerificationCode owner "login" >>= getJSON 200 >>= asString + bindResponse (loginWith2ndFactor owner email defPassword code) $ \resp -> do + resp.status `shouldMatchInt` 200 + +-- @SF.Channel @TSFI.RESTfulAPI @S2 +-- +-- Test that login fails with wrong second factor email verification code +testLoginVerify6DigitWrongCodeFails :: HasCallStack => App () +testLoginVerify6DigitWrongCodeFails = do + (owner, team, []) <- createTeam OwnDomain 0 + email <- owner %. "email" + setTeamFeatureLockStatus owner team "sndFactorPasswordChallenge" "unlocked" + setTeamFeatureStatus owner team "sndFactorPasswordChallenge" "enabled" + generateVerificationCode owner email + correctCode <- getVerificationCode owner "login" >>= getJSON 200 >>= asString + let wrongCode :: String = printf "%06d" $ (read @Int correctCode) + 1 `mod` 1000000 + bindResponse (loginWith2ndFactor owner email defPassword wrongCode) $ \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "code-authentication-failed" + +-- @END + +-- @SF.Channel @TSFI.RESTfulAPI @S2 +-- +-- Test that login without verification code fails if SndFactorPasswordChallenge feature is enabled in team +testLoginVerify6DigitMissingCodeFails :: HasCallStack => App () +testLoginVerify6DigitMissingCodeFails = do + (owner, team, []) <- createTeam OwnDomain 0 + email <- owner %. "email" + setTeamFeatureLockStatus owner team "sndFactorPasswordChallenge" "unlocked" + setTeamFeatureStatus owner team "sndFactorPasswordChallenge" "enabled" + bindResponse (login owner email defPassword) $ \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "code-authentication-required" + +-- @END + +-- @SF.Channel @TSFI.RESTfulAPI @S2 +-- +-- Test that login fails with expired second factor email verification code +testLoginVerify6DigitExpiredCodeFails :: HasCallStack => App () +testLoginVerify6DigitExpiredCodeFails = do + withModifiedBackend + (def {brigCfg = setField "optSettings.setVerificationTimeout" (Aeson.Number 1)}) + $ \domain -> do + (owner, team, []) <- createTeam domain 0 + email <- owner %. "email" + setTeamFeatureLockStatus owner team "sndFactorPasswordChallenge" "unlocked" + setTeamFeatureStatus owner team "sndFactorPasswordChallenge" "enabled" + generateVerificationCode owner email + code <- getVerificationCode owner "login" >>= getJSON 200 >>= asString + liftIO $ threadDelay 2_000_100 + bindResponse (loginWith2ndFactor owner email defPassword code) \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "code-authentication-failed" + +-- @END + +testLoginVerify6DigitResendCodeSuccessAndRateLimiting :: HasCallStack => App () +testLoginVerify6DigitResendCodeSuccessAndRateLimiting = do + (owner, team, []) <- createTeam OwnDomain 0 + email <- owner %. "email" + setTeamFeatureLockStatus owner team "sndFactorPasswordChallenge" "unlocked" + setTeamFeatureStatus owner team "sndFactorPasswordChallenge" "enabled" + generateVerificationCode owner email + fstCode <- getVerificationCode owner "login" >>= getJSON 200 >>= asString + bindResponse (generateVerificationCode' owner email) $ \resp -> do + resp.status `shouldMatchInt` 429 + mostRecentCode <- retryT $ do + resp <- generateVerificationCode' owner email + resp.status `shouldMatchInt` 200 + getVerificationCode owner "login" >>= getJSON 200 >>= asString + + bindResponse (loginWith2ndFactor owner email defPassword fstCode) \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "code-authentication-failed" + + bindResponse (loginWith2ndFactor owner email defPassword mostRecentCode) \resp -> do + resp.status `shouldMatchInt` 200 + +testLoginVerify6DigitLimitRetries :: HasCallStack => App () +testLoginVerify6DigitLimitRetries = do + (owner, team, []) <- createTeam OwnDomain 0 + email <- owner %. "email" + setTeamFeatureLockStatus owner team "sndFactorPasswordChallenge" "unlocked" + setTeamFeatureStatus owner team "sndFactorPasswordChallenge" "enabled" + generateVerificationCode owner email + correctCode <- getVerificationCode owner "login" >>= getJSON 200 >>= asString + let wrongCode :: String = printf "%06d" $ (read @Int correctCode) + 1 `mod` 1000000 + -- try login with wrong code should fail 3 times + forM_ [1 .. 3] $ \(_ :: Int) -> do + bindResponse (loginWith2ndFactor owner email defPassword wrongCode) \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "code-authentication-failed" + -- after 3 failed attempts, login with correct code should fail as well + bindResponse (loginWith2ndFactor owner email defPassword correctCode) \resp -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "code-authentication-failed" diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index fcb4ad5b9ca..3e59633e2d2 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -25,7 +25,6 @@ module API.User.Auth where import API.Team.Util -import API.User.Util qualified as Util import Bilge hiding (body) import Bilge qualified as Http import Bilge.Assert hiding (assert) @@ -38,7 +37,6 @@ import Cassandra hiding (Value) import Cassandra qualified as DB import Control.Arrow ((&&&)) import Control.Lens (set, (^.)) -import Control.Monad.Catch (MonadCatch) import Control.Retry import Data.Aeson as Aeson hiding (json) import Data.ByteString qualified as BS @@ -49,9 +47,7 @@ import Data.Id import Data.Misc (PlainTextPassword6, plainTextPassword6, plainTextPassword6Unsafe) import Data.Proxy import Data.Qualified -import Data.Range (unsafeRange) import Data.Text qualified as Text -import Data.Text.Ascii (AsciiChars (validate)) import Data.Text.IO (hPutStrLn) import Data.Text.Lazy qualified as Lazy import Data.Time.Clock @@ -67,7 +63,6 @@ import UnliftIO.Async hiding (wait) import Util import Wire.API.Conversation (Conversation (..)) import Wire.API.Password (Password, mkSafePassword) -import Wire.API.Team.Feature qualified as Public import Wire.API.User as Public import Wire.API.User.Auth as Auth import Wire.API.User.Auth.LegalHold @@ -130,15 +125,6 @@ tests conf m z db b g n = [ test m "nginz-login" (testNginz b n), test m "nginz-legalhold-login" (onlyIfLhWhitelisted (testNginzLegalHold b g n)), test m "nginz-login-multiple-cookies" (testNginzMultipleCookies conf b n) - ], - testGroup - "snd-factor-password-challenge" - [ test m "test-login-verify6-digit-email-code-success" $ testLoginVerify6DigitEmailCodeSuccess b g db, - test m "test-login-verify6-digit-wrong-code-fails" $ testLoginVerify6DigitWrongCodeFails b g, - test m "test-login-verify6-digit-missing-code-fails" $ testLoginVerify6DigitMissingCodeFails b g, - test m "test-login-verify6-digit-expired-code-fails" $ testLoginVerify6DigitExpiredCodeFails conf b g db, - test m "test-login-verify6-digit-resend-code-success-and-rate-limiting" $ testLoginVerify6DigitResendCodeSuccessAndRateLimiting b g conf db, - test m "test-login-verify6-digit-limit-retries" $ testLoginVerify6DigitLimitRetries b g conf db ] ], testGroup @@ -436,157 +422,6 @@ testSendLoginCode brig = do let _timeout = fromLoginCodeTimeout <$> responseJsonMaybe rsp2 liftIO $ assertEqual "timeout" (Just (Code.Timeout 600)) _timeout -testLoginVerify6DigitEmailCodeSuccess :: Brig -> Galley -> DB.ClientState -> Http () -testLoginVerify6DigitEmailCodeSuccess brig galley db = do - (u, tid) <- createUserWithTeam' brig - let Just email = userEmail u - let checkLoginSucceeds body = login brig body PersistentCookie !!! const 200 === statusCode - Util.setTeamFeatureLockStatus @Public.SndFactorPasswordChallengeConfig galley tid Public.LockStatusUnlocked - Util.setTeamSndFactorPasswordChallenge galley tid Public.FeatureStatusEnabled - Util.generateVerificationCode brig (Public.SendVerificationCode Public.Login email) - key <- Code.mkKey (Code.ForEmail email) - Just vcode <- Util.lookupCode db key Code.AccountLogin - checkLoginSucceeds $ - PasswordLogin $ - PasswordLoginData - (LoginByEmail email) - defPassword - (Just defCookieLabel) - (Just $ Code.codeValue vcode) - -testLoginVerify6DigitResendCodeSuccessAndRateLimiting :: Brig -> Galley -> Opts.Opts -> DB.ClientState -> Http () -testLoginVerify6DigitResendCodeSuccessAndRateLimiting brig galley _opts db = do - (u, tid) <- createUserWithTeam' brig - let Just email = userEmail u - let checkLoginSucceeds body = login brig body PersistentCookie !!! const 200 === statusCode - let getCodeFromDb = do - key <- Code.mkKey (Code.ForEmail email) - Just c <- Util.lookupCode db key Code.AccountLogin - pure c - - Util.setTeamFeatureLockStatus @Public.SndFactorPasswordChallengeConfig galley tid Public.LockStatusUnlocked - Util.setTeamSndFactorPasswordChallenge galley tid Public.FeatureStatusEnabled - - Util.generateVerificationCode brig (Public.SendVerificationCode Public.Login email) - fstCode <- getCodeFromDb - - let tooManyRequests = 429 - Util.generateVerificationCodeExpect tooManyRequests brig (Public.SendVerificationCode Public.Login email) - - void $ retryWhileN 10 ((==) 429 . statusCode) $ Util.generateVerificationCode' brig (Public.SendVerificationCode Public.Login email) - mostRecentCode <- getCodeFromDb - - checkLoginFails brig $ - PasswordLogin $ - PasswordLoginData - (LoginByEmail email) - defPassword - (Just defCookieLabel) - (Just $ Code.codeValue fstCode) - checkLoginSucceeds $ - PasswordLogin $ - PasswordLoginData - (LoginByEmail email) - defPassword - (Just defCookieLabel) - (Just $ Code.codeValue mostRecentCode) - -testLoginVerify6DigitLimitRetries :: Brig -> Galley -> Opts.Opts -> DB.ClientState -> Http () -testLoginVerify6DigitLimitRetries brig galley _opts db = do - (u, tid) <- createUserWithTeam' brig - let Just email = userEmail u - Util.setTeamFeatureLockStatus @Public.SndFactorPasswordChallengeConfig galley tid Public.LockStatusUnlocked - Util.setTeamSndFactorPasswordChallenge galley tid Public.FeatureStatusEnabled - Util.generateVerificationCode brig (Public.SendVerificationCode Public.Login email) - key <- Code.mkKey (Code.ForEmail email) - Just correctCode <- Util.lookupCode db key Code.AccountLogin - let wrongCode = Code.Value $ unsafeRange (fromRight undefined (validate "123456")) - -- login with wrong code should fail 3 times - forM_ [1 .. 3] $ \(_ :: Int) -> - checkLoginFails brig $ - PasswordLogin $ - PasswordLoginData - (LoginByEmail email) - defPassword - (Just defCookieLabel) - (Just wrongCode) - -- after 3 failed attempts, login with correct code should fail as well - checkLoginFails brig $ - PasswordLogin $ - PasswordLoginData - (LoginByEmail email) - defPassword - (Just defCookieLabel) - (Just (Code.codeValue correctCode)) - --- @SF.Channel @TSFI.RESTfulAPI @S2 --- --- Test that login fails with wrong second factor email verification code -testLoginVerify6DigitWrongCodeFails :: Brig -> Galley -> Http () -testLoginVerify6DigitWrongCodeFails brig galley = do - (u, tid) <- createUserWithTeam' brig - let Just email = userEmail u - Util.setTeamFeatureLockStatus @Public.SndFactorPasswordChallengeConfig galley tid Public.LockStatusUnlocked - Util.setTeamSndFactorPasswordChallenge galley tid Public.FeatureStatusEnabled - Util.generateVerificationCode brig (Public.SendVerificationCode Public.Login email) - let wrongCode = Code.Value $ unsafeRange (fromRight undefined (validate "123456")) - checkLoginFails brig $ - PasswordLogin $ - PasswordLoginData - (LoginByEmail email) - defPassword - (Just defCookieLabel) - (Just wrongCode) - --- @END - --- @SF.Channel @TSFI.RESTfulAPI @S2 --- --- Test that login without verification code fails if SndFactorPasswordChallenge feature is enabled in team -testLoginVerify6DigitMissingCodeFails :: Brig -> Galley -> Http () -testLoginVerify6DigitMissingCodeFails brig galley = do - (u, tid) <- createUserWithTeam' brig - let Just email = userEmail u - Util.setTeamFeatureLockStatus @Public.SndFactorPasswordChallengeConfig galley tid Public.LockStatusUnlocked - Util.setTeamSndFactorPasswordChallenge galley tid Public.FeatureStatusEnabled - Util.generateVerificationCode brig (Public.SendVerificationCode Public.Login email) - let body = - PasswordLogin $ - PasswordLoginData - (LoginByEmail email) - defPassword - (Just defCookieLabel) - Nothing - login brig body PersistentCookie !!! do - const 403 === statusCode - const (Just "code-authentication-required") === errorLabel - --- @END - --- @SF.Channel @TSFI.RESTfulAPI @S2 --- --- Test that login fails with expired second factor email verification code -testLoginVerify6DigitExpiredCodeFails :: Opts.Opts -> Brig -> Galley -> DB.ClientState -> Http () -testLoginVerify6DigitExpiredCodeFails opts brig galley db = do - (u, tid) <- createUserWithTeam' brig - let Just email = userEmail u - Util.setTeamFeatureLockStatus @Public.SndFactorPasswordChallengeConfig galley tid Public.LockStatusUnlocked - Util.setTeamSndFactorPasswordChallenge galley tid Public.FeatureStatusEnabled - Util.generateVerificationCode brig (Public.SendVerificationCode Public.Login email) - key <- Code.mkKey (Code.ForEmail email) - Just vcode <- Util.lookupCode db key Code.AccountLogin - let verificationTimeout = round (Opts.setVerificationTimeout (Opts.optSettings opts)) - threadDelay $ ((verificationTimeout + 1) * 1000_000) - checkLoginFails brig $ - PasswordLogin $ - PasswordLoginData - (LoginByEmail email) - defPassword - (Just defCookieLabel) - (Just $ Code.codeValue vcode) - --- @END - -- The testLoginFailure test conforms to the following testing standards: -- @SF.Provisioning @TSFI.RESTfulAPI @S2 -- @@ -1513,9 +1348,3 @@ remJson p l ids = wait :: MonadIO m => m () wait = liftIO $ threadDelay 1000000 - -checkLoginFails :: (MonadHttp m, MonadIO m, MonadCatch m) => Brig -> Login -> m () -checkLoginFails brig body = do - login brig body PersistentCookie !!! do - const 403 === statusCode - const (Just "code-authentication-failed") === errorLabel From 54552add012e1eb51734831aa2d5fdfaaa778fad Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Tue, 9 Apr 2024 17:24:52 +0200 Subject: [PATCH 080/117] Edit source file Redis is only in one as Julia suggested. Updated to follow the Wire graphical/style guideline (colors, font) while I was at it. --- .../install/img/architecture-server-ha.drawio | 161 +++++++++--------- 1 file changed, 78 insertions(+), 83 deletions(-) diff --git a/docs/src/how-to/install/img/architecture-server-ha.drawio b/docs/src/how-to/install/img/architecture-server-ha.drawio index bf43f299408..574f01fb7dd 100644 --- a/docs/src/how-to/install/img/architecture-server-ha.drawio +++ b/docs/src/how-to/install/img/architecture-server-ha.drawio @@ -1,227 +1,222 @@ - + + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - - - - + - + - + - + - + - + - + - + - + - + - + @@ -233,93 +228,93 @@ - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + - + From bebdb64d6c01ff30b2cb6869b04b4d88262a43fa Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Tue, 9 Apr 2024 17:25:49 +0200 Subject: [PATCH 081/117] Minor correction in XML source file. --- docs/src/how-to/install/img/architecture-server-ha.drawio | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/src/how-to/install/img/architecture-server-ha.drawio b/docs/src/how-to/install/img/architecture-server-ha.drawio index 574f01fb7dd..1f1de668fbc 100644 --- a/docs/src/how-to/install/img/architecture-server-ha.drawio +++ b/docs/src/how-to/install/img/architecture-server-ha.drawio @@ -321,3 +321,4 @@ + From 5e60f528703dcaa134eb47191e07b5054dceac61 Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Tue, 9 Apr 2024 17:30:29 +0200 Subject: [PATCH 082/117] edit ha arch diagram, remove redis as julia asked and update style with font and colors from wire specs --- .../install/img/architecture-server-ha.png | Bin 216366 -> 242679 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/src/how-to/install/img/architecture-server-ha.png b/docs/src/how-to/install/img/architecture-server-ha.png index a7113b2d95c2240205af1ae08f7f644b141dbdc1..6c8746bcae3c5b1fc56d1005c44c9669ac8c6ca4 100644 GIT binary patch literal 242679 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

    please pick an api version

    " <> mconcat [ let url = "/" <> toQueryParam v <> "/api/swagger-ui/" - in " cs url <> "\">" <> cs url <> "
    " + in " (fromStrict . Text.encodeUtf8 $ url) + <> "\">" + <> (fromStrict . Text.encodeUtf8 $ url) + <> "
    " | v <- [minBound :: Version ..] ] <> "" diff --git a/services/brig/src/Brig/API/User.hs b/services/brig/src/Brig/API/User.hs index 6db13202f28..05e4a2b43aa 100644 --- a/services/brig/src/Brig/API/User.hs +++ b/services/brig/src/Brig/API/User.hs @@ -157,7 +157,7 @@ import Data.Misc import Data.Qualified import Data.Time.Clock (UTCTime, addUTCTime, diffUTCTime) import Data.UUID.V4 (nextRandom) -import Imports hiding (cs) +import Imports import Network.Wai.Utilities import Polysemy import Polysemy.Input (Input) diff --git a/services/brig/src/Brig/API/Util.hs b/services/brig/src/Brig/API/Util.hs index 67693d37300..54f04975a51 100644 --- a/services/brig/src/Brig/API/Util.hs +++ b/services/brig/src/Brig/API/Util.hs @@ -48,6 +48,7 @@ import Data.Handle (Handle, parseHandle) import Data.Id import Data.Maybe import Data.Qualified +import Data.Text qualified as T import Data.Text.Ascii (AsciiText (toText)) import Imports import Polysemy @@ -88,7 +89,7 @@ validateHandle = maybe (throwStd (errorToWai @'InvalidHandle)) pure . parseHandl logEmail :: Email -> (Msg -> Msg) logEmail email = - Log.field "email_sha256" (sha256String . cs . show $ email) + Log.field "email_sha256" (sha256String . T.pack . show $ email) logInvitationCode :: InvitationCode -> (Msg -> Msg) logInvitationCode code = Log.field "invitation_code" (toText $ fromInvitationCode code) diff --git a/services/brig/src/Brig/Calling.hs b/services/brig/src/Brig/Calling.hs index 49c79b0a9de..c9501b3fcad 100644 --- a/services/brig/src/Brig/Calling.hs +++ b/services/brig/src/Brig/Calling.hs @@ -63,6 +63,7 @@ import Data.Misc import Data.Range import Data.Text qualified as Text import Data.Text.Encoding qualified as Text +import Data.Text.Encoding.Error import Data.Text.IO qualified as Text import Data.Time.Clock (DiffTime, diffTimeToPicoseconds) import Imports @@ -343,7 +344,15 @@ startDNSBasedTurnDiscovery logger opts deprecatedUdpRef udpRef tcpRef tlsRef = d turnURIFromSRV :: Scheme -> Maybe Transport -> SrvEntry -> TurnURI turnURIFromSRV sch mtr SrvEntry {..} = - turnURI sch (TurnHostName . cs . stripDot $ srvTargetDomain srvTarget) (Port $ srvTargetPort srvTarget) mtr + turnURI + sch + ( TurnHostName + . Text.decodeUtf8With lenientDecode + . stripDot + $ srvTargetDomain srvTarget + ) + (Port $ srvTargetPort srvTarget) + mtr where stripDot h | "." `BS.isSuffixOf` h = BS.take (BS.length h - 1) h diff --git a/services/brig/src/Brig/Effects/JwtTools.hs b/services/brig/src/Brig/Effects/JwtTools.hs index e6304fb90b4..1b9a1773413 100644 --- a/services/brig/src/Brig/Effects/JwtTools.hs +++ b/services/brig/src/Brig/Effects/JwtTools.hs @@ -12,6 +12,8 @@ import Data.Jwt.Tools qualified as Jwt import Data.Misc (HttpsUrl) import Data.Nonce (Nonce) import Data.PEMKeys +import Data.Text.Encoding +import Data.Text.Encoding.Error import Imports import Network.HTTP.Types (StdMethod (..)) import Network.HTTP.Types qualified as HTTP @@ -77,4 +79,4 @@ interpretJwtTools = interpret $ \case ) where urlEncode :: Text -> Text - urlEncode = cs . HTTP.urlEncode False . cs + urlEncode = decodeUtf8With lenientDecode . HTTP.urlEncode False . encodeUtf8 diff --git a/services/brig/src/Brig/Effects/SFT.hs b/services/brig/src/Brig/Effects/SFT.hs index 24fe09bfa7d..d1cdd9d2cde 100644 --- a/services/brig/src/Brig/Effects/SFT.hs +++ b/services/brig/src/Brig/Effects/SFT.hs @@ -29,6 +29,7 @@ where import Data.Aeson qualified as Aeson import Data.ByteString.Conversion +import Data.ByteString.UTF8 qualified as UTF8 import Data.Map qualified as Map import Data.Misc import Data.Schema @@ -58,7 +59,7 @@ interpretSFT :: Members [Embed IO, TinyLog] r => Manager -> Sem (SFT ': r) a -> interpretSFT httpManager = interpret $ \(SFTGetAllServers url) -> do let urlWithPath = ensureHttpsUrl $ (httpsUrl url) {uriPath = "/sft_servers_all.json"} fmap SFTGetResponse . runSftError urlWithPath $ do - let req = parseRequest_ . cs . toByteString' $ urlWithPath + let req = parseRequest_ . UTF8.toString . toByteString' $ urlWithPath response <- fromExceptionVia @HttpException (SFTError . show) (responseBody <$> httpLbs req httpManager) let eList = Aeson.eitherDecode @AllURLs response res <- fromEither $ bimap SFTError (fmap sftServer . unAllURLs) eList @@ -92,6 +93,6 @@ interpretSFTInMemory m = interpret $ \(SFTGetAllServers url) -> case Map.lookup url m of Nothing -> do let msg = "No value in the lookup map" - err $ Log.field "url" (show url) . Log.msg (cs msg :: ByteString) + err $ Log.field "url" (show url) . Log.msg (UTF8.fromString msg :: ByteString) pure . SFTGetResponse . Left . SFTError $ msg Just ss -> pure ss diff --git a/services/brig/src/Brig/Index/Eval.hs b/services/brig/src/Brig/Index/Eval.hs index 7d4ea3f5dd8..4d8f163dfdb 100644 --- a/services/brig/src/Brig/Index/Eval.hs +++ b/services/brig/src/Brig/Index/Eval.hs @@ -32,6 +32,7 @@ import Control.Monad.Catch import Control.Retry import Data.Aeson (FromJSON) import Data.Aeson qualified as Aeson +import Data.ByteString.Lazy.UTF8 qualified as UTF8 import Data.Credentials (Credentials (..)) import Data.Metrics qualified as Metrics import Database.Bloodhound qualified as ES @@ -130,7 +131,7 @@ waitForTaskToComplete timeoutSeconds taskNodeId = do throwM $ ReindexFromAnotherIndexError $ "Task failed with error: " - <> cs (Aeson.encode $ ES.taskResponseError task) + <> UTF8.toString (Aeson.encode $ ES.taskResponseError task) where isTaskComplete :: Either ES.EsError (ES.TaskResponse a) -> m Bool isTaskComplete (Left e) = throwM $ ReindexFromAnotherIndexError $ "Error response while getting task: " <> show e diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index 90316762356..e7219fef819 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -45,11 +45,13 @@ import Control.Lens (view, (.~), (^.)) import Control.Monad.Catch (MonadCatch, finally) import Control.Monad.Random (randomRIO) import Data.Aeson qualified as Aeson +import Data.ByteString.UTF8 qualified as UTF8 import Data.Id (RequestId (..)) import Data.Metrics.AWS (gaugeTokenRemaing) import Data.Metrics.Servant qualified as Metrics import Data.Proxy (Proxy (Proxy)) import Data.Text (unpack) +import Data.Text.Encoding import Data.UUID as UUID import Data.UUID.V4 as UUID import Imports hiding (head) @@ -155,7 +157,7 @@ lookupRequestIdMiddleware logger mkapp req cont = do Just rid -> do mkapp (RequestId rid) req cont Nothing -> do - localRid <- RequestId . cs . UUID.toText <$> UUID.nextRandom + localRid <- RequestId . encodeUtf8 . UUID.toText <$> UUID.nextRandom Log.info logger $ "request-id" .= localRid ~~ "method" .= Wai.requestMethod req @@ -173,7 +175,7 @@ bodyParserErrorFormatter :: Servant.ErrorFormatter bodyParserErrorFormatter _ _ errMsg = Servant.ServerError { Servant.errHTTPCode = HTTP.statusCode HTTP.status400, - Servant.errReasonPhrase = cs $ HTTP.statusMessage HTTP.status400, + Servant.errReasonPhrase = UTF8.toString $ HTTP.statusMessage HTTP.status400, Servant.errBody = Aeson.encode $ Aeson.object @@ -218,7 +220,7 @@ pendingActivationCleanup = do safeForever funName action = forever $ action `catchAny` \exc -> do - err $ "error" .= show exc ~~ msg (val $ cs funName <> " failed") + err $ "error" .= show exc ~~ msg (val $ UTF8.fromString funName <> " failed") -- pause to keep worst-case noise in logs manageable threadDelay 60_000_000 diff --git a/services/brig/src/Brig/Team/API.hs b/services/brig/src/Brig/Team/API.hs index cb42bc6f010..c14ac164d13 100644 --- a/services/brig/src/Brig/Team/API.hs +++ b/services/brig/src/Brig/Team/API.hs @@ -56,6 +56,7 @@ import Data.Id import Data.List1 qualified as List1 import Data.Qualified (Local) import Data.Range +import Data.Text.Lazy qualified as LT import Data.Time.Clock (UTCTime) import Imports hiding (head) import Network.Wai.Utilities hiding (code, message) @@ -201,7 +202,12 @@ logInvitationRequest context action = eith <- action' case eith of Left err' -> do - Log.warn $ context . Log.msg @Text ("Failed to create invitation, label: " <> (cs . errorLabel) err') + Log.warn $ + context + . Log.msg @Text + ( "Failed to create invitation, label: " + <> (LT.toStrict . errorLabel) err' + ) pure (Left err') Right result@(_, code) -> do Log.info $ (context . logInvitationCode code) . Log.msg @Text "Successfully created invitation" diff --git a/services/brig/src/Brig/User/Auth/Cookie.hs b/services/brig/src/Brig/User/Auth/Cookie.hs index 5da62fb8e43..1bd569bc352 100644 --- a/services/brig/src/Brig/User/Auth/Cookie.hs +++ b/services/brig/src/Brig/User/Auth/Cookie.hs @@ -56,7 +56,7 @@ import Data.Metrics qualified as Metrics import Data.Proxy import Data.RetryAfter import Data.Time.Clock -import Imports hiding (cs) +import Imports import Network.Wai (Response) import Network.Wai.Utilities.Response (addHeader) import System.Logger.Class (field, msg, val, (~~)) diff --git a/services/brig/src/Brig/User/Auth/Cookie/Limit.hs b/services/brig/src/Brig/User/Auth/Cookie/Limit.hs index 5c25c4588f2..034f3ced85f 100644 --- a/services/brig/src/Brig/User/Auth/Cookie/Limit.hs +++ b/services/brig/src/Brig/User/Auth/Cookie/Limit.hs @@ -22,7 +22,7 @@ import Data.RetryAfter import Data.Time.Clock import Data.Time.Clock.POSIX import Data.Vector qualified as Vector -import Imports hiding (cs) +import Imports import Statistics.Sample qualified as Stats import Wire.API.User.Auth diff --git a/services/brig/src/Brig/User/Auth/DB/Cookie.hs b/services/brig/src/Brig/User/Auth/DB/Cookie.hs index d52dfbf1944..c0d43ef2341 100644 --- a/services/brig/src/Brig/User/Auth/DB/Cookie.hs +++ b/services/brig/src/Brig/User/Auth/DB/Cookie.hs @@ -23,7 +23,7 @@ import Brig.User.Auth.DB.Instances () import Cassandra import Data.Id import Data.Time.Clock -import Imports hiding (cs) +import Imports import Wire.API.User.Auth newtype TTL = TTL {ttlSeconds :: Int32} diff --git a/services/brig/src/Brig/User/EJPD.hs b/services/brig/src/Brig/User/EJPD.hs index 31392bfd84b..b5afec1f8f0 100644 --- a/services/brig/src/Brig/User/EJPD.hs +++ b/services/brig/src/Brig/User/EJPD.hs @@ -36,6 +36,7 @@ import Data.ByteString.Conversion import Data.Handle (Handle) import Data.Id (UserId) import Data.Set qualified as Set +import Data.Text qualified as T import Imports hiding (head) import Network.HTTP.Types.Method import Polysemy (Member) @@ -118,7 +119,7 @@ ejpdRequest (fromMaybe False -> includeContacts) (EJPDRequestBody handles) = do case (statusCode resp, responseJsonEither resp) of (200, Right (A.String loc)) -> loc _ -> - cs $ + T.pack $ "could not fetch asset: " <> show key <> ", error: " diff --git a/services/brig/src/Brig/User/Search/Index.hs b/services/brig/src/Brig/User/Search/Index.hs index b3a0b0834e5..fb0ba23c78a 100644 --- a/services/brig/src/Brig/User/Search/Index.hs +++ b/services/brig/src/Brig/User/Search/Index.hs @@ -69,6 +69,7 @@ import Control.Retry (RetryPolicy, exponentialBackoff, limitRetries, recovering) import Data.Aeson as Aeson import Data.Aeson.Encoding import Data.Aeson.Lens +import Data.ByteString (toStrict) import Data.ByteString.Builder (Builder, toLazyByteString) import Data.ByteString.Conversion (toByteString') import Data.ByteString.Conversion qualified as Bytes @@ -80,7 +81,8 @@ import Data.Map qualified as Map import Data.Metrics import Data.Text qualified as T import Data.Text qualified as Text -import Data.Text.Encoding (decodeUtf8, encodeUtf8) +import Data.Text.Encoding +import Data.Text.Encoding.Error import Data.Text.Lazy qualified as LT import Data.Text.Lazy.Builder.Int (decimal) import Data.Text.Lens hiding (text) @@ -339,7 +341,12 @@ createIndex' failIfExists (CreateIndexSettings settings shardCount mbDeleteTempl for_ mbDeleteTemplate $ \templateName@(ES.TemplateName tname) -> do tExists <- ES.templateExists templateName when tExists $ do - dr <- traceES (cs ("Delete index template " <> "\"" <> tname <> "\"")) $ ES.deleteTemplate templateName + dr <- + traceES + ( encodeUtf8 + ("Delete index template " <> "\"" <> tname <> "\"") + ) + $ ES.deleteTemplate templateName unless (ES.isSuccess dr) $ throwM (IndexError "Deleting index template failed.") @@ -895,7 +902,11 @@ reindexRowToIndexUser idpUrl (UserScimExternalId _) = Nothing fromUri :: URI -> Text - fromUri = cs . toLazyByteString . serializeURIRef + fromUri = + decodeUtf8With lenientDecode + . toStrict + . toLazyByteString + . serializeURIRef sso :: UserSSOId -> Maybe Sso sso userSsoId = do diff --git a/services/brig/src/Brig/User/Search/TeamUserSearch.hs b/services/brig/src/Brig/User/Search/TeamUserSearch.hs index 9722be1ad74..90bcb969e96 100644 --- a/services/brig/src/Brig/User/Search/TeamUserSearch.hs +++ b/services/brig/src/Brig/User/Search/TeamUserSearch.hs @@ -33,6 +33,7 @@ import Brig.User.Search.Index import Control.Error (lastMay) import Control.Monad.Catch (MonadThrow (throwM)) import Data.Aeson (decode', encode) +import Data.ByteString (fromStrict, toStrict) import Data.Id (TeamId, idToText) import Data.Range (Range (..)) import Data.Text.Ascii (decodeBase64Url, encodeBase64Url) @@ -66,10 +67,10 @@ teamUserSearch tid mbSearchText mRoleFilter mSortBy mSortOrder (fromRange -> siz either (throwM . IndexLookupError) (pure . mkResult) r where toSearchAfterKey :: PagingState -> Maybe ES.SearchAfterKey - toSearchAfterKey ps = decode' . cs =<< (decodeBase64Url . unPagingState $ ps) + toSearchAfterKey ps = decode' . fromStrict =<< (decodeBase64Url . unPagingState $ ps) fromSearchAfterKey :: ES.SearchAfterKey -> PagingState - fromSearchAfterKey = PagingState . encodeBase64Url . cs . encode + fromSearchAfterKey = PagingState . encodeBase64Url . toStrict . encode mkResult es = let hitsPlusOne = ES.hits . ES.searchHits $ es diff --git a/services/brig/test/integration/API/Calling.hs b/services/brig/test/integration/API/Calling.hs index b6a355b1264..c8008d01cb6 100644 --- a/services/brig/test/integration/API/Calling.hs +++ b/services/brig/test/integration/API/Calling.hs @@ -33,6 +33,7 @@ import Data.List.NonEmpty (NonEmpty ((:|))) import Data.List.NonEmpty qualified as NonEmpty import Data.Misc (Port (..), mkHttpsUrl) import Data.Set qualified as Set +import Data.String.Conversions import Imports import System.FilePath (()) import Test.Tasty diff --git a/services/brig/test/integration/API/Internal/Util.hs b/services/brig/test/integration/API/Internal/Util.hs index 733c23620b2..b37bff338a2 100644 --- a/services/brig/test/integration/API/Internal/Util.hs +++ b/services/brig/test/integration/API/Internal/Util.hs @@ -31,6 +31,7 @@ import Control.Lens ((^.)) import Control.Monad.Catch (MonadCatch) import Data.Id import Data.Proxy (Proxy (Proxy)) +import Data.String.Conversions import Imports import Servant.API ((:>)) import Servant.API.ContentTypes (NoContent) diff --git a/services/brig/test/integration/API/OAuth.hs b/services/brig/test/integration/API/OAuth.hs index 162a920b431..cd08aae8317 100644 --- a/services/brig/test/integration/API/OAuth.hs +++ b/services/brig/test/integration/API/OAuth.hs @@ -37,6 +37,7 @@ import Data.Id import Data.Qualified (Qualified (qUnqualified)) import Data.Range import Data.Set as Set hiding (delete, null, (\\)) +import Data.String.Conversions import Data.Text.Ascii (encodeBase16) import Data.Text.Encoding qualified as T import Data.Time diff --git a/services/brig/test/integration/API/Search.hs b/services/brig/test/integration/API/Search.hs index 4a1e359149d..43216781b57 100644 --- a/services/brig/test/integration/API/Search.hs +++ b/services/brig/test/integration/API/Search.hs @@ -43,6 +43,7 @@ import Data.Handle (fromHandle) import Data.Id import Data.Map.Strict qualified as Map import Data.Qualified (Qualified (qDomain, qUnqualified)) +import Data.String.Conversions import Data.Text qualified as Text import Data.Text.Encoding qualified as Text import Database.Bloodhound qualified as ES diff --git a/services/brig/test/integration/API/Search/Util.hs b/services/brig/test/integration/API/Search/Util.hs index 10e738a0eab..3141b4be83f 100644 --- a/services/brig/test/integration/API/Search/Util.hs +++ b/services/brig/test/integration/API/Search/Util.hs @@ -26,6 +26,7 @@ import Data.Domain (Domain) import Data.Id import Data.Qualified (Qualified (..)) import Data.Range (Range) +import Data.String.Conversions import Data.Text.Encoding (encodeUtf8) import Database.Bloodhound qualified as ES import Imports diff --git a/services/brig/test/integration/API/SystemSettings.hs b/services/brig/test/integration/API/SystemSettings.hs index 1b2f600140c..40b20c0606f 100644 --- a/services/brig/test/integration/API/SystemSettings.hs +++ b/services/brig/test/integration/API/SystemSettings.hs @@ -24,6 +24,7 @@ import Control.Lens import Data.ByteString.Char8 qualified as BS import Data.ByteString.Conversion (toByteString') import Data.Id +import Data.String.Conversions import Imports import Network.Wai.Test as WaiTest import Test.Tasty diff --git a/services/brig/test/integration/API/TeamUserSearch.hs b/services/brig/test/integration/API/TeamUserSearch.hs index c939dc5d16c..84e2a8a3701 100644 --- a/services/brig/test/integration/API/TeamUserSearch.hs +++ b/services/brig/test/integration/API/TeamUserSearch.hs @@ -28,6 +28,7 @@ import Data.ByteString.Conversion (toByteString) import Data.Handle (fromHandle) import Data.Id (TeamId, UserId) import Data.Range (unsafeRange) +import Data.String.Conversions import Imports import System.Random.Shuffle (shuffleM) import Test.Tasty (TestTree, testGroup) diff --git a/services/brig/test/integration/API/User/Account.hs b/services/brig/test/integration/API/User/Account.hs index 4d82aa1382f..66254e93fa1 100644 --- a/services/brig/test/integration/API/User/Account.hs +++ b/services/brig/test/integration/API/User/Account.hs @@ -57,6 +57,7 @@ import Data.Proxy import Data.Qualified import Data.Range import Data.Set qualified as Set +import Data.String.Conversions import Data.Text qualified as T import Data.Text qualified as Text import Data.Text.Encoding qualified as T diff --git a/services/brig/test/integration/API/User/Auth.hs b/services/brig/test/integration/API/User/Auth.hs index 3e59633e2d2..a52cd738b41 100644 --- a/services/brig/test/integration/API/User/Auth.hs +++ b/services/brig/test/integration/API/User/Auth.hs @@ -53,7 +53,7 @@ import Data.Text.Lazy qualified as Lazy import Data.Time.Clock import Data.UUID.V4 qualified as UUID import Data.ZAuth.Token qualified as ZAuth -import Imports hiding (cs) +import Imports import Network.HTTP.Client (equivCookie) import Network.Wai.Utilities.Error qualified as Error import Test.Tasty diff --git a/services/brig/test/integration/API/User/Client.hs b/services/brig/test/integration/API/User/Client.hs index 3372c1b3e56..df4b7c5faaa 100644 --- a/services/brig/test/integration/API/User/Client.hs +++ b/services/brig/test/integration/API/User/Client.hs @@ -52,6 +52,7 @@ import Data.Nonce (isValidBase64UrlEncodedUUID) import Data.Qualified (Qualified (..)) import Data.Range (unsafeRange) import Data.Set qualified as Set +import Data.String.Conversions import Data.Text.Ascii (AsciiChars (validate), encodeBase64UrlUnpadded, toText) import Data.Text.Encoding qualified as T import Data.Time (addUTCTime) diff --git a/services/brig/test/integration/API/User/PasswordReset.hs b/services/brig/test/integration/API/User/PasswordReset.hs index 26f1b5c41e4..55f19b34c28 100644 --- a/services/brig/test/integration/API/User/PasswordReset.hs +++ b/services/brig/test/integration/API/User/PasswordReset.hs @@ -30,7 +30,7 @@ import Cassandra qualified as DB import Data.Aeson as A import Data.Aeson.KeyMap qualified as KeyMap import Data.Misc -import Imports hiding (cs) +import Imports import Test.Tasty hiding (Timeout) import Util import Wire.API.User diff --git a/services/brig/test/integration/API/User/Property.hs b/services/brig/test/integration/API/User/Property.hs index e87733c5d2d..fd16f35793f 100644 --- a/services/brig/test/integration/API/User/Property.hs +++ b/services/brig/test/integration/API/User/Property.hs @@ -27,6 +27,7 @@ import Brig.Options import Brig.Options qualified as Opt import Data.Aeson import Data.ByteString.Char8 qualified as C +import Data.String.Conversions import Data.Text qualified as T import Imports import Network.Wai.Utilities.Error qualified as Error diff --git a/services/brig/test/integration/API/User/Util.hs b/services/brig/test/integration/API/User/Util.hs index 031a521e504..dfd26fb1c26 100644 --- a/services/brig/test/integration/API/User/Util.hs +++ b/services/brig/test/integration/API/User/Util.hs @@ -47,6 +47,7 @@ import Data.List1 qualified as List1 import Data.Misc import Data.Qualified import Data.Range (unsafeRange) +import Data.String.Conversions import Data.Text.Ascii qualified as Ascii import Data.Vector qualified as Vec import Data.ZAuth.Token qualified as ZAuth diff --git a/services/brig/test/integration/API/UserPendingActivation.hs b/services/brig/test/integration/API/UserPendingActivation.hs index 0537b2af448..db47762b6e4 100644 --- a/services/brig/test/integration/API/UserPendingActivation.hs +++ b/services/brig/test/integration/API/UserPendingActivation.hs @@ -36,6 +36,7 @@ import Data.Aeson.Lens (key, _String) import Data.ByteString.Conversion (fromByteString, toByteString') import Data.Id (InvitationId, TeamId, UserId) import Data.Range (unsafeRange) +import Data.String.Conversions import Data.Text.Encoding (encodeUtf8) import Data.UUID qualified as UUID import Data.UUID.V4 qualified as UUID diff --git a/services/brig/test/integration/Federation/End2end.hs b/services/brig/test/integration/Federation/End2end.hs index b696338e0fe..cf1beffc23c 100644 --- a/services/brig/test/integration/Federation/End2end.hs +++ b/services/brig/test/integration/Federation/End2end.hs @@ -38,7 +38,7 @@ import Data.Qualified import Data.Range (checked) import Data.Set qualified as Set import Federation.Util -import Imports hiding (cs) +import Imports import System.IO.Temp import System.Logger qualified as Log import Test.Tasty diff --git a/services/brig/test/integration/Util.hs b/services/brig/test/integration/Util.hs index 46b8aa917c3..c75d25fe2c1 100644 --- a/services/brig/test/integration/Util.hs +++ b/services/brig/test/integration/Util.hs @@ -61,6 +61,7 @@ import Data.Proxy import Data.Qualified hiding (isLocal) import Data.Range import Data.Sequence qualified as Seq +import Data.String.Conversions import Data.Text qualified as T import Data.Text qualified as Text import Data.Text.Ascii qualified as Ascii diff --git a/services/cannon/src/Cannon/Types.hs b/services/cannon/src/Cannon/Types.hs index 63dbdb42c37..9dc29f2e2e8 100644 --- a/services/cannon/src/Cannon/Types.hs +++ b/services/cannon/src/Cannon/Types.hs @@ -126,7 +126,7 @@ lookupReqId :: Logger -> Request -> IO RequestId lookupReqId l r = case lookup requestIdName (requestHeaders r) of Just rid -> pure $ RequestId rid Nothing -> do - localRid <- RequestId . cs . UUID.toText <$> UUID.nextRandom + localRid <- RequestId . UUID.toASCIIBytes <$> UUID.nextRandom Log.info l $ "request-id" .= localRid ~~ "method" .= requestMethod r diff --git a/services/cannon/src/Cannon/WS.hs b/services/cannon/src/Cannon/WS.hs index 1cc3b3aaa84..6837457b636 100644 --- a/services/cannon/src/Cannon/WS.hs +++ b/services/cannon/src/Cannon/WS.hs @@ -67,7 +67,7 @@ import Data.List.Extra (chunksOf) import Data.Text.Encoding (decodeUtf8) import Data.Timeout (TimeoutUnit (..), (#)) import Gundeck.Types -import Imports hiding (cs, threadDelay) +import Imports hiding (threadDelay) import Network.HTTP.Types.Method import Network.HTTP.Types.Status import Network.Wai.Utilities.Error diff --git a/services/cargohold/src/CargoHold/API/Public.hs b/services/cargohold/src/CargoHold/API/Public.hs index 0430c110ef3..794e4ae0318 100644 --- a/services/cargohold/src/CargoHold/API/Public.hs +++ b/services/cargohold/src/CargoHold/API/Public.hs @@ -33,6 +33,8 @@ import Data.Domain import Data.Id import Data.Kind import Data.Qualified +import Data.Text.Encoding +import Data.Text.Encoding.Error import Imports hiding (head) import qualified Network.HTTP.Types as HTTP import Servant.API @@ -88,7 +90,11 @@ iDownloadAssetV3 key = do where -- (NB: don't use HttpsUrl here, as in some test environments we legitimately use "http"!) render :: URI.URI -> Text - render = cs . Builder.toLazyByteString . URI.serializeURIRef + render = + decodeUtf8With lenientDecode + . LBS.toStrict + . Builder.toLazyByteString + . URI.serializeURIRef class HasLocation (tag :: PrincipalTag) where assetLocation :: Local AssetKey -> [Text] diff --git a/services/cargohold/src/CargoHold/Run.hs b/services/cargohold/src/CargoHold/Run.hs index be0228da1ef..2a7165e162d 100644 --- a/services/cargohold/src/CargoHold/Run.hs +++ b/services/cargohold/src/CargoHold/Run.hs @@ -104,7 +104,7 @@ mkApp o = Codensity $ \k -> lookupReqId l r = case lookup requestIdName $ Wai.requestHeaders r of Just rid -> pure $ RequestId rid Nothing -> do - localRid <- RequestId . cs . UUID.toText <$> UUID.nextRandom + localRid <- RequestId . UUID.toASCIIBytes <$> UUID.nextRandom Log.info l $ "request-id" .= localRid ~~ "method" .= Wai.requestMethod r diff --git a/services/federator/default.nix b/services/federator/default.nix index 5cb5b5b2830..423926f9509 100644 --- a/services/federator/default.nix +++ b/services/federator/default.nix @@ -52,6 +52,7 @@ , servant-client , servant-client-core , servant-server +, string-conversions , tasty , tasty-hunit , tasty-quickcheck @@ -61,6 +62,7 @@ , transformers , types-common , unix +, utf8-string , uuid , wai , wai-extra @@ -120,6 +122,7 @@ mkDerivation { transformers types-common unix + utf8-string uuid wai wai-utilities @@ -154,6 +157,7 @@ mkDerivation { QuickCheck random servant-client-core + string-conversions tasty-hunit text types-common @@ -188,6 +192,7 @@ mkDerivation { servant-client servant-client-core servant-server + string-conversions tasty tasty-hunit tasty-quickcheck diff --git a/services/federator/federator.cabal b/services/federator/federator.cabal index 0a0767edeb2..4fa411b3be0 100644 --- a/services/federator/federator.cabal +++ b/services/federator/federator.cabal @@ -147,6 +147,7 @@ library , transformers , types-common , unix + , utf8-string , uuid , wai , wai-utilities @@ -303,6 +304,7 @@ executable federator-integration , QuickCheck , random , servant-client-core + , string-conversions , tasty-hunit , text , types-common @@ -404,6 +406,7 @@ test-suite federator-tests , servant-client , servant-client-core , servant-server + , string-conversions , tasty , tasty-hunit , tasty-quickcheck diff --git a/services/federator/src/Federator/Discovery.hs b/services/federator/src/Federator/Discovery.hs index 9050125e68c..901042f86ba 100644 --- a/services/federator/src/Federator/Discovery.hs +++ b/services/federator/src/Federator/Discovery.hs @@ -96,7 +96,7 @@ runFederatorDiscovery = interpret $ \case -- FUTUREWORK(federation): This string conversion is wrong, we should encode -- this using IDNA encoding or expect domain to be bytestring everywhere -- (https://wearezeta.atlassian.net/browse/SQCORE-912) - domainSrv d = cs $ "_wire-server-federator._tcp." <> domainText d + domainSrv d = Text.encodeUtf8 $ "_wire-server-federator._tcp." <> domainText d lookupDomainByDNS :: ( Member DNSLookup r, diff --git a/services/federator/src/Federator/ExternalServer.hs b/services/federator/src/Federator/ExternalServer.hs index 513bf5d73e8..4a2f83d4c5f 100644 --- a/services/federator/src/Federator/ExternalServer.hs +++ b/services/federator/src/Federator/ExternalServer.hs @@ -148,7 +148,7 @@ callInward component (RPC rpc) mReqId originDomain (CertHeader cert) wreq = do rid <- case mReqId of Just r -> pure r Nothing -> do - localRid <- liftIO $ RequestId . cs . UUID.toText <$> UUID.nextRandom + localRid <- liftIO $ RequestId . Text.encodeUtf8 . UUID.toText <$> UUID.nextRandom info $ "request-id" .= localRid ~~ "method" .= Wai.requestMethod wreq diff --git a/services/federator/src/Federator/Health.hs b/services/federator/src/Federator/Health.hs index 7a2228b74d0..857a3e56415 100644 --- a/services/federator/src/Federator/Health.hs +++ b/services/federator/src/Federator/Health.hs @@ -1,5 +1,7 @@ module Federator.Health where +import Data.ByteString (fromStrict) +import Data.ByteString.UTF8 qualified as UTF8 import Imports import Network.HTTP.Client import Network.HTTP.Types.Status qualified as HTTP @@ -20,4 +22,11 @@ status mgr otherName otherPort False = do res <- liftIO $ httpNoBody req mgr if HTTP.statusIsSuccessful $ responseStatus res then pure NoContent - else throwError Servant.err500 {Servant.errBody = otherName <> " server responded with status code = " <> cs (show (responseStatus res))} + else + throwError + Servant.err500 + { Servant.errBody = + otherName + <> " server responded with status code = " + <> (fromStrict . UTF8.fromString . show . responseStatus $ res) + } diff --git a/services/federator/src/Federator/InternalServer.hs b/services/federator/src/Federator/InternalServer.hs index 13dd401f3b6..ef6cbd0cce4 100644 --- a/services/federator/src/Federator/InternalServer.hs +++ b/services/federator/src/Federator/InternalServer.hs @@ -27,6 +27,7 @@ import Data.Domain import Data.Id import Data.Metrics.Servant qualified as Metrics import Data.Proxy +import Data.Text.Encoding qualified as T import Data.UUID as UUID import Data.UUID.V4 as UUID import Federator.Env @@ -117,7 +118,7 @@ callOutward mReqId targetDomain component (RPC path) req = do rid <- case mReqId of Just r -> pure r Nothing -> do - localRid <- liftIO $ RequestId . cs . UUID.toText <$> UUID.nextRandom + localRid <- liftIO $ RequestId . T.encodeUtf8 . UUID.toText <$> UUID.nextRandom info $ "request-id" .= localRid ~~ "method" .= Wai.requestMethod req diff --git a/services/federator/src/Federator/Response.hs b/services/federator/src/Federator/Response.hs index 04662a1da3a..6f70df5a390 100644 --- a/services/federator/src/Federator/Response.hs +++ b/services/federator/src/Federator/Response.hs @@ -29,6 +29,7 @@ import Control.Lens import Control.Monad.Codensity import Data.ByteString.Builder import Data.Kind +import Data.Text qualified as T import Federator.Discovery import Federator.Env import Federator.Error @@ -172,7 +173,7 @@ getFederationDomainConfigs :: Env -> IO FedUp.FederationDomainConfigs getFederationDomainConfigs env = do let mgr = env ^. httpManager Endpoint h p = env ^. service $ Brig - baseurl = BaseUrl Http (cs h) (fromIntegral p) "" + baseurl = BaseUrl Http (T.unpack h) (fromIntegral p) "" clientEnv = mkClientEnv mgr baseurl FedUp.getFederationDomainConfigs clientEnv >>= \case Right v -> pure v diff --git a/services/federator/src/Federator/Service.hs b/services/federator/src/Federator/Service.hs index 691f4629dff..b4f859d52bf 100644 --- a/services/federator/src/Federator/Service.hs +++ b/services/federator/src/Federator/Service.hs @@ -94,7 +94,7 @@ interpretServiceHTTP = interpret $ \case path = rpcPath, requestHeaders = [ ("Content-Type", "application/json"), - (originDomainHeaderName, cs (domainText domain)), + (originDomainHeaderName, Text.encodeUtf8 (domainText domain)), (RPC.requestIdName, unRequestId rid) ] <> headers diff --git a/services/federator/test/integration/Test/Federator/IngressSpec.hs b/services/federator/test/integration/Test/Federator/IngressSpec.hs index e8eb7151c7e..62b4f1b7401 100644 --- a/services/federator/test/integration/Test/Federator/IngressSpec.hs +++ b/services/federator/test/integration/Test/Federator/IngressSpec.hs @@ -25,6 +25,7 @@ import Data.Binary.Builder import Data.Domain import Data.Id import Data.LegalHold (UserLegalHoldStatus (UserLegalHoldNoConsent)) +import Data.String.Conversions import Data.Text.Encoding qualified as Text import Federator.Discovery import Federator.Monitor (FederationSetupError) diff --git a/services/federator/test/integration/Test/Federator/Util.hs b/services/federator/test/integration/Test/Federator/Util.hs index 6d8a61f0093..549590cb6af 100644 --- a/services/federator/test/integration/Test/Federator/Util.hs +++ b/services/federator/test/integration/Test/Federator/Util.hs @@ -37,6 +37,7 @@ import Data.Aeson.Types qualified as Aeson import Data.ByteString.Char8 qualified as C8 import Data.Id import Data.Misc +import Data.String.Conversions import Data.Text qualified as Text import Data.UUID qualified as UUID import Data.UUID.V4 qualified as UUID diff --git a/services/federator/test/unit/Test/Federator/ExternalServer.hs b/services/federator/test/unit/Test/Federator/ExternalServer.hs index 48680d09d33..ec0b0438e24 100644 --- a/services/federator/test/unit/Test/Federator/ExternalServer.hs +++ b/services/federator/test/unit/Test/Federator/ExternalServer.hs @@ -24,6 +24,7 @@ import Data.ByteString qualified as BS import Data.Default import Data.Domain import Data.Sequence as Seq +import Data.String.Conversions import Data.Text.Encoding qualified as Text import Federator.Discovery import Federator.Error.ServerError (ServerError (..)) diff --git a/services/galley/default.nix b/services/galley/default.nix index 279ee871813..3e82122d703 100644 --- a/services/galley/default.nix +++ b/services/galley/default.nix @@ -89,6 +89,7 @@ , ssl-util , stm , streaming-commons +, string-conversions , tagged , tasty , tasty-ant-xml @@ -109,6 +110,7 @@ , unliftio , unordered-containers , uri-bytestring +, utf8-string , uuid , uuid-types , vector @@ -206,6 +208,7 @@ mkDerivation { types-common-journal unliftio uri-bytestring + utf8-string uuid wai wai-extra @@ -281,6 +284,7 @@ mkDerivation { sop-core ssl-util streaming-commons + string-conversions tagged tasty tasty-ant-xml diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index 91556478195..58ae6271940 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -357,6 +357,7 @@ library , types-common-journal >=0.1 , unliftio >=0.2 , uri-bytestring >=0.2 + , utf8-string , uuid >=1.3 , wai >=3.0 , wai-extra >=3.0 @@ -527,6 +528,7 @@ executable galley-integration , sop-core , ssl-util , streaming-commons + , string-conversions , tagged , tasty >=0.8 , tasty-ant-xml diff --git a/services/galley/src/Galley/API/Internal.hs b/services/galley/src/Galley/API/Internal.hs index 5e66acec806..c2a12f98315 100644 --- a/services/galley/src/Galley/API/Internal.hs +++ b/services/galley/src/Galley/API/Internal.hs @@ -26,6 +26,7 @@ where import Control.Exception.Safe (catchAny) import Control.Lens hiding (Getter, Setter, (.=)) +import Data.ByteString.UTF8 qualified as UTF8 import Data.Id as Id import Data.Json.Util (ToJSONObject (toJSONObject)) import Data.Map qualified as Map @@ -453,7 +454,7 @@ safeForever :: String -> App () -> App () safeForever funName action = forever $ action `catchAny` \exc -> do - err $ "error" .= show exc ~~ msg (val $ cs funName <> " failed") + err $ "error" .= show exc ~~ msg (val $ UTF8.fromString funName <> " failed") threadDelay 60000000 -- pause to keep worst-case noise in logs manageable guardLegalholdPolicyConflictsH :: diff --git a/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs index 484f5812332..364feaf5ce2 100644 --- a/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs +++ b/services/galley/src/Galley/API/MLS/Commit/ExternalCommit.hs @@ -33,7 +33,7 @@ import Galley.API.MLS.Types import Galley.API.MLS.Util import Galley.Effects import Galley.Effects.MemberStore -import Imports hiding (cs) +import Imports import Polysemy import Polysemy.Error import Polysemy.Resource (Resource) diff --git a/services/galley/src/Galley/API/MLS/One2One.hs b/services/galley/src/Galley/API/MLS/One2One.hs index f0632f737c5..a5b01e129a3 100644 --- a/services/galley/src/Galley/API/MLS/One2One.hs +++ b/services/galley/src/Galley/API/MLS/One2One.hs @@ -30,7 +30,7 @@ import Galley.API.MLS.Types import Galley.Data.Conversation.Types qualified as Data import Galley.Effects.ConversationStore import Galley.Types.UserList -import Imports hiding (cs) +import Imports import Polysemy import Wire.API.Conversation hiding (Member) import Wire.API.Conversation.Protocol diff --git a/services/galley/src/Galley/API/MLS/Proposal.hs b/services/galley/src/Galley/API/MLS/Proposal.hs index 9047db2a946..4df31ac4c97 100644 --- a/services/galley/src/Galley/API/MLS/Proposal.hs +++ b/services/galley/src/Galley/API/MLS/Proposal.hs @@ -48,7 +48,7 @@ import Galley.Effects.BrigAccess import Galley.Effects.ProposalStore import Galley.Env import Galley.Options -import Imports hiding (cs) +import Imports import Polysemy import Polysemy.Error import Polysemy.Input diff --git a/services/galley/src/Galley/API/MLS/Removal.hs b/services/galley/src/Galley/API/MLS/Removal.hs index f8491cdba47..ce7f4c97f38 100644 --- a/services/galley/src/Galley/API/MLS/Removal.hs +++ b/services/galley/src/Galley/API/MLS/Removal.hs @@ -42,7 +42,7 @@ import Galley.Effects.ProposalStore import Galley.Effects.SubConversationStore import Galley.Env import Galley.Types.Conversations.Members -import Imports hiding (cs) +import Imports import Polysemy import Polysemy.Error import Polysemy.Input diff --git a/services/galley/src/Galley/API/MLS/SubConversation.hs b/services/galley/src/Galley/API/MLS/SubConversation.hs index ba13a24757b..b78d8965a8b 100644 --- a/services/galley/src/Galley/API/MLS/SubConversation.hs +++ b/services/galley/src/Galley/API/MLS/SubConversation.hs @@ -50,7 +50,7 @@ import Galley.Effects import Galley.Effects.FederatorAccess import Galley.Effects.MemberStore qualified as Eff import Galley.Effects.SubConversationStore qualified as Eff -import Imports hiding (cs) +import Imports import Polysemy import Polysemy.Error import Polysemy.Input diff --git a/services/galley/src/Galley/API/MLS/Types.hs b/services/galley/src/Galley/API/MLS/Types.hs index 13a14d9b6a4..5cfce7bd88a 100644 --- a/services/galley/src/Galley/API/MLS/Types.hs +++ b/services/galley/src/Galley/API/MLS/Types.hs @@ -27,7 +27,7 @@ import Data.Qualified import GHC.Records (HasField (..)) import Galley.Data.Conversation.Types import Galley.Types.Conversations.Members -import Imports hiding (cs) +import Imports import Wire.API.Conversation import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite diff --git a/services/galley/src/Galley/API/MLS/Welcome.hs b/services/galley/src/Galley/API/MLS/Welcome.hs index 188d73e6d0d..6f051263247 100644 --- a/services/galley/src/Galley/API/MLS/Welcome.hs +++ b/services/galley/src/Galley/API/MLS/Welcome.hs @@ -34,7 +34,7 @@ import Galley.API.Push import Galley.Effects.ExternalAccess import Galley.Effects.FederatorAccess import Gundeck.Types.Push.V2 (RecipientClients (..)) -import Imports hiding (cs) +import Imports import Network.Wai.Utilities.JSONResponse import Polysemy import Polysemy.Input diff --git a/services/galley/src/Galley/API/Query.hs b/services/galley/src/Galley/API/Query.hs index aaa01a9daa4..950099e7680 100644 --- a/services/galley/src/Galley/API/Query.hs +++ b/services/galley/src/Galley/API/Query.hs @@ -79,7 +79,7 @@ import Galley.Env import Galley.Options import Galley.Types.Conversations.Members import Galley.Types.Teams -import Imports hiding (cs) +import Imports import Network.Wai import Network.Wai.Predicate hiding (Error, result, setStatus) import Network.Wai.Utilities hiding (Error) diff --git a/services/galley/src/Galley/API/Teams/Features.hs b/services/galley/src/Galley/API/Teams/Features.hs index e9085fca925..06c7a09d3d0 100644 --- a/services/galley/src/Galley/API/Teams/Features.hs +++ b/services/galley/src/Galley/API/Teams/Features.hs @@ -38,6 +38,7 @@ where import Control.Lens import Data.ByteString.Conversion (toByteString') +import Data.ByteString.UTF8 qualified as UTF8 import Data.Id import Data.Json.Util import Data.Kind @@ -204,7 +205,7 @@ pushFeatureConfigEvent tid event = do P.warn $ Log.field "action" (Log.val "Features.pushFeatureConfigEvent") . Log.field "feature" (Log.val (toByteString' . Event._eventFeatureName $ event)) - . Log.field "team" (Log.val (cs . show $ tid)) + . Log.field "team" (Log.val (UTF8.fromString . show $ tid)) . Log.msg @Text "Fanout limit exceeded. Events will not be sent." else do let recipients = membersToRecipients Nothing (memList ^. teamMembers) diff --git a/services/galley/src/Galley/Cassandra/Conversation.hs b/services/galley/src/Galley/Cassandra/Conversation.hs index 286fd4b3264..e685085b0a0 100644 --- a/services/galley/src/Galley/Cassandra/Conversation.hs +++ b/services/galley/src/Galley/Cassandra/Conversation.hs @@ -48,7 +48,7 @@ import Galley.Effects.ConversationStore (ConversationStore (..)) import Galley.Types.Conversations.Members import Galley.Types.ToUserRole import Galley.Types.UserList -import Imports hiding (cs) +import Imports import Polysemy import Polysemy.Input import Polysemy.TinyLog diff --git a/services/galley/src/Galley/Cassandra/Conversation/Members.hs b/services/galley/src/Galley/Cassandra/Conversation/Members.hs index 2bda5331335..4b0482f712b 100644 --- a/services/galley/src/Galley/Cassandra/Conversation/Members.hs +++ b/services/galley/src/Galley/Cassandra/Conversation/Members.hs @@ -44,7 +44,7 @@ import Galley.Effects.MemberStore (MemberStore (..)) import Galley.Types.Conversations.Members import Galley.Types.ToUserRole import Galley.Types.UserList -import Imports hiding (Set, cs) +import Imports hiding (Set) import Polysemy import Polysemy.Input import Polysemy.TinyLog diff --git a/services/galley/src/Galley/Cassandra/Store.hs b/services/galley/src/Galley/Cassandra/Store.hs index a25fa5ab289..16794523557 100644 --- a/services/galley/src/Galley/Cassandra/Store.hs +++ b/services/galley/src/Galley/Cassandra/Store.hs @@ -21,7 +21,7 @@ module Galley.Cassandra.Store where import Cassandra -import Imports hiding (cs) +import Imports import Polysemy import Polysemy.Input diff --git a/services/galley/src/Galley/Cassandra/SubConversation.hs b/services/galley/src/Galley/Cassandra/SubConversation.hs index 687c904402a..4a00cf0a29e 100644 --- a/services/galley/src/Galley/Cassandra/SubConversation.hs +++ b/services/galley/src/Galley/Cassandra/SubConversation.hs @@ -33,7 +33,7 @@ import Galley.Cassandra.Queries qualified as Cql import Galley.Cassandra.Store (embedClient) import Galley.Cassandra.Util import Galley.Effects.SubConversationStore (SubConversationStore (..)) -import Imports hiding (cs) +import Imports import Polysemy import Polysemy.Input import Polysemy.TinyLog diff --git a/services/galley/src/Galley/Intra/User.hs b/services/galley/src/Galley/Intra/User.hs index 01c1dc64ccd..27b3497afae 100644 --- a/services/galley/src/Galley/Intra/User.hs +++ b/services/galley/src/Galley/Intra/User.hs @@ -46,6 +46,7 @@ import Data.ByteString.Char8 qualified as BSC import Data.ByteString.Conversion import Data.Id import Data.Qualified +import Data.Text qualified as Text import Data.Text.Lazy qualified as Lazy import Galley.API.Error import Galley.Env @@ -253,11 +254,11 @@ runHereClientM action = do mgr <- view manager brigep <- view brig let env = Client.mkClientEnv mgr baseurl - baseurl = Client.BaseUrl Client.Http (cs $ brigep ^. host) (fromIntegral $ brigep ^. port) "" + baseurl = Client.BaseUrl Client.Http (Text.unpack $ brigep ^. host) (fromIntegral $ brigep ^. port) "" liftIO $ Client.runClientM action env handleServantResp :: Either Client.ClientError a -> App a handleServantResp (Right cfg) = pure cfg -handleServantResp (Left errmsg) = throwM . internalErrorWithDescription . cs . show $ errmsg +handleServantResp (Left errmsg) = throwM . internalErrorWithDescription . Lazy.pack . show $ errmsg diff --git a/services/galley/src/Galley/Monad.hs b/services/galley/src/Galley/Monad.hs index c9be145a29e..1780f3d827c 100644 --- a/services/galley/src/Galley/Monad.hs +++ b/services/galley/src/Galley/Monad.hs @@ -26,7 +26,7 @@ import Control.Lens import Control.Monad.Catch import Control.Monad.Except import Galley.Env -import Imports hiding (cs, log) +import Imports hiding (log) import Polysemy import Polysemy.Input import System.Logger diff --git a/services/galley/src/Galley/Run.hs b/services/galley/src/Galley/Run.hs index 592ad9f3ed8..744e9dc4220 100644 --- a/services/galley/src/Galley/Run.hs +++ b/services/galley/src/Galley/Run.hs @@ -32,6 +32,7 @@ import Control.Exception (finally) import Control.Lens (view, (.~), (^.)) import Control.Monad.Codensity import Data.Aeson qualified as Aeson +import Data.ByteString.UTF8 qualified as UTF8 import Data.Id import Data.Metrics (Metrics) import Data.Metrics.AWS (gaugeTokenRemaing) @@ -133,7 +134,7 @@ mkApp opts = lookupReqId l r = case lookup requestIdName $ requestHeaders r of Just rid -> pure $ RequestId rid Nothing -> do - localRid <- RequestId . cs . UUID.toText <$> UUID.nextRandom + localRid <- RequestId . UUID.toASCIIBytes <$> UUID.nextRandom Log.info l $ "request-id" .= localRid ~~ "method" .= requestMethod r @@ -155,7 +156,7 @@ bodyParserErrorFormatter' :: Servant.ErrorFormatter bodyParserErrorFormatter' _ _ errMsg = Servant.ServerError { Servant.errHTTPCode = HTTP.statusCode HTTP.status400, - Servant.errReasonPhrase = cs $ HTTP.statusMessage HTTP.status400, + Servant.errReasonPhrase = UTF8.toString $ HTTP.statusMessage HTTP.status400, Servant.errBody = Aeson.encode $ Aeson.object diff --git a/services/galley/test/integration/API/Federation/Util.hs b/services/galley/test/integration/API/Federation/Util.hs index 9d15edc5ee9..9f2f052365c 100644 --- a/services/galley/test/integration/API/Federation/Util.hs +++ b/services/galley/test/integration/API/Federation/Util.hs @@ -29,6 +29,7 @@ where import Data.Kind import Data.Qualified import Data.SOP +import Data.String.Conversions import GHC.TypeLits import Imports import Servant diff --git a/services/galley/test/integration/API/Teams/LegalHold/Util.hs b/services/galley/test/integration/API/Teams/LegalHold/Util.hs index fec9706579b..85e2e37d195 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/Util.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/Util.hs @@ -30,6 +30,7 @@ import Data.List1 qualified as List1 import Data.Misc (PlainTextPassword6) import Data.PEM import Data.Streaming.Network (bindRandomPortTCP) +import Data.String.Conversions import Data.Tagged import Data.Text.Encoding (encodeUtf8) import Galley.Options diff --git a/services/galley/test/integration/API/Util.hs b/services/galley/test/integration/API/Util.hs index 936548d6363..5f80a490368 100644 --- a/services/galley/test/integration/API/Util.hs +++ b/services/galley/test/integration/API/Util.hs @@ -65,6 +65,7 @@ import Data.Range import Data.Serialize (runPut) import Data.Set qualified as Set import Data.Singletons +import Data.String.Conversions import Data.Text qualified as Text import Data.Text.Encoding qualified as T import Data.Text.Encoding qualified as Text diff --git a/services/gundeck/default.nix b/services/gundeck/default.nix index bf63e2899c8..a1c3759ede2 100644 --- a/services/gundeck/default.nix +++ b/services/gundeck/default.nix @@ -57,6 +57,7 @@ , safe-exceptions , scientific , servant-server +, string-conversions , tagged , tasty , tasty-ant-xml @@ -205,6 +206,7 @@ mkDerivation { quickcheck-instances quickcheck-state-machine scientific + string-conversions tasty tasty-hunit tasty-quickcheck diff --git a/services/gundeck/gundeck.cabal b/services/gundeck/gundeck.cabal index f5c42bf0ddf..45b786e9ca2 100644 --- a/services/gundeck/gundeck.cabal +++ b/services/gundeck/gundeck.cabal @@ -474,6 +474,7 @@ test-suite gundeck-tests , quickcheck-instances , quickcheck-state-machine , scientific + , string-conversions , tasty , tasty-hunit , tasty-quickcheck diff --git a/services/gundeck/src/Gundeck/Monad.hs b/services/gundeck/src/Gundeck/Monad.hs index 809c7192d1d..66b234569d3 100644 --- a/services/gundeck/src/Gundeck/Monad.hs +++ b/services/gundeck/src/Gundeck/Monad.hs @@ -187,7 +187,7 @@ lookupReqId :: Logger -> Request -> IO RequestId lookupReqId l r = case lookup requestIdName (requestHeaders r) of Just rid -> pure $ RequestId rid Nothing -> do - localRid <- RequestId . cs . UUID.toText <$> UUID.nextRandom + localRid <- RequestId . UUID.toASCIIBytes <$> UUID.nextRandom Log.info l $ "request-id" .= localRid ~~ "method" .= requestMethod r diff --git a/services/gundeck/src/Gundeck/Notification/Data.hs b/services/gundeck/src/Gundeck/Notification/Data.hs index d680e68f2c4..de18c7f5eaf 100644 --- a/services/gundeck/src/Gundeck/Notification/Data.hs +++ b/services/gundeck/src/Gundeck/Notification/Data.hs @@ -39,7 +39,7 @@ import Data.Sequence qualified as Seq import Gundeck.Env import Gundeck.Options (NotificationTTL (..), internalPageSize, maxPayloadLoadSize, settings) import Gundeck.Push.Native.Serialise () -import Imports hiding (cs) +import Imports import UnliftIO (pooledForConcurrentlyN_) import UnliftIO.Async (pooledMapConcurrentlyN) import Wire.API.Internal.Notification diff --git a/services/gundeck/src/Gundeck/Push.hs b/services/gundeck/src/Gundeck/Push.hs index 56ae375680e..3e6fa5c05c6 100644 --- a/services/gundeck/src/Gundeck/Push.hs +++ b/services/gundeck/src/Gundeck/Push.hs @@ -62,7 +62,7 @@ import Gundeck.ThreadBudget import Gundeck.Types import Gundeck.Types.Presence qualified as Presence import Gundeck.Util -import Imports hiding (cs) +import Imports import Network.HTTP.Types import Network.Wai.Utilities import System.Logger.Class (msg, val, (+++), (.=), (~~)) diff --git a/services/gundeck/src/Gundeck/Push/Websocket.hs b/services/gundeck/src/Gundeck/Push/Websocket.hs index c6a9190aba0..64a51c5f9d9 100644 --- a/services/gundeck/src/Gundeck/Push/Websocket.hs +++ b/services/gundeck/src/Gundeck/Push/Websocket.hs @@ -44,7 +44,7 @@ import Gundeck.Monad import Gundeck.Presence.Data qualified as Presence import Gundeck.Types.Presence import Gundeck.Util -import Imports hiding (cs) +import Imports import Network.HTTP.Client (HttpExceptionContent (..)) import Network.HTTP.Client.Internal qualified as Http import Network.HTTP.Types (StdMethod (POST), status200, status410) diff --git a/services/gundeck/src/Gundeck/Run.hs b/services/gundeck/src/Gundeck/Run.hs index 46d6b407c33..4a919bd0ba7 100644 --- a/services/gundeck/src/Gundeck/Run.hs +++ b/services/gundeck/src/Gundeck/Run.hs @@ -102,7 +102,7 @@ run o = do Just rid -> do mkapp (RequestId rid) req cont Nothing -> do - localRid <- RequestId . cs . UUID.toText <$> UUID.nextRandom + localRid <- RequestId . UUID.toASCIIBytes <$> UUID.nextRandom Log.info logger $ "request-id" .= localRid ~~ "method" .= Wai.requestMethod req diff --git a/services/gundeck/test/unit/MockGundeck.hs b/services/gundeck/test/unit/MockGundeck.hs index 0dacd4e378f..19d35241dbd 100644 --- a/services/gundeck/test/unit/MockGundeck.hs +++ b/services/gundeck/test/unit/MockGundeck.hs @@ -61,6 +61,7 @@ import Data.Misc (Milliseconds (Ms)) import Data.Range import Data.Scientific qualified as Scientific import Data.Set qualified as Set +import Data.String.Conversions import Gundeck.Aws.Arn as Aws import Gundeck.Options import Gundeck.Push diff --git a/services/gundeck/test/unit/ThreadBudget.hs b/services/gundeck/test/unit/ThreadBudget.hs index bd06c866dea..f9f21656aa3 100644 --- a/services/gundeck/test/unit/ThreadBudget.hs +++ b/services/gundeck/test/unit/ThreadBudget.hs @@ -30,6 +30,7 @@ import Control.Concurrent.Async import Control.Lens import Control.Monad.Catch (MonadCatch, catch) import Data.Metrics.Middleware (metrics) +import Data.String.Conversions import Data.Time import GHC.Generics import Gundeck.Options diff --git a/services/proxy/src/Proxy/Proxy.hs b/services/proxy/src/Proxy/Proxy.hs index 9cbb5d20899..cc7f6c5f8fc 100644 --- a/services/proxy/src/Proxy/Proxy.hs +++ b/services/proxy/src/Proxy/Proxy.hs @@ -17,11 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Proxy.Proxy - ( Proxy, - runProxy, - ) -where +module Proxy.Proxy (Proxy, runProxy) where import Bilge.Request (requestIdName) import Control.Lens hiding ((.=)) @@ -40,7 +36,7 @@ import System.Logger.Class hiding (Error, info) newtype Proxy a = Proxy { unProxy :: ReaderT Env IO a } - deriving + deriving newtype ( Functor, Applicative, Monad, @@ -68,7 +64,7 @@ lookupReqId :: Logger -> Request -> IO RequestId lookupReqId l r = case lookup requestIdName (requestHeaders r) of Just rid -> pure $ RequestId rid Nothing -> do - localRid <- RequestId . cs . UUID.toText <$> UUID.nextRandom + localRid <- RequestId . UUID.toASCIIBytes <$> UUID.nextRandom Log.info l $ "request-id" .= localRid ~~ "method" .= requestMethod r diff --git a/services/spar/default.nix b/services/spar/default.nix index f31e52c6ff1..ff861547cec 100644 --- a/services/spar/default.nix +++ b/services/spar/default.nix @@ -59,6 +59,7 @@ , servant-openapi3 , servant-server , silently +, string-conversions , tasty-hunit , text , text-latin1 @@ -67,6 +68,7 @@ , transformers , types-common , uri-bytestring +, utf8-string , uuid , vector , wai @@ -125,6 +127,7 @@ mkDerivation { transformers types-common uri-bytestring + utf8-string uuid wai wai-utilities @@ -179,6 +182,7 @@ mkDerivation { servant servant-server silently + string-conversions tasty-hunit text time @@ -186,6 +190,7 @@ mkDerivation { transformers types-common uri-bytestring + utf8-string uuid vector wai-extra @@ -220,6 +225,7 @@ mkDerivation { saml2-web-sso servant servant-openapi3 + string-conversions time tinylog types-common diff --git a/services/spar/migrate-data/src/Spar/DataMigration/V2_UserV2.hs b/services/spar/migrate-data/src/Spar/DataMigration/V2_UserV2.hs index ae532696d8d..ac7d49efca6 100644 --- a/services/spar/migrate-data/src/Spar/DataMigration/V2_UserV2.hs +++ b/services/spar/migrate-data/src/Spar/DataMigration/V2_UserV2.hs @@ -21,12 +21,14 @@ module Spar.DataMigration.V2_UserV2 (migration) where import Cassandra import qualified Conduit as C +import qualified Data.ByteString.UTF8 as UTF8 import Data.Conduit import qualified Data.Conduit.Combinators as CC import Data.Conduit.Internal (zipSources) import qualified Data.Conduit.List as CL import Data.Id import qualified Data.Map.Strict as Map +import qualified Data.Text as T import Data.Time (UTCTime) import Imports import qualified SAML2.WebSSO as SAML @@ -173,16 +175,16 @@ filterResolved resolver migMapInv = mbAssoc <- await for_ mbAssoc $ \(new@(issuer, nid), olds) -> do let yieldOld (nameId, uid) = yield (issuer, nid, nameId, uid) - let issuerURI = cs . serializeURIRef' . _fromIssuer $ issuer + let issuerURI = UTF8.toString . serializeURIRef' . _fromIssuer $ issuer case olds of [] -> pure () [old] -> yieldOld old (old1 : old2 : rest) -> lift (resolver new (List2 old1 old2 rest)) >>= \case Left _ -> - lift $ logError $ unwords ["Couldnt resolve collisision of", issuerURI, cs (unNormalizedUNameID nid), show olds] + lift $ logError $ unwords ["Couldnt resolve collisision of", issuerURI, T.unpack (unNormalizedUNameID nid), show olds] Right old -> do - lift $ logInfo $ unwords ["Resolved collision", issuerURI, cs (unNormalizedUNameID nid), show (fmap snd olds), "to", show (snd old)] + lift $ logInfo $ unwords ["Resolved collision", issuerURI, T.unpack (unNormalizedUNameID nid), show (fmap snd olds), "to", show (snd old)] yieldOld old go diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index 43e9582ceda..daa07906416 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -189,6 +189,7 @@ library , transformers , types-common , uri-bytestring + , utf8-string , uuid , wai , wai-utilities @@ -377,6 +378,7 @@ executable spar-integration , servant-server , silently , spar + , string-conversions , tasty-hunit , text , time @@ -472,6 +474,7 @@ executable spar-migrate-data , tinylog , types-common , uri-bytestring + , utf8-string default-language: Haskell2010 @@ -626,6 +629,7 @@ test-suite spec , servant , servant-openapi3 , spar + , string-conversions , time , tinylog , types-common diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index 8f27fab4dbc..ae3a3d94d90 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -55,7 +55,9 @@ import Data.Id import Data.Proxy import Data.Range import qualified Data.Set as Set +import Data.Text.Encoding.Error import qualified Data.Text.Lazy as T +import Data.Text.Lazy.Encoding import Data.Time import Imports import Polysemy @@ -357,7 +359,7 @@ idpGetRaw zusr idpid = do _ <- authorizeIdP zusr idp IdPRawMetadataStore.get idpid >>= \case Just txt -> pure $ RawIdPMetadata txt - Nothing -> throwSparSem $ SparIdPNotFound (cs $ show idpid) + Nothing -> throwSparSem $ SparIdPNotFound (T.pack $ show idpid) idpGetAll :: ( Member Random r, @@ -702,7 +704,10 @@ validateIdPUpdate zusr _idpMetadata _idpId = withDebugLog "validateIdPUpdate" (J where errUnknownIdP = SAML.UnknownIdP $ enc uri where - enc = cs . toLazyByteString . URI.serializeURIRef + enc = + decodeUtf8With lenientDecode + . toLazyByteString + . URI.serializeURIRef uri = _idpMetadata ^. SAML.edIssuer . SAML.fromIssuer withDebugLog :: Member (Logger String) r => String -> (a -> Maybe String) -> Sem r a -> Sem r a @@ -723,7 +728,11 @@ authorizeIdP :: Maybe UserId -> IdP -> Sem r (UserId, TeamId) -authorizeIdP Nothing _ = throw (SAML.CustomError $ SparNoPermission (cs $ show CreateUpdateDeleteIdp)) +authorizeIdP Nothing _ = + throw + ( SAML.CustomError $ + SparNoPermission (T.pack $ show CreateUpdateDeleteIdp) + ) authorizeIdP (Just zusr) idp = do let teamid = idp ^. SAML.idpExtraInfo . team GalleyAccess.assertHasPermission teamid CreateUpdateDeleteIdp zusr diff --git a/services/spar/src/Spar/App.hs b/services/spar/src/Spar/App.hs index eb51bd4232c..722b65ab91c 100644 --- a/services/spar/src/Spar/App.hs +++ b/services/spar/src/Spar/App.hs @@ -43,11 +43,17 @@ import Control.Lens hiding ((.=)) import Control.Monad.Except import Data.Aeson as Aeson (encode, object, (.=)) import Data.Aeson.Text as Aeson (encodeToLazyText) +import Data.ByteString (toStrict) import qualified Data.ByteString.Builder as Builder +import qualified Data.ByteString.UTF8 as UTF8 import qualified Data.CaseInsensitive as CI import Data.Id +import qualified Data.Text as Text import Data.Text.Ascii (encodeBase64, toText) +import qualified Data.Text.Encoding as Text +import Data.Text.Encoding.Error import qualified Data.Text.Lazy as LText +import qualified Data.Text.Lazy.Encoding as LText import Imports hiding (MonadReader, asks, log) import qualified Network.HTTP.Types.Status as Http import qualified Network.Wai.Utilities.Error as Wai @@ -182,7 +188,9 @@ createSamlUserWithId :: Role -> Sem r () createSamlUserWithId teamid buid suid role = do - uname <- either (throwSparSem . SparBadUserName . cs) pure $ Intra.mkUserName Nothing (UrefOnly suid) + uname <- + either (throwSparSem . SparBadUserName . LText.pack) pure $ + Intra.mkUserName Nothing (UrefOnly suid) buid' <- BrigAccess.createSAML suid buid teamid uname ManagedByWire Nothing Nothing Nothing role assert (buid == buid') $ pure () SAMLUserStore.insert suid buid @@ -212,7 +220,7 @@ autoprovisionSamlUser idp buid suid = do guardReplacedIdP :: Sem r () guardReplacedIdP = do unless (isNothing $ idp ^. idpExtraInfo . replacedBy) $ do - throwSparSem $ SparCannotCreateUsersOnReplacedIdP (cs . SAML.idPIdToST $ idp ^. idpId) + throwSparSem $ SparCannotCreateUsersOnReplacedIdP (LText.fromStrict . SAML.idPIdToST $ idp ^. idpId) -- IdPs in teams with scim tokens are not allowed to auto-provision. guardScimTokens :: Sem r () @@ -284,7 +292,7 @@ verdictHandler aresp verdict idp = do -- [...] If the containing message is in response to an , then -- the InResponseTo attribute MUST match the request's ID. Logger.log Logger.Debug $ "entering verdictHandler: " <> show (aresp, verdict) - reqid <- either (throwSparSem . SparNoRequestRefInResponse . cs) pure $ SAML.rspInResponseTo aresp + reqid <- either (throwSparSem . SparNoRequestRefInResponse . LText.pack) pure $ SAML.rspInResponseTo aresp format :: Maybe VerdictFormat <- VerdictFormatStore.get reqid resp <- case format of Just VerdictFormatWeb -> @@ -337,8 +345,17 @@ catchVerdictErrors = (`catch` hndlr) hndlr err = do waiErr <- renderSparErrorWithLogging err pure $ case waiErr of - Right (werr :: Wai.Error) -> VerifyHandlerError (cs $ Wai.label werr) (cs $ Wai.message werr) - Left (serr :: ServerError) -> VerifyHandlerError "unknown-error" (cs (errReasonPhrase serr) <> " " <> cs (errBody serr)) + Right (werr :: Wai.Error) -> + VerifyHandlerError + (LText.toStrict $ Wai.label werr) + (LText.toStrict $ Wai.message werr) + Left (serr :: ServerError) -> + VerifyHandlerError + "unknown-error" + ( Text.pack (errReasonPhrase serr) + <> " " + <> (Text.decodeUtf8With lenientDecode . toStrict . errBody $ serr) + ) -- | If a user attempts to login presenting a new IdP issuer, but there is no entry in -- @"spar.user"@ for her: lookup @"old_issuers"@ from @"spar.idp"@ for the new IdP, and @@ -397,7 +414,7 @@ verdictHandlerResultCore idp = \case SAML.AccessGranted uref -> do uid :: UserId <- do let team' = idp ^. idpExtraInfo . team - err = SparUserRefInNoOrMultipleTeams . cs . show $ uref + err = SparUserRefInNoOrMultipleTeams . LText.pack . show $ uref getUserByUrefUnsafe uref >>= \case Just usr -> do if userTeam usr == Just team' @@ -438,12 +455,12 @@ verdictHandlerWeb = forbiddenPage errlbl reasons = ServerError { errHTTPCode = 200, - errReasonPhrase = cs errlbl, -- (not sure what this is used for) + errReasonPhrase = Text.unpack errlbl, -- (not sure what this is used for) errBody = easyHtml $ "" <> " wire:sso:error:" - <> cs errlbl + <> LText.fromStrict errlbl <> "" <> "

    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 Date: Wed, 10 Apr 2024 09:59:57 +0200 Subject: [PATCH 083/117] Credential Provisioning for SFT authentication (#3915) --- changelog.d/0-release-notes/WPB-227 | 32 ++++ charts/brig/templates/configmap.yaml | 8 + charts/brig/templates/tests/configmap.yaml | 2 + charts/brig/values.yaml | 2 + charts/sftd/values.yaml | 2 +- .../src/developer/reference/config-options.md | 24 +++ hack/helm_vars/wire-server/values.yaml.gotmpl | 2 + integration/test/API/Brig.hs | 6 + integration/test/Test/Brig.hs | 107 +++++++++++++- integration/test/Testlib/JSON.hs | 12 +- libs/wire-api/src/Wire/API/Call/Config.hs | 138 +++++++++++++++++- .../golden/Test/Wire/API/Golden/Generated.hs | 3 +- .../Golden/Generated/RTCConfiguration_user.hs | 111 +++++++------- .../API/Golden/Generated/SFTServer_user.hs | 65 ++++----- .../testObject_RTCConfiguration_user_7.json | 4 +- .../testObject_RTCConfiguration_user_8.json | 16 ++ .../golden/testObject_SFTServer_user_1.json | 4 +- services/brig/brig.integration.yaml | 2 + services/brig/src/Brig/API/Public.hs | 19 ++- services/brig/src/Brig/App.hs | 9 +- services/brig/src/Brig/Calling.hs | 24 ++- services/brig/src/Brig/Calling/API.hs | 97 +++++++----- .../brig/src/Brig/CanonicalInterpreter.hs | 5 +- services/brig/src/Brig/Options.hs | 21 ++- services/brig/test/integration/API/Calling.hs | 2 +- services/brig/test/unit/Test/Brig/Calling.hs | 36 ++--- 26 files changed, 579 insertions(+), 174 deletions(-) create mode 100644 changelog.d/0-release-notes/WPB-227 create mode 100644 libs/wire-api/test/golden/testObject_RTCConfiguration_user_8.json diff --git a/changelog.d/0-release-notes/WPB-227 b/changelog.d/0-release-notes/WPB-227 new file mode 100644 index 00000000000..4d5c5989a39 --- /dev/null +++ b/changelog.d/0-release-notes/WPB-227 @@ -0,0 +1,32 @@ +There is a new optional Boolean option, `multiSFT.enabled`, in `brig.yaml`, +allowing calls between federated SFT servers. If provided, the field +`is_federating` in the response of `/calls/config/v2` will reflect +`multiSFT.enabled`'s value. + +Example: + +``` +# [brig.yaml] +multiSFT: + enabled: true +``` + +Also, the optional object `sftToken` with its fields `ttl` and `secret` define +whether an SFT credential would be rendered in the response of +`/calls/config/v2`. The field `ttl` determines the seconds for the credential to +be valid and `secret` is the path to the secret shared with SFT to create +credentials. + +Example: + +``` +# [brig.yaml] +sft: + sftBaseDomain: sft.wire.example.com + sftSRVServiceName: sft + sftDiscoveryIntervalSeconds: 10 + sftListLength: 20 + sftToken: + ttl: 120 + secret: /path/to/secret +``` diff --git a/charts/brig/templates/configmap.yaml b/charts/brig/templates/configmap.yaml index 7065407f57c..171ae2373d4 100644 --- a/charts/brig/templates/configmap.yaml +++ b/charts/brig/templates/configmap.yaml @@ -57,6 +57,7 @@ data: host: gundeck port: 8080 + multiSFT: {{ .multiSFT.enabled }} {{- if .enableFederation }} # TODO remove this federator: @@ -209,6 +210,13 @@ data: {{- if .sftDiscoveryIntervalSeconds }} sftDiscoveryIntervalSeconds: {{ .sftDiscoveryIntervalSeconds }} {{- end }} + {{- if .sftToken }} + sftToken: + {{- with .sftToken }} + ttl: {{ .ttl }} + secret: {{ .secret }} + {{- end }} + {{- end }} {{- end }} {{- end }} diff --git a/charts/brig/templates/tests/configmap.yaml b/charts/brig/templates/tests/configmap.yaml index 56667e55ed3..f4f2ce08fe9 100644 --- a/charts/brig/templates/tests/configmap.yaml +++ b/charts/brig/templates/tests/configmap.yaml @@ -33,6 +33,8 @@ data: host: spar port: 8080 + multiSFT: false + # TODO remove this federator: host: federator diff --git a/charts/brig/values.yaml b/charts/brig/values.yaml index 6afcd1a853d..a500b9e9cc7 100644 --- a/charts/brig/values.yaml +++ b/charts/brig/values.yaml @@ -40,6 +40,8 @@ config: # -- If set to false, 'dynamoDBEndpoint' _must_ be set. randomPrekeys: true useSES: true + multiSFT: + enabled: false # keep multiSFT default in sync with sft chart's multiSFT.enabled enableFederation: false # keep enableFederation default in sync with galley and cargohold chart's config.enableFederation as well as wire-server chart's tags.federation # Not used if enableFederation is false rabbitmq: diff --git a/charts/sftd/values.yaml b/charts/sftd/values.yaml index c9e23fa2990..4a3b90c6a0a 100644 --- a/charts/sftd/values.yaml +++ b/charts/sftd/values.yaml @@ -96,7 +96,7 @@ turnDiscoveryEnabled: false # Allow establishing calls involving remote SFT servers (e.g. for Federation) # Requires appVersion 3.0.9 or later multiSFT: - enabled: false + enabled: false # keep multiSFT default in sync with brig chart's config.multiSFT # For sftd versions up to 3.1.3, sftd uses the TURN servers advertised at a # discovery URL. turnDiscoveryURL: "" diff --git a/docs/src/developer/reference/config-options.md b/docs/src/developer/reference/config-options.md index d45c805dc65..0823bc3d0e2 100644 --- a/docs/src/developer/reference/config-options.md +++ b/docs/src/developer/reference/config-options.md @@ -517,6 +517,30 @@ This setting assumes that the sft load balancer has been deployed with the `sftd Additionally if `setSftListAllServers` is set to `enabled` (disabled by default) then the `/calls/config/v2` endpoint will include a list of all servers that are load balanced by `setSftStaticUrl` at field `sft_servers_all`. This is required to enable calls between federated instances of Wire. +Calls between federated SFT servers can be enabled using the optional boolean `multiSFT.enabled`. If provided, the field `is_federating` in the response of `/calls/config/v2` will reflect `multiSFT.enabled`'s value. + +``` +# [brig.yaml] +multiSFT: + enabled: true +``` + +Also, the optional object `sftToken` with its fields `ttl` and `secret` define whether an SFT credential would be rendered in the response of `/calls/config/v2`. The field `ttl` determines the seconds for the credential to be valid and `secret` is the path to the secret shared with SFT to create credentials. + +Example: + +``` +# [brig.yaml] +sft: + sftBaseDomain: sft.wire.example.com + sftSRVServiceName: sft + sftDiscoveryIntervalSeconds: 10 + sftListLength: 20 + sftToken: + ttl: 120 + secret: /path/to/secret +``` + ### Locale diff --git a/hack/helm_vars/wire-server/values.yaml.gotmpl b/hack/helm_vars/wire-server/values.yaml.gotmpl index dec2183e9c5..b7ef73d8fa8 100644 --- a/hack/helm_vars/wire-server/values.yaml.gotmpl +++ b/hack/helm_vars/wire-server/values.yaml.gotmpl @@ -73,6 +73,8 @@ brig: accessTokenTimeout: 30 providerTokenTimeout: 60 enableFederation: true # keep in sync with galley.config.enableFederation, cargohold.config.enableFederation and tags.federator! + multiSFT: + enabled: false # keep multiSFT default in sync with brig and sft chart's config.multiSFT optSettings: setActivationTimeout: 10 setVerificationTimeout: 10 diff --git a/integration/test/API/Brig.hs b/integration/test/API/Brig.hs index 908a0db996d..ff825f0aa90 100644 --- a/integration/test/API/Brig.hs +++ b/integration/test/API/Brig.hs @@ -636,3 +636,9 @@ renewToken :: (HasCallStack, MakesValue uid) => uid -> String -> App Response renewToken caller cookie = do req <- baseRequest caller Brig Versioned "access" submit "POST" (addHeader "Cookie" ("zuid=" <> cookie) req) + +-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/get_calls_config_v2 +getCallsConfigV2 :: (HasCallStack, MakesValue user) => user -> App Response +getCallsConfigV2 user = do + req <- baseRequest user Brig Versioned $ joinHttpPath ["calls", "config", "v2"] + submit "GET" req diff --git a/integration/test/Test/Brig.hs b/integration/test/Test/Brig.hs index 17753fd3ea9..0feb154388e 100644 --- a/integration/test/Test/Brig.hs +++ b/integration/test/Test/Brig.hs @@ -1,15 +1,19 @@ module Test.Brig where +import API.Brig import qualified API.BrigInternal as BrigI -import API.Common (randomName) +import API.Common import Data.Aeson.Types hiding ((.=)) +import Data.List.Split import Data.String.Conversions import qualified Data.UUID as UUID import qualified Data.UUID.V4 as UUID import GHC.Stack import SetupHelpers +import System.IO.Extra import Testlib.Assertions import Testlib.Prelude +import UnliftIO.Temporary testCrudFederationRemotes :: HasCallStack => App () testCrudFederationRemotes = do @@ -124,3 +128,104 @@ testCrudFederationRemoteTeams = do l <- resp.json & asList remoteTeams <- forM l (\e -> e %. "team_id" & asString) when (any (\t -> t `notElem` remoteTeams) tids) $ assertFailure "Expected response to contain all of the teams" + +testSFTCredentials :: HasCallStack => App () +testSFTCredentials = do + let ttl = (60 :: Int) + withSystemTempFile "sft-secret" $ \secretFile secretHandle -> do + liftIO $ do + hPutStr secretHandle "xMtZyTpu=Leb?YKCoq#BXQR:gG^UrE83dNWzFJ2VcD" + hClose secretHandle + withModifiedBackend + ( def + { brigCfg = + ( setField "sft.sftBaseDomain" "integration-tests.zinfra.io" + . setField "sft.sftToken.ttl" ttl + . setField "sft.sftToken.secret" secretFile + . setField "optSettings.setSftListAllServers" "enabled" + ) + } + ) + $ \domain -> do + user <- randomUser domain def + bindResponse (getCallsConfigV2 user) \resp -> do + sftServersAll <- resp.json %. "sft_servers_all" & asList + when (null sftServersAll) $ assertFailure "sft_servers_all missing" + for_ sftServersAll $ \s -> do + cred <- s %. "credential" & asString + when (null cred) $ assertFailure "credential missing" + usr <- s %. "username" & asString + let parts = splitOn "." usr + when (length parts /= 5) $ assertFailure "username should have 5 parts" + when (take 2 (head parts) /= "d=") $ assertFailure "missing expiry time identifier" + when (take 2 (parts !! 1) /= "v=") $ assertFailure "missing version identifier" + when (take 2 (parts !! 2) /= "k=") $ assertFailure "missing key ID identifier" + when (take 2 (parts !! 3) /= "s=") $ assertFailure "missing federation identifier" + when (take 2 (parts !! 4) /= "r=") $ assertFailure "missing random data identifier" + for_ parts $ \part -> when (length part < 3) $ assertFailure ("value missing for " <> part) + +testSFTNoCredentials :: HasCallStack => App () +testSFTNoCredentials = withModifiedBackend + ( def + { brigCfg = + ( setField "sft.sftBaseDomain" "integration-tests.zinfra.io" + . setField "optSettings.setSftListAllServers" "enabled" + ) + } + ) + $ \domain -> do + user <- randomUser domain def + bindResponse (getCallsConfigV2 user) \resp -> do + sftServersAll <- resp.json %. "sft_servers_all" & asList + when (null sftServersAll) $ assertFailure "sft_servers_all missing" + for_ sftServersAll $ \s -> do + credM <- lookupField s "credential" + when (isJust credM) $ assertFailure "should not generate credential" + usrM <- lookupField s "username" + when (isJust usrM) $ assertFailure "should not generate username" + +testSFTFederation :: HasCallStack => App () +testSFTFederation = do + withModifiedBackend + ( def + { brigCfg = + ( setField "sft.sftBaseDomain" "integration-tests.zinfra.io" + . removeField "multiSFT" + ) + } + ) + $ \domain -> do + user <- randomUser domain def + bindResponse (getCallsConfigV2 user) \resp -> do + isFederatingM <- lookupField resp.json "is_federating" + when (isJust isFederatingM) $ assertFailure "is_federating should not be present" + withModifiedBackend + ( def + { brigCfg = + ( setField "sft.sftBaseDomain" "integration-tests.zinfra.io" + . setField "multiSFT" True + ) + } + ) + $ \domain -> do + user <- randomUser domain def + bindResponse (getCallsConfigV2 user) \resp -> do + isFederating <- + maybe (assertFailure "is_federating missing") asBool + =<< lookupField resp.json "is_federating" + unless isFederating $ assertFailure "is_federating should be true" + withModifiedBackend + ( def + { brigCfg = + ( setField "sft.sftBaseDomain" "integration-tests.zinfra.io" + . setField "multiSFT" False + ) + } + ) + $ \domain -> do + user <- randomUser domain def + bindResponse (getCallsConfigV2 user) \resp -> do + isFederating <- + maybe (assertFailure "is_federating missing") asBool + =<< lookupField resp.json "is_federating" + when isFederating $ assertFailure "is_federating should be false" diff --git a/integration/test/Testlib/JSON.hs b/integration/test/Testlib/JSON.hs index ee21cf2f7f7..fb0a99462ae 100644 --- a/integration/test/Testlib/JSON.hs +++ b/integration/test/Testlib/JSON.hs @@ -236,8 +236,9 @@ lookupField val selector = do go k [] v = get v k go k (k2 : ks) v = get v k >>= assertField v k >>= go k2 ks --- Update nested fields +-- | Update nested fields -- E.g. ob & "foo.bar.baz" %.= ("quux" :: String) +-- The selector path will be created if non-existing. setField :: forall a b. (HasCallStack, MakesValue a, ToJSON b) => @@ -253,7 +254,8 @@ setField selector v x = do member :: (HasCallStack, MakesValue a) => String -> a -> App Bool member k x = KM.member (KM.fromString k) <$> (make x >>= asObject) --- Update nested fields, using the old value with a stateful action +-- | Update nested fields, using the old value with a stateful action +-- The selector path will be created if non-existing. modifyField :: (HasCallStack, MakesValue a, ToJSON b) => String -> (Maybe Value -> App b) -> a -> App Value modifyField selector up x = do v <- make x @@ -268,7 +270,7 @@ modifyField selector up x = do newValue <- toJSON <$> up (KM.lookup k' ob) pure $ Object $ KM.insert k' newValue ob go k (k2 : ks) v = do - val <- v %. k + val <- fromMaybe (Object $ KM.empty) <$> lookupField v k newValue <- go k2 ks val ob <- asObject v pure $ Object $ KM.insert (KM.fromString k) newValue ob @@ -339,9 +341,9 @@ objQid ob = do Just v -> pure v where select x = runMaybeT $ do - vdom <- MaybeT $ lookupField x "domain" + vdom <- lookupFieldM x "domain" dom <- MaybeT $ asStringM vdom - vid <- MaybeT $ lookupField x "id" + vid <- lookupFieldM x "id" id_ <- MaybeT $ asStringM vid pure (dom, id_) diff --git a/libs/wire-api/src/Wire/API/Call/Config.hs b/libs/wire-api/src/Wire/API/Call/Config.hs index 18289ca1706..442f81fd4af 100644 --- a/libs/wire-api/src/Wire/API/Call/Config.hs +++ b/libs/wire-api/src/Wire/API/Call/Config.hs @@ -27,6 +27,7 @@ module Wire.API.Call.Config rtcConfSftServers, rtcConfSftServersAll, rtcConfTTL, + rtcConfIsFederating, -- * RTCIceServer RTCIceServer, @@ -47,6 +48,15 @@ module Wire.API.Call.Config TurnHost (..), isHostName, + -- * SFTUsername + SFTUsername (SFTUsername), + mkSFTUsername, + suExpiresAt, + suVersion, + suKeyindex, + suShared, + suRandom, + -- * TurnUsername TurnUsername, turnUsername, @@ -61,6 +71,14 @@ module Wire.API.Call.Config sftServer, sftURL, + -- * AuthSFTServer + AuthSFTServer, + authSFTServer, + nauthSFTServer, + authURL, + authUsername, + authCredential, + -- * convenience isUdp, isTcp, @@ -106,7 +124,8 @@ data RTCConfiguration = RTCConfiguration { _rtcConfIceServers :: NonEmpty RTCIceServer, _rtcConfSftServers :: Maybe (NonEmpty SFTServer), _rtcConfTTL :: Word32, - _rtcConfSftServersAll :: Maybe [SFTServer] + _rtcConfSftServersAll :: Maybe [AuthSFTServer], + _rtcConfIsFederating :: Maybe Bool } deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform RTCConfiguration) @@ -116,7 +135,8 @@ rtcConfiguration :: NonEmpty RTCIceServer -> Maybe (NonEmpty SFTServer) -> Word32 -> - Maybe [SFTServer] -> + Maybe [AuthSFTServer] -> + Maybe Bool -> RTCConfiguration rtcConfiguration = RTCConfiguration @@ -132,6 +152,8 @@ instance ToSchema RTCConfiguration where .= fieldWithDocModifier "ttl" (description ?~ "Number of seconds after which the configuration should be refreshed (advisory)") schema <*> _rtcConfSftServersAll .= maybe_ (optFieldWithDocModifier "sft_servers_all" (description ?~ "Array of all SFT servers") (array schema)) + <*> _rtcConfIsFederating + .= maybe_ (optFieldWithDocModifier "is_federating" (description ?~ "True if the client should connect to an SFT in the sft_servers_all and request it to federate") schema) -------------------------------------------------------------------------------- -- SFTServer @@ -157,6 +179,39 @@ instance ToSchema SFTServer where sftServer :: HttpsUrl -> SFTServer sftServer = SFTServer +-------------------------------------------------------------------------------- +-- AuthSFTServer + +data AuthSFTServer = AuthSFTServer + { _authURL :: HttpsUrl, + _authUsername :: Maybe SFTUsername, + _authCredential :: Maybe AsciiBase64 + } + deriving stock (Eq, Show, Ord, Generic) + deriving (Arbitrary) via (GenericUniform AuthSFTServer) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema AuthSFTServer) + +instance ToSchema AuthSFTServer where + schema = + objectWithDocModifier "SftServer" (description ?~ "Inspired by WebRTC 'RTCIceServer' object, contains details of SFT servers") $ + AuthSFTServer + <$> (pure . _authURL) + .= fieldWithDocModifier "urls" (description ?~ "Array containing exactly one SFT server address of the form 'https://:'") (withParser (array schema) p) + <*> _authUsername + .= maybe_ (optFieldWithDocModifier "username" (description ?~ "String containing the SFT username") schema) + <*> _authCredential + .= maybe_ (optFieldWithDocModifier "credential" (description ?~ "String containing the SFT credential") schema) + where + p :: [HttpsUrl] -> A.Parser HttpsUrl + p [url] = pure url + p xs = fail $ "SFTServer can only have exactly one URL, found " <> show (length xs) + +nauthSFTServer :: SFTServer -> AuthSFTServer +nauthSFTServer = (\u -> AuthSFTServer u Nothing Nothing) . _sftURL + +authSFTServer :: SFTServer -> SFTUsername -> AsciiBase64 -> AuthSFTServer +authSFTServer svr u = AuthSFTServer (_sftURL svr) (Just u) . Just + -------------------------------------------------------------------------------- -- RTCIceServer @@ -388,6 +443,83 @@ instance ToSchema Transport where element "tcp" TransportTCP ] +-------------------------------------------------------------------------------- +-- SFTUsername + +data SFTUsername = SFTUsername + { -- | must be positive, integral number of seconds + _suExpiresAt :: POSIXTime, + _suVersion :: Word, + -- | seems to large, but uint32_t is used in C + _suKeyindex :: Word32, + -- | whether the user is allowed to initialise an SFT conference + _suShared :: Bool, + -- | [a-z0-9]+ + _suRandom :: Text + } + deriving stock (Eq, Ord, Show, Generic) + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema SFTUsername) + +-- note that the random value is not checked for well-formedness +mkSFTUsername :: POSIXTime -> Text -> SFTUsername +mkSFTUsername expires rnd = + SFTUsername + { _suExpiresAt = expires, + _suVersion = 1, + _suKeyindex = 0, + _suShared = True, + _suRandom = rnd + } + +instance ToSchema SFTUsername where + schema = toText .= parsedText "" fromText + where + fromText :: Text -> Either String SFTUsername + fromText = parseOnly (parseSFTUsername <* endOfInput) + + toText :: SFTUsername -> Text + toText = cs . toByteString + +instance BC.ToByteString SFTUsername where + builder su = + shortByteString "d=" + <> word64Dec (round (_suExpiresAt su)) + <> shortByteString ".v=" + <> wordDec (_suVersion su) + <> shortByteString ".k=" + <> word32Dec (_suKeyindex su) + <> shortByteString ".s=" + <> wordDec (boolToWord $ _suShared su) + <> shortByteString ".r=" + <> byteString (view (re utf8) (_suRandom su)) + where + boolToWord :: Num a => Bool -> a + boolToWord False = 0 + boolToWord True = 1 + +parseSFTUsername :: Text.Parser SFTUsername +parseSFTUsername = + SFTUsername + <$> (string "d=" *> fmap (fromIntegral :: Word64 -> POSIXTime) decimal) + <*> (string ".v=" *> decimal) + <*> (string ".k=" *> decimal) + <*> (string ".s=" *> (wordToBool <$> decimal)) + <*> (string ".r=" *> takeWhile1 (inClass "a-z0-9")) + where + wordToBool :: Word -> Bool + wordToBool = odd + +instance Arbitrary SFTUsername where + arbitrary = + SFTUsername + <$> (fromIntegral <$> arbitrary @Word64) + <*> arbitrary + <*> arbitrary + <*> arbitrary + <*> (Text.pack <$> QC.listOf1 genAlphaNum) + where + genAlphaNum = QC.elements $ ['a' .. 'z'] <> ['0' .. '9'] + -------------------------------------------------------------------------------- -- TurnUsername @@ -509,5 +641,7 @@ isTls uri = makeLenses ''RTCConfiguration makeLenses ''RTCIceServer makeLenses ''TurnURI +makeLenses ''SFTUsername makeLenses ''TurnUsername makeLenses ''SFTServer +makeLenses ''AuthSFTServer diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs index 88433fe1f78..c530bf3234b 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated.hs @@ -350,7 +350,8 @@ tests = (Test.Wire.API.Golden.Generated.RTCConfiguration_user.testObject_RTCConfiguration_user_4, "testObject_RTCConfiguration_user_4.json"), (Test.Wire.API.Golden.Generated.RTCConfiguration_user.testObject_RTCConfiguration_user_5, "testObject_RTCConfiguration_user_5.json"), (Test.Wire.API.Golden.Generated.RTCConfiguration_user.testObject_RTCConfiguration_user_6, "testObject_RTCConfiguration_user_6.json"), - (Test.Wire.API.Golden.Generated.RTCConfiguration_user.testObject_RTCConfiguration_user_7, "testObject_RTCConfiguration_user_7.json") + (Test.Wire.API.Golden.Generated.RTCConfiguration_user.testObject_RTCConfiguration_user_7, "testObject_RTCConfiguration_user_7.json"), + (Test.Wire.API.Golden.Generated.RTCConfiguration_user.testObject_RTCConfiguration_user_8, "testObject_RTCConfiguration_user_8.json") ], testGroup "Golden: SFTServer_user" $ testObjects diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/RTCConfiguration_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/RTCConfiguration_user.hs index 1a1414c6204..29c9555f4ba 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/RTCConfiguration_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/RTCConfiguration_user.hs @@ -19,46 +19,15 @@ module Test.Wire.API.Golden.Generated.RTCConfiguration_user where -import Control.Lens ((.~)) -import Data.Coerce (coerce) -import Data.List.NonEmpty (NonEmpty (..)) -import Data.Misc (HttpsUrl (HttpsUrl), IpAddr (IpAddr)) -import Data.Text.Ascii (AsciiChars (validate)) -import Data.Time (secondsToNominalDiffTime) -import Imports (Maybe (Just, Nothing), fromRight, read, undefined, (&)) +import Control.Lens +import Data.Coerce +import Data.List.NonEmpty +import Data.Misc +import Data.Text.Ascii +import Data.Time +import Imports import URI.ByteString - ( Authority - ( Authority, - authorityHost, - authorityPort, - authorityUserInfo - ), - Host (Host, hostBS), - Query (Query, queryPairs), - Scheme (Scheme, schemeBS), - URIRef - ( URI, - uriAuthority, - uriFragment, - uriPath, - uriQuery, - uriScheme - ), - ) import Wire.API.Call.Config - ( RTCConfiguration, - Scheme (SchemeTurn, SchemeTurns), - Transport (TransportTCP, TransportUDP), - TurnHost (TurnHostIp, TurnHostName), - rtcConfiguration, - rtcIceServer, - sftServer, - tuKeyindex, - tuT, - tuVersion, - turnURI, - turnUsername, - ) testObject_RTCConfiguration_user_1 :: RTCConfiguration testObject_RTCConfiguration_user_1 = @@ -154,6 +123,7 @@ testObject_RTCConfiguration_user_1 = Nothing 2 Nothing + Nothing testObject_RTCConfiguration_user_2 :: RTCConfiguration testObject_RTCConfiguration_user_2 = @@ -332,6 +302,7 @@ testObject_RTCConfiguration_user_2 = ) 4 Nothing + Nothing testObject_RTCConfiguration_user_3 :: RTCConfiguration testObject_RTCConfiguration_user_3 = @@ -477,6 +448,7 @@ testObject_RTCConfiguration_user_3 = ) 9 Nothing + Nothing testObject_RTCConfiguration_user_4 :: RTCConfiguration testObject_RTCConfiguration_user_4 = @@ -672,6 +644,7 @@ testObject_RTCConfiguration_user_4 = ) 2 Nothing + Nothing testObject_RTCConfiguration_user_5 :: RTCConfiguration testObject_RTCConfiguration_user_5 = @@ -714,6 +687,7 @@ testObject_RTCConfiguration_user_5 = ) 2 Nothing + Nothing testObject_RTCConfiguration_user_6 :: RTCConfiguration testObject_RTCConfiguration_user_6 = @@ -736,6 +710,7 @@ testObject_RTCConfiguration_user_6 = Nothing 2 Nothing + Nothing testObject_RTCConfiguration_user_7 :: RTCConfiguration testObject_RTCConfiguration_user_7 = @@ -758,22 +733,50 @@ testObject_RTCConfiguration_user_7 = Nothing 2 ( Just - [ sftServer - ( coerce - URI - { uriScheme = Scheme {schemeBS = "https"}, - uriAuthority = - Just - ( Authority - { authorityUserInfo = Nothing, - authorityHost = Host {hostBS = "example.com"}, - authorityPort = Nothing - } - ), - uriPath = "", - uriQuery = Query {queryPairs = []}, - uriFragment = Nothing - } + [ authSFTServer + ( sftServer + ( coerce + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "example.com"}, + authorityPort = Nothing + } + ), + uriPath = "", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + } + ) ) + (mkSFTUsername (secondsToNominalDiffTime 12) "username") + "credential" ] ) + Nothing + +testObject_RTCConfiguration_user_8 :: RTCConfiguration +testObject_RTCConfiguration_user_8 = + rtcConfiguration + ( rtcIceServer + ( turnURI SchemeTurns (TurnHostIp (IpAddr (read "248.187.155.126"))) (read "1") Nothing + :| [ turnURI SchemeTurn (TurnHostIp (IpAddr (read "166.155.90.230"))) (read "0") (Just TransportTCP), + turnURI SchemeTurns (TurnHostName "xn--mgbh0fb.xn--kgbechtv") (read "1") (Just TransportTCP), + turnURI SchemeTurn (TurnHostName "host.name") (read "1") (Just TransportTCP) + ] + ) + ( turnUsername (secondsToNominalDiffTime 2.000000000000) "tj" + & tuVersion .~ 0 + & tuKeyindex .~ 0 + & tuT .~ '\1011805' + ) + (fromRight undefined (validate "")) + :| [] + ) + Nothing + 2 + Nothing + (Just True) diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SFTServer_user.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SFTServer_user.hs index 535fd2683c5..b34fc94d32e 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SFTServer_user.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Generated/SFTServer_user.hs @@ -19,46 +19,33 @@ module Test.Wire.API.Golden.Generated.SFTServer_user where -import Data.Coerce (coerce) -import Data.Misc (HttpsUrl (HttpsUrl)) -import Imports (Maybe (Just, Nothing)) +import Data.Coerce +import Data.Misc +import Data.Time.Clock +import Imports import URI.ByteString - ( Authority - ( Authority, - authorityHost, - authorityPort, - authorityUserInfo - ), - Host (Host, hostBS), - Query (Query, queryPairs), - Scheme (Scheme, schemeBS), - URIRef - ( URI, - uriAuthority, - uriFragment, - uriPath, - uriQuery, - uriScheme - ), - ) -import Wire.API.Call.Config (SFTServer, sftServer) +import Wire.API.Call.Config -testObject_SFTServer_user_1 :: SFTServer +testObject_SFTServer_user_1 :: AuthSFTServer testObject_SFTServer_user_1 = - sftServer - ( coerce - URI - { uriScheme = Scheme {schemeBS = "https"}, - uriAuthority = - Just - ( Authority - { authorityUserInfo = Nothing, - authorityHost = Host {hostBS = "example.com"}, - authorityPort = Nothing - } - ), - uriPath = "", - uriQuery = Query {queryPairs = []}, - uriFragment = Nothing - } + authSFTServer + ( sftServer + ( coerce + URI + { uriScheme = Scheme {schemeBS = "https"}, + uriAuthority = + Just + ( Authority + { authorityUserInfo = Nothing, + authorityHost = Host {hostBS = "example.com"}, + authorityPort = Nothing + } + ), + uriPath = "", + uriQuery = Query {queryPairs = []}, + uriFragment = Nothing + } + ) ) + (mkSFTUsername (secondsToNominalDiffTime 12) "username") + "credential" diff --git a/libs/wire-api/test/golden/testObject_RTCConfiguration_user_7.json b/libs/wire-api/test/golden/testObject_RTCConfiguration_user_7.json index 8e9fa8b7808..bdd7b330834 100644 --- a/libs/wire-api/test/golden/testObject_RTCConfiguration_user_7.json +++ b/libs/wire-api/test/golden/testObject_RTCConfiguration_user_7.json @@ -13,9 +13,11 @@ ], "sft_servers_all": [ { + "credential": "credential", "urls": [ "https://example.com" - ] + ], + "username": "d=12.v=1.k=0.s=1.r=username" } ], "ttl": 2 diff --git a/libs/wire-api/test/golden/testObject_RTCConfiguration_user_8.json b/libs/wire-api/test/golden/testObject_RTCConfiguration_user_8.json new file mode 100644 index 00000000000..f3ffe63ade9 --- /dev/null +++ b/libs/wire-api/test/golden/testObject_RTCConfiguration_user_8.json @@ -0,0 +1,16 @@ +{ + "ice_servers": [ + { + "credential": "", + "urls": [ + "turns:248.187.155.126:1", + "turn:166.155.90.230:0?transport=tcp", + "turns:xn--mgbh0fb.xn--kgbechtv:1?transport=tcp", + "turn:host.name:1?transport=tcp" + ], + "username": "d=2.v=0.k=0.t=󷁝.r=tj" + } + ], + "is_federating": true, + "ttl": 2 +} diff --git a/libs/wire-api/test/golden/testObject_SFTServer_user_1.json b/libs/wire-api/test/golden/testObject_SFTServer_user_1.json index 957a0ccbff7..1e2fbf7a23d 100644 --- a/libs/wire-api/test/golden/testObject_SFTServer_user_1.json +++ b/libs/wire-api/test/golden/testObject_SFTServer_user_1.json @@ -1,5 +1,7 @@ { + "credential": "credential", "urls": [ "https://example.com" - ] + ], + "username": "d=12.v=1.k=0.s=1.r=username" } diff --git a/services/brig/brig.integration.yaml b/services/brig/brig.integration.yaml index a536c77626d..451e753ccac 100644 --- a/services/brig/brig.integration.yaml +++ b/services/brig/brig.integration.yaml @@ -36,6 +36,8 @@ federatorInternal: host: 127.0.0.1 port: 8097 +multiSFT: false + # You can set up local SQS/Dynamo running e.g. `../../deploy/dockerephemeral/run.sh` aws: userJournalQueue: integration-user-events.fifo diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index 0a4137e24b7..c72ee3d2db8 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -55,6 +55,7 @@ import Brig.Effects.GalleyProvider qualified as GalleyProvider import Brig.Effects.JwtTools (JwtTools) import Brig.Effects.PasswordResetStore (PasswordResetStore) import Brig.Effects.PublicKeyBundle (PublicKeyBundle) +import Brig.Effects.SFT import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import Brig.Options hiding (internalEvents, sesQueue) import Brig.Provider.API @@ -270,20 +271,22 @@ servantSitemap :: Member BlacklistStore r, Member CodeStore r, Member (Concurrency 'Unsafe) r, + Member (ConnectionStore InternalPaging) r, + Member (Embed HttpClientIO) r, + Member (Embed IO) r, + Member FederationConfigStore r, Member GalleyProvider r, + Member (Input (Local ())) r, + Member (Input UTCTime) r, + Member Jwk r, Member JwtTools r, + Member NotificationSubsystem r, Member Now r, Member PasswordResetStore r, Member PublicKeyBundle r, - Member (UserPendingActivationStore p) r, - Member Jwk r, - Member FederationConfigStore r, - Member (Embed HttpClientIO) r, - Member NotificationSubsystem r, + Member SFT r, Member TinyLog r, - Member (Input (Local ())) r, - Member (Input UTCTime) r, - Member (ConnectionStore InternalPaging) r + Member (UserPendingActivationStore p) r ) => ServerT BrigAPI (Handler r) servantSitemap = diff --git a/services/brig/src/Brig/App.hs b/services/brig/src/Brig/App.hs index 7c0f49a0cba..195f69f2d12 100644 --- a/services/brig/src/Brig/App.hs +++ b/services/brig/src/Brig/App.hs @@ -66,6 +66,7 @@ module Brig.App rabbitmqChannel, fsWatcher, disabledVersions, + enableSFTFederation, -- * App Monad AppT (..), @@ -197,7 +198,8 @@ data Env = Env _randomPrekeyLocalLock :: Maybe (MVar ()), _keyPackageLocalLock :: MVar (), _rabbitmqChannel :: Maybe (MVar Q.Channel), - _disabledVersions :: Set Version + _disabledVersions :: Set Version, + _enableSFTFederation :: Maybe Bool } makeLenses ''Env @@ -248,7 +250,7 @@ newEnv o = do eventsQueue <- case Opt.internalEventsQueue (Opt.internalEvents o) of StompQueue q -> pure (StompQueue q) SqsQueue q -> SqsQueue <$> AWS.getQueueUrl (aws ^. AWS.amazonkaEnv) q - mSFTEnv <- mapM Calling.mkSFTEnv $ Opt.sft o + mSFTEnv <- mapM (Calling.mkSFTEnv sha512) $ Opt.sft o prekeyLocalLock <- case Opt.randomPrekeys o of Just True -> do Log.info lgr $ Log.msg (Log.val "randomPrekeys: active") @@ -300,7 +302,8 @@ newEnv o = do _randomPrekeyLocalLock = prekeyLocalLock, _keyPackageLocalLock = kpLock, _rabbitmqChannel = rabbitChan, - _disabledVersions = allDisabledVersions + _disabledVersions = allDisabledVersions, + _enableSFTFederation = Opt.multiSFT o } where emailConn _ (Opt.EmailAWS aws) = pure (Just aws, Nothing) diff --git a/services/brig/src/Brig/Calling.hs b/services/brig/src/Brig/Calling.hs index 890531bd63a..49c79b0a9de 100644 --- a/services/brig/src/Brig/Calling.hs +++ b/services/brig/src/Brig/Calling.hs @@ -25,6 +25,7 @@ module Brig.Calling unSFTServers, mkSFTServers, SFTEnv (..), + SFTTokenEnv (..), Discovery (..), TurnEnv, TurnServers (..), @@ -133,7 +134,9 @@ data SFTEnv = SFTEnv sftDiscoveryInterval :: Int, -- | maximum amount of servers to give out, -- even if more are in the SRV record - sftListLength :: Range 1 100 Int + sftListLength :: Range 1 100 Int, + -- | token parameters + sftToken :: Maybe SFTTokenEnv } data Discovery a @@ -182,6 +185,13 @@ srvDiscoveryLoop domain discoveryInterval saveAction = forever $ do forM_ servers saveAction delay discoveryInterval +data SFTTokenEnv = SFTTokenEnv + { sftTokenTTL :: Word32, + sftTokenSecret :: ByteString, + sftTokenPRNG :: GenIO, + sftTokenSHA :: Digest + } + mkSFTDomain :: SFTOptions -> DNS.Domain mkSFTDomain SFTOptions {..} = DNS.normalize $ maybe defSftServiceName ("_" <>) sftSRVServiceName <> "._tcp." <> sftBaseDomain @@ -190,13 +200,21 @@ sftDiscoveryLoop SFTEnv {..} = srvDiscoveryLoop sftDomain sftDiscoveryInterval $ atomicWriteIORef sftServers . Discovered . SFTServers -mkSFTEnv :: SFTOptions -> IO SFTEnv -mkSFTEnv opts = +mkSFTEnv :: Digest -> SFTOptions -> IO SFTEnv +mkSFTEnv digest opts = SFTEnv <$> newIORef NotDiscoveredYet <*> pure (mkSFTDomain opts) <*> pure (diffTimeToMicroseconds (fromMaybe defSrvDiscoveryIntervalSeconds (Opts.sftDiscoveryIntervalSeconds opts))) <*> pure (fromMaybe defSftListLength (Opts.sftListLength opts)) + <*> forM (Opts.sftTokenOptions opts) (mkSFTTokenEnv digest) + +mkSFTTokenEnv :: Digest -> Opts.SFTTokenOptions -> IO SFTTokenEnv +mkSFTTokenEnv digest opts = + SFTTokenEnv (Opts.sttTTL opts) + <$> BS.readFile (Opts.sttSecret opts) + <*> createSystemRandom + <*> pure digest -- | Start SFT service discovery synchronously startSFTServiceDiscovery :: Log.Logger -> SFTEnv -> IO () diff --git a/services/brig/src/Brig/Calling/API.hs b/services/brig/src/Brig/Calling/API.hs index 90998b1fb3a..998b92ee874 100644 --- a/services/brig/src/Brig/Calling/API.hs +++ b/services/brig/src/Brig/Calling/API.hs @@ -1,4 +1,5 @@ {-# LANGUAGE BlockArguments #-} +{-# LANGUAGE RecordWildCards #-} -- This file is part of the Wire Server implementation. -- @@ -48,35 +49,37 @@ import Data.Misc (HttpsUrl) import Data.Range import Data.Text.Ascii (AsciiBase64, encodeBase64) import Data.Text.Strict.Lens -import Data.Time.Clock.POSIX (getPOSIXTime) +import Data.Time.Clock.POSIX import Imports hiding (head) import OpenSSL.EVP.Digest (Digest, hmacBS) import Polysemy import Polysemy.Error qualified as Polysemy import System.Logger.Class qualified as Log import System.Random.MWC qualified as MWC -import Wire.API.Call.Config (SFTServer) import Wire.API.Call.Config qualified as Public import Wire.Network.DNS.SRV (srvTarget) -import Wire.Sem.Logger.TinyLog (loggerToTinyLog) -- | ('UserId', 'ConnId' are required as args here to make sure this is an authenticated end-point.) -getCallsConfigV2 :: UserId -> ConnId -> Maybe (Range 1 10 Int) -> (Handler r) Public.RTCConfiguration +getCallsConfigV2 :: + ( Member (Embed IO) r, + Member SFT r + ) => + UserId -> + ConnId -> + Maybe (Range 1 10 Int) -> + (Handler r) Public.RTCConfiguration getCallsConfigV2 _ _ limit = do env <- view turnEnv staticUrl <- view $ settings . Opt.sftStaticUrl sftListAllServers <- fromMaybe Opt.HideAllSFTServers <$> view (settings . Opt.sftListAllServers) sftEnv' <- view sftEnv - logger <- view applog - manager <- view httpManager + sftFederation <- view enableSFTFederation discoveredServers <- turnServersV2 (env ^. turnServers) eitherConfig <- - liftIO - . runM @IO - . loggerToTinyLog logger - . interpretSFT manager + lift + . liftSem . Polysemy.runError - $ newConfig env discoveredServers staticUrl sftEnv' limit sftListAllServers CallsConfigV2 + $ newConfig env discoveredServers staticUrl sftEnv' limit sftListAllServers (CallsConfigV2 sftFederation) handleNoTurnServers eitherConfig -- | Throws '500 Internal Server Error' when no turn servers are found. This is @@ -91,18 +94,20 @@ handleNoTurnServers (Left NoTurnServers) = do Log.err $ Log.msg (Log.val "Call config requested before TURN URIs could be discovered.") throwE $ StdError internalServerError -getCallsConfig :: UserId -> ConnId -> (Handler r) Public.RTCConfiguration +getCallsConfig :: + ( Member (Embed IO) r, + Member SFT r + ) => + UserId -> + ConnId -> + (Handler r) Public.RTCConfiguration getCallsConfig _ _ = do env <- view turnEnv - logger <- view applog - manager <- view httpManager discoveredServers <- turnServersV1 (env ^. turnServers) eitherConfig <- (dropTransport <$$>) - . liftIO - . runM @IO - . loggerToTinyLog logger - . interpretSFT manager + . lift + . liftSem . Polysemy.runError $ newConfig env discoveredServers Nothing Nothing Nothing HideAllSFTServers CallsConfigDeprecated handleNoTurnServers eitherConfig @@ -116,7 +121,7 @@ getCallsConfig _ _ = do data CallsConfigVersion = CallsConfigDeprecated - | CallsConfigV2 + | CallsConfigV2 (Maybe Bool) data NoTurnServers = NoTurnServers deriving (Show) @@ -129,7 +134,10 @@ instance Exception NoTurnServers -- to be set or only one of them (perhaps Data.These combined with error -- handling). newConfig :: - Members [Embed IO, SFT, Polysemy.Error NoTurnServers] r => + ( Member (Embed IO) r, + Member SFT r, + Member (Polysemy.Error NoTurnServers) r + ) => Calling.TurnEnv -> Discovery (NonEmpty Public.TurnURI) -> Maybe HttpsUrl -> @@ -139,7 +147,6 @@ newConfig :: CallsConfigVersion -> Sem r Public.RTCConfiguration newConfig env discoveredServers sftStaticUrl mSftEnv limit listAllServers version = do - let (sha, secret, tTTL, cTTL, prng) = (env ^. turnSHA512, env ^. turnSecret, env ^. turnTokenTTL, env ^. turnConfigTTL, env ^. turnPrng) -- randomize list of servers (before limiting the list, to ensure not always the same servers are chosen if limit is set) randomizedUris <- liftIO . randomize @@ -150,8 +157,8 @@ newConfig env discoveredServers sftStaticUrl mSftEnv limit listAllServers versio -- randomize again (as limitedList partially re-orders uris) finalUris <- liftIO $ randomize limitedUris srvs <- for finalUris $ \uri -> do - u <- liftIO $ genUsername tTTL prng - pure $ Public.rtcIceServer (pure uri) u (computeCred sha secret u) + u <- liftIO $ genTurnUsername (env ^. turnTokenTTL) (env ^. turnPrng) + pure . Public.rtcIceServer (pure uri) u $ computeCred (env ^. turnSHA512) (env ^. turnSecret) u let staticSft = pure . Public.sftServer <$> sftStaticUrl allSrvEntries <- @@ -163,16 +170,21 @@ newConfig env discoveredServers sftStaticUrl mSftEnv limit listAllServers versio let subsetLength = Calling.sftListLength actualSftEnv mapM (getRandomElements subsetLength) allSrvEntries - mSftServersAll :: Maybe [SFTServer] <- case version of - CallsConfigDeprecated -> pure Nothing - CallsConfigV2 -> - case (listAllServers, sftStaticUrl) of - (HideAllSFTServers, _) -> pure Nothing - (ListAllSFTServers, Nothing) -> pure . Just $ sftServerFromSrvTarget . srvTarget <$> maybe [] toList allSrvEntries - (ListAllSFTServers, Just url) -> hush . unSFTGetResponse <$> sftGetAllServers url + let sftFederation' = case version of + CallsConfigDeprecated -> Nothing + CallsConfigV2 fed -> fed + + mSftServersAll <- + case version of + CallsConfigDeprecated -> pure Nothing + CallsConfigV2 _ -> + case (listAllServers, sftStaticUrl) of + (HideAllSFTServers, _) -> pure Nothing + (ListAllSFTServers, Nothing) -> mapM (mapM authenticate) . pure $ sftServerFromSrvTarget . srvTarget <$> maybe [] toList allSrvEntries + (ListAllSFTServers, Just url) -> mapM (mapM authenticate) . hush . unSFTGetResponse =<< sftGetAllServers url let mSftServers = staticSft <|> sftServerFromSrvTarget . srvTarget <$$> srvEntries - pure $ Public.rtcConfiguration srvs mSftServers cTTL mSftServersAll + pure $ Public.rtcConfiguration srvs mSftServers (env ^. turnConfigTTL) mSftServersAll sftFederation' where limitedList :: NonEmpty Public.TurnURI -> Range 1 10 Int -> NonEmpty Public.TurnURI limitedList uris lim = @@ -182,10 +194,27 @@ newConfig env discoveredServers sftStaticUrl mSftEnv limit listAllServers versio -- it should also be safe to assume the returning list has length >= 1 NonEmpty.nonEmpty (Public.limitServers (NonEmpty.toList uris) (fromRange lim)) & fromMaybe (error "newConfig:limitedList: empty list of servers") - genUsername :: Word32 -> MWC.GenIO -> IO Public.TurnUsername + genUsername :: Word32 -> MWC.GenIO -> IO (POSIXTime, Text) genUsername ttl prng = do rnd <- view (packedBytes . utf8) <$> replicateM 16 (MWC.uniformR (97, 122) prng) t <- fromIntegral . (+ ttl) . round <$> getPOSIXTime - pure $ Public.turnUsername t rnd - computeCred :: Digest -> ByteString -> Public.TurnUsername -> AsciiBase64 + pure $ (t, rnd) + genTurnUsername :: Word32 -> MWC.GenIO -> IO Public.TurnUsername + genTurnUsername = (fmap (uncurry Public.turnUsername) .) . genUsername + genSFTUsername :: Word32 -> MWC.GenIO -> IO Public.SFTUsername + genSFTUsername = (fmap (uncurry Public.mkSFTUsername) .) . genUsername + computeCred :: ToByteString a => Digest -> ByteString -> a -> AsciiBase64 computeCred dig secret = encodeBase64 . hmacBS dig secret . toByteString' + authenticate :: + Member (Embed IO) r => + Public.SFTServer -> + Sem r Public.AuthSFTServer + authenticate = + maybe + (pure . Public.nauthSFTServer) + ( \SFTTokenEnv {..} sftsvr -> do + username <- liftIO $ genSFTUsername sftTokenTTL sftTokenPRNG + let credential = computeCred sftTokenSHA sftTokenSecret username + pure $ Public.authSFTServer sftsvr username credential + ) + (sftToken =<< mSftEnv) diff --git a/services/brig/src/Brig/CanonicalInterpreter.hs b/services/brig/src/Brig/CanonicalInterpreter.hs index 9a77bbae6dc..b23a8c2a6dc 100644 --- a/services/brig/src/Brig/CanonicalInterpreter.hs +++ b/services/brig/src/Brig/CanonicalInterpreter.hs @@ -17,6 +17,7 @@ import Brig.Effects.JwtTools import Brig.Effects.PasswordResetStore (PasswordResetStore) import Brig.Effects.PasswordResetStore.CodeStore (passwordResetStoreToCodeStore) import Brig.Effects.PublicKeyBundle +import Brig.Effects.SFT (SFT, interpretSFT) import Brig.Effects.UserPendingActivationStore (UserPendingActivationStore) import Brig.Effects.UserPendingActivationStore.Cassandra (userPendingActivationStoreToCassandra) import Brig.Options (ImplicitNoFederationRestriction (federationDomainConfig), federationDomainConfigs, federationStrategy) @@ -49,7 +50,8 @@ import Wire.Sem.Now.IO (nowToIOAction) import Wire.Sem.Paging.Cassandra (InternalPaging) type BrigCanonicalEffects = - '[ ConnectionStore InternalPaging, + '[ SFT, + ConnectionStore InternalPaging, Input UTCTime, Input (Local ()), NotificationSubsystem, @@ -110,6 +112,7 @@ runBrigToIO e (AppT ma) = do . runInputConst (toLocalUnsafe (e ^. settings . Opt.federationDomain) ()) . runInputSem (embed getCurrentTime) . connectionStoreToCassandra + . interpretSFT (e ^. httpManager) ) ) $ runReaderT ma e diff --git a/services/brig/src/Brig/Options.hs b/services/brig/src/Brig/Options.hs index 027e1a1ec3e..dca9d2b3a18 100644 --- a/services/brig/src/Brig/Options.hs +++ b/services/brig/src/Brig/Options.hs @@ -396,6 +396,8 @@ data Opts = Opts cassandra :: !CassandraOpts, -- | ElasticSearch settings elasticsearch :: !ElasticSearchOpts, + -- | SFT Federation + multiSFT :: !(Maybe Bool), -- | RabbitMQ settings, required when federation is enabled. rabbitmq :: !(Maybe RabbitMqOpts), -- | AWS settings @@ -806,7 +808,8 @@ data SFTOptions = SFTOptions { sftBaseDomain :: !DNS.Domain, sftSRVServiceName :: !(Maybe ByteString), -- defaults to defSftServiceName if unset sftDiscoveryIntervalSeconds :: !(Maybe DiffTime), -- defaults to defSftDiscoveryIntervalSeconds - sftListLength :: !(Maybe (Range 1 100 Int)) -- defaults to defSftListLength + sftListLength :: !(Maybe (Range 1 100 Int)), -- defaults to defSftListLength + sftTokenOptions :: !(Maybe SFTTokenOptions) } deriving (Show, Generic) @@ -817,6 +820,19 @@ instance FromJSON SFTOptions where <*> (mapM asciiOnly =<< o .:? "sftSRVServiceName") <*> (secondsToDiffTime <$$> o .:? "sftDiscoveryIntervalSeconds") <*> (o .:? "sftListLength") + <*> (o .:? "sftToken") + +data SFTTokenOptions = SFTTokenOptions + { sttTTL :: !Word32, + sttSecret :: !FilePath + } + deriving (Show, Generic) + +instance FromJSON SFTTokenOptions where + parseJSON = Y.withObject "SFTTokenOptions" $ \o -> + SFTTokenOptions + <$> (o .: "ttl") + <*> (o .: "secret") asciiOnly :: Text -> Y.Parser ByteString asciiOnly t = @@ -886,7 +902,8 @@ Lens.makeLensesFor [ ("optSettings", "optionSettings"), ("elasticsearch", "elasticsearchL"), ("sft", "sftL"), - ("turn", "turnL") + ("turn", "turnL"), + ("multiSFT", "multiSFTL") ] ''Opts diff --git a/services/brig/test/integration/API/Calling.hs b/services/brig/test/integration/API/Calling.hs index ffcaad9399f..b6a355b1264 100644 --- a/services/brig/test/integration/API/Calling.hs +++ b/services/brig/test/integration/API/Calling.hs @@ -101,7 +101,7 @@ testSFT b opts = do "when SFT discovery is not enabled, sft_servers shouldn't be returned" Nothing (cfg ^. rtcConfSftServers) - withSettingsOverrides (opts & Opts.sftL ?~ Opts.SFTOptions "integration-tests.zinfra.io" Nothing (Just 0.001) Nothing) $ do + withSettingsOverrides (opts & Opts.sftL ?~ Opts.SFTOptions "integration-tests.zinfra.io" Nothing (Just 0.001) Nothing Nothing) $ do cfg1 <- retryWhileN 10 (isNothing . view rtcConfSftServers) (getTurnConfigurationV2 uid b) -- These values are controlled by https://github.com/zinfra/cailleach/tree/77ca2d23cf2959aa183dd945d0a0b13537a8950d/environments/dns-integration-tests let Right server1 = mkHttpsUrl =<< first show (parseURI laxURIParserOptions "https://sft01.integration-tests.zinfra.io:443") diff --git a/services/brig/test/unit/Test/Brig/Calling.hs b/services/brig/test/unit/Test/Brig/Calling.hs index 044c289f9d1..007a2041743 100644 --- a/services/brig/test/unit/Test/Brig/Calling.hs +++ b/services/brig/test/unit/Test/Brig/Calling.hs @@ -21,13 +21,13 @@ module Test.Brig.Calling (tests) where import Brig.Calling -import Brig.Calling.API (CallsConfigVersion (..), NoTurnServers, newConfig) -import Brig.Calling.Internal (sftServerFromSrvTarget) +import Brig.Calling.API +import Brig.Calling.Internal import Brig.Effects.SFT import Brig.Options import Control.Concurrent.Timeout qualified as System import Control.Lens ((^.)) -import Control.Monad.Catch (throwM) +import Control.Monad.Catch import Data.Bifunctor import Data.List.NonEmpty (NonEmpty (..)) import Data.List.NonEmpty qualified as NonEmpty @@ -39,14 +39,14 @@ import Data.Timeout import Imports import Network.DNS import OpenSSL -import OpenSSL.EVP.Digest (getDigestByName) +import OpenSSL.EVP.Digest import Polysemy import Polysemy.Error import Polysemy.TinyLog import Test.Brig.Effects.Delay import Test.Tasty import Test.Tasty.HUnit -import Test.Tasty.QuickCheck (Arbitrary (..), generate) +import Test.Tasty.QuickCheck import URI.ByteString import UnliftIO.Async qualified as Async import Wire.API.Call.Config @@ -83,12 +83,12 @@ tests = assertEqual "should use the service name to form domain" "_foo._tcp.example.com." - (mkSFTDomain (SFTOptions "example.com" (Just "foo") Nothing Nothing)), + (mkSFTDomain (SFTOptions "example.com" (Just "foo") Nothing Nothing Nothing)), testCase "when service name is not provided" $ assertEqual "should assume service name to be 'sft'" "_sft._tcp.example.com." - (mkSFTDomain (SFTOptions "example.com" Nothing Nothing Nothing)) + (mkSFTDomain (SFTOptions "example.com" Nothing Nothing Nothing Nothing)) ], testGroup "sftDiscoveryLoop" $ [ testCase "when service can be discovered" $ void testSFTDiscoveryLoopWhenSuccessful @@ -126,7 +126,8 @@ testSFTDiscoveryLoopWhenSuccessful = do fakeDNSEnv <- newFakeDNSEnv (\_ -> SrvAvailable returnedEntries) let intervalInSeconds = 0.001 intervalInMicroseconds = 1000 - sftEnv <- mkSFTEnv $ SFTOptions "foo.example.com" Nothing (Just intervalInSeconds) Nothing + Just sha512 <- getDigestByName "SHA512" + sftEnv <- mkSFTEnv sha512 $ SFTOptions "foo.example.com" Nothing (Just intervalInSeconds) Nothing Nothing tick <- newEmptyMVar delayCallsTVar <- newTVarIO [] @@ -315,17 +316,18 @@ testSFTStaticV2NoStaticUrl = do <*> pure "foo.example.com" <*> pure 5 <*> pure (unsafeRange 1) + <*> pure Nothing turnUri <- generate arbitrary cfg <- runM @IO . ignoreLogs . interpretSFTInMemory mempty . throwErrorInIO @_ @NoTurnServers - $ newConfig env (Discovered turnUri) Nothing (Just sftEnv) (Just . unsafeRange $ 2) ListAllSFTServers CallsConfigV2 + $ newConfig env (Discovered turnUri) Nothing (Just sftEnv) (Just . unsafeRange $ 2) ListAllSFTServers (CallsConfigV2 Nothing) assertEqual "when SFT static URL is disabled, sft_servers_all should be from SFT environment" - (Just . fmap (sftServerFromSrvTarget . srvTarget) . toList $ servers) - (cfg ^. rtcConfSftServersAll) + (Just . fmap ((^. sftURL) . sftServerFromSrvTarget . srvTarget) . toList $ servers) + ((^. authURL) <$$> cfg ^. rtcConfSftServersAll) -- The v2 endpoint `GET /calls/config/v2` with an SFT static URL that gives an error testSFTStaticV2StaticUrlError :: IO () @@ -337,7 +339,7 @@ testSFTStaticV2StaticUrlError = do . ignoreLogs . interpretSFTInMemory mempty -- an empty lookup map, meaning there was an error . throwErrorInIO @_ @NoTurnServers - $ newConfig env (Discovered turnUri) (Just staticUrl) Nothing (Just . unsafeRange $ 2) ListAllSFTServers CallsConfigV2 + $ newConfig env (Discovered turnUri) (Just staticUrl) Nothing (Just . unsafeRange $ 2) ListAllSFTServers (CallsConfigV2 Nothing) assertEqual "when SFT static URL is enabled (and setSftListAllServers is enabled), but returns error, sft_servers_all should be omitted" Nothing @@ -354,13 +356,13 @@ testSFTStaticV2StaticUrlList = do cfg <- runM @IO . ignoreLogs - . interpretSFTInMemory (Map.singleton staticUrl (SFTGetResponse . Right $ servers)) + . interpretSFTInMemory (Map.singleton staticUrl (SFTGetResponse $ Right servers)) . throwErrorInIO @_ @NoTurnServers - $ newConfig env (Discovered turnUri) (Just staticUrl) Nothing (Just . unsafeRange $ 3) ListAllSFTServers CallsConfigV2 + $ newConfig env (Discovered turnUri) (Just staticUrl) Nothing (Just . unsafeRange $ 3) ListAllSFTServers (CallsConfigV2 Nothing) assertEqual "when SFT static URL and setSftListAllServers are enabled, sft_servers_all should be from /sft_servers_all.json" - (Just servers) - (cfg ^. rtcConfSftServersAll) + ((^. sftURL) <$$> Just servers) + ((^. authURL) <$$> cfg ^. rtcConfSftServersAll) testSFTStaticV2ListAllServersDisabled :: IO () testSFTStaticV2ListAllServersDisabled = do @@ -374,7 +376,7 @@ testSFTStaticV2ListAllServersDisabled = do . ignoreLogs . interpretSFTInMemory (Map.singleton staticUrl (SFTGetResponse . Right $ servers)) . throwErrorInIO @_ @NoTurnServers - $ newConfig env (Discovered turnUri) (Just staticUrl) Nothing (Just . unsafeRange $ 3) HideAllSFTServers CallsConfigV2 + $ newConfig env (Discovered turnUri) (Just staticUrl) Nothing (Just . unsafeRange $ 3) HideAllSFTServers (CallsConfigV2 Nothing) assertEqual "when SFT static URL is enabled and setSftListAllServers is \"disabled\" then sft_servers_all is missing" Nothing From dce1e8fd1e9beb030a5b39b2dce014759a8ff37f Mon Sep 17 00:00:00 2001 From: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> Date: Wed, 10 Apr 2024 11:36:34 +0200 Subject: [PATCH 084/117] [WPB-5687] more legalhold tests (#3966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - [feat] port more legalhold tests to /integration - [feat] introduce combinators for lazy Notifications in App --------- Co-authored-by: Marko Dimjašević Co-authored-by: Igor Ranieri Co-authored-by: Akshay Mankar --- changelog.d/5-internal/WPB-7021 | 1 + charts/integration/templates/configmap.yaml | 1 + charts/integration/templates/service.yaml | 11 + integration/test/Notifications.hs | 19 +- integration/test/SetupHelpers.hs | 4 +- integration/test/Test/LegalHold.hs | 1298 +++++++++-------- integration/test/Testlib/App.hs | 28 + integration/test/Testlib/Env.hs | 2 + .../test/Testlib/MockIntegrationService.hs | 18 +- integration/test/Testlib/Types.hs | 16 +- .../src/Network/Wai/Utilities/MockServer.hs | 6 +- .../test/integration/API/Teams/LegalHold.hs | 255 +--- .../API/Teams/LegalHold/DisabledByDefault.hs | 1 - .../integration/API/Teams/LegalHold/Util.hs | 57 - services/integration.yaml | 5 +- 15 files changed, 811 insertions(+), 911 deletions(-) create mode 100644 changelog.d/5-internal/WPB-7021 diff --git a/changelog.d/5-internal/WPB-7021 b/changelog.d/5-internal/WPB-7021 new file mode 100644 index 00000000000..bcdb36d2cc6 --- /dev/null +++ b/changelog.d/5-internal/WPB-7021 @@ -0,0 +1 @@ +port more of the legalhold test-suite from galley-integration to /integration and get rid of the need for startDynamicBackends diff --git a/charts/integration/templates/configmap.yaml b/charts/integration/templates/configmap.yaml index f211ab25105..2c2178dc14f 100644 --- a/charts/integration/templates/configmap.yaml +++ b/charts/integration/templates/configmap.yaml @@ -164,3 +164,4 @@ data: stern: host: stern.wire-federation-v0.svc.cluster.local port: 8080 + integrationTestHostName: integration-headless.{{ .Release.Namespace }}.svc.cluster.local diff --git a/charts/integration/templates/service.yaml b/charts/integration/templates/service.yaml index d445160ad4e..350b33f11f7 100644 --- a/charts/integration/templates/service.yaml +++ b/charts/integration/templates/service.yaml @@ -1,4 +1,15 @@ {{- $newLabels := eq (include "integrationTestHelperNewLabels" .) "true" -}} +--- +apiVersion: v1 +kind: Service +metadata: + name: integration-headless +spec: + selector: + app: integration-integration + type: ClusterIP + clusterIP: None + --- apiVersion: v1 kind: Service diff --git a/integration/test/Notifications.hs b/integration/test/Notifications.hs index 4cd03abc95c..9186f325f07 100644 --- a/integration/test/Notifications.hs +++ b/integration/test/Notifications.hs @@ -115,6 +115,11 @@ isMemberJoinNotif n = fieldEquals n "payload.0.type" "conversation.member-join" isConvLeaveNotif :: MakesValue a => a -> App Bool isConvLeaveNotif n = fieldEquals n "payload.0.type" "conversation.member-leave" +isConvLeaveNotifWithLeaver :: (MakesValue user, MakesValue a) => user -> a -> App Bool +isConvLeaveNotifWithLeaver user n = + fieldEquals n "payload.0.type" "conversation.member-leave" + &&~ (n %. "payload.0.data.user_ids.0") `isEqual` (user %. "id") + isNotifConv :: (MakesValue conv, MakesValue a, HasCallStack) => conv -> a -> App Bool isNotifConv conv n = fieldEquals n "payload.0.qualified_conversation" (objQidObject conv) @@ -145,6 +150,12 @@ isConvAccessUpdateNotif n = isConvCreateNotif :: MakesValue a => a -> App Bool isConvCreateNotif n = fieldEquals n "payload.0.type" "conversation.create" +-- | like 'isConvCreateNotif' but excludes self conversations +isConvCreateNotifNotSelf :: MakesValue a => a -> App Bool +isConvCreateNotifNotSelf n = + fieldEquals n "payload.0.type" "conversation.create" + &&~ do not <$> fieldEquals n "payload.0.data.access" ["private"] + isConvDeleteNotif :: MakesValue a => a -> App Bool isConvDeleteNotif n = fieldEquals n "payload.0.type" "conversation.delete" @@ -177,9 +188,11 @@ isUserConnectionNotif = notifTypeIsEqual "user.connection" isConnectionNotif :: MakesValue a => String -> a -> App Bool isConnectionNotif status n = - (&&) - <$> nPayload n %. "type" `isEqual` "user.connection" - <*> nPayload n %. "connection.status" `isEqual` status + -- NB: + -- (&&) <$> (print "hello" *> pure False) <*> fail "bla" === _|_ + -- runMaybeT $ (lift (print "hello") *> MaybeT (pure Nothing)) *> lift (fail "bla") === pure Nothing + nPayload n %. "type" `isEqual` "user.connection" + &&~ nPayload n %. "connection.status" `isEqual` status assertLeaveNotification :: ( HasCallStack, diff --git a/integration/test/SetupHelpers.hs b/integration/test/SetupHelpers.hs index 0181d9325c8..1d6803e0beb 100644 --- a/integration/test/SetupHelpers.hs +++ b/integration/test/SetupHelpers.hs @@ -289,8 +289,8 @@ setUpLHDevice :: tid -> owner -> uid -> - -- | the port the LH service is running on - Int -> + -- | the host and port the LH service is running on + (String, Int) -> App () setUpLHDevice tid alice bob lhPort = do legalholdWhitelistTeam tid alice diff --git a/integration/test/Test/LegalHold.hs b/integration/test/Test/LegalHold.hs index af2968206f8..175721bf399 100644 --- a/integration/test/Test/LegalHold.hs +++ b/integration/test/Test/LegalHold.hs @@ -14,7 +14,6 @@ -- -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . - module Test.LegalHold where import API.Brig @@ -48,40 +47,39 @@ import UnliftIO (Chan, readChan, timeout) testLHPreventAddingNonConsentingUsers :: App () testLHPreventAddingNonConsentingUsers = do - startDynamicBackends [mempty] $ \[dom] -> do - withMockServer lhMockApp $ \lhPort _chan -> do - (owner, tid, [alice, alex]) <- createTeam dom 3 - - legalholdWhitelistTeam tid owner >>= assertSuccess - legalholdIsTeamInWhitelist tid owner >>= assertSuccess - postLegalHoldSettings tid owner (mkLegalHoldSettings lhPort) >>= assertStatus 201 - - george <- randomUser dom def - georgeQId <- george %. "qualified_id" - connectUsers =<< forM [alice, george] make - connectUsers =<< forM [alex, george] make - conv <- postConversation alice (defProteus {qualifiedUsers = [alex], team = Just tid}) >>= getJSON 201 - - -- the guest should be added to the conversation - bindResponse (addMembers alice conv def {users = [georgeQId]}) $ \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "type" `shouldMatch` "conversation.member-join" + withMockServer lhMockApp $ \lhDomAndPort _chan -> do + (owner, tid, [alice, alex]) <- createTeam OwnDomain 3 + + legalholdWhitelistTeam tid owner >>= assertSuccess + legalholdIsTeamInWhitelist tid owner >>= assertSuccess + postLegalHoldSettings tid owner (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 - -- assert that the guest is in the conversation - checkConvHasOtherMembers conv alice [alex, george] + george <- randomUser OwnDomain def + georgeQId <- george %. "qualified_id" + connectUsers =<< forM [alice, george] make + connectUsers =<< forM [alex, george] make + conv <- postConversation alice (defProteus {qualifiedUsers = [alex], team = Just tid}) >>= getJSON 201 + + -- the guest should be added to the conversation + bindResponse (addMembers alice conv def {users = [georgeQId]}) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "type" `shouldMatch` "conversation.member-join" - -- now request legalhold for alex (but not alice) - requestLegalHoldDevice tid owner alex >>= assertSuccess + -- assert that the guest is in the conversation + checkConvHasOtherMembers conv alice [alex, george] - -- the guest should be removed from the conversation - checkConvHasOtherMembers conv alice [alex] + -- now request legalhold for alex (but not alice) + requestLegalHoldDevice tid owner alex >>= assertSuccess - -- it should not be possible neither for alex nor for alice to add the guest back - addMembers alex conv def {users = [georgeQId]} - >>= assertLabel 403 "not-connected" + -- the guest should be removed from the conversation + checkConvHasOtherMembers conv alice [alex] - addMembers alice conv def {users = [georgeQId]} - >>= assertLabel 403 "missing-legalhold-consent" + -- it should not be possible neither for alex nor for alice to add the guest back + addMembers alex conv def {users = [georgeQId]} + >>= assertLabel 403 "not-connected" + + addMembers alice conv def {users = [georgeQId]} + >>= assertLabel 403 "missing-legalhold-consent" where checkConvHasOtherMembers :: HasCallStack => Value -> Value -> [Value] -> App () checkConvHasOtherMembers conv u us = @@ -100,100 +98,99 @@ testLHMessageExchange :: TaggedBool "consentFrom2" -> App () testLHMessageExchange (TaggedBool clients1New) (TaggedBool clients2New) (TaggedBool consentFrom1) (TaggedBool consentFrom2) = do - startDynamicBackends [mempty] $ \[dom] -> do - withMockServer lhMockApp $ \lhPort _chan -> do - (owner, tid, [mem1, mem2]) <- createTeam dom 3 - - let clientSettings :: Bool -> AddClient - clientSettings allnew = - if allnew - then def -- (`{acapabilities = Just ["legalhold-implicit-consent"]}` is the default) - else def {acapabilities = Nothing} - client1 <- objId $ addClient (mem1 %. "qualified_id") (clientSettings clients1New) >>= getJSON 201 - _client2 <- objId $ addClient (mem2 %. "qualified_id") (clientSettings clients2New) >>= getJSON 201 - - legalholdWhitelistTeam tid owner >>= assertSuccess - legalholdIsTeamInWhitelist tid owner >>= assertSuccess - postLegalHoldSettings tid owner (mkLegalHoldSettings lhPort) >>= assertStatus 201 - - conv <- postConversation mem1 (defProteus {qualifiedUsers = [mem2], team = Just tid}) >>= getJSON 201 - - requestLegalHoldDevice tid owner mem1 >>= assertSuccess - requestLegalHoldDevice tid owner mem2 >>= assertSuccess - when consentFrom1 $ do - approveLegalHoldDevice tid (mem1 %. "qualified_id") defPassword >>= assertSuccess - when consentFrom2 $ do - approveLegalHoldDevice tid (mem2 %. "qualified_id") defPassword >>= assertSuccess - - let getCls :: Value -> App [String] - getCls mem = do - res <- getClientsQualified mem dom mem - val <- getJSON 200 res - cls <- asList val - objId `mapM` cls - cs1 :: [String] <- getCls mem1 -- it's ok to include the sender, backend will filter it out. - cs2 :: [String] <- getCls mem2 - - length cs1 `shouldMatchInt` if consentFrom1 then 2 else 1 - length cs2 `shouldMatchInt` if consentFrom2 then 2 else 1 - - do - successfulMsgForOtherUsers <- mkProteusRecipients mem1 [(mem1, cs1), (mem2, cs2)] "hey there" - let successfulMsg = - Proto.defMessage @Proto.QualifiedNewOtrMessage - & #sender . Proto.client .~ (client1 ^?! hex) - & #recipients .~ [successfulMsgForOtherUsers] - & #reportAll .~ Proto.defMessage - bindResponse (postProteusMessage mem1 (conv %. "qualified_id") successfulMsg) $ \resp -> do - let check :: HasCallStack => Int -> Maybe String -> App () - check status Nothing = do - resp.status `shouldMatchInt` status - check status (Just label) = do - resp.status `shouldMatchInt` status - resp.json %. "label" `shouldMatch` label - - let -- there are two equally valid ways to write this down (feel free to remove one if it gets in your way): - _oneWay = case (clients1New, clients2New, consentFrom1, consentFrom2) of - (_, _, False, False) -> - -- no LH in the picture - check 201 Nothing - (True, True, _, _) -> - if consentFrom1 /= consentFrom2 - then -- no old clients, but users disagree on LH - check 403 (Just "missing-legalhold-consent") - else -- everybody likes LH - check 201 Nothing - _ -> - -- everything else - check 403 (Just "missing-legalhold-consent-old-clients") - - theOtherWay = case (clients1New, clients2New, consentFrom1, consentFrom2) of - -- NB: "consent" always implies "has an active LH device" - (False, False, False, False) -> - -- no LH in the picture - check 201 Nothing - (False, True, False, False) -> - -- no LH in the picture - check 201 Nothing - (True, False, False, False) -> - -- no LH in the picture - check 201 Nothing - (True, True, False, False) -> - -- no LH in the picture - check 201 Nothing - (True, True, False, True) -> - -- all clients new, no consent from sender, recipient has LH device - check 403 (Just "missing-legalhold-consent") - (True, True, True, False) -> - -- all clients new, no consent from recipient, sender has LH device - check 403 (Just "missing-legalhold-consent") - (True, True, True, True) -> - -- everybody happy with LH - check 201 Nothing - _ -> pure () - - -- _oneWay -- run this if you want to make sure both ways are equivalent, but please don't commit! - theOtherWay + withMockServer lhMockApp $ \lhDomAndPort _chan -> do + (owner, tid, [mem1, mem2]) <- createTeam OwnDomain 3 + + let clientSettings :: Bool -> AddClient + clientSettings allnew = + if allnew + then def -- (`{acapabilities = Just ["legalhold-implicit-consent"]}` is the default) + else def {acapabilities = Nothing} + client1 <- objId $ addClient (mem1 %. "qualified_id") (clientSettings clients1New) >>= getJSON 201 + _client2 <- objId $ addClient (mem2 %. "qualified_id") (clientSettings clients2New) >>= getJSON 201 + + legalholdWhitelistTeam tid owner >>= assertSuccess + legalholdIsTeamInWhitelist tid owner >>= assertSuccess + postLegalHoldSettings tid owner (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 + + conv <- postConversation mem1 (defProteus {qualifiedUsers = [mem2], team = Just tid}) >>= getJSON 201 + + requestLegalHoldDevice tid owner mem1 >>= assertSuccess + requestLegalHoldDevice tid owner mem2 >>= assertSuccess + when consentFrom1 $ do + approveLegalHoldDevice tid (mem1 %. "qualified_id") defPassword >>= assertSuccess + when consentFrom2 $ do + approveLegalHoldDevice tid (mem2 %. "qualified_id") defPassword >>= assertSuccess + + let getCls :: Value -> App [String] + getCls mem = do + res <- getClientsQualified mem OwnDomain mem + val <- getJSON 200 res + cls <- asList val + objId `mapM` cls + cs1 :: [String] <- getCls mem1 -- it's ok to include the sender, backend will filter it out. + cs2 :: [String] <- getCls mem2 + + length cs1 `shouldMatchInt` if consentFrom1 then 2 else 1 + length cs2 `shouldMatchInt` if consentFrom2 then 2 else 1 + + do + successfulMsgForOtherUsers <- mkProteusRecipients mem1 [(mem1, cs1), (mem2, cs2)] "hey there" + let successfulMsg = + Proto.defMessage @Proto.QualifiedNewOtrMessage + & #sender . Proto.client .~ (client1 ^?! hex) + & #recipients .~ [successfulMsgForOtherUsers] + & #reportAll .~ Proto.defMessage + bindResponse (postProteusMessage mem1 (conv %. "qualified_id") successfulMsg) $ \resp -> do + let check :: HasCallStack => Int -> Maybe String -> App () + check status Nothing = do + resp.status `shouldMatchInt` status + check status (Just label) = do + resp.status `shouldMatchInt` status + resp.json %. "label" `shouldMatch` label + + let -- there are two equally valid ways to write this down (feel free to remove one if it gets in your way): + _oneWay = case (clients1New, clients2New, consentFrom1, consentFrom2) of + (_, _, False, False) -> + -- no LH in the picture + check 201 Nothing + (True, True, _, _) -> + if consentFrom1 /= consentFrom2 + then -- no old clients, but users disagree on LH + check 403 (Just "missing-legalhold-consent") + else -- everybody likes LH + check 201 Nothing + _ -> + -- everything else + check 403 (Just "missing-legalhold-consent-old-clients") + + theOtherWay = case (clients1New, clients2New, consentFrom1, consentFrom2) of + -- NB: "consent" always implies "has an active LH device" + (False, False, False, False) -> + -- no LH in the picture + check 201 Nothing + (False, True, False, False) -> + -- no LH in the picture + check 201 Nothing + (True, False, False, False) -> + -- no LH in the picture + check 201 Nothing + (True, True, False, False) -> + -- no LH in the picture + check 201 Nothing + (True, True, False, True) -> + -- all clients new, no consent from sender, recipient has LH device + check 403 (Just "missing-legalhold-consent") + (True, True, True, False) -> + -- all clients new, no consent from recipient, sender has LH device + check 403 (Just "missing-legalhold-consent") + (True, True, True, True) -> + -- everybody happy with LH + check 201 Nothing + _ -> pure () + + -- _oneWay -- run this if you want to make sure both ways are equivalent, but please don't commit! + theOtherWay data TestClaimKeys = TCKConsentMissing -- (team not whitelisted, that is) @@ -203,59 +200,58 @@ data TestClaimKeys -- | Cannot fetch prekeys of LH users if requester has not given consent or has old clients. testLHClaimKeys :: TestClaimKeys -> App () testLHClaimKeys testmode = do - startDynamicBackends [mempty] $ \[dom] -> do - withMockServer lhMockApp $ \lhPort _chan -> do - (lowner, ltid, [lmem]) <- createTeam dom 2 - (powner, ptid, [pmem]) <- createTeam dom 2 - - legalholdWhitelistTeam ltid lowner >>= assertSuccess - legalholdIsTeamInWhitelist ltid lowner >>= assertSuccess - postLegalHoldSettings ltid lowner (mkLegalHoldSettings lhPort) >>= assertStatus 201 - - requestLegalHoldDevice ltid lowner lmem >>= assertSuccess - approveLegalHoldDevice ltid (lmem %. "qualified_id") defPassword >>= assertSuccess - - let addc caps = addClient pmem (settings caps) >>= assertSuccess - settings caps = - def - { prekeys = Just $ take 10 somePrekeysRendered, - lastPrekey = Just $ head someLastPrekeysRendered, - acapabilities = caps - } - in case testmode of - TCKConsentMissing -> - addc $ Just ["legalhold-implicit-consent"] - TCKConsentAndNewClients -> do - addc $ Just ["legalhold-implicit-consent"] - legalholdWhitelistTeam ptid powner >>= assertSuccess - legalholdIsTeamInWhitelist ptid powner >>= assertSuccess - - llhdev :: String <- do - let getCls :: Value -> App [String] - getCls mem = do - res <- getClientsQualified mem dom mem - val <- getJSON 200 res - cls <- asList val - objId `mapM` cls - getCls lmem <&> \case - [d] -> d - bad -> error $ show bad - - let assertResp :: HasCallStack => Response -> App () - assertResp resp = case testmode of - TCKConsentMissing -> do - resp.status `shouldMatchInt` 403 - resp.json %. "label" `shouldMatch` "missing-legalhold-consent" - TCKConsentAndNewClients -> do - resp.status `shouldMatchInt` 200 - - bindResponse (getUsersPrekeysClient pmem (lmem %. "qualified_id") llhdev) $ assertResp - bindResponse (getUsersPrekeyBundle pmem (lmem %. "qualified_id")) $ assertResp - - slmemdom <- asString $ lmem %. "qualified_id.domain" - slmemid <- asString $ lmem %. "qualified_id.id" - let userClients = Map.fromList [(slmemdom, Map.fromList [(slmemid, Set.fromList [llhdev])])] - bindResponse (getMultiUserPrekeyBundle pmem userClients) $ assertResp + withMockServer lhMockApp $ \lhDomAndPort _chan -> do + (lowner, ltid, [lmem]) <- createTeam OwnDomain 2 + (powner, ptid, [pmem]) <- createTeam OwnDomain 2 + + legalholdWhitelistTeam ltid lowner >>= assertSuccess + legalholdIsTeamInWhitelist ltid lowner >>= assertSuccess + postLegalHoldSettings ltid lowner (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 + + requestLegalHoldDevice ltid lowner lmem >>= assertSuccess + approveLegalHoldDevice ltid (lmem %. "qualified_id") defPassword >>= assertSuccess + + let addc caps = addClient pmem (settings caps) >>= assertSuccess + settings caps = + def + { prekeys = Just $ take 10 somePrekeysRendered, + lastPrekey = Just $ head someLastPrekeysRendered, + acapabilities = caps + } + in case testmode of + TCKConsentMissing -> + addc $ Just ["legalhold-implicit-consent"] + TCKConsentAndNewClients -> do + addc $ Just ["legalhold-implicit-consent"] + legalholdWhitelistTeam ptid powner >>= assertSuccess + legalholdIsTeamInWhitelist ptid powner >>= assertSuccess + + llhdev :: String <- do + let getCls :: Value -> App [String] + getCls mem = do + res <- getClientsQualified mem OwnDomain mem + val <- getJSON 200 res + cls <- asList val + objId `mapM` cls + getCls lmem <&> \case + [d] -> d + bad -> error $ show bad + + let assertResp :: HasCallStack => Response -> App () + assertResp resp = case testmode of + TCKConsentMissing -> do + resp.status `shouldMatchInt` 403 + resp.json %. "label" `shouldMatch` "missing-legalhold-consent" + TCKConsentAndNewClients -> do + resp.status `shouldMatchInt` 200 + + bindResponse (getUsersPrekeysClient pmem (lmem %. "qualified_id") llhdev) $ assertResp + bindResponse (getUsersPrekeyBundle pmem (lmem %. "qualified_id")) $ assertResp + + slmemdom <- asString $ lmem %. "qualified_id.domain" + slmemid <- asString $ lmem %. "qualified_id.id" + let userClients = Map.fromList [(slmemdom, Map.fromList [(slmemid, Set.fromList [llhdev])])] + bindResponse (getMultiUserPrekeyBundle pmem userClients) $ assertResp testLHAddClientManually :: App () testLHAddClientManually = do @@ -282,50 +278,49 @@ testLHDeleteClientManually = do resp.json %. "message" `shouldMatch` "LegalHold clients cannot be deleted. LegalHold must be disabled on this user by an admin" testLHRequestDevice :: App () -testLHRequestDevice = - startDynamicBackends [mempty] $ \[dom] -> do - (alice, tid, [bob]) <- createTeam dom 2 - let reqNotEnabled requester requestee = - requestLegalHoldDevice tid requester requestee - >>= assertLabel 403 "legalhold-not-enabled" - - reqNotEnabled alice bob - - lpk <- getLastPrekey - pks <- replicateM 3 getPrekey - - withMockServer (lhMockAppWithPrekeys MkCreateMock {nextLastPrey = pure lpk, somePrekeys = pure pks}) \lhPort _chan -> do - let statusShouldBe :: String -> App () - statusShouldBe status = - legalholdUserStatus tid alice bob `bindResponse` \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "status" `shouldMatch` status - - -- the user has not agreed to be under legalhold - for_ [alice, bob] \requester -> do - reqNotEnabled requester bob - statusShouldBe "no_consent" - - legalholdWhitelistTeam tid alice >>= assertSuccess - postLegalHoldSettings tid alice (mkLegalHoldSettings lhPort) >>= assertSuccess - - statusShouldBe "disabled" - - requestLegalHoldDevice tid alice bob >>= assertStatus 201 - statusShouldBe "pending" - - -- requesting twice should be idempotent wrt the approval - -- mind that requesting twice means two "user.legalhold-request" notifications - -- for the clients of the user under legalhold (bob) - requestLegalHoldDevice tid alice bob >>= assertStatus 204 - statusShouldBe "pending" - - [bobc1, bobc2] <- replicateM 2 do - objId $ addClient bob def `bindResponse` getJSON 201 - for_ [bobc1, bobc2] \client -> - awaitNotification bob client noValue isUserLegalholdRequestNotif >>= \notif -> do - notif %. "payload.0.last_prekey" `shouldMatch` lpk - notif %. "payload.0.id" `shouldMatch` objId bob +testLHRequestDevice = do + (alice, tid, [bob]) <- createTeam OwnDomain 2 + let reqNotEnabled requester requestee = + requestLegalHoldDevice tid requester requestee + >>= assertLabel 403 "legalhold-not-enabled" + + reqNotEnabled alice bob + + lpk <- getLastPrekey + pks <- replicateM 3 getPrekey + + withMockServer (lhMockAppWithPrekeys MkCreateMock {nextLastPrey = pure lpk, somePrekeys = pure pks}) \lhDomAndPort _chan -> do + let statusShouldBe :: String -> App () + statusShouldBe status = + legalholdUserStatus tid alice bob `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "status" `shouldMatch` status + + -- the user has not agreed to be under legalhold + for_ [alice, bob] \requester -> do + reqNotEnabled requester bob + statusShouldBe "no_consent" + + legalholdWhitelistTeam tid alice >>= assertSuccess + postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertSuccess + + statusShouldBe "disabled" + + requestLegalHoldDevice tid alice bob >>= assertStatus 201 + statusShouldBe "pending" + + -- requesting twice should be idempotent wrt the approval + -- mind that requesting twice means two "user.legalhold-request" notifications + -- for the clients of the user under legalhold (bob) + requestLegalHoldDevice tid alice bob >>= assertStatus 204 + statusShouldBe "pending" + + [bobc1, bobc2] <- replicateM 2 do + objId $ addClient bob def `bindResponse` getJSON 201 + for_ [bobc1, bobc2] \client -> + awaitNotification bob client noValue isUserLegalholdRequestNotif >>= \notif -> do + notif %. "payload.0.last_prekey" `shouldMatch` lpk + notif %. "payload.0.id" `shouldMatch` objId bob -- | pops a channel until it finds an event that returns a 'Just' -- upon running the matcher function @@ -344,136 +339,134 @@ checkChanVal chan match = checkChan chan \(_, bs) -> runMaybeT do testLHApproveDevice :: App () testLHApproveDevice = do - startDynamicBackends [mempty] \[dom] -> do - -- team users - -- alice (boss) and bob and charlie (member) - (alice, tid, [bob, charlie]) <- createTeam dom 3 + -- team users + -- alice (boss) and bob and charlie (member) + (alice, tid, [bob, charlie]) <- createTeam OwnDomain 3 + + -- ollie the outsider + ollie <- do + o <- randomUser OwnDomain def + connectTwoUsers o alice + pure o + + -- sandy the stranger + sandy <- randomUser OwnDomain def + + legalholdWhitelistTeam tid alice >>= assertStatus 200 + approveLegalHoldDevice tid (bob %. "qualified_id") defPassword + >>= assertLabel 412 "legalhold-not-pending" + + withMockServer lhMockApp \lhDomAndPort chan -> do + legalholdWhitelistTeam tid alice + >>= assertStatus 200 + postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) + >>= assertStatus 201 + requestLegalHoldDevice tid alice bob + >>= assertStatus 201 + + let uidsAndTidMatch val = do + actualTid <- + lookupFieldM val "team_id" + >>= lift . asString + actualUid <- + lookupFieldM val "user_id" + >>= lift . asString + bobUid <- lift $ objId bob - -- ollie the outsider - ollie <- do - o <- randomUser dom def - connectTwoUsers o alice - pure o + -- we pass the check on equality + unless ((actualTid, actualUid) == (tid, bobUid)) do + mzero - -- sandy the stranger - sandy <- randomUser dom def + checkChanVal chan uidsAndTidMatch - legalholdWhitelistTeam tid alice >>= assertStatus 200 - approveLegalHoldDevice tid (bob %. "qualified_id") defPassword - >>= assertLabel 412 "legalhold-not-pending" + -- the team owner cannot approve for bob + approveLegalHoldDevice' tid alice bob defPassword + >>= assertLabel 403 "access-denied" + -- bob needs to provide a password + approveLegalHoldDevice tid bob "wrong-password" + >>= assertLabel 403 "access-denied" + -- now bob finally found his password + approveLegalHoldDevice tid bob defPassword + >>= assertStatus 200 + + let matchAuthToken val = + lookupFieldM val "refresh_token" + >>= lift . asString + + checkChanVal chan matchAuthToken + >>= renewToken bob + >>= assertStatus 200 + + lhdId <- lhDeviceIdOf bob - withMockServer lhMockApp \lhPort chan -> do + legalholdUserStatus tid alice bob `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "client.id" `shouldMatch` lhdId + resp.json %. "status" `shouldMatch` "enabled" + + replicateM 2 do + objId $ addClient bob def `bindResponse` getJSON 201 + >>= traverse_ \client -> + awaitNotification bob client noValue isUserClientAddNotif >>= \notif -> do + notif %. "payload.0.client.type" `shouldMatch` "legalhold" + notif %. "payload.0.client.class" `shouldMatch` "legalhold" + + -- the other team members receive a notification about the + -- legalhold device being approved in their team + for_ [alice, charlie] \user -> do + client <- objId $ addClient user def `bindResponse` getJSON 201 + awaitNotification user client noValue isUserLegalholdEnabledNotif >>= \notif -> do + notif %. "payload.0.id" `shouldMatch` objId bob + for_ [ollie, sandy] \outsider -> do + outsiderClient <- objId $ addClient outsider def `bindResponse` getJSON 201 + assertNoNotifications outsider outsiderClient Nothing isUserLegalholdEnabledNotif + +testLHGetDeviceStatus :: App () +testLHGetDeviceStatus = do + -- team users + -- alice (team owner) and bob (member) + (alice, tid, [bob]) <- createTeam OwnDomain 2 + for_ [alice, bob] \user -> do + legalholdUserStatus tid alice user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "status" `shouldMatch` "no_consent" + + lpk <- getLastPrekey + pks <- replicateM 3 getPrekey + + withMockServer + do lhMockAppWithPrekeys MkCreateMock {nextLastPrey = pure lpk, somePrekeys = pure pks} + \lhDomAndPort _chan -> do legalholdWhitelistTeam tid alice >>= assertStatus 200 - postLegalHoldSettings tid alice (mkLegalHoldSettings lhPort) + + legalholdUserStatus tid alice bob `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "status" `shouldMatch` "disabled" + lookupField resp.json "last_prekey" + >>= assertNothing + runMaybeT (lookupFieldM resp.json "client" >>= flip lookupFieldM "id") + >>= assertNothing + + -- the status messages for these have already been tested + postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 + requestLegalHoldDevice tid alice bob >>= assertStatus 201 - let uidsAndTidMatch val = do - actualTid <- - lookupFieldM val "team_id" - >>= lift . asString - actualUid <- - lookupFieldM val "user_id" - >>= lift . asString - bobUid <- lift $ objId bob - - -- we pass the check on equality - unless ((actualTid, actualUid) == (tid, bobUid)) do - mzero - - checkChanVal chan uidsAndTidMatch - - -- the team owner cannot approve for bob - approveLegalHoldDevice' tid alice bob defPassword - >>= assertLabel 403 "access-denied" - -- bob needs to provide a password - approveLegalHoldDevice tid bob "wrong-password" - >>= assertLabel 403 "access-denied" - -- now bob finally found his password approveLegalHoldDevice tid bob defPassword >>= assertStatus 200 - let matchAuthToken val = - lookupFieldM val "refresh_token" - >>= lift . asString - - checkChanVal chan matchAuthToken - >>= renewToken bob - >>= assertStatus 200 - lhdId <- lhDeviceIdOf bob - legalholdUserStatus tid alice bob `bindResponse` \resp -> do resp.status `shouldMatchInt` 200 - resp.json %. "client.id" `shouldMatch` lhdId resp.json %. "status" `shouldMatch` "enabled" + resp.json %. "last_prekey" `shouldMatch` lpk + resp.json %. "client.id" `shouldMatch` lhdId - replicateM 2 do - objId $ addClient bob def `bindResponse` getJSON 201 - >>= traverse_ \client -> - awaitNotification bob client noValue isUserClientAddNotif >>= \notif -> do - notif %. "payload.0.client.type" `shouldMatch` "legalhold" - notif %. "payload.0.client.class" `shouldMatch` "legalhold" - - -- the other team members receive a notification about the - -- legalhold device being approved in their team - for_ [alice, charlie] \user -> do - client <- objId $ addClient user def `bindResponse` getJSON 201 - awaitNotification user client noValue isUserLegalholdEnabledNotif >>= \notif -> do - notif %. "payload.0.id" `shouldMatch` objId bob - for_ [ollie, sandy] \outsider -> do - outsiderClient <- objId $ addClient outsider def `bindResponse` getJSON 201 - assertNoNotifications outsider outsiderClient Nothing isUserLegalholdEnabledNotif - -testLHGetDeviceStatus :: App () -testLHGetDeviceStatus = - startDynamicBackends [mempty] \[dom] -> do - -- team users - -- alice (team owner) and bob (member) - (alice, tid, [bob]) <- createTeam dom 2 - for_ [alice, bob] \user -> do - legalholdUserStatus tid alice user `bindResponse` \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "status" `shouldMatch` "no_consent" - - lpk <- getLastPrekey - pks <- replicateM 3 getPrekey - - withMockServer - do lhMockAppWithPrekeys MkCreateMock {nextLastPrey = pure lpk, somePrekeys = pure pks} - \lhPort _chan -> do - legalholdWhitelistTeam tid alice - >>= assertStatus 200 - - legalholdUserStatus tid alice bob `bindResponse` \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "status" `shouldMatch` "disabled" - lookupField resp.json "last_prekey" - >>= assertNothing - runMaybeT (lookupFieldM resp.json "client" >>= flip lookupFieldM "id") - >>= assertNothing - - -- the status messages for these have already been tested - postLegalHoldSettings tid alice (mkLegalHoldSettings lhPort) - >>= assertStatus 201 - - requestLegalHoldDevice tid alice bob - >>= assertStatus 201 - - approveLegalHoldDevice tid bob defPassword - >>= assertStatus 200 - - lhdId <- lhDeviceIdOf bob - legalholdUserStatus tid alice bob `bindResponse` \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "status" `shouldMatch` "enabled" - resp.json %. "last_prekey" `shouldMatch` lpk - resp.json %. "client.id" `shouldMatch` lhdId - - requestLegalHoldDevice tid alice bob - >>= assertLabel 409 "legalhold-already-enabled" + requestLegalHoldDevice tid alice bob + >>= assertLabel 409 "legalhold-already-enabled" -- | this sets the timeout to a higher number; we need -- this because the SQS queue on the brig is super slow @@ -485,118 +478,113 @@ setTimeoutTo :: Int -> Env -> Env setTimeoutTo tSecs env = env {timeOutSeconds = tSecs} testLHDisableForUser :: App () -testLHDisableForUser = - startDynamicBackends [mempty] \[dom] -> do - -- team users - -- alice (team owner) and bob (member) - (alice, tid, [bob]) <- createTeam dom 2 +testLHDisableForUser = do + (alice, tid, [bob]) <- createTeam OwnDomain 2 - withMockServer lhMockApp \lhPort chan -> do - setUpLHDevice tid alice bob lhPort + withMockServer lhMockApp \lhDomAndPort chan -> do + setUpLHDevice tid alice bob lhDomAndPort - bobc <- objId $ addClient bob def `bindResponse` getJSON 201 + bobc <- objId $ addClient bob def `bindResponse` getJSON 201 - awaitNotification bob bobc noValue isUserClientAddNotif >>= \notif -> do - notif %. "payload.0.client.type" `shouldMatch` "legalhold" - notif %. "payload.0.client.class" `shouldMatch` "legalhold" + awaitNotification bob bobc noValue isUserClientAddNotif >>= \notif -> do + notif %. "payload.0.client.type" `shouldMatch` "legalhold" + notif %. "payload.0.client.class" `shouldMatch` "legalhold" - -- only an admin can disable legalhold - disableLegalHold tid bob bob defPassword - >>= assertLabel 403 "operation-denied" + -- only an admin can disable legalhold + disableLegalHold tid bob bob defPassword + >>= assertLabel 403 "operation-denied" - disableLegalHold tid alice bob "fix ((\"the password always is \" <>) . show)" - >>= assertLabel 403 "access-denied" + disableLegalHold tid alice bob "fix ((\"the password always is \" <>) . show)" + >>= assertLabel 403 "access-denied" - disableLegalHold tid alice bob defPassword - >>= assertStatus 200 + disableLegalHold tid alice bob defPassword + >>= assertStatus 200 - checkChan chan \(req, _) -> runMaybeT do - unless - do - BS8.unpack req.requestMethod == "POST" - && req.pathInfo == (T.pack <$> ["legalhold", "remove"]) - mzero + checkChan chan \(req, _) -> runMaybeT do + unless + do + BS8.unpack req.requestMethod == "POST" + && req.pathInfo == (T.pack <$> ["legalhold", "remove"]) + mzero - void $ local (setTimeoutTo 90) do - awaitNotification bob bobc noValue isUserClientRemoveNotif - *> awaitNotification bob bobc noValue isUserLegalholdDisabledNotif + void $ local (setTimeoutTo 90) do + awaitNotification bob bobc noValue isUserClientRemoveNotif + *> awaitNotification bob bobc noValue isUserLegalholdDisabledNotif - bobId <- objId bob - lhClients <- - BrigI.getClientsFull bob [bobId] `bindResponse` \resp -> do - resp.json %. bobId - & asList - >>= filterM \val -> (== "legalhold") <$> (val %. "type" & asString) + bobId <- objId bob + lhClients <- + BrigI.getClientsFull bob [bobId] `bindResponse` \resp -> do + resp.json %. bobId + & asList + >>= filterM \val -> (== "legalhold") <$> (val %. "type" & asString) - shouldBeEmpty lhClients + shouldBeEmpty lhClients testLHEnablePerTeam :: App () testLHEnablePerTeam = do - startDynamicBackends [mempty] \[dom] -> do - -- team users - -- alice (team owner) and bob (member) - (alice, tid, [bob]) <- createTeam dom 2 - legalholdIsEnabled tid alice `bindResponse` \resp -> do + -- team users + -- alice (team owner) and bob (member) + (alice, tid, [bob]) <- createTeam OwnDomain 2 + legalholdIsEnabled tid alice `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "lockStatus" `shouldMatch` "unlocked" + resp.json %. "status" `shouldMatch` "disabled" + + withMockServer lhMockApp \lhDomAndPort _chan -> do + setUpLHDevice tid alice bob lhDomAndPort + + legalholdUserStatus tid alice bob `bindResponse` \resp -> do resp.status `shouldMatchInt` 200 - resp.json %. "lockStatus" `shouldMatch` "unlocked" - resp.json %. "status" `shouldMatch` "disabled" - - withMockServer lhMockApp \lhPort _chan -> do - setUpLHDevice tid alice bob lhPort - - legalholdUserStatus tid alice bob `bindResponse` \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "status" `shouldMatch` "enabled" + resp.json %. "status" `shouldMatch` "enabled" - putLegalholdStatus tid alice "disabled" - `bindResponse` assertLabel 403 "legalhold-whitelisted-only" + putLegalholdStatus tid alice "disabled" + `bindResponse` assertLabel 403 "legalhold-whitelisted-only" - -- the put doesn't have any influence on the status being "enabled" - legalholdUserStatus tid alice bob `bindResponse` \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "status" `shouldMatch` "enabled" + -- the put doesn't have any influence on the status being "enabled" + legalholdUserStatus tid alice bob `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "status" `shouldMatch` "enabled" testLHGetMembersIncludesStatus :: App () testLHGetMembersIncludesStatus = do - startDynamicBackends [mempty] \[dom] -> do - -- team users - -- alice (team owner) and bob (member) - (alice, tid, [bob]) <- createTeam dom 2 + -- team users + -- alice (team owner) and bob (member) + (alice, tid, [bob]) <- createTeam OwnDomain 2 - let statusShouldBe :: String -> App () - statusShouldBe status = do - getTeamMembers alice tid `bindResponse` \resp -> do - resp.status `shouldMatchInt` 200 - [bobMember] <- - resp.json %. "members" & asList >>= filterM \u -> do - (==) <$> asString (u %. "user") <*> objId bob - bobMember %. "legalhold_status" `shouldMatch` status + let statusShouldBe :: String -> App () + statusShouldBe status = do + getTeamMembers alice tid `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + [bobMember] <- + resp.json %. "members" & asList >>= filterM \u -> do + (==) <$> asString (u %. "user") <*> objId bob + bobMember %. "legalhold_status" `shouldMatch` status + statusShouldBe "no_consent" + withMockServer lhMockApp \lhDomAndPort _chan -> do statusShouldBe "no_consent" - withMockServer lhMockApp \lhPort _chan -> do - statusShouldBe "no_consent" - legalholdWhitelistTeam tid alice - >>= assertStatus 200 + legalholdWhitelistTeam tid alice + >>= assertStatus 200 - -- the status messages for these have already been tested - postLegalHoldSettings tid alice (mkLegalHoldSettings lhPort) - >>= assertStatus 201 + -- the status messages for these have already been tested + postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) + >>= assertStatus 201 - -- legalhold has been requested but is disabled - statusShouldBe "disabled" + -- legalhold has been requested but is disabled + statusShouldBe "disabled" - requestLegalHoldDevice tid alice bob - >>= assertStatus 201 + requestLegalHoldDevice tid alice bob + >>= assertStatus 201 - -- legalhold has been set to pending after requesting device - statusShouldBe "pending" + -- legalhold has been set to pending after requesting device + statusShouldBe "pending" - approveLegalHoldDevice tid bob defPassword - >>= assertStatus 200 + approveLegalHoldDevice tid bob defPassword + >>= assertStatus 200 - -- bob has accepted the legalhold device - statusShouldBe "enabled" + -- bob has accepted the legalhold device + statusShouldBe "enabled" type TB s = TaggedBool s @@ -605,172 +593,322 @@ testLHNoConsentBlockOne2OneConv (MkTagged connectFirst) (MkTagged teampeer) (MkTagged approveLH) - (MkTagged testPendingConnection) = - startDynamicBackends [mempty] \[dom1] -> do - -- team users - -- alice (team owner) and bob (member) - (alice, tid, []) <- createTeam dom1 1 - bob <- - if teampeer - then do - (walice, _tid, []) <- createTeam dom1 1 - -- FUTUREWORK(mangoiv): creating a team on a second backend - -- causes this bug: https://wearezeta.atlassian.net/browse/WPB-6640 - pure walice - else randomUser dom1 def - - legalholdWhitelistTeam tid alice - >>= assertStatus 200 - - let doEnableLH :: HasCallStack => App (Maybe String) - doEnableLH = do - -- alice requests a legalhold device for herself - requestLegalHoldDevice tid alice alice - >>= assertStatus 201 - - when approveLH do - approveLegalHoldDevice tid alice defPassword - >>= assertStatus 200 - legalholdUserStatus tid alice alice `bindResponse` \resp -> do - resp.status `shouldMatchInt` 200 - resp.json %. "status" `shouldMatch` if approveLH then "enabled" else "pending" - if approveLH - then Just <$> lhDeviceIdOf alice - else pure Nothing - - doDisableLH :: HasCallStack => App () - doDisableLH = - disableLegalHold tid alice alice defPassword + (MkTagged testPendingConnection) = do + -- team users + -- alice (team owner) and bob (member) + (alice, tid, []) <- createTeam OwnDomain 1 + bob <- + if teampeer + then do + (walice, _tid, []) <- createTeam OwnDomain 1 + -- FUTUREWORK(mangoiv): creating a team on a second backend + -- causes this bug: https://wearezeta.atlassian.net/browse/WPB-6640 + pure walice + else randomUser OwnDomain def + + legalholdWhitelistTeam tid alice + >>= assertStatus 200 + + let doEnableLH :: HasCallStack => App (Maybe String) + doEnableLH = do + -- alice requests a legalhold device for herself + requestLegalHoldDevice tid alice alice + >>= assertStatus 201 + + when approveLH do + approveLegalHoldDevice tid alice defPassword >>= assertStatus 200 + legalholdUserStatus tid alice alice `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "status" `shouldMatch` if approveLH then "enabled" else "pending" + if approveLH + then Just <$> lhDeviceIdOf alice + else pure Nothing + + doDisableLH :: HasCallStack => App () + doDisableLH = + disableLegalHold tid alice alice defPassword + >>= assertStatus 200 + + withMockServer lhMockApp \lhDomAndPort _chan -> do + postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) + >>= assertStatus 201 - withMockServer lhMockApp \lhPort _chan -> do - postLegalHoldSettings tid alice (mkLegalHoldSettings lhPort) - >>= assertStatus 201 - - if not connectFirst - then do - void doEnableLH - postConnection alice bob - >>= assertLabel 403 "missing-legalhold-consent" - - postConnection bob alice - >>= assertLabel 403 "missing-legalhold-consent" - else do - alicec <- objId $ addClient alice def >>= getJSON 201 - bobc <- objId $ addClient bob def >>= getJSON 201 - - postConnection alice bob - >>= assertStatus 201 - mbConvId <- - if testPendingConnection - then pure Nothing - else - Just - <$> do - putConnection bob alice "accepted" - >>= getJSON 200 - %. "qualified_conversation" - - -- we need to take away the pending/ sent status for the connections - [lastNotifAlice, lastNotifBob] <- for [(alice, alicec), (bob, bobc)] \(user, client) -> do - -- we get two events if bob accepts alice's request - let numEvents = if testPendingConnection then 1 else 2 - last <$> awaitNotifications user client Nothing numEvents isUserConnectionNotif - - mbLHDevice <- doEnableLH - - let assertConnectionsMissingLHConsent = - for_ [(bob, alice), (alice, bob)] \(a, b) -> - getConnections a `bindResponse` \resp -> do - resp.status `shouldMatchInt` 200 - conn <- assertOne =<< do resp.json %. "connections" & asList - conn %. "status" `shouldMatch` "missing-legalhold-consent" - conn %. "from" `shouldMatch` objId a - conn %. "to" `shouldMatch` objId b - - assertConnectionsMissingLHConsent - - [lastNotifAlice', lastNotifBob'] <- for [(alice, alicec, lastNotifAlice), (bob, bobc, lastNotifBob)] \(user, client, lastNotif) -> do - awaitNotification user client (Just lastNotif) isUserConnectionNotif >>= \notif -> - notif %. "payload.0.connection.status" `shouldMatch` "missing-legalhold-consent" - $> notif - - for_ [(bob, alice), (alice, bob)] \(a, b) -> - putConnection a b "accepted" - >>= assertLabel 403 "bad-conn-update" - - -- putting the connection to "accepted" with 403 doesn't change the - -- connection status - assertConnectionsMissingLHConsent - - bobc2 <- objId $ addClient bob def >>= getJSON 201 - - let -- \| we send a message from bob to alice, but only if - -- we have a conversation id and a legalhold device - -- we first create a message that goes to recipients - -- chosen by the first callback passed - -- then send the message using proteus - -- and in the end running the assertino callback to - -- verify the result - sendMessageFromBobToAlice :: - HasCallStack => - (String -> [String]) -> - -- \^ if we have the legalhold device registered, this - -- callback will be passed the lh device - (Response -> App ()) -> - -- \^ the callback to verify our response (an assertion) - App () - sendMessageFromBobToAlice recipients assertion = - for_ ((,) <$> mbConvId <*> mbLHDevice) \(convId, device) -> do - successfulMsgForOtherUsers <- - mkProteusRecipients - bob -- bob is the sender - [(alice, recipients device), (bob, [bobc])] - -- we send to clients of alice, maybe the legalhold device - -- we need to send to our other clients (bobc) - "hey alice (and eve)" -- the message - let bobaliceMessage = - Proto.defMessage @Proto.QualifiedNewOtrMessage - & #sender . Proto.client .~ (bobc2 ^?! hex) - & #recipients .~ [successfulMsgForOtherUsers] - & #reportAll .~ Proto.defMessage - -- make sure that `convId` is not just the `convId` but also - -- contains the domain because `postProteusMessage` will take the - -- comain from the `convId` json object - postProteusMessage bob convId bobaliceMessage - `bindResponse` assertion - - sendMessageFromBobToAlice (\device -> [alicec, device]) \resp -> do - resp.status `shouldMatchInt` 404 - - -- now we disable legalhold - doDisableLH - - for_ mbLHDevice \lhd -> - local (setTimeoutTo 90) $ - awaitNotification alice alicec noValue isUserClientRemoveNotif >>= \notif -> - notif %. "payload.0.client.id" `shouldMatch` lhd - - let assertStatusFor user status = - getConnections user `bindResponse` \resp -> do + if not connectFirst + then do + void doEnableLH + postConnection alice bob + >>= assertLabel 403 "missing-legalhold-consent" + + postConnection bob alice + >>= assertLabel 403 "missing-legalhold-consent" + else do + alicec <- objId $ addClient alice def >>= getJSON 201 + bobc <- objId $ addClient bob def >>= getJSON 201 + + postConnection alice bob + >>= assertStatus 201 + mbConvId <- + if testPendingConnection + then pure Nothing + else + Just + <$> do + putConnection bob alice "accepted" + >>= getJSON 200 + %. "qualified_conversation" + + -- we need to take away the pending/ sent status for the connections + [lastNotifAlice, lastNotifBob] <- for [(alice, alicec), (bob, bobc)] \(user, client) -> do + -- we get two events if bob accepts alice's request + let numEvents = if testPendingConnection then 1 else 2 + last <$> awaitNotifications user client Nothing numEvents isUserConnectionNotif + + mbLHDevice <- doEnableLH + + let assertConnectionsMissingLHConsent = + for_ [(bob, alice), (alice, bob)] \(a, b) -> + getConnections a `bindResponse` \resp -> do resp.status `shouldMatchInt` 200 conn <- assertOne =<< do resp.json %. "connections" & asList - conn %. "status" `shouldMatch` status + conn %. "status" `shouldMatch` "missing-legalhold-consent" + conn %. "from" `shouldMatch` objId a + conn %. "to" `shouldMatch` objId b + + assertConnectionsMissingLHConsent + + [lastNotifAlice', lastNotifBob'] <- for [(alice, alicec, lastNotifAlice), (bob, bobc, lastNotifBob)] \(user, client, lastNotif) -> do + awaitNotification user client (Just lastNotif) isUserConnectionNotif >>= \notif -> + notif %. "payload.0.connection.status" `shouldMatch` "missing-legalhold-consent" + $> notif + + for_ [(bob, alice), (alice, bob)] \(a, b) -> + putConnection a b "accepted" + >>= assertLabel 403 "bad-conn-update" + + -- putting the connection to "accepted" with 403 doesn't change the + -- connection status + assertConnectionsMissingLHConsent + + bobc2 <- objId $ addClient bob def >>= getJSON 201 + + let -- \| we send a message from bob to alice, but only if + -- we have a conversation id and a legalhold device + -- we first create a message that goes to recipients + -- chosen by the first callback passed + -- then send the message using proteus + -- and in the end running the assertino callback to + -- verify the result + sendMessageFromBobToAlice :: + HasCallStack => + (String -> [String]) -> + -- \^ if we have the legalhold device registered, this + -- callback will be passed the lh device + (Response -> App ()) -> + -- \^ the callback to verify our response (an assertion) + App () + sendMessageFromBobToAlice recipients assertion = + for_ ((,) <$> mbConvId <*> mbLHDevice) \(convId, device) -> do + successfulMsgForOtherUsers <- + mkProteusRecipients + bob -- bob is the sender + [(alice, recipients device), (bob, [bobc])] + -- we send to clients of alice, maybe the legalhold device + -- we need to send to our other clients (bobc) + "hey alice (and eve)" -- the message + let bobaliceMessage = + Proto.defMessage @Proto.QualifiedNewOtrMessage + & #sender . Proto.client .~ (bobc2 ^?! hex) + & #recipients .~ [successfulMsgForOtherUsers] + & #reportAll .~ Proto.defMessage + -- make sure that `convId` is not just the `convId` but also + -- contains the domain because `postProteusMessage` will take the + -- comain from the `convId` json object + postProteusMessage bob convId bobaliceMessage + `bindResponse` assertion + + sendMessageFromBobToAlice (\device -> [alicec, device]) \resp -> do + resp.status `shouldMatchInt` 404 + + -- now we disable legalhold + doDisableLH + + for_ mbLHDevice \lhd -> + local (setTimeoutTo 90) $ + awaitNotification alice alicec noValue isUserClientRemoveNotif >>= \notif -> + notif %. "payload.0.client.id" `shouldMatch` lhd + + let assertStatusFor user status = + getConnections user `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + conn <- assertOne =<< do resp.json %. "connections" & asList + conn %. "status" `shouldMatch` status + + if testPendingConnection + then do + assertStatusFor alice "sent" + assertStatusFor bob "pending" + else do + assertStatusFor alice "accepted" + assertStatusFor bob "accepted" + + for_ [(alice, alicec, lastNotifAlice'), (bob, bobc, lastNotifBob')] \(user, client, lastNotif) -> do + awaitNotification user client (Just lastNotif) isUserConnectionNotif >>= \notif -> + notif %. "payload.0.connection.status" `shouldMatchOneOf` ["sent", "pending", "accepted"] + + sendMessageFromBobToAlice (const [alicec]) \resp -> do + resp.status `shouldMatchInt` 201 + + sendMessageFromBobToAlice (\device -> [device]) \resp -> do + resp.status `shouldMatchInt` 412 + +data GroupConvAdmin + = LegalholderIsAdmin + | PeerIsAdmin + | BothAreAdmins + deriving (Show, Generic) - if testPendingConnection - then do - assertStatusFor alice "sent" - assertStatusFor bob "pending" - else do - assertStatusFor alice "accepted" - assertStatusFor bob "accepted" - - for_ [(alice, alicec, lastNotifAlice'), (bob, bobc, lastNotifBob')] \(user, client, lastNotif) -> do - awaitNotification user client (Just lastNotif) isUserConnectionNotif >>= \notif -> - notif %. "payload.0.connection.status" `shouldMatchOneOf` ["sent", "pending", "accepted"] - - sendMessageFromBobToAlice (const [alicec]) \resp -> do - resp.status `shouldMatchInt` 201 - - sendMessageFromBobToAlice (\device -> [device]) \resp -> do - resp.status `shouldMatchInt` 412 +-- | If a member of an existing conversation is assigned a LH device, users are removed from +-- the conversation until policy conflicts are resolved. +-- +-- As to who gets to stay: +-- - admins will stay over members +-- - local members will stay over remote members. +testLHNoConsentRemoveFromGroup :: GroupConvAdmin -> App () +testLHNoConsentRemoveFromGroup admin = do + (alice, tidAlice, []) <- createTeam OwnDomain 1 + (bob, tidBob, []) <- createTeam OwnDomain 1 + legalholdWhitelistTeam tidAlice alice >>= assertStatus 200 + withMockServer lhMockApp \lhDomAndPort _chan -> do + postLegalHoldSettings tidAlice alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 + withWebSockets [alice, bob] \[aws, bws] -> do + connectTwoUsers alice bob + (convId, qConvId) <- do + let (inviter, tidInviter, invitee, inviteeRole) = case admin of + LegalholderIsAdmin -> (alice, tidAlice, bob, "wire_member") + BothAreAdmins -> (alice, tidAlice, bob, "wire_admin") + PeerIsAdmin -> (bob, tidBob, alice, "wire_member") + + let createConv = defProteus {qualifiedUsers = [invitee], newUsersRole = inviteeRole, team = Just tidInviter} + postConversation inviter createConv `bindResponse` \resp -> do + resp.json %. "members.self.conversation_role" `shouldMatch` "wire_admin" + resp.json %. "members.others.0.conversation_role" `shouldMatch` case admin of + BothAreAdmins -> "wire_admin" + PeerIsAdmin -> "wire_member" + LegalholderIsAdmin -> "wire_member" + (,) <$> resp.json %. "id" <*> resp.json %. "qualified_id" + for_ [aws, bws] \ws -> do + awaitMatch isConvCreateNotifNotSelf ws >>= \pl -> pl %. "payload.0.conversation" `shouldMatch` convId + + for_ [alice, bob] \user -> + getConversation user qConvId >>= assertStatus 200 + + requestLegalHoldDevice tidAlice alice alice >>= assertStatus 201 + approveLegalHoldDevice tidAlice alice defPassword >>= assertStatus 200 + legalholdUserStatus tidAlice alice alice `bindResponse` \resp -> do + resp.json %. "status" `shouldMatch` "enabled" + resp.status `shouldMatchInt` 200 + + case admin of + LegalholderIsAdmin -> do + for_ [aws, bws] do awaitMatch (isConvLeaveNotifWithLeaver bob) + getConversation alice qConvId >>= assertStatus 200 + getConversation bob qConvId >>= assertLabel 403 "access-denied" + PeerIsAdmin -> do + for_ [aws, bws] do awaitMatch (isConvLeaveNotifWithLeaver alice) + getConversation bob qConvId >>= assertStatus 200 + getConversation alice qConvId >>= assertLabel 403 "access-denied" + BothAreAdmins -> do + for_ [aws, bws] do awaitMatch (isConvLeaveNotifWithLeaver bob) + getConversation alice qConvId >>= assertStatus 200 + getConversation bob qConvId >>= assertLabel 403 "access-denied" + +testLHHappyFlow :: App () +testLHHappyFlow = do + (alice, tid, [bob]) <- createTeam OwnDomain 2 + let statusShouldBe :: String -> App () + statusShouldBe status = + legalholdUserStatus tid alice bob `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "status" `shouldMatch` status + + legalholdWhitelistTeam tid alice >>= assertStatus 200 + lpk <- getLastPrekey + pks <- replicateM 3 getPrekey + + withMockServer (lhMockAppWithPrekeys MkCreateMock {nextLastPrey = pure lpk, somePrekeys = pure pks}) \lhDomAndPort _chan -> do + postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 + + -- implicit consent + statusShouldBe "disabled" + -- whitelisting is idempotent + legalholdWhitelistTeam tid alice >>= assertStatus 200 + statusShouldBe "disabled" + + -- memmbers cannot request LH devices + requestLegalHoldDevice tid bob alice >>= assertLabel 403 "operation-denied" + + -- owners can; bob should now have a pending request + requestLegalHoldDevice tid alice bob >>= assertStatus 201 + statusShouldBe "pending" + + -- owner cannot approve on behalf on user under legalhold + approveLegalHoldDevice' tid alice bob defPassword >>= assertLabel 403 "access-denied" + + -- user can approve the request, however + approveLegalHoldDevice tid bob defPassword `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + + legalholdUserStatus tid alice bob `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "status" `shouldMatch` "enabled" + _ <- + resp.json `lookupField` "client.id" + >>= assertJust "client id is present" + resp.json %. "last_prekey" `shouldMatch` lpk + +testLHGetStatus :: App () +testLHGetStatus = do + (alice, tid, [bob]) <- createTeam OwnDomain 2 + (charlie, _tidCharlie, [debora]) <- createTeam OwnDomain 2 + emil <- randomUser OwnDomain def + + let check :: HasCallStack => (MakesValue getter, MakesValue target) => getter -> target -> String -> App () + check getter target status = do + profile <- getUser getter target >>= getJSON 200 + pStatus <- profile %. "legalhold_status" & asString + status `shouldMatch` pStatus + + for_ [alice, bob, charlie, debora, emil] \u -> do + check u bob "no_consent" + check u emil "no_consent" + legalholdWhitelistTeam tid alice >>= assertStatus 200 + withMockServer lhMockApp \lhDomAndPort _chan -> do + postLegalHoldSettings tid alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 + for_ [alice, bob, charlie, debora, emil] \u -> do + check u bob "disabled" + requestLegalHoldDevice tid alice bob >>= assertStatus 201 + check debora bob "pending" + approveLegalHoldDevice tid bob defPassword >>= assertStatus 200 + check debora bob "enabled" + +testLHCannotCreateGroupWithUsersInConflict :: App () +testLHCannotCreateGroupWithUsersInConflict = do + (alice, tidAlice, [bob]) <- createTeam OwnDomain 2 + (charlie, _tidCharlie, [debora]) <- createTeam OwnDomain 2 + legalholdWhitelistTeam tidAlice alice >>= assertStatus 200 + connectTwoUsers bob charlie + connectTwoUsers bob debora + withMockServer lhMockApp \lhDomAndPort _chan -> do + postLegalHoldSettings tidAlice alice (mkLegalHoldSettings lhDomAndPort) >>= assertStatus 201 + postConversation bob defProteus {qualifiedUsers = [charlie, alice], newUsersRole = "wire_member", team = Just tidAlice} + >>= assertStatus 201 + + requestLegalHoldDevice tidAlice alice alice >>= assertStatus 201 + approveLegalHoldDevice tidAlice alice defPassword >>= assertStatus 200 + legalholdUserStatus tidAlice alice alice `bindResponse` \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "status" `shouldMatch` "enabled" + + postConversation bob defProteus {qualifiedUsers = [debora, alice], newUsersRole = "wire_member", team = Just tidAlice} + >>= assertLabel 403 "missing-legalhold-consent" diff --git a/integration/test/Testlib/App.hs b/integration/test/Testlib/App.hs index ee6f5e4da77..904386a791e 100644 --- a/integration/test/Testlib/App.hs +++ b/integration/test/Testlib/App.hs @@ -1,9 +1,13 @@ module Testlib.App where +import Control.Applicative ((<|>)) import Control.Monad.Reader +import Control.Monad.Trans.Maybe (MaybeT (MaybeT), runMaybeT) import qualified Control.Retry as Retry import Data.Aeson hiding ((.=)) +import Data.Bool (bool) import Data.IORef +import Data.Maybe (isJust) import qualified Data.Text as T import qualified Data.Yaml as Yaml import GHC.Exception @@ -72,3 +76,27 @@ instance MakesValue FedDomain where -- backwards-compatible way so everybody can benefit. retryT :: App a -> App a retryT action = Retry.recoverAll (Retry.exponentialBackoff 8000 <> Retry.limitRetries 10) (const action) + +-- | make Bool lazy +liftBool :: Functor f => f Bool -> BoolT f +liftBool = MaybeT . fmap (bool Nothing (Just ())) + +-- | make Bool strict +unliftBool :: Functor f => BoolT f -> f Bool +unliftBool = fmap isJust . runMaybeT + +-- | lazy (&&) +(&&~) :: App Bool -> App Bool -> App Bool +b1 &&~ b2 = unliftBool $ liftBool b1 *> liftBool b2 + +infixr 3 &&~ + +-- | lazy (||) +(||~) :: App Bool -> App Bool -> App Bool +b1 ||~ b2 = unliftBool $ liftBool b1 <|> liftBool b2 + +infixr 2 ||~ + +-- | lazy (&&): (*>) +-- lazy (||): (<|>) +type BoolT f = MaybeT f () diff --git a/integration/test/Testlib/Env.hs b/integration/test/Testlib/Env.hs index 4becf8eb9a3..7ff7d2559bb 100644 --- a/integration/test/Testlib/Env.hs +++ b/integration/test/Testlib/Env.hs @@ -100,6 +100,7 @@ mkGlobalEnv cfgFile = do { gServiceMap = sm, gDomain1 = intConfig.backendOne.originDomain, gDomain2 = intConfig.backendTwo.originDomain, + gIntegrationTestHostName = intConfig.integrationTestHostName, gFederationV0Domain = intConfig.federationV0.originDomain, gDynamicDomains = (.domain) <$> Map.elems intConfig.dynamicBackends, gDefaultAPIVersion = 6, @@ -138,6 +139,7 @@ mkEnv ge = do { serviceMap = gServiceMap ge, domain1 = gDomain1 ge, domain2 = gDomain2 ge, + integrationTestHostName = gIntegrationTestHostName ge, federationV0Domain = gFederationV0Domain ge, dynamicDomains = gDynamicDomains ge, defaultAPIVersion = gDefaultAPIVersion ge, diff --git a/integration/test/Testlib/MockIntegrationService.hs b/integration/test/Testlib/MockIntegrationService.hs index c7c279211e4..7e91be4b7b5 100644 --- a/integration/test/Testlib/MockIntegrationService.hs +++ b/integration/test/Testlib/MockIntegrationService.hs @@ -13,7 +13,7 @@ import Network.Wai as Wai import qualified Network.Wai.Handler.Warp as Warp import qualified Network.Wai.Handler.Warp.Internal as Warp import qualified Network.Wai.Handler.WarpTLS as Warp -import Testlib.Prelude +import Testlib.Prelude hiding (IntegrationConfig (integrationTestHostName)) import UnliftIO (MonadUnliftIO (withRunInIO)) import UnliftIO.Async import UnliftIO.Chan @@ -86,14 +86,11 @@ mockServerCert = \T45GXxRd18neXtuYa/OoAw9UQFDN5XfXN0g=\n\ \-----END CERTIFICATE-----" -botHost :: String -botHost = "localhost" - withFreePortAnyAddr :: (MonadMask m, MonadIO m) => ((Warp.Port, Socket) -> m a) -> m a withFreePortAnyAddr = bracket openFreePortAnyAddr (liftIO . Socket.close . snd) openFreePortAnyAddr :: MonadIO m => m (Warp.Port, Socket) -openFreePortAnyAddr = liftIO $ bindRandomPortTCP (fromString "*") +openFreePortAnyAddr = liftIO $ bindRandomPortTCP (fromString "*6") type LiftedApplication = Request -> (Wai.Response -> App ResponseReceived) -> App ResponseReceived @@ -102,10 +99,11 @@ withMockServer :: -- | the mock server (Chan e -> LiftedApplication) -> -- | the test - (Warp.Port -> Chan e -> App a) -> + ((String, Warp.Port) -> Chan e -> App a) -> App a -withMockServer mkApp go = withFreePortAnyAddr $ \(sPort, sock) -> do +withMockServer mkApp go = withFreePortAnyAddr \(sPort, sock) -> do serverStarted <- newEmptyMVar + host <- asks integrationTestHostName let tlss = Warp.tlsSettingsMemory (cs mockServerCert) (cs mockServerPrivKey) let defs = Warp.defaultSettings {Warp.settingsPort = sPort, Warp.settingsBeforeMainLoop = putMVar serverStarted ()} buf <- newChan @@ -114,7 +112,7 @@ withMockServer mkApp go = withFreePortAnyAddr $ \(sPort, sock) -> do inIO $ mkApp buf req (liftIO . respond) srvMVar <- UnliftIO.Timeout.timeout 5_000_000 (takeMVar serverStarted) case srvMVar of - Just () -> go sPort buf `finally` cancel srv + Just () -> go (host, sPort) buf `finally` cancel srv Nothing -> error . show =<< poll srv lhMockApp :: Chan (Wai.Request, LBS.ByteString) -> LiftedApplication @@ -172,8 +170,8 @@ lhMockAppWithPrekeys mks ch req cont = withRunInIO \inIO -> do getRequestHeader :: String -> Wai.Request -> Maybe ByteString getRequestHeader name = lookup (fromString name) . requestHeaders -mkLegalHoldSettings :: Warp.Port -> Value -mkLegalHoldSettings lhPort = +mkLegalHoldSettings :: (String, Warp.Port) -> Value +mkLegalHoldSettings (botHost, lhPort) = object [ "base_url" .= ("https://" <> botHost <> ":" <> show lhPort <> "/legalhold"), "public_key" .= mockServerPubKey, diff --git a/integration/test/Testlib/Types.hs b/integration/test/Testlib/Types.hs index 4009cd99144..430f8d84d0a 100644 --- a/integration/test/Testlib/Types.hs +++ b/integration/test/Testlib/Types.hs @@ -102,6 +102,7 @@ data GlobalEnv = GlobalEnv { gServiceMap :: Map String ServiceMap, gDomain1 :: String, gDomain2 :: String, + gIntegrationTestHostName :: String, gFederationV0Domain :: String, gDynamicDomains :: [String], gDefaultAPIVersion :: Int, @@ -118,6 +119,7 @@ data IntegrationConfig = IntegrationConfig { backendOne :: BackendConfig, backendTwo :: BackendConfig, federationV0 :: BackendConfig, + integrationTestHostName :: String, dynamicBackends :: Map String DynamicBackendConfig, rabbitmq :: RabbitMQConfig, cassandra :: CassandraConfig @@ -131,6 +133,7 @@ instance FromJSON IntegrationConfig where <$> parseJSON (Object o) <*> o .: fromString "backendTwo" <*> o .: fromString "federation-v0" + <*> o .: fromString "integrationTestHostName" <*> o .: fromString "dynamicBackends" <*> o .: fromString "rabbitmq" <*> o .: fromString "cassandra" @@ -195,6 +198,7 @@ data Env = Env { serviceMap :: Map String ServiceMap, domain1 :: String, domain2 :: String, + integrationTestHostName :: String, federationV0Domain :: String, dynamicDomains :: [String], defaultAPIVersion :: Int, @@ -445,7 +449,17 @@ lookupConfigOverride overrides = \case Stern -> overrides.sternCfg FederatorInternal -> overrides.federatorInternalCfg -data Service = Brig | Galley | Cannon | Gundeck | Cargohold | Nginz | Spar | BackgroundWorker | Stern | FederatorInternal +data Service + = Brig + | Galley + | Cannon + | Gundeck + | Cargohold + | Nginz + | Spar + | BackgroundWorker + | Stern + | FederatorInternal deriving ( Show, Eq, diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/MockServer.hs b/libs/wai-utilities/src/Network/Wai/Utilities/MockServer.hs index d0072d6fbd9..407c0d47863 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/MockServer.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/MockServer.hs @@ -20,7 +20,7 @@ module Network.Wai.Utilities.MockServer where import Control.Concurrent.Async qualified as Async -import Control.Exception (throw) +import Control.Exception (throwIO) import Control.Exception qualified as E import Control.Monad.Catch import Control.Monad.Codensity @@ -83,10 +83,10 @@ startMockServer mtlsSettings app = do me <- Async.poll serverThread case me of Nothing -> Async.cancel serverThread - Just (Left e) -> throw e + Just (Left e) -> throwIO e Just (Right a) -> pure a case serverStartedSignal of Nothing -> do Async.cancel serverThread - throw (MockTimeout port) + throwIO (MockTimeout port) Just _ -> pure (closeMock, port) diff --git a/services/galley/test/integration/API/Teams/LegalHold.hs b/services/galley/test/integration/API/Teams/LegalHold.hs index 1cd1f785a01..c9a3118bcf6 100644 --- a/services/galley/test/integration/API/Teams/LegalHold.hs +++ b/services/galley/test/integration/API/Teams/LegalHold.hs @@ -38,22 +38,18 @@ import Data.Range import Data.Time.Clock qualified as Time import Galley.Cassandra.LegalHold import Galley.Env qualified as Galley -import Galley.Options (featureFlags, settings) -import Galley.Types.Teams import Imports import Network.HTTP.Types.Status (status200, status404) import Network.Wai as Wai import Network.Wai.Handler.Warp qualified as Warp import Network.Wai.Utilities.Error qualified as Error -import System.IO (hPutStrLn) import Test.QuickCheck.Instances () import Test.Tasty -import Test.Tasty.Cannon qualified as WS import Test.Tasty.HUnit import TestHelpers import TestSetup import Wire.API.Connection qualified as Conn -import Wire.API.Conversation.Role (roleNameWireAdmin, roleNameWireMember) +import Wire.API.Conversation.Role (roleNameWireAdmin) import Wire.API.Provider.Service import Wire.API.Routes.Internal.Brig.Connection import Wire.API.Team.LegalHold @@ -63,21 +59,6 @@ import Wire.API.Team.Permission import Wire.API.Team.Role import Wire.API.User.Client -onlyIfLhWhitelisted :: TestM () -> TestM () -onlyIfLhWhitelisted action = do - featureLegalHold <- view (tsGConf . settings . featureFlags . flagLegalHold) - case featureLegalHold of - FeatureLegalHoldDisabledPermanently -> - liftIO $ hPutStrLn stderr errmsg - FeatureLegalHoldDisabledByDefault -> - liftIO $ hPutStrLn stderr errmsg - FeatureLegalHoldWhitelistTeamsAndImplicitConsent -> action - where - errmsg = - "*** skipping test. This test only works if you manually adjust the server config files\ - \(the 'withLHWhitelist' trick does not work because it does not allow \ - \brig to talk to the dynamically spawned galley)." - tests :: IO TestSetup -> TestTree tests s = testGroup "Legalhold" [testsPublic s, testsInternal s] @@ -93,7 +74,6 @@ testsPublic s = -- behavior of existing end-points testOnlyIfLhWhitelisted s "POST /clients" testCannotCreateLegalHoldDeviceOldAPI, testOnlyIfLhWhitelisted s "POST /register - can add team members above fanout limit when whitelisting is enabled" testAddTeamUserTooLargeWithLegalholdWhitelisted, - testOnlyIfLhWhitelisted s "GET legalhold status in user profile" testGetLegalholdStatus, {- TODO: conversations/{cnv}/otr/messages - possibly show the legal hold device (if missing) as a different device type (or show that on device level, depending on how client teams prefer) GET /team/{tid}/members - show legal hold status of all members @@ -103,32 +83,12 @@ testsPublic s = "settings.legalholdEnabledTeams" -- FUTUREWORK: ungroup this level [ testGroup -- FUTUREWORK: ungroup this level "teams listed" - [ test s "happy flow" testInWhitelist, - testGroup - "Legalhold is activated for user A in a group conversation" - [ testOnlyIfLhWhitelisted s "All admins are consenting: all non-consenters get removed from conversation" (testNoConsentRemoveFromGroupConv LegalholderIsAdmin), - testOnlyIfLhWhitelisted s "Some admins are consenting: all non-consenters get removed from conversation" (testNoConsentRemoveFromGroupConv BothAreAdmins), - testOnlyIfLhWhitelisted s "No admins are consenting: all LH activated/pending users get removed from conversation" (testNoConsentRemoveFromGroupConv PeerIsAdmin) - ], - testGroup + [ testGroup "Users are invited to a group conversation." [ testGroup - "At least one invited user has activated legalhold. At least one admin of the group has given consent." - [ test - s - "If all all users in the invite have given consent then the invite succeeds and all non-consenters from the group get removed" - (onlyIfLhWhitelisted (testGroupConvInvitationHandlesLHConflicts InviteOnlyConsenters)), - test - s - "If any user in the invite has not given consent then the invite fails" - (onlyIfLhWhitelisted (testGroupConvInvitationHandlesLHConflicts InviteAlsoNonConsenters)) - ], - testGroup "The group conversation contains legalhold activated users." - [ testOnlyIfLhWhitelisted s "If any user in the invite has not given consent then the invite fails" testNoConsentCannotBeInvited - ] + [testOnlyIfLhWhitelisted s "If any user in the invite has not given consent then the invite fails" testNoConsentCannotBeInvited] ], - testOnlyIfLhWhitelisted s "Cannot create conversation with both LH activated and non-consenting users" testCannotCreateGroupWithUsersInConflict, test s "bench hack" testBenchHack ] ] @@ -310,185 +270,9 @@ testCannotCreateLegalHoldDeviceOldAPI = do post req !!! const 400 === statusCode assertZeroLegalHoldDevices uid -testInWhitelist :: TestM () -testInWhitelist = do - g <- viewGalley - (owner, tid) <- createBindingTeam - member <- randomUser - addTeamMemberInternal tid member (rolePermissions RoleMember) Nothing - cannon <- view tsCannon - - putLHWhitelistTeam tid !!! const 200 === statusCode - - WS.bracketR2 cannon member member $ \(_ws, _ws') -> withDummyTestServiceForTeam owner tid $ \_chan -> do - do - -- members have granted consent (implicitly)... - lhs <- view legalHoldStatus <$> withLHWhitelist tid (getTeamMember' g member tid member) - liftIO $ assertEqual "" lhs UserLegalHoldDisabled - - -- ... and can do so again (idempotency). - _ <- withLHWhitelist tid (void $ putLHWhitelistTeam' g tid) - lhs' <- withLHWhitelist tid $ view legalHoldStatus <$> getTeamMember' g member tid member - liftIO $ assertEqual "" lhs' UserLegalHoldDisabled - - do - -- members can't request LH devices - withLHWhitelist tid (requestLegalHoldDevice' g member member tid) !!! testResponse 403 (Just "operation-denied") - UserLegalHoldStatusResponse userStatus _ _ <- withLHWhitelist tid (getUserStatusTyped' g member tid) - liftIO $ - assertEqual - "User with insufficient permissions should be unable to start flow" - UserLegalHoldDisabled - userStatus - do - -- owners can - withLHWhitelist tid (requestLegalHoldDevice' g owner member tid) !!! testResponse 201 Nothing - UserLegalHoldStatusResponse userStatus _ _ <- withLHWhitelist tid (getUserStatusTyped' g member tid) - liftIO $ - assertEqual - "requestLegalHoldDevice should set user status to Pending" - UserLegalHoldPending - userStatus - do - -- request device is idempotent - withLHWhitelist tid (requestLegalHoldDevice' g owner member tid) !!! testResponse 204 Nothing - UserLegalHoldStatusResponse userStatus _ _ <- withLHWhitelist tid (getUserStatusTyped' g member tid) - liftIO $ - assertEqual - "requestLegalHoldDevice when already pending should leave status as Pending" - UserLegalHoldPending - userStatus - do - -- owner cannot approve legalhold device - withLHWhitelist tid (approveLegalHoldDevice' g (Just defPassword) owner member tid) !!! testResponse 403 (Just "access-denied") - do - -- approve works - withLHWhitelist tid (approveLegalHoldDevice' g (Just defPassword) member member tid) !!! testResponse 200 Nothing - UserLegalHoldStatusResponse userStatus lastPrekey' clientId' <- withLHWhitelist tid (getUserStatusTyped' g member tid) - liftIO $ - do - assertEqual "approving should change status to Enabled" UserLegalHoldEnabled userStatus - assertEqual "last_prekey should be set when LH is pending" (Just (head someLastPrekeys)) lastPrekey' - assertEqual "client.id should be set when LH is pending" (Just someClientId) clientId' - -data GroupConvAdmin - = LegalholderIsAdmin - | PeerIsAdmin - | BothAreAdmins - deriving (Show, Eq, Ord, Bounded, Enum) - -testNoConsentRemoveFromGroupConv :: GroupConvAdmin -> HasCallStack => TestM () -testNoConsentRemoveFromGroupConv whoIsAdmin = do - (legalholder :: UserId, tid) <- createBindingTeam - qLegalHolder <- Qualified legalholder <$> viewFederationDomain - (peer :: UserId, teamPeer) <- createBindingTeam - qPeer <- Qualified peer <$> viewFederationDomain - galley <- viewGalley - - let enableLHForLegalholder :: HasCallStack => TestM () - enableLHForLegalholder = do - requestLegalHoldDevice legalholder legalholder tid !!! testResponse 201 Nothing - approveLegalHoldDevice (Just defPassword) legalholder legalholder tid !!! testResponse 200 Nothing - UserLegalHoldStatusResponse userStatus _ _ <- getUserStatusTyped' galley legalholder tid - liftIO $ assertEqual "approving should change status" UserLegalHoldEnabled userStatus - - cannon <- view tsCannon - - putLHWhitelistTeam tid !!! const 200 === statusCode - WS.bracketR2 cannon legalholder peer $ \(legalholderWs, peerWs) -> withDummyTestServiceForTeam legalholder tid $ \_chan -> do - postConnection legalholder peer !!! const 201 === statusCode - void $ putConnection peer legalholder Conn.Accepted (qLegalHolder, tid, qPeer, roleNameWireMember) - PeerIsAdmin -> (qPeer, teamPeer, qLegalHolder, roleNameWireMember) - BothAreAdmins -> (qLegalHolder, tid, qPeer, roleNameWireAdmin) - - convId <- createTeamConvWithRole (qUnqualified inviter) tidInviter [qUnqualified invitee] (Just "group chat with external peer") Nothing Nothing inviteeRole - mapM_ (assertConvMemberWithRole roleNameWireAdmin convId) ([inviter] <> [invitee | whoIsAdmin == BothAreAdmins]) - mapM_ (assertConvMemberWithRole roleNameWireMember convId) [invitee | whoIsAdmin /= BothAreAdmins] - pure convId - qconvId <- Qualified convId <$> viewFederationDomain - - checkConvCreateEvent convId legalholderWs - checkConvCreateEvent convId peerWs - - assertConvMember qLegalHolder convId - assertConvMember qPeer convId - - void enableLHForLegalholder - - case whoIsAdmin of - LegalholderIsAdmin -> do - assertConvMember qLegalHolder convId - assertNotConvMember peer convId - checkConvMemberLeaveEvent qconvId qPeer legalholderWs - checkConvMemberLeaveEvent qconvId qPeer peerWs - PeerIsAdmin -> do - assertConvMember qPeer convId - assertNotConvMember legalholder convId - checkConvMemberLeaveEvent qconvId qLegalHolder legalholderWs - checkConvMemberLeaveEvent qconvId qLegalHolder peerWs - BothAreAdmins -> do - assertConvMember qLegalHolder convId - assertNotConvMember peer convId - checkConvMemberLeaveEvent qconvId qPeer legalholderWs - checkConvMemberLeaveEvent qconvId qPeer peerWs - data GroupConvInvCase = InviteOnlyConsenters | InviteAlsoNonConsenters deriving (Show, Eq, Ord, Bounded, Enum) -testGroupConvInvitationHandlesLHConflicts :: HasCallStack => GroupConvInvCase -> TestM () -testGroupConvInvitationHandlesLHConflicts inviteCase = do - localDomain <- viewFederationDomain - -- team that is legalhold whitelisted - (legalholder :: UserId, tid) <- createBindingTeam - let qLegalHolder = Qualified legalholder localDomain - userWithConsent <- (^. Team.userId) <$> addUserToTeam legalholder tid - userWithConsent2 <- do - uid <- (^. Team.userId) <$> addUserToTeam legalholder tid - pure $ Qualified uid localDomain - putLHWhitelistTeam tid !!! const 200 === statusCode - - -- team without legalhold - (peer :: UserId, teamPeer) <- createBindingTeam - peer2 <- (^. Team.userId) <$> addUserToTeam peer teamPeer - let qpeer2 = Qualified peer2 localDomain - - do - postConnection userWithConsent peer !!! const 201 === statusCode - void $ putConnection peer userWithConsent Conn.Accepted do - -- conversation with 1) userWithConsent and 2) peer - convId <- createTeamConvWithRole userWithConsent tid [peer] (Just "corp + us") Nothing Nothing roleNameWireAdmin - let qconvId = Qualified convId localDomain - - -- activate legalhold for legalholder - do - galley <- viewGalley - requestLegalHoldDevice legalholder legalholder tid !!! testResponse 201 Nothing - approveLegalHoldDevice (Just defPassword) legalholder legalholder tid !!! testResponse 200 Nothing - UserLegalHoldStatusResponse userStatus _ _ <- getUserStatusTyped' galley legalholder tid - liftIO $ assertEqual "approving should change status" UserLegalHoldEnabled userStatus - - case inviteCase of - InviteOnlyConsenters -> do - API.Util.postMembers userWithConsent (qLegalHolder :| [userWithConsent2]) qconvId - !!! const 200 === statusCode - - assertConvMember qLegalHolder convId - assertConvMember userWithConsent2 convId - assertNotConvMember peer convId - InviteAlsoNonConsenters -> do - API.Util.postMembers userWithConsent (qLegalHolder :| [qpeer2]) qconvId - >>= errWith 403 (\err -> Error.label err == "missing-legalhold-consent") - testNoConsentCannotBeInvited :: HasCallStack => TestM () testNoConsentCannotBeInvited = do localDomain <- viewFederationDomain @@ -532,39 +316,6 @@ testNoConsentCannotBeInvited = do API.Util.postQualifiedMembers userLHNotActivated (Qualified peer2 localdomain :| []) qconvId >>= errWith 403 (\err -> Error.label err == "missing-legalhold-consent") -testCannotCreateGroupWithUsersInConflict :: HasCallStack => TestM () -testCannotCreateGroupWithUsersInConflict = do - -- team that is legalhold whitelisted - (legalholder :: UserId, tid) <- createBindingTeam - userLHNotActivated <- (^. Team.userId) <$> addUserToTeam legalholder tid - putLHWhitelistTeam tid !!! const 200 === statusCode - - -- team without legalhold - (peer :: UserId, teamPeer) <- createBindingTeam - peer2 <- (^. Team.userId) <$> addUserToTeam peer teamPeer - - do - postConnection userLHNotActivated peer !!! const 201 === statusCode - void $ putConnection peer userLHNotActivated Conn.Accepted do - createTeamConvAccessRaw userLHNotActivated tid [peer, legalholder] (Just "corp + us") Nothing Nothing Nothing (Just roleNameWireMember) - !!! const 201 === statusCode - - -- activate legalhold for legalholder - do - galley <- viewGalley - requestLegalHoldDevice legalholder legalholder tid !!! testResponse 201 Nothing - approveLegalHoldDevice (Just defPassword) legalholder legalholder tid !!! testResponse 200 Nothing - UserLegalHoldStatusResponse userStatus _ _ <- getUserStatusTyped' galley legalholder tid - liftIO $ assertEqual "approving should change status" UserLegalHoldEnabled userStatus - - createTeamConvAccessRaw userLHNotActivated tid [peer2, legalholder] (Just "corp + us") Nothing Nothing Nothing (Just roleNameWireMember) - >>= errWith 403 (\err -> Error.label err == "missing-legalhold-consent") - testBenchHack :: HasCallStack => TestM () testBenchHack = do {- representative sample run on an old laptop: diff --git a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs index 023da95ed90..a9315929573 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/DisabledByDefault.hs @@ -92,7 +92,6 @@ tests s = testOnlyIfLhEnabled s "POST /clients" testCannotCreateLegalHoldDeviceOldAPI, testOnlyIfLhEnabled s "GET /teams/{tid}/members" testGetTeamMembersIncludesLHStatus, testOnlyIfLhEnabled s "POST /register - cannot add team members above fanout limit" testAddTeamUserTooLargeWithLegalhold, - testOnlyIfLhEnabled s "GET legalhold status in user profile" testGetLegalholdStatus, {- TODO: conversations/{cnv}/otr/messages - possibly show the legal hold device (if missing) as a different device type (or show that on device level, depending on how client teams prefer) GET /team/{tid}/members - show legal hold status of all members diff --git a/services/galley/test/integration/API/Teams/LegalHold/Util.hs b/services/galley/test/integration/API/Teams/LegalHold/Util.hs index e0b2d06481b..fec9706579b 100644 --- a/services/galley/test/integration/API/Teams/LegalHold/Util.hs +++ b/services/galley/test/integration/API/Teams/LegalHold/Util.hs @@ -25,7 +25,6 @@ import Data.ByteString.Char8 qualified as BS import Data.ByteString.Conversion import Data.CallStack import Data.Id -import Data.LegalHold import Data.List.NonEmpty qualified as NonEmpty import Data.List1 qualified as List1 import Data.Misc (PlainTextPassword6) @@ -57,8 +56,6 @@ import Wire.API.Provider.Service import Wire.API.Team.Feature qualified as Public import Wire.API.Team.LegalHold import Wire.API.Team.LegalHold.External -import Wire.API.Team.Member qualified as Team -import Wire.API.User (UserProfile (..)) import Wire.API.User.Client import Wire.API.UserEvent qualified as Ev @@ -221,60 +218,6 @@ publicKeyNotMatchingService = ] in k -testGetLegalholdStatus :: TestM () -testGetLegalholdStatus = do - (owner1, tid1) <- createBindingTeam - member1 <- view Team.userId <$> addUserToTeam owner1 tid1 - - (owner2, tid2) <- createBindingTeam - member2 <- view Team.userId <$> addUserToTeam owner2 tid2 - - personal <- randomUser - - let check :: HasCallStack => UserId -> UserId -> Maybe TeamId -> UserLegalHoldStatus -> TestM () - check getter targetUser targetTeam stat = do - profile <- getUserProfile getter targetUser - when (profileLegalholdStatus profile /= stat) $ do - meminfo <- getUserStatusTyped targetUser `mapM` targetTeam - - liftIO . forM_ meminfo $ \mem -> do - assertEqual "member LH status" stat (ulhsrStatus mem) - assertEqual "team id in brig user record" targetTeam (profileTeam profile) - - liftIO $ assertEqual "user profile status info" stat (profileLegalholdStatus profile) - - requestDev :: HasCallStack => UserId -> UserId -> TeamId -> TestM () - requestDev requestor target tid = do - requestLegalHoldDevice requestor target tid !!! testResponse 201 Nothing - - approveDev :: HasCallStack => UserId -> TeamId -> TestM () - approveDev target tid = do - approveLegalHoldDevice (Just defPassword) target target tid !!! testResponse 200 Nothing - - check owner1 member1 (Just tid1) UserLegalHoldNoConsent - check member1 member1 (Just tid1) UserLegalHoldNoConsent - check owner2 member1 (Just tid1) UserLegalHoldNoConsent - check member2 member1 (Just tid1) UserLegalHoldNoConsent - check personal member1 (Just tid1) UserLegalHoldNoConsent - check owner1 personal Nothing UserLegalHoldNoConsent - check member1 personal Nothing UserLegalHoldNoConsent - check owner2 personal Nothing UserLegalHoldNoConsent - check member2 personal Nothing UserLegalHoldNoConsent - check personal personal Nothing UserLegalHoldNoConsent - - putLHWhitelistTeam tid1 !!! const 200 === statusCode - - withDummyTestServiceForTeam owner1 tid1 $ \_chan -> do - check owner1 member1 (Just tid1) UserLegalHoldDisabled - check member2 member1 (Just tid1) UserLegalHoldDisabled - check personal member1 (Just tid1) UserLegalHoldDisabled - - requestDev owner1 member1 tid1 - check personal member1 (Just tid1) UserLegalHoldPending - - approveDev member1 tid1 - check personal member1 (Just tid1) UserLegalHoldEnabled - ---------------------------------------------------------------------- -- API helpers diff --git a/services/integration.yaml b/services/integration.yaml index 00d54a5efa3..dbfc516bf87 100644 --- a/services/integration.yaml +++ b/services/integration.yaml @@ -61,7 +61,7 @@ federatorExternal: host: 127.0.0.1 port: 8098 -# This domain is configured using coredns runing along with the rest of +# This domain is configured using coredns running along with the rest of # docker-ephemeral setup. There is only an SRV record for # _wire-server-federator._tcp.example.com originDomain: example.com @@ -118,7 +118,6 @@ backendTwo: originDomain: b.example.com - redis2: host: 127.0.0.1 port: 6379 @@ -181,3 +180,5 @@ federation-v0: stern: host: 127.0.0.1 port: 21091 + +integrationTestHostName: "localhost" From 6e5594de2be613920ecdd1452932aea0a9deb911 Mon Sep 17 00:00:00 2001 From: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> Date: Wed, 10 Apr 2024 17:48:44 +0200 Subject: [PATCH 085/117] [WPB-7021] clean up code around associating saml idps and scim tokens in spar (#3974) --- changelog.d/5-internal/WPB-7021 | 2 +- libs/hscim/src/Web/Scim/Schema/User.hs | 2 +- libs/hscim/test/Test/Class/UserSpec.hs | 5 +- libs/wire-api/src/Wire/API/User/Scim.hs | 4 +- services/spar/spar.cabal | 6 + services/spar/src/Spar/API.hs | 34 ++-- services/spar/src/Spar/Error.hs | 17 +- services/spar/src/Spar/Scim/Auth.hs | 5 +- .../src/Spar/Sem/ScimTokenStore/Cassandra.hs | 5 +- .../test-integration/Test/Spar/APISpec.hs | 167 ++++++++++++------ .../test-integration/Test/Spar/AppSpec.hs | 16 +- .../test-integration/Test/Spar/DataSpec.hs | 3 +- .../Test/Spar/Scim/AuthSpec.hs | 30 +++- .../Test/Spar/Scim/UserSpec.hs | 26 ++- services/spar/test-integration/Util/Core.hs | 32 ++-- services/spar/test-integration/Util/Scim.hs | 18 +- 16 files changed, 245 insertions(+), 127 deletions(-) diff --git a/changelog.d/5-internal/WPB-7021 b/changelog.d/5-internal/WPB-7021 index bcdb36d2cc6..f9efaee1f7e 100644 --- a/changelog.d/5-internal/WPB-7021 +++ b/changelog.d/5-internal/WPB-7021 @@ -1 +1 @@ -port more of the legalhold test-suite from galley-integration to /integration and get rid of the need for startDynamicBackends +some small refactorings to make it more clear in code what is happening when registering a scim token and an IdP diff --git a/libs/hscim/src/Web/Scim/Schema/User.hs b/libs/hscim/src/Web/Scim/Schema/User.hs index 16cac92a880..84655c898a0 100644 --- a/libs/hscim/src/Web/Scim/Schema/User.hs +++ b/libs/hscim/src/Web/Scim/Schema/User.hs @@ -95,7 +95,7 @@ import Web.Scim.Schema.User.Phone (Phone) import Web.Scim.Schema.User.Photo (Photo) import Web.Scim.Schema.UserTypes --- | SCIM user record, parametrized with type-level tag @t@ (see 'UserTypes'). +-- | SCIM user record, parametrized with type-level @tag@ (see 'UserTypes'). data User tag = User { schemas :: [Schema], -- Mandatory fields diff --git a/libs/hscim/test/Test/Class/UserSpec.hs b/libs/hscim/test/Test/Class/UserSpec.hs index 3d3d16d0e17..8bfc45bb945 100644 --- a/libs/hscim/test/Test/Class/UserSpec.hs +++ b/libs/hscim/test/Test/Class/UserSpec.hs @@ -17,10 +17,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Test.Class.UserSpec - ( spec, - ) -where +module Test.Class.UserSpec (spec) where import Data.ByteString.Lazy (ByteString) import Network.Wai (Application) diff --git a/libs/wire-api/src/Wire/API/User/Scim.hs b/libs/wire-api/src/Wire/API/User/Scim.hs index 991acf717f5..1b3e4e50b5e 100644 --- a/libs/wire-api/src/Wire/API/User/Scim.hs +++ b/libs/wire-api/src/Wire/API/User/Scim.hs @@ -61,7 +61,7 @@ import Data.Map qualified as Map import Data.Misc (PlainTextPassword6) import Data.OpenApi hiding (Operation) import Data.Proxy -import Data.Text.Encoding (encodeUtf8) +import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Time.Clock (UTCTime) import Imports import SAML2.WebSSO qualified as SAML @@ -129,7 +129,7 @@ data ScimTokenLookupKey hashScimToken :: ScimToken -> ScimTokenHash hashScimToken token = let digest = hash @ByteString @SHA512 (encodeUtf8 (fromScimToken token)) - in ScimTokenHash (cs @ByteString @Text (convertToBase Base64 digest)) + in ScimTokenHash (decodeUtf8 (convertToBase Base64 digest)) -- | Metadata that we store about each token. data ScimTokenInfo = ScimTokenInfo diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index f86140dbb83..27fcc4015bd 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -102,6 +102,7 @@ library default-extensions: AllowAmbiguousTypes BangPatterns + BlockArguments ConstraintKinds DataKinds DefaultSignatures @@ -204,6 +205,7 @@ executable spar default-extensions: AllowAmbiguousTypes BangPatterns + BlockArguments ConstraintKinds DataKinds DefaultSignatures @@ -282,6 +284,7 @@ executable spar-integration default-extensions: AllowAmbiguousTypes BangPatterns + BlockArguments ConstraintKinds DataKinds DefaultSignatures @@ -407,6 +410,7 @@ executable spar-migrate-data default-extensions: AllowAmbiguousTypes BangPatterns + BlockArguments ConstraintKinds DataKinds DefaultSignatures @@ -477,6 +481,7 @@ executable spar-schema default-extensions: AllowAmbiguousTypes BangPatterns + BlockArguments ConstraintKinds DataKinds DefaultSignatures @@ -550,6 +555,7 @@ test-suite spec default-extensions: AllowAmbiguousTypes BangPatterns + BlockArguments ConstraintKinds DataKinds DefaultSignatures diff --git a/services/spar/src/Spar/API.hs b/services/spar/src/Spar/API.hs index 81146b5de06..8f27fab4dbc 100644 --- a/services/spar/src/Spar/API.hs +++ b/services/spar/src/Spar/API.hs @@ -55,6 +55,7 @@ import Data.Id import Data.Proxy import Data.Range import qualified Data.Set as Set +import qualified Data.Text.Lazy as T import Data.Time import Imports import Polysemy @@ -193,12 +194,12 @@ apiIDP :: ) => ServerT APIIDP (Sem r) apiIDP = - idpGet - :<|> idpGetRaw - :<|> idpGetAll - :<|> idpCreate - :<|> idpUpdate - :<|> idpDelete + idpGet -- get, json, captures idp id + :<|> idpGetRaw -- get, raw xml, capture idp id + :<|> idpGetAll -- get, json + :<|> idpCreate -- post, created + :<|> idpUpdate -- put, okay + :<|> idpDelete -- delete, no content apiINTERNAL :: ( Member ScimTokenStore r, @@ -476,6 +477,18 @@ idpCreate :: idpCreate zusr (IdPMetadataValue raw xml) = idpCreateXML zusr raw xml -- | We generate a new UUID for each IdP used as IdPConfig's path, thereby ensuring uniqueness. +-- +-- NOTE(mangoiv): currently registering an IdP and scim token works as follows: +-- - an owner creates a team with some teamId +-- - the owner registers and IdP +-- - the owner registers a scim token and passes the idp id along to associate +-- the scim token with the IdP +-- +-- This doesn't support some flows we may want to support, like: (1) register +-- a scim token and then associate an IdP with it; (2) have scim token and +-- create an idp that is *not* associated with it; ... +-- +-- Related internal docs: https://wearezeta.atlassian.net/wiki/spaces/PAD/pages/1107001440/2024-03-27+scim+user+provisioning+and+saml2+sso+associating+scim+peers+and+saml2+idps idpCreateXML :: ( Member Random r, Member (Logger String) r, @@ -493,14 +506,14 @@ idpCreateXML :: Maybe WireIdPAPIVersion -> Maybe (Range 1 32 Text) -> Sem r IdP -idpCreateXML zusr raw idpmeta mReplaces (fromMaybe defWireIdPAPIVersion -> apiversion) mHandle = withDebugLog "idpCreateXML" (Just . show . (^. SAML.idpId)) $ do +idpCreateXML zusr rawIdpMetadata idpmeta mReplaces (fromMaybe defWireIdPAPIVersion -> apiversion) mHandle = withDebugLog "idpCreateXML" (Just . show . (^. SAML.idpId)) $ do teamid <- Brig.getZUsrCheckPerm zusr CreateUpdateDeleteIdp GalleyAccess.assertSSOEnabled teamid assertNoScimOrNoIdP teamid idp <- maybe (IdPConfigStore.newHandle teamid) (pure . IdPHandle . fromRange) mHandle >>= validateNewIdP apiversion idpmeta teamid mReplaces - IdPRawMetadataStore.store (idp ^. SAML.idpId) raw + IdPRawMetadataStore.store (idp ^. SAML.idpId) rawIdpMetadata IdPConfigStore.insertConfig idp forM_ mReplaces $ \replaces -> IdPConfigStore.setReplacedBy (Replaced replaces) (Replacing (idp ^. SAML.idpId)) @@ -522,8 +535,7 @@ assertNoScimOrNoIdP teamid = do numIdps <- length <$> IdPConfigStore.getConfigsByTeam teamid when (numTokens > 0 && numIdps > 0) $ throwSparSem $ - SparProvisioningMoreThanOneIdP - "Teams with SCIM tokens can only have at most one IdP" + SparProvisioningMoreThanOneIdP ScimTokenAndSecondIdpForbidden -- | Check that issuer is not used anywhere in the system ('WireIdPAPIV1', here it is a -- database key for finding IdPs), or anywhere in this team ('WireIdPAPIV2'), that request @@ -720,7 +732,7 @@ authorizeIdP (Just zusr) idp = do enforceHttps :: Member (Error SparError) r => URI.URI -> Sem r () enforceHttps uri = unless ((uri ^. URI.uriSchemeL . URI.schemeBSL) == "https") $ do - throwSparSem . SparNewIdPWantHttps . cs . SAML.renderURI $ uri + throwSparSem . SparNewIdPWantHttps . T.fromStrict . SAML.renderURI $ uri ---------------------------------------------------------------------------- -- Internal API diff --git a/services/spar/src/Spar/Error.hs b/services/spar/src/Spar/Error.hs index c273e465c8b..901bd9488ca 100644 --- a/services/spar/src/Spar/Error.hs +++ b/services/spar/src/Spar/Error.hs @@ -27,6 +27,7 @@ module Spar.Error ( SparError, SparCustomError (..), + SparProvisioningMoreThanOneIdP (..), IdpDbError (..), throwSpar, sparToServerErrorWithLogging, @@ -96,7 +97,8 @@ data SparCustomError | SparIdPIssuerInUse | SparIdPCannotDeleteOwnIdp | IdpDbError IdpDbError - | SparProvisioningMoreThanOneIdP LText + | -- | scim tokens can only be created in case where there's at most one idp + SparProvisioningMoreThanOneIdP SparProvisioningMoreThanOneIdP | SparProvisioningTokenLimitReached | -- | FUTUREWORK(fisx): This constructor is used in exactly one place (see -- "Spar.Sem.SAML2.Library"), for an error that immediately gets caught. @@ -108,6 +110,13 @@ data SparCustomError SparScimError Scim.ScimError deriving (Eq, Show) +data SparProvisioningMoreThanOneIdP + = -- | a scim token and an idp exist; it is forbidden to create a second IdP + ScimTokenAndSecondIdpForbidden + | -- | two IdPs exist and a scim token is forbidden to create + TwoIdpsAndScimTokenForbidden + deriving (Eq, Show) + data IdpDbError = InsertIdPConfigCannotMixApiVersions | AttemptToGetV1IssuerViaV2API @@ -191,7 +200,11 @@ renderSparError (SAML.CustomError (IdpDbError IdpNonUnique)) = Right $ Wai.mkErr renderSparError (SAML.CustomError (IdpDbError IdpWrongTeam)) = Right $ Wai.mkError status409 "idp-wrong-team" "The IdP is not part of this team." renderSparError (SAML.CustomError (IdpDbError IdpNotFound)) = renderSparError (SAML.CustomError (SparIdPNotFound "")) -- Errors related to provisioning -renderSparError (SAML.CustomError (SparProvisioningMoreThanOneIdP msg)) = Right $ Wai.mkError status400 "more-than-one-idp" ("Team can have at most one IdP configured: " <> msg) +renderSparError (SAML.CustomError (SparProvisioningMoreThanOneIdP msg)) = Right $ + Wai.mkError status400 "more-than-one-idp" do + "Team can have at most one IdP configured: " <> case msg of + ScimTokenAndSecondIdpForbidden -> "teams with SCIM tokens can only have at most one IdP" + TwoIdpsAndScimTokenForbidden -> "SCIM tokens can only be created for a team with at most one IdP" renderSparError (SAML.CustomError SparProvisioningTokenLimitReached) = Right $ Wai.mkError status403 "token-limit-reached" "The limit of provisioning tokens per team has been reached" -- SCIM errors renderSparError (SAML.CustomError (SparScimError err)) = Left $ Scim.scimToServerError err diff --git a/services/spar/src/Spar/Scim/Auth.hs b/services/spar/src/Spar/Scim/Auth.hs index 024731a981a..db8ec7cce5c 100644 --- a/services/spar/src/Spar/Scim/Auth.hs +++ b/services/spar/src/Spar/Scim/Auth.hs @@ -157,10 +157,7 @@ createScimToken zusr Api.CreateScimToken {..} = do -- NB: if the following case does not result in errors, 'validateScimUser' needs to -- be changed. currently, it relies on the fact that there is never more than one IdP. -- https://wearezeta.atlassian.net/browse/SQSERVICES-165 - _ -> - throwSparSem $ - E.SparProvisioningMoreThanOneIdP - "SCIM tokens can only be created for a team with at most one IdP" + _ -> throwSparSem $ E.SparProvisioningMoreThanOneIdP E.TwoIdpsAndScimTokenForbidden -- | > docs/reference/provisioning/scim-token.md {#RefScimTokenDelete} -- diff --git a/services/spar/src/Spar/Sem/ScimTokenStore/Cassandra.hs b/services/spar/src/Spar/Sem/ScimTokenStore/Cassandra.hs index 02480ac4264..1dd8e176921 100644 --- a/services/spar/src/Spar/Sem/ScimTokenStore/Cassandra.hs +++ b/services/spar/src/Spar/Sem/ScimTokenStore/Cassandra.hs @@ -20,10 +20,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Spar.Sem.ScimTokenStore.Cassandra - ( scimTokenStoreToCassandra, - ) -where +module Spar.Sem.ScimTokenStore.Cassandra (scimTokenStoreToCassandra) where import Cassandra as Cas import Control.Arrow (Arrow ((&&&))) diff --git a/services/spar/test-integration/Test/Spar/APISpec.hs b/services/spar/test-integration/Test/Spar/APISpec.hs index 2e4f8aa0b4c..4265be75176 100644 --- a/services/spar/test-integration/Test/Spar/APISpec.hs +++ b/services/spar/test-integration/Test/Spar/APISpec.hs @@ -18,10 +18,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . -module Test.Spar.APISpec - ( spec, - ) -where +module Test.Spar.APISpec (spec) where import Bilge import Bilge.Assert @@ -145,6 +142,12 @@ specMisc = do it "does not trigger on https urls" $ check True it "does trigger on http urls" $ check False +-- | auxiliary function to create a team +callCreateUserWithTeam :: TestSpar (UserId, TeamId) +callCreateUserWithTeam = do + env <- ask + call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + specMetadata :: SpecWith TestEnv specMetadata = do describe "metadata" $ do @@ -179,7 +182,8 @@ specInitiateLogin = do context "known IdP" $ do it "responds with 200" $ do env <- ask - (_, _, idPIdToST . (^. idpId) -> idp) <- registerTestIdP + (user, _tid) <- callCreateUserWithTeam + (idPIdToST . (^. idpId) -> idp) <- registerTestIdP user void . call $ head ((env ^. teSpar) . path (cs $ "/sso/initiate-login/" -/ idp) . expect2xx) describe "GET /sso/initiate-login/:idp" $ do context "unknown IdP" $ do @@ -200,7 +204,8 @@ specInitiateLogin = do context "known IdP" $ do it "responds with authentication request" $ do env <- ask - (_, _, idPIdToST . (^. idpId) -> idp) <- registerTestIdP + (user, _tid) <- callCreateUserWithTeam + (idPIdToST . (^. idpId) -> idp) <- registerTestIdP user resp <- call $ get ((env ^. teSpar) . path (cs $ "/sso/initiate-login/" -/ idp) . expect2xx) liftIO $ do resp `shouldSatisfy` checkRespBody @@ -212,7 +217,8 @@ specFinalizeLogin = do -- Send authentication error and no cookie if response from SSO IdP was rejected context "rejectsSAMLResponseSayingAccessNotGranted" $ do it "responds with a very peculiar 'forbidden' HTTP response" $ do - (_, tid, idp, (_, privcreds)) <- registerTestIdPWithMeta + (user, tid) <- callCreateUserWithTeam + (idp, (_, privcreds)) <- registerTestIdPWithMeta user authnreq <- negotiateAuthnRequest idp spmeta <- getTestSPMetadata tid authnresp <- runSimpleSP $ mkAuthnResponse privcreds idp spmeta authnreq False @@ -262,7 +268,8 @@ specFinalizeLogin = do it "responds with a very peculiar 'allowed' HTTP response" $ do env <- ask let apiVer = env ^. teWireIdPAPIVersion - (_, tid, idp, (_, privcreds)) <- registerTestIdPWithMeta + (user, tid) <- callCreateUserWithTeam + (idp, (_, privcreds)) <- registerTestIdPWithMeta user liftIO $ fromMaybe defWireIdPAPIVersion (idp ^. idpExtraInfo . apiVersion) `shouldBe` apiVer spmeta <- getTestSPMetadata tid authnreq <- negotiateAuthnRequest idp @@ -280,7 +287,8 @@ specFinalizeLogin = do -- (In fact, to get this to work was the reason to introduce 'WireIdPAPIVesion'.) ] env <- ask - (_, tid1, idp1, (IdPMetadataValue _ metadata, privcreds)) <- registerTestIdPWithMeta + (user, tid1) <- callCreateUserWithTeam + (idp1, (IdPMetadataValue _ metadata, privcreds)) <- registerTestIdPWithMeta user (tid2, idp2) <- liftIO . runHttpT (env ^. teMgr) $ do (owner2, tid2) <- createUserWithTeam (env ^. teBrig) (env ^. teGalley) idp2 :: IdP <- callIdpCreate (env ^. teWireIdPAPIVersion) (env ^. teSpar) (Just owner2) metadata @@ -314,7 +322,8 @@ specFinalizeLogin = do -- (In fact, to get this to work was the reason to introduce 'WireIdPAPIVesion'.) ] env <- ask - (_, tid1, idp1, (IdPMetadataValue _ metadata, privcreds)) <- registerTestIdPWithMeta + (user, tid1) <- callCreateUserWithTeam + (idp1, (IdPMetadataValue _ metadata, privcreds)) <- registerTestIdPWithMeta user (tid2, idp2) <- liftIO . runHttpT (env ^. teMgr) $ do (owner2, tid2) <- createUserWithTeam (env ^. teBrig) (env ^. teGalley) idp2 :: IdP <- callIdpCreate (env ^. teWireIdPAPIVersion) (env ^. teSpar) (Just owner2) metadata @@ -335,7 +344,8 @@ specFinalizeLogin = do context "user is created once, then deleted in team settings, then can login again." $ do it "responds with 'allowed'" $ do - (ownerid, teamid, idp, (_, privcreds)) <- registerTestIdPWithMeta + (ownerid, teamid) <- callCreateUserWithTeam + (idp, (_, privcreds)) <- registerTestIdPWithMeta ownerid spmeta <- getTestSPMetadata teamid -- first login newUserAuthnResp :: SignedAuthnResponse <- do @@ -402,7 +412,8 @@ specFinalizeLogin = do (ResponseLBS -> IO ()) -> TestSpar () check mkareq mkaresp submitaresp checkresp = do - (_, teamid, idp, (_, privcreds)) <- registerTestIdPWithMeta + (ownerid, teamid) <- callCreateUserWithTeam + (idp, (_, privcreds)) <- registerTestIdPWithMeta ownerid authnreq <- mkareq idp spmeta <- getTestSPMetadata teamid authnresp <- @@ -454,7 +465,8 @@ specFinalizeLogin = do -- @SF.Channel @TSFI.RESTfulAPI @S2 @S3 -- Do not authenticate if SSO IdP response is signed with wrong key it "rejectsSAMLResponseSignedWithWrongKey" $ do - (_, _, _, (_, badprivcreds)) <- registerTestIdPWithMeta + (ownerid, _teamid) <- callCreateUserWithTeam + (_, (_, badprivcreds)) <- registerTestIdPWithMeta ownerid let mkareq = negotiateAuthnRequest mkaresp _ idp spmeta authnreq = mkAuthnResponse @@ -505,7 +517,8 @@ specFinalizeLogin = do context "IdP changes response format" $ do it "treats NameId case-insensitively" $ do - (_ownerid, tid, idp, (_, privcreds)) <- registerTestIdPWithMeta + (ownerid, tid) <- callCreateUserWithTeam + (idp, (_, privcreds)) <- registerTestIdPWithMeta ownerid spmeta <- getTestSPMetadata tid let loginSuccess :: HasCallStack => ResponseLBS -> TestSpar () @@ -543,27 +556,31 @@ testGetPutDelete whichone = do context "unknown IdP" $ do it "responds with 'not found'" $ do env <- ask - (_, _, _, (idpmeta, _)) <- registerTestIdPWithMeta + (ownerid, _tid) <- callCreateUserWithTeam + (_, (idpmeta, _)) <- registerTestIdPWithMeta ownerid whichone (env ^. teSpar) Nothing (IdPId UUID.nil) idpmeta `shouldRespondWith` checkErrHspec 404 "not-found" context "no zuser" $ do it "responds with 'insufficient permissions'" $ do env <- ask - (_, _, (^. idpId) -> idpid, (idpmeta, _)) <- registerTestIdPWithMeta + (ownerid, _tid) <- callCreateUserWithTeam + ((^. idpId) -> idpid, (idpmeta, _)) <- registerTestIdPWithMeta ownerid whichone (env ^. teSpar) Nothing idpid idpmeta `shouldRespondWith` checkErrHspec 403 "insufficient-permissions" context "zuser has no team" $ do it "responds with 'insufficient permissions'" $ do env <- ask - (_, _, (^. idpId) -> idpid, (idpmeta, _)) <- registerTestIdPWithMeta + (ownerid, _tid) <- callCreateUserWithTeam + ((^. idpId) -> idpid, (idpmeta, _)) <- registerTestIdPWithMeta ownerid (uid, _) <- call $ createRandomPhoneUser (env ^. teBrig) whichone (env ^. teSpar) (Just uid) idpid idpmeta `shouldRespondWith` checkErrHspec 403 "insufficient-permissions" context "zuser is a team member, but not a team owner" $ do it "responds with 'insufficient-permissions' and a helpful message" $ do env <- ask - (_, teamid, (^. idpId) -> idpid, (idpmeta, _)) <- registerTestIdPWithMeta + (ownerid, teamid) <- callCreateUserWithTeam + ((^. idpId) -> idpid, (idpmeta, _)) <- registerTestIdPWithMeta ownerid newmember <- let perms = noPermissions in call $ createTeamMember (env ^. teBrig) (env ^. teGalley) teamid perms @@ -591,19 +608,22 @@ specCRUDIdentityProvider = do context "zuser has wrong team" $ do it "responds with 'insufficient permissions'" $ do env <- ask - (_, _, (^. idpId) -> idpid) <- registerTestIdP + (ownerid, _teamid) <- callCreateUserWithTeam + ((^. idpId) -> idpid) <- registerTestIdP ownerid (uid, _) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) callIdpGet' (env ^. teSpar) (Just uid) idpid `shouldRespondWith` checkErrHspec 403 "insufficient-permissions" context "known IdP, client is team owner" $ do it "responds with 2xx and IdP" $ do env <- ask - (owner, _, (^. idpId) -> idpid) <- registerTestIdP + (owner, _teamid) <- callCreateUserWithTeam + ((^. idpId) -> idpid) <- registerTestIdP owner void . call $ callIdpGet (env ^. teSpar) (Just owner) idpid context "known IdP, client is team owner (authenticated via sso, user without email)" $ do it "responds with 2xx and IdP" $ do env <- ask - (firstOwner, tid, idp, (_, privcreds)) <- registerTestIdPWithMeta + (firstOwner, tid) <- callCreateUserWithTeam + (idp, (_, privcreds)) <- registerTestIdPWithMeta firstOwner ssoOwner <- mkSsoOwner firstOwner tid idp privcreds void . call $ callIdpGet (env ^. teSpar) (Just ssoOwner) (idp ^. idpId) describe "GET /identity-providers" $ do @@ -630,14 +650,16 @@ specCRUDIdentityProvider = do it "returns a non-empty empty list" $ do env <- ask (SampleIdP metadata _ _ _) <- makeSampleIdPMetadata - (owner, _, _) <- registerTestIdPFrom metadata (env ^. teMgr) (env ^. teBrig) (env ^. teGalley) (env ^. teSpar) + (owner, _tid) <- callCreateUserWithTeam + _ <- registerTestIdPFrom metadata (env ^. teMgr) owner (env ^. teSpar) callIdpGetAll (env ^. teSpar) (Just owner) `shouldRespondWith` (not . null . _providers) context "client is team owner without email" $ do it "returns a non-empty empty list" $ do env <- ask (SampleIdP metadata privcreds _ _) <- makeSampleIdPMetadata - (firstOwner, tid, idp) <- registerTestIdPFrom metadata (env ^. teMgr) (env ^. teBrig) (env ^. teGalley) (env ^. teSpar) + (firstOwner, tid) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + idp <- registerTestIdPFrom metadata (env ^. teMgr) firstOwner (env ^. teSpar) ssoOwner <- mkSsoOwner firstOwner tid idp privcreds callIdpGetAll (env ^. teSpar) (Just ssoOwner) `shouldRespondWith` (not . null . _providers) @@ -646,14 +668,16 @@ specCRUDIdentityProvider = do context "zuser has wrong team" $ do it "responds with 'no team member'" $ do env <- ask - (_, _, (^. idpId) -> idpid) <- registerTestIdP + (owner, _tid) <- callCreateUserWithTeam + ((^. idpId) -> idpid) <- registerTestIdP owner (uid, _) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) callIdpDelete' (env ^. teSpar) (Just uid) idpid `shouldRespondWith` checkErrHspec 403 "insufficient-permissions" context "zuser is admin resp. member" $ do it "responds 204 resp. 403" $ do env <- ask - (_, tid, (^. idpId) -> idpid) <- registerTestIdP + (owner, tid) <- callCreateUserWithTeam + ((^. idpId) -> idpid) <- registerTestIdP owner let mkUser :: Role -> TestSpar UserId mkUser role = do let perms = rolePermissions role @@ -667,7 +691,8 @@ specCRUDIdentityProvider = do context "known IdP, IdP empty, client is team owner, without email" $ do it "responds with 2xx and removes IdP" $ do env <- ask - (userid, _, (^. idpId) -> idpid) <- registerTestIdP + (userid, _tid) <- callCreateUserWithTeam + ((^. idpId) -> idpid) <- registerTestIdP userid callIdpDelete' (env ^. teSpar) (Just userid) idpid `shouldRespondWith` \resp -> statusCode resp < 300 callIdpGet' (env ^. teSpar) (Just userid) idpid @@ -677,7 +702,8 @@ specCRUDIdentityProvider = do context "with email, idp non-empty, purge=false" $ do it "responds with 412 and does not remove IdP" $ do env <- ask - (firstOwner, tid, idp, (_, privcreds)) <- registerTestIdPWithMeta + (firstOwner, tid) <- callCreateUserWithTeam + (idp, (_, privcreds)) <- registerTestIdPWithMeta firstOwner _ <- mkSsoOwner firstOwner tid idp privcreds callIdpDelete' (env ^. teSpar) (Just firstOwner) (idp ^. idpId) `shouldRespondWith` checkErrHspec 412 "idp-has-bound-users" @@ -686,7 +712,8 @@ specCRUDIdentityProvider = do context "with email, idp non-empty, purge=true" $ do it "responds with 2xx and removes IdP and users *synchronously*" $ do env <- ask - (firstOwner, tid, idp, (_, privcreds)) <- registerTestIdPWithMeta + (firstOwner, tid) <- callCreateUserWithTeam + (idp, (_, privcreds)) <- registerTestIdPWithMeta firstOwner ssoOwner <- mkSsoOwner firstOwner tid idp privcreds callIdpDeletePurge' (env ^. teSpar) (Just firstOwner) (idp ^. idpId) `shouldRespondWith` \resp -> statusCode resp < 300 @@ -701,7 +728,8 @@ specCRUDIdentityProvider = do context "with email, user who tries to delete is authenticated by the IdP, purge=true" $ do it "responds with 409 'cannot-delete-own-idp'" $ do env <- ask - (firstOwner, tid, idp, (_, privcreds)) <- registerTestIdPWithMeta + (firstOwner, tid) <- callCreateUserWithTeam + (idp, (_, privcreds)) <- registerTestIdPWithMeta firstOwner ssoOwner <- mkSsoOwner firstOwner tid idp privcreds callIdpDeletePurge' (env ^. teSpar) (Just ssoOwner) (idp ^. idpId) `shouldRespondWith` checkErrHspec 409 "cannot-delete-own-idp" @@ -711,7 +739,8 @@ specCRUDIdentityProvider = do context "known IdP, client is team owner" $ do it "responds with 2xx and updates IdP" $ do env <- ask - (owner, _, (^. idpId) -> idpid, (IdPMetadataValue _ idpmeta, _)) <- registerTestIdPWithMeta + (owner, _tid) <- callCreateUserWithTeam + ((^. idpId) -> idpid, (IdPMetadataValue _ idpmeta, _)) <- registerTestIdPWithMeta owner (_, _, cert1) <- liftIO $ mkSignCredsWithCert Nothing 96 (_, _, cert2) <- liftIO $ mkSignCredsWithCert Nothing 96 let idpmeta' = idpmeta & edCertAuthnResponse .~ (cert1 :| [cert2]) @@ -748,14 +777,17 @@ specCRUDIdentityProvider = do context "invalid body" $ do it "rejects" $ do env <- ask - (owner, _, (^. idpId) -> idpid) <- registerTestIdP + (owner, _tid) <- callCreateUserWithTeam + ((^. idpId) -> idpid) <- registerTestIdP owner callIdpUpdate (env ^. teSpar) (Just owner) idpid (IdPMetadataValue "bloo" undefined) `shouldRespondWith` checkErrHspec 400 "invalid-metadata" describe "issuer changed to one that already exists in *another* team" $ do it "rejects if V1, succeeds if V2" $ do env <- ask - (owner1, _, (^. idpId) -> idpid1) <- registerTestIdP - (_, _, _, (IdPMetadataValue _ idpmeta2, _)) <- registerTestIdPWithMeta + (owner1, _tid) <- callCreateUserWithTeam + ((^. idpId) -> idpid1) <- registerTestIdP owner1 + (owner2, _tid) <- callCreateUserWithTeam + (_, (IdPMetadataValue _ idpmeta2, _)) <- registerTestIdPWithMeta owner2 callIdpUpdate (env ^. teSpar) (Just owner1) idpid1 (IdPMetadataValue (cs $ SAML.encode idpmeta2) undefined) `shouldRespondWith` ( case env ^. teWireIdPAPIVersion of WireIdPAPIV1 -> checkErrHspec 400 "idp-issuer-in-use" @@ -764,7 +796,8 @@ specCRUDIdentityProvider = do describe "issuer changed to one that already exists in *the same* team" $ do it "rejects" $ do env <- ask - (owner1, _, (^. idpId) -> idpid1, (IdPMetadataValue _ idpmeta1, _)) <- registerTestIdPWithMeta + (owner1, _tid) <- callCreateUserWithTeam + ((^. idpId) -> idpid1, (IdPMetadataValue _ idpmeta1, _)) <- registerTestIdPWithMeta owner1 (SampleIdP idpmeta2 _ _ _) <- makeSampleIdPMetadata _ <- call $ callIdpCreate (env ^. teWireIdPAPIVersion) (env ^. teSpar) (Just owner1) idpmeta2 let idpmeta1' = idpmeta1 & edIssuer .~ (idpmeta2 ^. edIssuer) @@ -776,7 +809,8 @@ specCRUDIdentityProvider = do describe "issuer changed to one that already existed in the same team in the past (but has been updated away)" $ do it "changes back to the old one and keeps the new in the `old_issuers` list." $ do env <- ask - (owner1, _, (^. idpId) -> idpid1, (IdPMetadataValue _ idpmeta1, _)) <- registerTestIdPWithMeta + (owner1, _tid) <- callCreateUserWithTeam + ((^. idpId) -> idpid1, (IdPMetadataValue _ idpmeta1, _)) <- registerTestIdPWithMeta owner1 idpmeta1' <- do (SampleIdP idpmeta2 _ _ _) <- makeSampleIdPMetadata pure $ idpmeta1 & edIssuer .~ (idpmeta2 ^. edIssuer) @@ -812,7 +846,8 @@ specCRUDIdentityProvider = do describe "issuer changed to one that is new" $ do it "updates old idp, updating both issuer and old_issuers" $ do env <- ask - (owner1, _, (^. idpId) -> idpid1, (IdPMetadataValue _ idpmeta1, _)) <- registerTestIdPWithMeta + (owner1, _tid) <- callCreateUserWithTeam + ((^. idpId) -> idpid1, (IdPMetadataValue _ idpmeta1, _)) <- registerTestIdPWithMeta owner1 issuer2 <- makeIssuer resp <- let idpmeta2 = idpmeta1 & edIssuer .~ issuer2 @@ -825,7 +860,8 @@ specCRUDIdentityProvider = do idp ^. idpExtraInfo . oldIssuers `shouldBe` [idpmeta1 ^. edIssuer] it "migrates old users to new idp on their next login (auto-prov)" $ do env <- ask - (owner1, _, idp1@((^. idpId) -> idpid1), (IdPMetadataValue _ idpmeta1, privkey1)) <- registerTestIdPWithMeta + (owner1, _tid) <- callCreateUserWithTeam + (idp1@((^. idpId) -> idpid1), (IdPMetadataValue _ idpmeta1, privkey1)) <- registerTestIdPWithMeta owner1 issuer2 <- makeIssuer let idpmeta2 = idpmeta1 & edIssuer .~ issuer2 privkey2 = privkey1 @@ -869,7 +905,8 @@ specCRUDIdentityProvider = do it "creates non-existent users" $ do env <- ask - (owner1, _, idp1@((^. idpId) -> idpid1), (IdPMetadataValue _ idpmeta1, privkey1)) <- registerTestIdPWithMeta + (owner1, _tid) <- callCreateUserWithTeam + (idp1@((^. idpId) -> idpid1), (IdPMetadataValue _ idpmeta1, privkey1)) <- registerTestIdPWithMeta owner1 issuer2 <- makeIssuer let idpmeta2 = idpmeta1 & edIssuer .~ issuer2 privkey2 = privkey1 @@ -882,7 +919,8 @@ specCRUDIdentityProvider = do getUserIdViaRef' newuref >>= \es -> liftIO $ es `shouldSatisfy` isJust it "logs in users that have already been moved or created in the new idp" $ do env <- ask - (owner1, _, idp1@((^. idpId) -> idpid1), (IdPMetadataValue _ idpmeta1, privkey1)) <- registerTestIdPWithMeta + (owner1, _tid) <- callCreateUserWithTeam + (idp1@((^. idpId) -> idpid1), (IdPMetadataValue _ idpmeta1, privkey1)) <- registerTestIdPWithMeta owner1 issuer2 <- makeIssuer let idpmeta2 = idpmeta1 & edIssuer .~ issuer2 privkey2 = privkey1 @@ -897,7 +935,8 @@ specCRUDIdentityProvider = do describe "new request uri" $ do it "uses it on next auth handshake" $ do env <- ask - (owner, _, (^. idpId) -> idpid, (IdPMetadataValue _ idpmeta, _)) <- registerTestIdPWithMeta + (owner, _tid) <- callCreateUserWithTeam + ((^. idpId) -> idpid, (IdPMetadataValue _ idpmeta, _)) <- registerTestIdPWithMeta owner let idpmeta' = idpmeta & edRequestURI .~ [uri|https://www.example.com|] callIdpUpdate (env ^. teSpar) (Just owner) idpid (IdPMetadataValue (cs $ SAML.encode idpmeta') undefined) `shouldRespondWith` ((== 200) . statusCode) @@ -908,7 +947,8 @@ specCRUDIdentityProvider = do initidp :: HasCallStack => TestSpar (IdP, SignPrivCreds, SignPrivCreds) initidp = do env <- ask - (owner, _, idp, (IdPMetadataValue _ idpmeta, oldPrivKey)) <- registerTestIdPWithMeta + (owner, _tid) <- callCreateUserWithTeam + (idp, (IdPMetadataValue _ idpmeta, oldPrivKey)) <- registerTestIdPWithMeta owner (SampleIdP _ newPrivKey _ sampleIdPCert2) <- makeSampleIdPMetadata let idpmeta' = idpmeta & edCertAuthnResponse .~ (sampleIdPCert2 :| []) callIdpUpdate (env ^. teSpar) (Just owner) (idp ^. idpId) (IdPMetadataValue (cs $ SAML.encode idpmeta') undefined) @@ -972,7 +1012,8 @@ specCRUDIdentityProvider = do context "zuser is a team member, but not a team owner" $ do it "responds with 'insufficient-permissions' and a helpful message" $ do env <- ask - (_owner, tid, idp) <- registerTestIdP + (owner, tid) <- callCreateUserWithTeam + idp <- registerTestIdP owner newmember <- let perms = noPermissions in call $ createTeamMember (env ^. teBrig) (env ^. teGalley) tid perms @@ -1118,7 +1159,8 @@ specCRUDIdentityProvider = do -- scim doesn't work with more than one idp, so we can't test the post variant -- that creates a second idp (https://wearezeta.atlassian.net/browse/WPB-689) when updateNotReplace . it ("creates new idp, setting old_issuer; sets replaced_by in old idp; scim user search still works: provisionViaScim=True, updateNotReplace=" <> show updateNotReplace <> ", externalIdIsEmail=" <> show externalIdIsEmail) $ do - (owner1, teamid, idp1, (IdPMetadataValue _ idpmeta1, _)) <- registerTestIdPWithMeta + (owner1, teamid) <- callCreateUserWithTeam + (idp1, (IdPMetadataValue _ idpmeta1, _)) <- registerTestIdPWithMeta owner1 let idp1id = idp1 ^. idpId tok <- registerScimToken teamid (Just idp1id) @@ -1147,7 +1189,8 @@ specCRUDIdentityProvider = do checkScimSearch scimStoredUser scimUser it ("creates new idp, setting old_issuer; sets replaced_by in old idp; scim user search still works: provisionViaScim=False, updateNotReplace=" <> show updateNotReplace <> ", externalIdIsEmail=" <> show externalIdIsEmail) $ do - (owner1, teamid, idp1, (IdPMetadataValue _ idpmeta1, privcreds)) <- registerTestIdPWithMeta + (owner1, teamid) <- callCreateUserWithTeam + (idp1, (IdPMetadataValue _ idpmeta1, privcreds)) <- registerTestIdPWithMeta owner1 let idp1id = idp1 ^. idpId (uid, mbEmail, hdl) :: (UserId, Maybe Text, Text) <- do @@ -1180,7 +1223,8 @@ specCRUDIdentityProvider = do describe "replaces an existing idp (cont.)" $ do it "users can still login on old idp as before" $ do env <- ask - (owner1, _, idp1, (IdPMetadataValue _ idpmeta1, privkey1)) <- registerTestIdPWithMeta + (owner1, _teamid) <- callCreateUserWithTeam + (idp1, (IdPMetadataValue _ idpmeta1, privkey1)) <- registerTestIdPWithMeta owner1 let userSubject = SAML.unspecifiedNameID "bloob" issuer1 = idpmeta1 ^. edIssuer olduref <- tryLogin privkey1 idp1 userSubject @@ -1199,7 +1243,8 @@ specCRUDIdentityProvider = do it "migrates old users to new idp on their next login on new idp; after that, login on old won't work any more" $ do env <- ask - (owner1, _, idp1, (IdPMetadataValue _ idpmeta1, privkey1)) <- registerTestIdPWithMeta + (owner1, _teamid) <- callCreateUserWithTeam + (idp1, (IdPMetadataValue _ idpmeta1, privkey1)) <- registerTestIdPWithMeta owner1 let userSubject = SAML.unspecifiedNameID "bloob" issuer1 = idpmeta1 ^. edIssuer privkey2 = privkey1 @@ -1220,7 +1265,8 @@ specCRUDIdentityProvider = do it "creates non-existent users on new idp" $ do env <- ask - (owner1, _, idp1, (IdPMetadataValue _ idpmeta1, privkey1)) <- registerTestIdPWithMeta + (owner1, _teamid) <- callCreateUserWithTeam + (idp1, (IdPMetadataValue _ idpmeta1, privkey1)) <- registerTestIdPWithMeta owner1 let userSubject = SAML.unspecifiedNameID "bloob" privkey2 = privkey1 issuer2 <- makeIssuer @@ -1237,7 +1283,8 @@ specDeleteCornerCases :: SpecWith TestEnv specDeleteCornerCases = describe "delete corner cases" $ do it "deleting the replacing idp2 before it has users does not block logins on idp1" $ do env <- ask - (owner1, _, idp1, (IdPMetadataValue _ idpmeta1, privkey1)) <- registerTestIdPWithMeta + (owner1, _teamid) <- callCreateUserWithTeam + (idp1, (IdPMetadataValue _ idpmeta1, privkey1)) <- registerTestIdPWithMeta owner1 let issuer1 = idpmeta1 ^. edIssuer issuer2 <- makeIssuer let userSubject = SAML.unspecifiedNameID "bloob" @@ -1257,7 +1304,8 @@ specDeleteCornerCases = describe "delete corner cases" $ do uref' `shouldBe` SAML.UserRef issuer1 userSubject it "deleting the replacing idp2 before it has users does not block registrations on idp1" $ do env <- ask - (owner1, _, idp1, (IdPMetadataValue _ idpmeta1, privkey1)) <- registerTestIdPWithMeta + (owner1, _teamid) <- callCreateUserWithTeam + (idp1, (IdPMetadataValue _ idpmeta1, privkey1)) <- registerTestIdPWithMeta owner1 let issuer1 = idpmeta1 ^. edIssuer issuer2 <- makeIssuer idp2 <- @@ -1283,7 +1331,8 @@ specDeleteCornerCases = describe "delete corner cases" $ do -- login once more. This should work despite the dangling database entry. it "re-create previously deleted, dangling users" $ do -- TODO: https://github.com/zinfra/backend-issues/issues/1200 - (_ownerid, _teamid, idp, (_, privcreds)) <- registerTestIdPWithMeta + (owner, _teamid) <- callCreateUserWithTeam + (idp, (_, privcreds)) <- registerTestIdPWithMeta owner uname :: SAML.UnqualifiedNameID <- do suffix <- cs <$> replicateM 7 (getRandomR ('0', '9')) either (error . show) pure $ @@ -1508,15 +1557,18 @@ specSsoSettings = do describe "SSO settings endpoint" $ do it "does not allow setting non-existing SSO code" $ do env <- ask - (_userid, _teamid, _idp) <- registerTestIdP + (owner, _teamid) <- callCreateUserWithTeam + _idp <- registerTestIdP owner nonExisting <- IdPId <$> liftIO UUID.nextRandom callSetDefaultSsoCode (env ^. teSpar) nonExisting `shouldRespondWith` \resp -> statusCode resp == 404 -- not quite right, see `internalPutSsoSettings` it "allows setting a default SSO code" $ do env <- ask - (_userid1, _teamid, (^. idpId) -> idpid1) <- registerTestIdP - (_userid2, _teamid, (^. idpId) -> idpid2) <- registerTestIdP + (userid1, _teamid) <- callCreateUserWithTeam + ((^. idpId) -> idpid1) <- registerTestIdP userid1 + (userid2, _teamid) <- callCreateUserWithTeam + ((^. idpId) -> idpid2) <- registerTestIdP userid2 -- set 1 -- TODO: authorization? callSetDefaultSsoCode (env ^. teSpar) idpid1 @@ -1540,7 +1592,8 @@ specSsoSettings = do ) it "allows removing the default SSO code" $ do env <- ask - (_userid, _teamid, (^. idpId) -> idpid) <- registerTestIdP + (userid, _teamid) <- callCreateUserWithTeam + ((^. idpId) -> idpid) <- registerTestIdP userid -- set callSetDefaultSsoCode (env ^. teSpar) idpid `shouldRespondWith` \resp -> @@ -1557,7 +1610,8 @@ specSsoSettings = do ) it "removes the default SSO code if the IdP gets removed" $ do env <- ask - (userid, _teamid, (^. idpId) -> idpid) <- registerTestIdP + (userid, _teamid) <- callCreateUserWithTeam + ((^. idpId) -> idpid) <- registerTestIdP userid -- set callSetDefaultSsoCode (env ^. teSpar) idpid `shouldRespondWith` \resp -> @@ -1595,7 +1649,8 @@ specSparUserMigration = do it "online migration - user in legacy table can log in" $ do env <- ask - (_ownerid, tid, idp, (_, privcreds)) <- registerTestIdPWithMeta + (owner, tid) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + (idp, (_, privcreds)) <- registerTestIdPWithMeta owner spmeta <- getTestSPMetadata tid (issuer, subject) <- do diff --git a/services/spar/test-integration/Test/Spar/AppSpec.hs b/services/spar/test-integration/Test/Spar/AppSpec.hs index 6009dd511e5..beff799585b 100644 --- a/services/spar/test-integration/Test/Spar/AppSpec.hs +++ b/services/spar/test-integration/Test/Spar/AppSpec.hs @@ -52,7 +52,9 @@ spec = describe "accessVerdict" $ do pending context "denied" $ do it "responds with status 200 and a valid html page with constant expected title." $ do - (_, _, idp) <- registerTestIdP + env <- ask + (owner, _teamId) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + idp <- registerTestIdP owner (Nothing, outcome, _, _) <- requestAccessVerdict idp False mkAuthnReqWeb liftIO $ do Servant.errHTTPCode outcome `shouldBe` 200 @@ -63,7 +65,9 @@ spec = describe "accessVerdict" $ do `shouldSatisfy` (isRight . snd) context "granted" $ do it "responds with status 200 and a valid html page with constant expected title." $ do - (_, _, idp) <- registerTestIdP + env <- ask + (owner, _teamId) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + idp <- registerTestIdP owner (Just _, outcome, _, _) <- requestAccessVerdict idp True mkAuthnReqWeb liftIO $ do Servant.errHTTPCode outcome `shouldBe` 200 @@ -80,7 +84,9 @@ spec = describe "accessVerdict" $ do pending context "denied" $ do it "responds with status 303 with appropriate details." $ do - (_, _, idp) <- registerTestIdP + env <- ask + (owner, _teamId) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + idp <- registerTestIdP owner (Nothing, outcome, loc, qry) <- requestAccessVerdict idp False mkAuthnReqMobile liftIO $ do Servant.errHTTPCode outcome `shouldBe` 303 @@ -92,7 +98,9 @@ spec = describe "accessVerdict" $ do List.lookup "label" qry `shouldBe` Just "forbidden" context "granted" $ do it "responds with status 303 with appropriate details." $ do - (_, _, idp) <- registerTestIdP + env <- ask + (owner, _teamId) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + idp <- registerTestIdP owner (Just uid, outcome, loc, qry) <- requestAccessVerdict idp True mkAuthnReqMobile liftIO $ do Servant.errHTTPCode outcome `shouldBe` 303 diff --git a/services/spar/test-integration/Test/Spar/DataSpec.hs b/services/spar/test-integration/Test/Spar/DataSpec.hs index b81715f6f89..886f084f2d6 100644 --- a/services/spar/test-integration/Test/Spar/DataSpec.hs +++ b/services/spar/test-integration/Test/Spar/DataSpec.hs @@ -67,7 +67,8 @@ spec = do describe "TTL" $ do it "works in seconds" $ do env <- ask - (_, _, (^. SAML.idpId) -> idpid) <- registerTestIdP + (owner, _teamid) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + ((^. SAML.idpId) -> idpid) <- registerTestIdP owner (_, req) <- call $ callAuthnReq (env ^. teSpar) idpid let probe :: (MonadIO m, MonadReader TestEnv m) => m Bool probe = runSpar $ AReqIDStore.isAlive (req ^. SAML.rqID) diff --git a/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs b/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs index b087f03e158..94f857a3f64 100644 --- a/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/AuthSpec.hs @@ -90,7 +90,8 @@ testCreateToken :: TestSpar () testCreateToken = do env <- ask -- Create a token - (owner, _, _) <- registerTestIdP + (owner, _tid) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + _ <- registerTestIdP owner CreateScimTokenResponse token _ <- createToken owner @@ -111,7 +112,8 @@ testCreateToken = do testCreateTokenWithVerificationCode :: TestSpar () testCreateTokenWithVerificationCode = do env <- ask - (owner, teamId, _) <- registerTestIdP + (owner, teamId) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + _ <- registerTestIdP owner unlockFeature (env ^. teGalley) teamId setSndFactorPasswordChallengeStatus (env ^. teGalley) teamId Public.FeatureStatusEnabled user <- getUserBrig owner @@ -168,7 +170,8 @@ testTokenLimit :: TestSpar () testTokenLimit = do env <- ask -- Create two tokens - (owner, _, _) <- registerTestIdP + (owner, _teamId) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + _ <- registerTestIdP owner _ <- createToken owner @@ -224,7 +227,8 @@ testNumIdPs = do testCreateTokenAuthorizesOnlyAdmins :: TestSpar () testCreateTokenAuthorizesOnlyAdmins = do env <- ask - (_, teamId, _) <- registerTestIdP + (owner, teamId) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + _ <- registerTestIdP owner let mkUser :: Role -> TestSpar UserId mkUser role = do @@ -261,7 +265,8 @@ testCreateTokenRequiresPassword :: TestSpar () testCreateTokenRequiresPassword = do env <- ask -- Create a new team - (owner, _, _) <- registerTestIdP + (owner, _) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + _ <- registerTestIdP owner -- Creating a token doesn't work without a password createToken_ owner @@ -296,7 +301,9 @@ specListTokens = describe "GET /auth-tokens" $ do testListTokens :: TestSpar () testListTokens = do -- Create two tokens - (owner, _, _) <- registerTestIdP + env <- ask + (owner, _) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + _ <- registerTestIdP owner _ <- createToken owner @@ -321,7 +328,9 @@ testListTokens = do testPlaintextTokensAreConverted :: TestSpar () testPlaintextTokensAreConverted = do - (_, teamId, _) <- registerTestIdP + env <- ask + (owner, teamId) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + _ <- registerTestIdP owner -- create a legacy plaintext token in the DB token <- createLegacyPlaintextToken teamId @@ -402,7 +411,8 @@ testDeletedTokensAreUnusable :: TestSpar () testDeletedTokensAreUnusable = do env <- ask -- Create a token - (owner, _, _) <- registerTestIdP + (owner, _teamId) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + _ <- registerTestIdP owner CreateScimTokenResponse token tokenInfo <- createToken owner @@ -425,7 +435,9 @@ testDeletedTokensAreUnusable = do testDeletedTokensAreUnlistable :: TestSpar () testDeletedTokensAreUnlistable = do -- Create a token - (owner, _, _) <- registerTestIdP + env <- ask + (owner, _teamId) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + _ <- registerTestIdP owner CreateScimTokenResponse _ tokenInfo <- createToken owner diff --git a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs index e3ec56d2f92..d0187ad956d 100644 --- a/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs +++ b/services/spar/test-integration/Test/Spar/Scim/UserSpec.hs @@ -124,7 +124,9 @@ specImportToScimFromSAML = where check :: Bool -> Bool -> Feature.FeatureStatus -> SpecWith TestEnv check sameHandle sameDisplayName valemail = it (show (sameHandle, sameDisplayName, valemail)) $ do - (_ownerid, teamid, idp, (_, privCreds)) <- registerTestIdPWithMeta + env <- ask + (owner, teamid) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + (idp, (_, privCreds)) <- registerTestIdPWithMeta owner setSamlEmailValidation teamid valemail -- saml-auto-provision a new user @@ -376,7 +378,9 @@ specSuspend = do describe "suspend" $ do let checkPreExistingUser :: Bool -> TestSpar () checkPreExistingUser isActive = do - (_, teamid, idp, (_, privCreds)) <- registerTestIdPWithMeta + env <- ask + (owner, teamid) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + (idp, (_, privCreds)) <- registerTestIdPWithMeta owner member <- loginSsoUserFirstTime idp privCreds -- NOTE: once SCIM is enabled, SSO Auto-provisioning is disabled tok <- registerScimToken teamid (Just (idp ^. SAML.idpId)) @@ -775,7 +779,7 @@ testCreateUserWithSamlIdP = do let uid = userId brigUser eid = Scim.User.externalId user sml :: HasCallStack => UserSSOId - sml = fromJust $ userIdentity >=> ssoIdentity $ brigUser + sml = fromJust $ ssoIdentity =<< userIdentity brigUser in testCsvData tid owner uid eid (Just sml) True -- members table contains an entry @@ -1021,7 +1025,9 @@ testRichInfo = do -- @spar.user@; create it via scim. This should work despite the dangling database entry. testScimCreateVsUserRef :: TestSpar () testScimCreateVsUserRef = do - (_ownerid, teamid, idp, (_, privCreds)) <- registerTestIdPWithMeta + env <- ask + (owner, teamid) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + (idp, (_, privCreds)) <- registerTestIdPWithMeta owner (usr, uname) :: (Scim.User.User SparTag, SAML.UnqualifiedNameID) <- randomScimUserWithSubject let uref = SAML.UserRef tenant subj @@ -1189,7 +1195,9 @@ testFindProvisionedUser = do -- The user is migrated by using the email as the externalId testFindSamlAutoProvisionedUserMigratedWithEmailInTeamWithSSO :: TestSpar () testFindSamlAutoProvisionedUserMigratedWithEmailInTeamWithSSO = do - (_owner, teamid, idp, (_, privCreds)) <- registerTestIdPWithMeta + env <- ask + (owner, teamid) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + (idp, (_, privCreds)) <- registerTestIdPWithMeta owner -- auto-provision user via saml memberWithSSO <- do @@ -1372,7 +1380,9 @@ shouldBeManagedBy uid flag = do -- the issue here. testGetNonScimSAMLUser :: TestSpar () testGetNonScimSAMLUser = do - (_, tid, idp, (_, privcreds)) <- registerTestIdPWithMeta + env <- ask + (owner, tid) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + (idp, (_, privcreds)) <- registerTestIdPWithMeta owner -- NOTE: once SCIM is enabled SSO Auto-provisioning is disabled, so we register the scim token later. uidSso <- loginSsoUserFirstTime idp privcreds @@ -1414,7 +1424,9 @@ testGetNonScimInviteUserNoIdP = do testGetUserWithNoHandle :: TestSpar () testGetUserWithNoHandle = do - (_, tid, idp, (_, privcreds)) <- registerTestIdPWithMeta + env <- ask + (owner, tid) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + (idp, (_, privcreds)) <- registerTestIdPWithMeta owner -- NOTE: once SCIM is enabled SSO Auto-provisioning is disabled, so we register the scim token later. uid <- loginSsoUserFirstTime idp privcreds tok <- registerScimToken tid (Just (idp ^. SAML.idpId)) diff --git a/services/spar/test-integration/Util/Core.hs b/services/spar/test-integration/Util/Core.hs index 16c48a576f1..e3c99b8224e 100644 --- a/services/spar/test-integration/Util/Core.hs +++ b/services/spar/test-integration/Util/Core.hs @@ -164,6 +164,7 @@ import Data.Range import Data.Text (pack) import qualified Data.Text.Ascii as Ascii import Data.Text.Encoding (encodeUtf8) +import qualified Data.Text.Lazy.Encoding as LT import Data.UUID as UUID hiding (fromByteString, null) import Data.UUID.V4 as UUID (nextRandom) import qualified Data.Yaml as Yaml @@ -805,36 +806,33 @@ getTestSPMetadata tid = do -- | See 'registerTestIdPWithMeta' registerTestIdP :: (HasCallStack, MonadRandom m, MonadIO m, MonadReader TestEnv m) => - m (UserId, TeamId, IdP) -registerTestIdP = do - (uid, tid, idp, _) <- registerTestIdPWithMeta - pure (uid, tid, idp) + UserId -> + m IdP +registerTestIdP owner = fst <$> registerTestIdPWithMeta owner --- | Create a fresh 'IdPMetadata' suitable for testing. Call 'createUserWithTeam' and create the --- idp in the resulting team. The user returned is the owner of the team. +-- | Create a fresh 'IdPMetadata' suitable for testing. registerTestIdPWithMeta :: (HasCallStack, MonadRandom m, MonadIO m, MonadReader TestEnv m) => - m (UserId, TeamId, IdP, (IdPMetadataInfo, SAML.SignPrivCreds)) -registerTestIdPWithMeta = do + UserId -> + m (IdP, (IdPMetadataInfo, SAML.SignPrivCreds)) +registerTestIdPWithMeta owner = do SampleIdP idpmeta privkey _ _ <- makeSampleIdPMetadata env <- ask - (uid, tid, idp) <- registerTestIdPFrom idpmeta (env ^. teMgr) (env ^. teBrig) (env ^. teGalley) (env ^. teSpar) - pure (uid, tid, idp, (IdPMetadataValue (cs $ SAML.encode idpmeta) idpmeta, privkey)) + idp <- registerTestIdPFrom idpmeta (env ^. teMgr) owner (env ^. teSpar) + pure (idp, (IdPMetadataValue (cs $ SAML.encode idpmeta) idpmeta, privkey)) -- | Helper for 'registerTestIdP'. registerTestIdPFrom :: (HasCallStack, MonadIO m, MonadReader TestEnv m) => IdPMetadata -> Manager -> - BrigReq -> - GalleyReq -> + UserId -> SparReq -> - m (UserId, TeamId, IdP) -registerTestIdPFrom metadata mgr brig galley spar = do + m IdP +registerTestIdPFrom metadata mgr owner spar = do apiVer <- view teWireIdPAPIVersion liftIO . runHttpT mgr $ do - (uid, tid) <- createUserWithTeam brig galley - (uid,tid,) <$> callIdpCreate apiVer spar (Just uid) metadata + callIdpCreate apiVer spar (Just owner) metadata getCookie :: KnownSymbol name => proxy name -> ResponseLBS -> Either String (SAML.SimpleSetCookie name) getCookie proxy rsp = do @@ -1090,7 +1088,7 @@ callIdpCreate' apiversion sparreq_ muid metadata = do WireIdPAPIV1 -> Bilge.query [("api_version", Just "v1") | explicitQueryParam] WireIdPAPIV2 -> Bilge.query [("api_version", Just "v2")] ) - . body (RequestBodyLBS . cs $ SAML.encode metadata) + . body (RequestBodyLBS . LT.encodeUtf8 $ SAML.encode metadata) . header "Content-Type" "application/xml" callIdpCreateRaw :: (MonadIO m, MonadHttp m) => SparReq -> Maybe UserId -> ByteString -> LByteString -> m IdP diff --git a/services/spar/test-integration/Util/Scim.hs b/services/spar/test-integration/Util/Scim.hs index 9b2ab331a12..aa9b5ad72a6 100644 --- a/services/spar/test-integration/Util/Scim.hs +++ b/services/spar/test-integration/Util/Scim.hs @@ -71,23 +71,33 @@ import Wire.API.User.Scim -- the IdP is registered with the team; the SCIM token can be used to manipulate the team. registerIdPAndScimToken :: HasCallStack => TestSpar (ScimToken, (UserId, TeamId, IdP)) registerIdPAndScimToken = do - team@(_owner, teamid, idp) <- registerTestIdP - (,team) <$> registerScimToken teamid (Just (idp ^. idpId)) + env <- ask + (owner, teamid) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + idp <- registerTestIdP owner + token <- registerScimToken teamid (Just (idp ^. idpId)) + let team = (owner, teamid, idp) + pure (token, team) -- | Call 'registerTestIdPWithMeta', then 'registerScimToken'. The user returned is the owner of the team; -- the IdP is registered with the team; the SCIM token can be used to manipulate the team. registerIdPAndScimTokenWithMeta :: HasCallStack => TestSpar (ScimToken, (UserId, TeamId, IdP, (IdPMetadataInfo, SAML.SignPrivCreds))) registerIdPAndScimTokenWithMeta = do - team@(_owner, teamid, idp, _) <- registerTestIdPWithMeta + env <- ask + (owner, teamid) <- call $ createUserWithTeam (env ^. teBrig) (env ^. teGalley) + (idp, meta) <- registerTestIdPWithMeta owner + let team = (owner, teamid, idp, meta) (,team) <$> registerScimToken teamid (Just (idp ^. idpId)) -- | Create a fresh SCIM token and register it for the team. +-- +-- FUTUREWORK(mangoiv): this is an integration test, it should use the +-- API, and not directly manipulate the database registerScimToken :: HasCallStack => TeamId -> Maybe IdPId -> TestSpar ScimToken registerScimToken teamid midpid = do tok <- ScimToken <$> do code <- liftIO UUID.nextRandom - pure $ "scim-test-token/" <> "team=" <> idToText teamid <> "/code=" <> UUID.toText code + pure $ "scim-test-token/team=" <> idToText teamid <> "/code=" <> UUID.toText code scimTokenId <- randomId now <- liftIO getCurrentTime runSpar $ From 0ba23ad3a29225b3212b8893c4049a5dfed2e369 Mon Sep 17 00:00:00 2001 From: Stefan Berthold Date: Thu, 11 Apr 2024 11:01:57 +0200 Subject: [PATCH 086/117] update certs for HTTP2 tests (#3991) --- .../http2-manager/test/resources/gen-certs.sh | 2 +- .../test/resources/localhost-key.pem | 50 +++++++++---------- .../resources/localhost.example.com-key.pem | 50 +++++++++---------- .../test/resources/localhost.example.com.pem | 32 ++++++------ .../test/resources/localhost.pem | 32 ++++++------ 5 files changed, 83 insertions(+), 83 deletions(-) diff --git a/libs/http2-manager/test/resources/gen-certs.sh b/libs/http2-manager/test/resources/gen-certs.sh index d24151e67bf..de1e377ee7a 100755 --- a/libs/http2-manager/test/resources/gen-certs.sh +++ b/libs/http2-manager/test/resources/gen-certs.sh @@ -48,7 +48,7 @@ generate() { fi } -generate cert and key based on CA given comma-separated hostnames as SANs +# generate cert and key based on CA given comma-separated hostnames as SANs generate "localhost" "$OUTPUTNAME_LOCALHOST_CERT" generate "localhost.example.com" "$OUTPUTNAME_EXAMPLE_COM_CERT" diff --git a/libs/http2-manager/test/resources/localhost-key.pem b/libs/http2-manager/test/resources/localhost-key.pem index 49d2f111296..d11ceae9ab3 100644 --- a/libs/http2-manager/test/resources/localhost-key.pem +++ b/libs/http2-manager/test/resources/localhost-key.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -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 -----END RSA PRIVATE KEY----- diff --git a/libs/http2-manager/test/resources/localhost.example.com-key.pem b/libs/http2-manager/test/resources/localhost.example.com-key.pem index 0ed64c38cfd..e7ecac84c8e 100644 --- a/libs/http2-manager/test/resources/localhost.example.com-key.pem +++ b/libs/http2-manager/test/resources/localhost.example.com-key.pem @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -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 -----END RSA PRIVATE KEY----- diff --git a/libs/http2-manager/test/resources/localhost.example.com.pem b/libs/http2-manager/test/resources/localhost.example.com.pem index 9e53052017f..68129459c33 100644 --- a/libs/http2-manager/test/resources/localhost.example.com.pem +++ b/libs/http2-manager/test/resources/localhost.example.com.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -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 -----END CERTIFICATE----- diff --git a/libs/http2-manager/test/resources/localhost.pem b/libs/http2-manager/test/resources/localhost.pem index 6e976ce4094..a4057c8e7f7 100644 --- a/libs/http2-manager/test/resources/localhost.pem +++ b/libs/http2-manager/test/resources/localhost.pem @@ -1,20 +1,20 @@ -----BEGIN CERTIFICATE----- -MIIDQTCCAimgAwIBAgIUGQi379sMAQZkgFfBZEqkvMHbVfwwDQYJKoZIhvcNAQEL -BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjMwNDA2MDc0MzAwWhcN -MjQwNDA1MDc0MzAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA -yEWHRBYdUDzWzqAToCFI1Pdp4sntIEvTxjurRdFzNv11rgchA5L0U/gsOp6kaybd -qN5TGe1oaCDOM44hMyeZB95WCq8bzIbALPI+0EMc0sHzNI8Cs8dMgdrviXpO06X4 -WNamZUFgTs0vNmy1SzSZMhMveAEe5k1B1JIdueheQAnjayIplZno/hD0ndl7Pvb+ -14Jm2INCHI/DuhV0qkebRLO/ierNb2b4xQcnTBu3TyYeDnJ6sVvJz3uEx35byaG+ -SVyZq7Hrjaz6jHxEpmHSZpuGjFTSgXkoq1WzISR/ohuRq6Y3uGJYmmXFbJQoUekI -xOPWPFVuKPJ3UY/rFZUA7QIDAQABo4GZMIGWMA4GA1UdDwEB/wQEAwIFoDAdBgNV +MIIDQTCCAimgAwIBAgIUH7ZhadI1BvsW7XjGhI9Eso8ol3gwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAxMOY2EuZXhhbXBsZS5jb20wHhcNMjQwNDExMDgwMzAwWhcN +MjUwNDExMDgwMzAwWjAAMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA +0r4IgyNodKaYcz363YNCjsUNgzIi3upmANohDkW+D4k/q8IRbQjR9/fdO1jgWt6N +nbkMa79OdwEj7RUwCMI2fjvOgA78CEn1/3JgZQ7YV/RtiavMawET2SHDldwzhY77 +J/EuvM2arog4KJCOqEsQl9a0+T3bbz0dAzQWNELp7z/P0SwtIw087bjY2VfcKlCZ +JqwpacQES2fGEtImHLJlpkQaWdyvURHf23KIraRm60A9aDOuoKl+7C5Wp7NI3Alk +DuGrAXR841Mc7qqvhEzNIfrabHTjrwVrwgijcPkkgPNzlmznI4sr5EBCx8imMdFc +mOFRStXFEJDa47459Zz1lQIDAQABo4GZMIGWMA4GA1UdDwEB/wQEAwIFoDAdBgNV HSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4E -FgQUMtICpNlqVmbe9tZlvdfnUBYmu5kwHwYDVR0jBBgwFoAUA3gYvghrcPIXmtrg +FgQUj3Fih2asl1UHZ26Ls/iZxaBuJq8wHwYDVR0jBBgwFoAUA3gYvghrcPIXmtrg 72RjCnGSl4UwFwYDVR0RAQH/BA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBCwUA -A4IBAQBOXrdbiQI7jprgB80hYyA82axsw+5COWQvECKZTmP7zTfPsqlmsNWYT3tP -9nhe9EEl6JjOkikClO6kpRxz3EWZ+neraufetzf+VJ/6cCv9tVRaozQPJE98myWd -hdpZ9+0ZCDl2oknDWuBwze+4phd+OV0IW0rm17oJX2YBuqs4HEnbdi+2N1/8twSt -MAhoG460aYWc2IzWEIS7OmBEjJAcjTag5s+tYXDa5GF5hnDiu+d6iIM0ct2oqSqh -e3IJgIcpb6GKSyNILvYPLuzarH0xnuIMX6/3NsMNukC9P9pz3RyE/FT1q+umjzXj -R2fcA2K2hLPfE1l3GH5LLMfirqNB +A4IBAQAOeD/2dPIbs4qQXVrlemxspeU4VRn0Pybxihxhwyy15d4v3l0bJkBXhSmh +ve8AzEbsVDvF34B6i82uDsas5DxRs5BUIW3svbJzGUMgVtfRal2gpUQVdlKS5FDF +rbQGqMs0NogdUkQ24JKElCoysgMAcsXEE1Kpgdr7ZxeZkhJHu20imXuBa7sC4s+I +Z24W2cU3HfF+5YNZd/kNUjY9StbiibHsSMcRcmW9Rq8ij3RCOoSFJ7HzCj4PVZKa +3+U82PjotZK3h6c2jPjaPbkh5Ua/+gBmebdWBqSyGqSV94CYbESqTMckXytx9Bx2 +BI9HpBioQiwTxadmALmv1guCQzV8 -----END CERTIFICATE----- From e4fa0f1c04c73c0a7e7eadc1b07cda2487b7f2cb Mon Sep 17 00:00:00 2001 From: Sven Tennie Date: Fri, 12 Apr 2024 12:24:42 +0200 Subject: [PATCH 087/117] elasticsearch-ephemeral: Provide password as value (#3994) This way we can use different passwords on our test systems. Ensuring that the password is really configurable (and not accidentally hardcoded somewhere.) --- .../5-internal/elasticsearch-ephemeral_configurable_password | 3 +++ charts/elasticsearch-ephemeral/templates/es.yaml | 3 +-- charts/elasticsearch-ephemeral/values.yaml | 3 +++ 3 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 changelog.d/5-internal/elasticsearch-ephemeral_configurable_password diff --git a/changelog.d/5-internal/elasticsearch-ephemeral_configurable_password b/changelog.d/5-internal/elasticsearch-ephemeral_configurable_password new file mode 100644 index 00000000000..fe65603b55d --- /dev/null +++ b/changelog.d/5-internal/elasticsearch-ephemeral_configurable_password @@ -0,0 +1,3 @@ +Provide password as value in `elasticsearch-ephemeral`. This way we can use +different passwords on our test systems. Ensuring that the password is really +configurable (and not accidentally hardcoded somewhere.) diff --git a/charts/elasticsearch-ephemeral/templates/es.yaml b/charts/elasticsearch-ephemeral/templates/es.yaml index 4a82cbd28bf..873b50b1693 100644 --- a/charts/elasticsearch-ephemeral/templates/es.yaml +++ b/charts/elasticsearch-ephemeral/templates/es.yaml @@ -34,9 +34,8 @@ spec: value: ".watches,.triggered_watches,.watcher-history-*,pod-*,node-*" - name: "xpack.security.enabled" value: "true" - # setting the password here is ok, as this chart is only used for integration tests on CI - name: "ELASTIC_PASSWORD" - value: "changeme" + value: {{ .Values.secrets.password }} ports: - containerPort: 9200 name: http diff --git a/charts/elasticsearch-ephemeral/values.yaml b/charts/elasticsearch-ephemeral/values.yaml index 9d0c5cae8ab..a09d05caeb4 100644 --- a/charts/elasticsearch-ephemeral/values.yaml +++ b/charts/elasticsearch-ephemeral/values.yaml @@ -14,3 +14,6 @@ resources: requests: cpu: "250m" memory: "500Mi" + +secrets: + password: "changeme" From b33b5d197167c847960a9dbd4e8c3166c164ef7e Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Fri, 12 Apr 2024 22:49:42 +0200 Subject: [PATCH 088/117] Bump hsaml2, saml2-web-sso dependencies. (#3995) --- changelog.d/5-internal/wpb6170-bump-hsaml2-dep | 1 + nix/haskell-pins.nix | 10 +++++----- services/galley/galley.cabal | 4 ++-- services/spar/spar.cabal | 8 ++++---- 4 files changed, 12 insertions(+), 11 deletions(-) create mode 100644 changelog.d/5-internal/wpb6170-bump-hsaml2-dep diff --git a/changelog.d/5-internal/wpb6170-bump-hsaml2-dep b/changelog.d/5-internal/wpb6170-bump-hsaml2-dep new file mode 100644 index 00000000000..b03b34a30ad --- /dev/null +++ b/changelog.d/5-internal/wpb6170-bump-hsaml2-dep @@ -0,0 +1 @@ +Bump hsaml2, saml2-web-sso dependencies. \ No newline at end of file diff --git a/nix/haskell-pins.nix b/nix/haskell-pins.nix index 831fe39eff0..1b3c0b97ae5 100644 --- a/nix/haskell-pins.nix +++ b/nix/haskell-pins.nix @@ -82,8 +82,8 @@ let saml2-web-sso = { src = fetchgit { url = "https://github.com/wireapp/saml2-web-sso"; - rev = "d50bddadf9bd9a96dd6036dad0e2dda27567ec1a"; - sha256 = "sha256-IKovI1h2Wkm3Y7Sz6XsxLOv654SgUasaWsDX6gi9hZw="; + rev = "0cf23a87b140ba5b960a848ecad3976e6fdaac88"; + sha256 = "sha256-Gm58Yjt5ZGh74cfEjcZSx6jvwkpFC324xTPLhLS29r0="; }; }; @@ -110,9 +110,9 @@ let hsaml2 = { src = fetchgit { - url = "https://github.com/wireapp/hsaml2"; - rev = "c11ad42e6bd6ef6a1eb298413ab131234b171224"; - sha256 = "sha256-OYqIxe+9M8YKUpqJPgOeqOTmez7JdOd351J5NgaHrMY="; + url = "https://github.com/dylex/hsaml2"; + rev = "95d9dc7502c2533f7927de00cbc2bd20ad989ace"; + sha256 = "sha256-z3s/ZkkCd2ThVBsu72pS/+XygHImuffz/HVy3hkQ6eo="; }; }; diff --git a/services/galley/galley.cabal b/services/galley/galley.cabal index b15592ed296..91556478195 100644 --- a/services/galley/galley.cabal +++ b/services/galley/galley.cabal @@ -336,7 +336,7 @@ library , resourcet >=1.1 , retry >=0.5 , safe-exceptions >=0.1 - , saml2-web-sso >=0.19 + , saml2-web-sso >=0.20 , schema-profunctor , servant , servant-client @@ -518,7 +518,7 @@ executable galley-integration , quickcheck-instances , random , retry - , saml2-web-sso >=0.19 + , saml2-web-sso >=0.20 , schema-profunctor , servant-client , servant-client-core diff --git a/services/spar/spar.cabal b/services/spar/spar.cabal index 27fcc4015bd..43e9582ceda 100644 --- a/services/spar/spar.cabal +++ b/services/spar/spar.cabal @@ -179,7 +179,7 @@ library , polysemy-wire-zoo , QuickCheck , raw-strings-qq - , saml2-web-sso >=0.19 + , saml2-web-sso >=0.20 , servant-multipart , servant-server , text @@ -372,7 +372,7 @@ executable spar-integration , random , raw-strings-qq , retry - , saml2-web-sso >=0.19 + , saml2-web-sso >=0.20 , servant , servant-server , silently @@ -465,7 +465,7 @@ executable spar-migrate-data , imports , lens , optparse-applicative - , saml2-web-sso >=0.19 + , saml2-web-sso >=0.20 , spar , text , time @@ -622,7 +622,7 @@ test-suite spec , polysemy-plugin , polysemy-wire-zoo , QuickCheck - , saml2-web-sso >=0.19 + , saml2-web-sso >=0.20 , servant , servant-openapi3 , spar From ddfa32fc944f242df5c408110c972dac69ed9f39 Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Fri, 12 Apr 2024 23:58:08 +0200 Subject: [PATCH 089/117] move IP config closer to port config, as requested by julia in PR review --- charts/coturn/values.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/charts/coturn/values.yaml b/charts/coturn/values.yaml index cb7baa39cca..10279a6aa3e 100644 --- a/charts/coturn/values.yaml +++ b/charts/coturn/values.yaml @@ -26,6 +26,10 @@ coturnTurnListenPort: 3478 coturnMetricsListenPort: 9641 coturnTurnTlsListenPort: 5349 +# If you need to specify which IP Coturn should bind to. +# This will typically be the IP of the kubenode. +# coturnTurnListenIP: "182.168.22.133" + tls: enabled: false # compliant with BSI TR-02102-2 @@ -109,6 +113,3 @@ readinessProbe: timeoutSeconds: 5 failureThreshold: 5 -# If you need to specify which IP Coturn should bind to. -# This will typically be the IP of the kubenode. -# coturnTurnListenIP: "182.168.22.133" From 226ec94ae86329f1d4778d8cb2732ecd20f025e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Mon, 15 Apr 2024 11:28:14 +0200 Subject: [PATCH 090/117] [WPB-7415] Fix the list of other members in an MLS 1-to-1 conversation (the `develop` counterpart) (#3998) * Test: confirm the bug from the report * Return the list of other members in MLS 1-to-1 * Add a changelog --- changelog.d/3-bug-fixes/WPB-7415 | 1 + integration/test/Test/MLS/One2One.hs | 33 +++++++++++++++++++ services/galley/src/Galley/API/MLS/One2One.hs | 2 +- 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 changelog.d/3-bug-fixes/WPB-7415 diff --git a/changelog.d/3-bug-fixes/WPB-7415 b/changelog.d/3-bug-fixes/WPB-7415 new file mode 100644 index 00000000000..ca4c5a1dd24 --- /dev/null +++ b/changelog.d/3-bug-fixes/WPB-7415 @@ -0,0 +1 @@ +Return an actual list of other users in a remote MLS 1-to-1 conversation diff --git a/integration/test/Test/MLS/One2One.hs b/integration/test/Test/MLS/One2One.hs index aac9725d9c9..8c6ce11355d 100644 --- a/integration/test/Test/MLS/One2One.hs +++ b/integration/test/Test/MLS/One2One.hs @@ -49,6 +49,39 @@ testGetMLSOne2One otherDomain = do conv2 %. "qualified_id" `shouldMatch` convId conv2 %. "epoch" `shouldMatch` (conv %. "epoch") +testMLSOne2OneOtherMember :: HasCallStack => One2OneScenario -> App () +testMLSOne2OneOtherMember scenario = do + alice <- randomUser OwnDomain def + let otherDomain = one2OneScenarioUserDomain scenario + convDomain = one2OneScenarioConvDomain scenario + bob <- createMLSOne2OnePartner otherDomain alice convDomain + conv <- getMLSOne2OneConversation alice bob >>= getJSON 200 + do + convId <- conv %. "qualified_id" + bobConv <- getMLSOne2OneConversation bob alice >>= getJSON 200 + convId `shouldMatch` (bobConv %. "qualified_id") + + [alice1, bob1] <- traverse (createMLSClient def) [alice, bob] + traverse_ uploadNewKeyPackage [bob1] + resetGroup alice1 conv + withWebSocket bob1 $ \ws -> do + commit <- createAddCommit alice1 [bob] + void $ sendAndConsumeCommitBundle commit + let isMessage n = nPayload n %. "type" `isEqual` "conversation.mls-welcome" + n <- awaitMatch isMessage ws + nPayload n %. "data" `shouldMatch` B8.unpack (Base64.encode (fold commit.welcome)) + + -- Make sure the membership info is OK both for the MLS 1-to-1 endpoint and + -- for the general conversation fetching endpoint. + let assertOthers other resp = do + bdy <- getJSON 200 resp + othersObj <- bdy %. "members.others" & asList + otherActual <- assertOne othersObj + otherActual %. "qualified_id" `shouldMatch` (other %. "qualified_id") + forM_ [(alice, bob), (bob, alice)] $ \(self, other) -> do + getMLSOne2OneConversation self other `bindResponse` assertOthers other + getConversation self conv `bindResponse` assertOthers other + testGetMLSOne2OneUnconnected :: HasCallStack => Domain -> App () testGetMLSOne2OneUnconnected otherDomain = do [alice, bob] <- for [OwnDomain, otherDomain] $ \domain -> randomUser domain def diff --git a/services/galley/src/Galley/API/MLS/One2One.hs b/services/galley/src/Galley/API/MLS/One2One.hs index c194d72302e..f0632f737c5 100644 --- a/services/galley/src/Galley/API/MLS/One2One.hs +++ b/services/galley/src/Galley/API/MLS/One2One.hs @@ -109,7 +109,7 @@ remoteMLSOne2OneConversation lself rother rc = let members = ConvMembers { cmSelf = defMember (tUntagged lself), - cmOthers = [] + cmOthers = rc.members.others } in Conversation { cnvQualifiedId = tUntagged (qualifyAs rother rc.id), From fdb1c1cf10740f4579366a19b4a0c7c82fec7a4b Mon Sep 17 00:00:00 2001 From: Amit Sagtani Date: Mon, 15 Apr 2024 16:31:15 +0530 Subject: [PATCH 091/117] add ldap-scim-bridge chart in wire release (#3999) * add ldap-scim-bridge chart in wire release * add changelog entry * Update changelog.d/5-internal/add-ldap-chart-in-release Co-authored-by: Sven Tennie --------- Co-authored-by: Sven Tennie --- Makefile | 2 +- changelog.d/5-internal/add-ldap-chart-in-release | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 changelog.d/5-internal/add-ldap-chart-in-release diff --git a/Makefile b/Makefile index f774012f3e6..779548b776a 100644 --- a/Makefile +++ b/Makefile @@ -18,7 +18,7 @@ fake-aws fake-aws-s3 fake-aws-sqs aws-ingress fluent-bit kibana backoffice \ calling-test demo-smtp elasticsearch-curator elasticsearch-external \ elasticsearch-ephemeral minio-external cassandra-external \ nginx-ingress-controller ingress-nginx-controller nginx-ingress-services reaper sftd restund coturn \ -inbucket k8ssandra-test-cluster postgresql +inbucket k8ssandra-test-cluster postgresql ldap-scim-bridge KIND_CLUSTER_NAME := wire-server HELM_PARALLELISM ?= 1 # 1 for sequential tests; 6 for all-parallel tests diff --git a/changelog.d/5-internal/add-ldap-chart-in-release b/changelog.d/5-internal/add-ldap-chart-in-release new file mode 100644 index 00000000000..c3014636b54 --- /dev/null +++ b/changelog.d/5-internal/add-ldap-chart-in-release @@ -0,0 +1 @@ +Add ldap-scim-bridge chart to the wire-server release \ No newline at end of file From 072da9916d9009e9b71e506afe463d9bbc70551e Mon Sep 17 00:00:00 2001 From: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> Date: Mon, 15 Apr 2024 17:21:03 +0200 Subject: [PATCH 092/117] [feat] use new script for sbom generation (#3942) * [feat] use new script for sbom generation --------- Co-authored-by: Sven Tennie --- Makefile | 7 +- changelog.d/5-internal/SEC-596 | 1 + hack/bin/Sbom.hs | 376 +++++++++++++++++++++++++++++++ hack/bin/bombon.hs | 35 +-- nix/all-toplevel-derivations.nix | 62 +++++ nix/default.nix | 4 +- nix/overlay.nix | 2 + nix/pkg-info.nix | 60 +++++ nix/pkgs/sbomqs/default.nix | 21 ++ nix/wire-server.nix | 34 ++- 10 files changed, 578 insertions(+), 24 deletions(-) create mode 100644 changelog.d/5-internal/SEC-596 create mode 100644 hack/bin/Sbom.hs create mode 100644 nix/all-toplevel-derivations.nix create mode 100644 nix/pkg-info.nix create mode 100644 nix/pkgs/sbomqs/default.nix diff --git a/Makefile b/Makefile index 779548b776a..29a5a3a8488 100644 --- a/Makefile +++ b/Makefile @@ -546,11 +546,12 @@ helm-template-%: clean-charts charts-integration ./hack/bin/helm-template.sh $(*) # Ask the security team for the `DEPENDENCY_TRACK_API_KEY` (if you need it) +# changing the directory is necessary because of some quirkiness of how +# runhaskell / ghci behaves (it doesn't find modules that aren't in the same +# directory as the script that is being executed) .PHONY: upload-bombon upload-bombon: - nix build -f nix wireServer.allLocalPackagesBom -o "bill-of-materials.$(HELM_SEMVER).json" - ./hack/bin/bombon.hs -- \ - --bom-filepath "./bill-of-materials.$(HELM_SEMVER).json" \ + cd ./hack/bin && ./bombon.hs -- \ --project-version $(HELM_SEMVER) \ --api-key $(DEPENDENCY_TRACK_API_KEY) \ --auto-create diff --git a/changelog.d/5-internal/SEC-596 b/changelog.d/5-internal/SEC-596 new file mode 100644 index 00000000000..e7af3b64def --- /dev/null +++ b/changelog.d/5-internal/SEC-596 @@ -0,0 +1 @@ +Create a new script (`Sbom.hs`) to generate the wire-server sbom (bill of material) file. diff --git a/hack/bin/Sbom.hs b/hack/bin/Sbom.hs new file mode 100644 index 00000000000..7226a19885d --- /dev/null +++ b/hack/bin/Sbom.hs @@ -0,0 +1,376 @@ +{-# LANGUAGE BlockArguments #-} +{-# LANGUAGE DeriveAnyClass #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE ImportQualifiedPost #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedLists #-} +{-# LANGUAGE OverloadedRecordDot #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE StrictData #-} +{-# LANGUAGE UndecidableInstances #-} +{-# LANGUAGE ViewPatterns #-} +{-# OPTIONS_GHC -Wall #-} +{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-} + +{- +- the only place that has the data we need about the package is the evaluated nix code, i.e. before + writing the derivation; this is where we have `meta` and friends to get the data we need +- say we now want to build a dependency tree; the issue is to find all dependencies of the derivation. + this is hard because + - there are normal input attrs that the builder will have a look at but also + - string contexts like + ```nix + x = /* bash */ '' + cp ${pkgs.bla}/bin $out + ''; + ``` + would ignore dependencies on `pkgs.bla` +- we can build the dependency graph independently (without knowing about the meta) but we somehow need + to obtain the meta itself +- people don't always have a complete package set but more commonly are hand assembling things; we need + to give the possibility to build meta "databases" from package sets +- we need to trace which dependencies are missing when querying the meta database against them +- collecting the meta also poses some issue + - nixpkgs is not a tree, but a more general graph + - it also not a DAG but it has loops + - this means more specifically that we cannot without care recurse into it + - even if we only recurse very shallowly, we soon start running out of memory, this means we probably need + to do some on the fly filtering by "actual" dependencies + - this is similarly an issue, because it means that for every package we have to evaluate the entirety + of the package set instead of being able to keep and persist the database + - a more clean solution would probably be to at each time we recurse, a derivation that does the evaluation + and outputs a JSON that can later be read + +how this relates to bombon: +- bombon uses a more coarse grained approach +- this builds a metadata "database" i.e. is two pass +- see the corresponding nix code in ./nix +-} + +module Sbom where + +import Control.Arrow ((&&&)) +import Data.Aeson +import Data.Aeson.Key qualified as KM +import Data.Aeson.KeyMap qualified as KM +import Data.Aeson.Types (typeMismatch) +import Data.Bifunctor (first) +import Data.Bitraversable (bitraverse) +import Data.ByteString (ByteString) +import Data.ByteString.Char8 qualified as C8 +import Data.ByteString.Lazy (LazyByteString) +import Data.ByteString.Lazy qualified as BSL +import Data.ByteString.Lazy.Char8 qualified as C8L +import Data.Containers.ListUtils (nubOrd, nubOrdOn) +import Data.Functor.Identity +import Data.Map (Map) +import Data.Map qualified as M +import Data.Maybe +import Data.Proxy +import Data.Text (Text) +import Data.Text qualified as T +import Data.Text.IO qualified as T +import Data.Time.Clock.POSIX +import Data.Traversable (for) +import Data.Tree +import Data.UUID qualified as UUID +import Data.UUID.V4 qualified as V4 +import Debug.Trace +import GHC.Generics hiding (Meta) +import GHC.IsList (IsList (fromList, toList)) +import Numeric.Natural (Natural) +import Options.Applicative (customExecParser, fullDesc, help, long, prefs, progDesc, showHelpOnEmpty, strOption, value) +import Options.Applicative qualified as Opt +import System.Directory +import System.Process + +data License = MkLicense + { id :: Maybe Text, + name :: Maybe Text + } + deriving stock (Eq, Ord, Show, Generic) + deriving anyclass (FromJSON, ToJSON) + +sadSbomMeta :: Text -> Text -> [Text] -> SBomMeta Identity +sadSbomMeta drvPath outPath directDeps = + MkSBomMeta + { drvPath = drvPath, + outPath = Identity outPath, + directDeps = Identity directDeps, + description = Nothing, + homepage = Nothing, + licenseSpdxId = [], + name = Nothing, + typ = Nothing, + urls = [], + version = Nothing + } + +data SBomMeta f = MkSBomMeta + { drvPath :: Text, + description :: Maybe Text, + homepage :: Maybe Text, + licenseSpdxId :: [Maybe License], + name :: Maybe Text, + typ :: Maybe Text, + urls :: [Maybe Text], + version :: Maybe Text, + outPath :: f Text, + directDeps :: f [Text] + } + +deriving stock instance (Eq (f [Text]), Eq (f Text)) => Eq (SBomMeta f) + +deriving stock instance (Ord (f [Text]), Ord (f Text)) => Ord (SBomMeta f) + +deriving stock instance (Show (f [Text]), Show (f Text)) => Show (SBomMeta f) + +type Meta = SBomMeta Proxy + +instance FromJSON Meta where + parseJSON (Object val) = + MkSBomMeta + <$> do val .: "drvPath" + <*> do val .: "description" + <*> do val .: "homepage" + <*> do val .: "licenseSpdxId" + <*> do val .: "name" + <*> do val .: "type" + <*> do val .: "urls" + <*> do val .: "version" + <*> pure Proxy + <*> pure Proxy + parseJSON invalid = typeMismatch "Object" invalid + +type SBom = Map Text (SBomMeta Identity) + +type MetaDB = Map Text (SBomMeta Proxy) + +type ClosureInfo = Tree ByteString + +type PathInfo = [(Text, (Text, [Text]))] + +data Visit a = Seen a | Unseen a + deriving stock (Eq, Ord, Show) + +data SerializeSBom = MkSerializeSBom + { -- | the version of the SBom; this is version of the old SBom + 1 + sbom'version :: Natural, + -- | name of the component the SBom is generated for + sbom'component :: Text, + -- | the creator of the component the SBom is generated for + sbom'manufacture :: Text, + -- | the supplier (manufacturer or repackager or distributor) + sbom'supplier :: Maybe Text, + -- | (spdxids of) licenses of the product + sbom'licenses :: [Text] + } + +defaultSerializeSBom :: SerializeSBom +defaultSerializeSBom = + MkSerializeSBom + { sbom'version = 1, + sbom'component = "wire-server", + sbom'manufacture = "wire", + sbom'supplier = Nothing, + sbom'licenses = ["AGPL-3.0-or-later"] + } + +-- FUTUREWORK(mangoiv): we can also have +-- +-- - qualifiers: extra qualifying data for a package such as an OS, architecture, a distro, etc. Optional and type-specific. +-- - subpath: extra subpath within a package, relative to the package root. Optional. +-- - use heuristics based approach to finding original repositories for packages, e.g. pkg:hackage.... +mkPurl :: SBomMeta Identity -> Text +mkPurl meta = + mconcat + [ "pkg:", + repo, + "/", + fromMaybe (runIdentity meta.outPath) meta.name, + maybe "" ("@" <>) meta.version + ] + where + repo + | any (maybe False (T.isInfixOf "hackage.haskell.org")) meta.urls = "hackage" + | otherwise = "nixpkgs" + +-- | serializes an SBom to JSON format +-- conventions: +-- - bomRef == outPath +serializeSBom :: SerializeSBom -> SBom -> IO LazyByteString +serializeSBom settings bom = do + uuid <- V4.nextRandom + curTime <- getCurrentTime + -- FUTUREWORK(mangoiv): "tools" (the tools used in the creation of the bom) + let mkDependencies :: SBomMeta Identity -> Array + mkDependencies meta = do + let d = + object + [ "ref" .= meta.outPath, + "dependsOn" .= runIdentity meta.directDeps + ] + [d] + mkComponents :: SBomMeta Identity -> Array + mkComponents meta = do + let c :: Value + c = + -- FUTUREWORK(mangoiv): swid? https://www.iso.org/standard/65666.html + -- FUTUREWORK(mangoiv): CPE? + -- FUTUREWORK(mangoiv): more information in the supplier section + object + [ "type" .= meta.typ, + "bom-ref" .= String (runIdentity meta.outPath), + "supplier" .= object ["url" .= nubOrd (maybeToList meta.homepage <> catMaybes meta.urls)], + "name" .= String (fromMaybe (st'name $ splitStorePath $ runIdentity meta.outPath) meta.name), + "version" .= meta.version, + "description" .= meta.description, + "scope" .= String "required", + "licenses" .= ((\ln -> object ["license" .= ln]) <$> filter (isJust . (>>= (.id))) meta.licenseSpdxId), + "purl" .= mkPurl meta + ] + [c] + (dependencies, components) = foldMap (mkDependencies &&& mkComponents) bom + + pure $ + encode @Value $ + object + [ "bomFormat" .= String "CycloneDX", + "specVersion" .= String "1.5", + "serialNumber" .= String ("urn:uuid:" <> UUID.toText uuid), + "version" .= Number (fromIntegral settings.sbom'version), + "metadata" + .= object + [ "timestamp" .= String (T.pack (show curTime)), + "component" + .= object + [ "name" .= String settings.sbom'component, + "type" .= String "application" + -- FUTUREWORK(mangoiv): this should be a choice in the settings above + ], + -- FUTUREWORK(mangoiv): "manufacture" can also have url + "manufacture" .= object ["name" .= String settings.sbom'manufacture], + "supplier" .= object ["name" .= String (fromMaybe settings.sbom'manufacture settings.sbom'supplier)], + "licenses" .= Array (fromList $ object . (\n -> ["id" .= n]) . String <$> settings.sbom'licenses) + ], + "components" .= Array components, + -- FUTUREWORK(mangoiv): services: allow to tell the program the name of the services like brig, galley, ... + "dependencies" .= Array dependencies + ] + +buildMetaDB :: [Meta] -> MetaDB +buildMetaDB = foldMap \MkSBomMeta {..} -> [(drvPath, MkSBomMeta {..})] + +discoverSBom :: FilePath -> MetaDB -> IO SBom +discoverSBom outP metaDb = do + canonicalOutP <- canonicalizePath =<< getSymbolicLinkTarget outP + info <- pathInfo canonicalOutP + let go :: PathInfo -> IO SBom -> IO SBom + go (k, (deriver, deps)) = do + let proxyToIdentity :: SBomMeta Proxy -> SBomMeta Identity + proxyToIdentity (MkSBomMeta {..}) = MkSBomMeta {directDeps = Identity deps, outPath = Identity k, ..} + case M.lookup deriver metaDb of + Nothing -> \x -> do + T.putStrLn ("no meta found for drv: " <> deriver <> "\ntrying approximate match") + x >>= maybe + do + \m -> do + T.putStrLn ("no approximate match found for: " <> deriver) + pure $ M.insert k (sadSbomMeta deriver k deps) m + do \match -> pure . M.insert k (proxyToIdentity match) + do approximateMatch deriver metaDb + Just pmeta -> fmap $ M.insert k $ proxyToIdentity pmeta + + foldr go mempty info + +data StorePath = MkStorePath + { st'hash :: Text, + st'name :: Text, + st'original :: Text + } + deriving stock (Eq, Ord, Show) + +-- >>> splitStorePath "/nix/store/m306sk6syihxp80zrr9xs8hi5mjricgh-sop-core-0.5.0.2" +-- MkStorePath {st'hash = "m306sk6syihxp80zrr9xs8hi5mjricgh", st'name = "sop-core-0.5.0.2", st'original = "/nix/store/m306sk6syihxp80zrr9xs8hi5mjricgh-sop-core-0.5.0.2"} +splitStorePath :: Text -> StorePath +splitStorePath stp = do + let rest = T.drop (T.length "/nix/store/") stp + (hash, T.drop 1 -> name) = T.breakOn "-" rest + MkStorePath {st'original = stp, st'hash = hash, st'name = name} + +approximateMatch :: Text -> MetaDB -> Maybe (SBomMeta Proxy) +approximateMatch stp db = + let goal = splitStorePath stp + metas = first splitStorePath <$> M.toList db + in case filter (\(m, _) -> m.st'name == goal.st'name) metas of + [(_stp, meta)] -> pure meta + _ -> Nothing + +parse :: IO (String, String) +parse = customExecParser (prefs showHelpOnEmpty) do + Opt.info + do drvAndTlParser + do + mconcat + [ fullDesc, + progDesc "build an sbom from a derivation and a package set" + ] + +drvAndTlParser :: Opt.Parser (String, String) +drvAndTlParser = + (,) + <$> strOption (long "drv" <> help "outpath of the derivation to build the sbom for" <> value "result") + <*> strOption do + long "tldfp" + <> help "path to the derivation containing the output of the allLocalPackages drv" + <> value "wire-server" + +main :: IO () +main = parse >>= mainNoParse >>= BSL.writeFile "sbom.json" + +-- | by not always parsing, we have an easy time to call directly from haskell +mainNoParse :: (String, String) -> IO LazyByteString +mainNoParse (tldFp, drv) = do + let mkMeta :: LazyByteString -> Maybe Meta + mkMeta = decodeStrict . BSL.toStrict + metaDB <- buildMetaDB . mapMaybe mkMeta . C8L.lines <$> BSL.readFile tldFp + sbom <- discoverSBom drv metaDB + serializeSBom defaultSerializeSBom sbom + +pathInfo :: FilePath -> IO PathInfo +pathInfo path = do + let nixPathInfo = proc "nix" ["path-info", path, "--json", "--recursive"] + withCreateProcess nixPathInfo {std_out = CreatePipe} \_in (Just out) _err _ph -> do + Just refs' <- decodeStrict @Value <$> C8.hGetContents out + let failureBecauseNixHasZeroContracts = fail "unexpected format: this may be due to the output of `nix path-info` having changed randomly lol" + tryFindOutpath :: Value -> IO (Key, Value) + tryFindOutpath val + | Object pc <- val, + Just (String k) <- KM.lookup "path" pc = + pure (KM.fromText k, val) + tryFindOutpath _ = failureBecauseNixHasZeroContracts + refs <- case refs' of + Object refs -> pure $ KM.toList refs + Array refs -> traverse tryFindOutpath $ toList refs + _ -> failureBecauseNixHasZeroContracts + + let parseObj :: Value -> Maybe (Text, [Text]) + parseObj info + | Object mp <- info, + Just (Array rs) <- KM.lookup "references" mp, + Just (String deriver) <- KM.lookup "deriver" mp, + Just rs' <- for rs \case + String s -> Just s + _ -> Nothing = + Just (deriver, toList rs') + parseObj _ = trace "could not parse object" Nothing + -- some heuristics based filtering + pure + -- remove derivations with the same deriver + . nubOrdOn (fst . snd) + -- remove derivations that are just docs + . filter ((/= "doc") . T.takeEnd 3 . fst) + . mapMaybe (bitraverse (pure . KM.toText) parseObj) + $ refs diff --git a/hack/bin/bombon.hs b/hack/bin/bombon.hs index 0c01c4cf80f..d4bc7fdec0b 100755 --- a/hack/bin/bombon.hs +++ b/hack/bin/bombon.hs @@ -1,9 +1,11 @@ -#!/usr/bin/env -S nix -Lv run github:wireapp/ghc-flakr/99fe5a331fdd37d52043f14e5c565ac29a30bcb4 +#!/usr/bin/env -S nix -Lv run github:wireapp/ghc-flakr/6311bb166bf835d4a587fe1661b86c9a1426f212 {-# LANGUAGE DataKinds #-} +{-# LANGUAGE LambdaCase #-} +{-# OPTIONS_GHC -Wall #-} import Data.Aeson import qualified Data.ByteString.Base64.Lazy as Base64 -import qualified Data.ByteString.Lazy.Char8 as BL +import Data.ByteString.Lazy import Data.Proxy import Data.Text.Lazy import Data.Text.Lazy.Encoding @@ -11,8 +13,11 @@ import GHC.Generics import qualified Network.HTTP.Client as HTTP import Network.HTTP.Client.TLS (tlsManagerSettings) import Options.Applicative +import Sbom hiding (main) import Servant.API import Servant.Client +import System.Exit +import System.Process data Payload = Payload { bom :: Text, @@ -46,8 +51,7 @@ putBOM :: Payload -> Maybe String -> ClientM ApiResponse putBOM = client api data CliOptions = CliOptions - { opBomPath :: String, - opProjectName :: String, + { opProjectName :: String, opProjectVersion :: String, opAutoCreate :: Bool, opApiKey :: String @@ -58,12 +62,6 @@ cliParser :: Parser CliOptions cliParser = CliOptions <$> ( strOption - ( long "bom-filepath" - <> short 'f' - <> metavar "FILENAME" - ) - ) - <*> ( strOption ( long "project-name" <> short 'p' <> metavar "PROJECT_NAME" @@ -100,7 +98,16 @@ main :: IO () main = do options <- execParser fullCliParser manager' <- HTTP.newManager tlsManagerSettings - bom <- readFile $ opBomPath options + buildWire <- spawnCommand "nix -Lv build -f ../../nix wireServer.allLocalPackages -o wire-server" + buildMeta <- spawnCommand "nix -Lv build -f ../../nix wireServer.toplevel-derivations --impure -o meta" + waitForProcess buildWire >>= \case + ExitFailure _ -> fail "process for building wire failed" + ExitSuccess -> putStrLn "finished building Wire" + waitForProcess buildMeta >>= \case + ExitFailure _ -> fail "process for building meta for wire failed" + ExitSuccess -> putStrLn "finished building meta" + + bom <- mainNoParse ("./meta", "./wire-server") let payload = Payload { bom = toBase64Text bom, @@ -114,7 +121,7 @@ main = do (mkClientEnv manager' (BaseUrl Https "deptrack.wire.link" 443 "")) case res of Left err -> print $ "Error: " ++ show err - Right res -> print res + Right res' -> print res' -toBase64Text :: String -> Text -toBase64Text = decodeUtf8 . Base64.encode . BL.pack +toBase64Text :: LazyByteString -> Text +toBase64Text = decodeUtf8 . Base64.encode diff --git a/nix/all-toplevel-derivations.nix b/nix/all-toplevel-derivations.nix new file mode 100644 index 00000000000..4c7954352f4 --- /dev/null +++ b/nix/all-toplevel-derivations.nix @@ -0,0 +1,62 @@ +# this tries to recurse into pkgs to collect metadata about packages within nixpkgs +# it needs a recusionDepth, because pkgs is actually not a tree but a graph so you +# will go around in circles; also it helps bounding the memory needed to build this +# we also pass a keyFilter to ignore certain package names +# else, this just goes through the packages, tries to evaluate them, if that succeeds +# it goes on and remembers their metadata +# there's a lot of obfuscation caused by the fact that everything needs to be tryEval'd +# reason being that there's not a single thing in nixpkgs that is reliably evaluatable +{ lib +, pkgSet +, fn +, recursionDepth +, keyFilter +, ... +}: +let + go = depth: set': + let + evaluateableSet = builtins.tryEval set'; + in + if evaluateableSet.success && builtins.isAttrs evaluateableSet.value + then + let + set = evaluateableSet.value; + in + ( + if (builtins.tryEval (lib.isDerivation set)).value + then + let + meta = builtins.tryEval (fn set); + in + builtins.deepSeq meta ( + builtins.trace ("reached leaf: " + toString set) + ( + if meta.success + then [ meta.value ] + else builtins.trace "package didn't evaluate" [ ] + ) + ) + else if depth >= recursionDepth + then builtins.trace ("max depth of " + toString recursionDepth + " reached") [ ] + else + let + attrVals = builtins.tryEval (builtins.attrValues (lib.filterAttrs (k: _v: keyFilter k) set)); + go' = d: s: + let + gone' = builtins.tryEval (go d s); + in + if gone'.success + then gone'.value + else builtins.trace "could not recurse because of eval error" [ ]; + in + if attrVals.success + then + (builtins.concatMap + (go' (builtins.trace ("depth was: " + toString depth) (depth + 1))) + attrVals.value) + else builtins.trace "could not evaluate attr values because of eval error" [ ] + ) + else builtins.trace "could not evaluate package or package was not an attrset" [ ]; +in +go 0 pkgSet diff --git a/nix/default.nix b/nix/default.nix index f77d8de6fc1..02c0d9e01a7 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -7,7 +7,6 @@ let # All wire-server specific packages (import ./overlay.nix) (import ./overlay-docs.nix) - (self: super: { lib = super.lib // (import sources.bombon).lib.${super.system}; }) ]; }; @@ -79,7 +78,6 @@ let pkgs.entr ] ++ docsPkgs; }; - mls-test-cli = pkgs.mls-test-cli; - rusty-jwt-tools = pkgs.rusty-jwt-tools; + inherit (pkgs) mls-test-cli; in { inherit pkgs profileEnv wireServer docs docsEnv mls-test-cli nginz nginz-disco; } diff --git a/nix/overlay.nix b/nix/overlay.nix index a6390ab1d38..08dd42c00d0 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -103,4 +103,6 @@ self: super: { }; rabbitmqadmin = super.callPackage ./pkgs/rabbitmqadmin { }; + + sbomqs = super.callPackage ./pkgs/sbomqs { }; } diff --git a/nix/pkg-info.nix b/nix/pkg-info.nix new file mode 100644 index 00000000000..9773bbaef9d --- /dev/null +++ b/nix/pkg-info.nix @@ -0,0 +1,60 @@ +# collects information about a single nixpkgs package +{ lib +, pkg +, ... +}: +with builtins; +assert lib.isDerivation pkg; let + # trace with reason + trc = info: pkg: trace (info + ": " + toString pkg); + + # if thing is a list, map the function, else apply f to thing and return a singleton of + # it + mapOrSingleton = f: x: + if isList x + then map f x + else [ (f x) ]; + + # things to save from the src attr (the derivation that was created by a fetcher) + srcInfo = { + urls = (pkg.src.urls or (trc "package didn't have src or url" pkg [ ])) ++ [ (pkg.src.url or null) ]; + }; + + dp = builtins.tryEval pkg.drvPath; + + # things to save from the meta attr + metaInfo = + let + m = pkg.meta or (trc "package didn't have meta" pkg { }); + in + { + homepage = m.homepage or (trc "package didn't have homepage" pkg null); + description = m.description or (trc "package didn't have description" pkg null); + licenseSpdxId = + mapOrSingleton + ( + l: { + id = l.spdxId or (trc "package license doesn't have a spdxId" pkg null); + name = l.fullName or (trc "package license doens't have a name" pkg null); + } + ) + (m.license or (trc "package does not have a license" pkg null)); + + # based on heuristics, figure out whether something is an application for now this only checks whether this + # componnent has a main program + type = + if m ? mainProgram + then "application" + else "library"; + + name = pkg.pname or pkg.name or (trc "name is missing" pkg null); + version = pkg.version or (trc "version is missing" pkg null); + }; +in +if dp.success +then + let + info = builtins.toJSON (srcInfo // metaInfo // { drvPath = builtins.unsafeDiscardStringContext dp.value; }); + in + info +else trc "drvPath of package could not be computed" pkg { } diff --git a/nix/pkgs/sbomqs/default.nix b/nix/pkgs/sbomqs/default.nix new file mode 100644 index 00000000000..d0e41ad5785 --- /dev/null +++ b/nix/pkgs/sbomqs/default.nix @@ -0,0 +1,21 @@ +{ buildGoModule, fetchFromGitHub, lib, ... }: +buildGoModule rec { + pname = "sbomqs"; + version = "0.0.30"; + + src = fetchFromGitHub { + owner = "interlynk-io"; + repo = "sbomqs"; + rev = "v${version}"; + hash = "sha256-+y7+xi+E8kjGUjhIRKNk6ogcQMP+Dp39LrL66B1XdrQ="; + }; + + vendorHash = "sha256-V6k7nF2ovyl4ELE8Cqe/xjpmPAKI0t5BNlssf41kd0Y="; + + meta = with lib; { + description = "SBOM quality score - Quality metrics for your sboms"; + homepage = "https://github.com/interlynk-io/sbomqs"; + license = licenses.asl20; + mainProgram = "sbomqs"; + }; +} diff --git a/nix/wire-server.nix b/nix/wire-server.nix index 8a7b85b5e91..9815de26869 100644 --- a/nix/wire-server.nix +++ b/nix/wire-server.nix @@ -462,9 +462,37 @@ let allLocalPackagesBom = lib.buildBom allLocalPackages { includeBuildtimeDependencies = true; }; + + haskellPackages = hPkgs localModsEnableAll; + haskellPackagesUnoptimizedNoDocs = hPkgs localModsOnlyTests; + + toplevel-derivations = + let + mk = pkg: + import ./pkg-info.nix { + inherit pkg; + inherit (pkgs) lib hostPlatform writeText; + }; + out = import ./all-toplevel-derivations.nix { + inherit (pkgs) lib; + fn = mk; + # more than two takes more than 32GB of RAM, so this is what + # we're limiting ourselves to + recursionDepth = 2; + keyFilter = k: k != "passthru"; + # only import the package sets we want; this makes the database + # less copmplete but makes it so that nix doesn't get OOMkilled + pkgSet = { + inherit pkgs; + inherit haskellPackages; + }; + }; + in + pkgs.writeText "all-toplevel.jsonl" (builtins.concatStringsSep "\n" out); in { - inherit ciImage hoogleImage allImages allLocalPackages allLocalPackagesBom; + inherit ciImage hoogleImage allImages allLocalPackages allLocalPackagesBom + toplevel-derivations haskellPackages haskellPackagesUnoptimizedNoDocs imagesList; images = images localModsEnableAll; imagesUnoptimizedNoDocs = images localModsOnlyTests; @@ -475,7 +503,6 @@ in enableTests = true; enableDocs = false; }; - inherit imagesList; devEnv = pkgs.buildEnv { name = "wire-server-dev-env"; @@ -508,6 +535,7 @@ in pkgs.yq pkgs.nginz pkgs.rabbitmqadmin + pkgs.sbomqs pkgs.cabal-install pkgs.nix-prefetch-git @@ -523,6 +551,4 @@ in }; inherit brig-templates; - haskellPackages = hPkgs localModsEnableAll; - haskellPackagesUnoptimizedNoDocs = hPkgs localModsOnlyTests; } // attrsets.genAttrs wireServerPackages (e: (hPkgs localModsEnableAll).${e}) From 52932022a520e78dac2f33b7df197eab3fdbac97 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Tue, 16 Apr 2024 13:23:51 +0200 Subject: [PATCH 093/117] Finish servantifying brig (#3996) * Remove remaining splinters of wai-routing, wai-predicate from brig. * [fix] remove the body check after removing the body in the routes --------- Co-authored-by: Akshay Mankar Co-authored-by: Magnus Viernickel --- changelog.d/5-internal/wpb8691 | 1 + libs/types-common/src/Data/Code.hs | 13 +++--- .../src/Wire/API/Routes/Internal/Brig.hs | 13 ++++++ services/brig/brig.cabal | 3 -- services/brig/default.nix | 4 -- services/brig/src/Brig/API.hs | 31 ------------- services/brig/src/Brig/API/Handler.hs | 45 ++----------------- services/brig/src/Brig/API/Internal.hs | 19 +------- services/brig/src/Brig/Code.hs | 4 ++ services/brig/src/Brig/Provider/API.hs | 33 +++----------- services/brig/src/Brig/Run.hs | 14 +----- .../brig/test/integration/API/Provider.hs | 4 +- services/brig/test/integration/Run.hs | 13 +----- services/federator/default.nix | 1 - services/federator/federator.cabal | 1 - .../integration/Test/Federator/InwardSpec.hs | 10 ++--- 16 files changed, 47 insertions(+), 162 deletions(-) create mode 100644 changelog.d/5-internal/wpb8691 delete mode 100644 services/brig/src/Brig/API.hs diff --git a/changelog.d/5-internal/wpb8691 b/changelog.d/5-internal/wpb8691 new file mode 100644 index 00000000000..de783e455e1 --- /dev/null +++ b/changelog.d/5-internal/wpb8691 @@ -0,0 +1 @@ +Remove remaining splinters of wai-routing, wai-predicate from brig. diff --git a/libs/types-common/src/Data/Code.hs b/libs/types-common/src/Data/Code.hs index ba176629701..c745b752caa 100644 --- a/libs/types-common/src/Data/Code.hs +++ b/libs/types-common/src/Data/Code.hs @@ -3,7 +3,6 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE StandaloneDeriving #-} -{-# LANGUAGE TemplateHaskell #-} -- This file is part of the Wire Server implementation. -- @@ -27,10 +26,8 @@ module Data.Code where import Cassandra hiding (Value) import Data.Aeson qualified as A -import Data.Aeson.TH import Data.Bifunctor (Bifunctor (first)) import Data.ByteString.Conversion -import Data.Json.Util import Data.OpenApi qualified as S import Data.OpenApi.ParamSchema import Data.Proxy (Proxy (Proxy)) @@ -123,5 +120,11 @@ data KeyValuePair = KeyValuePair code :: !Value } deriving (Eq, Generic, Show) - -deriveJSON toJSONFieldName ''KeyValuePair + deriving (A.ToJSON, A.FromJSON, S.ToSchema) via Schema KeyValuePair + +instance ToSchema KeyValuePair where + schema = + object "KeyValuePair" $ + KeyValuePair + <$> key .= field "key" schema + <*> code .= field "code" schema diff --git a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs index 42af0b8ca40..43ca007abb1 100644 --- a/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs +++ b/libs/wire-api/src/Wire/API/Routes/Internal/Brig.hs @@ -30,6 +30,7 @@ module Wire.API.Routes.Internal.Brig FederationRemotesAPI, EJPDRequest, ISearchIndexAPI, + ProviderAPI, GetAccountConferenceCallingConfig, PutAccountConferenceCallingConfig, DeleteAccountConferenceCallingConfig, @@ -538,6 +539,7 @@ type API = :<|> OAuthAPI :<|> ISearchIndexAPI :<|> FederationRemotesAPI + :<|> ProviderAPI ) type IStatusAPI = @@ -766,6 +768,17 @@ type FederationRemotesAPI = :> Delete '[JSON] () ) +type ProviderAPI = + ( Named + "get-provider-activation-code" + ( Summary "Retrieve activation code via api instead of email (for testing only)" + :> "provider" + :> "activation-code" + :> QueryParam' '[Required, Strict] "email" Email + :> MultiVerb1 'GET '[JSON] (Respond 200 "" Code.KeyValuePair) + ) + ) + type FederationRemotesAPIDescription = "See https://docs.wire.com/understand/federation/backend-communication.html#configuring-remote-connections for background. " diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index 4e0c272153d..f24644d2d67 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -77,7 +77,6 @@ library -- cabal-fmt: expand src exposed-modules: Brig.Allowlists - Brig.API Brig.API.Auth Brig.API.Client Brig.API.Connection @@ -358,8 +357,6 @@ library , wai >=3.0 , wai-extra >=3.0 , wai-middleware-gunzip >=0.0.2 - , wai-predicates >=0.8 - , wai-routing >=0.12 , wai-utilities >=0.16 , wire-api , wire-api-federation diff --git a/services/brig/default.nix b/services/brig/default.nix index 6c13f6194ea..37c5d355190 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -149,9 +149,7 @@ , wai , wai-extra , wai-middleware-gunzip -, wai-predicates , wai-route -, wai-routing , wai-utilities , warp , warp-tls @@ -284,8 +282,6 @@ mkDerivation { wai wai-extra wai-middleware-gunzip - wai-predicates - wai-routing wai-utilities wire-api wire-api-federation diff --git a/services/brig/src/Brig/API.hs b/services/brig/src/Brig/API.hs deleted file mode 100644 index ba318c3f2b5..00000000000 --- a/services/brig/src/Brig/API.hs +++ /dev/null @@ -1,31 +0,0 @@ --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Brig.API - ( sitemap, - ) -where - -import Brig.API.Handler (Handler) -import Brig.API.Internal qualified as Internal -import Brig.Effects.GalleyProvider (GalleyProvider) -import Network.Wai.Routing (Routes) -import Polysemy - -sitemap :: forall r. (Member GalleyProvider r) => Routes () (Handler r) () -sitemap = do - Internal.sitemap diff --git a/services/brig/src/Brig/API/Handler.hs b/services/brig/src/Brig/API/Handler.hs index bbea923f517..4c6e92e341a 100644 --- a/services/brig/src/Brig/API/Handler.hs +++ b/services/brig/src/Brig/API/Handler.hs @@ -18,11 +18,9 @@ module Brig.API.Handler ( -- * Handler Monad Handler, - runHandler, toServantHandler, -- * Utilities - JSON, parseJsonBody, checkAllowlist, checkAllowlistWithError, @@ -53,13 +51,9 @@ import Data.Text.Encoding qualified as Text import Data.ZAuth.Validation qualified as ZV import Imports import Network.HTTP.Types (Status (statusCode, statusMessage)) -import Network.Wai (Request, ResponseReceived) -import Network.Wai.Predicate (Media) -import Network.Wai.Routing (Continue) import Network.Wai.Utilities.Error ((!>>)) import Network.Wai.Utilities.Error qualified as WaiError import Network.Wai.Utilities.Request (JsonRequest, parseBody) -import Network.Wai.Utilities.Response (addHeader, json, setStatus) import Network.Wai.Utilities.Server qualified as Server import Servant qualified import System.Logger qualified as Log @@ -72,18 +66,6 @@ import Wire.API.Error.Brig type Handler r = ExceptT Error (AppT r) -runHandler :: - Env -> - Request -> - (Handler BrigCanonicalEffects) ResponseReceived -> - Continue IO -> - IO ResponseReceived -runHandler e r h k = do - a <- - runBrigToIO e (runExceptT h) - `catches` brigErrorHandlers (view applog e) (unRequestId (view requestId e)) - either (onError (view applog e) r k) pure a - toServantHandler :: Env -> (Handler BrigCanonicalEffects) a -> Servant.Handler a toServantHandler env action = do let logger = view applog env @@ -135,33 +117,12 @@ brigErrorHandlers logger reqId = throwIO e ] -onError :: Logger -> Request -> Continue IO -> Error -> IO ResponseReceived -onError g r k e = do - Server.logError g (Just r) we - -- This function exists to workaround a problem that existed in nginx 5 years - -- ago. Context here: - -- https://github.com/zinfra/wai-utilities/commit/3d7e8349d3463e5ee2c3ebe89c717baeef1a8241 - -- So, this can probably be deleted and is not part of the new servant - -- handler. - Server.flushRequestBody r - k - $ setStatus (WaiError.code we) - . appEndo (foldMap (Endo . uncurry addHeader) hs) - $ json e - where - (we, hs) = case e of - StdError x -> (x, []) - RichError x _ h -> (x, h) - ------------------------------------------------------------------------------- -- Utilities --- TODO: move to libs/wai-utilities? -type JSON = Media "application" "json" - --- TODO: move to libs/wai-utilities? there is a parseJson' in "Network.Wai.Utilities.Request", --- but adjusting its signature to this here would require to move more code out of brig (at least --- badRequest and probably all the other errors). +-- This could go to libs/wai-utilities. There is a `parseJson'` in +-- "Network.Wai.Utilities.Request", but adding `parseJsonBody` there would require to move +-- more code out of brig. parseJsonBody :: (FromJSON a, MonadIO m) => JsonRequest a -> ExceptT Error m a parseJsonBody req = parseBody req !>> StdError . badRequest diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 75c4d6a3976..28d004f2ec8 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -15,9 +15,7 @@ -- You should have received a copy of the GNU Affero General Public License along -- with this program. If not, see . module Brig.API.Internal - ( sitemap, - servantSitemap, - BrigIRoutes.API, + ( servantSitemap, getMLSClients, ) where @@ -74,7 +72,6 @@ import Data.Set qualified as Set import Data.Time.Clock (UTCTime) import Data.Time.Clock.System import Imports hiding (head) -import Network.Wai.Routing hiding (toList) import Network.Wai.Utilities as Utilities import Polysemy import Polysemy.Input (Input) @@ -104,9 +101,6 @@ import Wire.Rpc import Wire.Sem.Concurrency import Wire.Sem.Paging.Cassandra (InternalPaging) ---------------------------------------------------------------------------- --- Sitemap (servant) - servantSitemap :: forall r p. ( Member BlacklistPhonePrefixStore r, @@ -139,6 +133,7 @@ servantSitemap = :<|> internalOauthAPI :<|> internalSearchIndexAPI :<|> federationRemotesAPI + :<|> Provider.internalProviderAPI istatusAPI :: forall r. ServerT BrigIRoutes.IStatusAPI (Handler r) istatusAPI = Named @"get-status" (pure NoContent) @@ -373,16 +368,6 @@ internalSearchIndexAPI = :<|> Named @"indexReindex" (NoContent <$ lift (wrapClient Search.reindexAll)) :<|> Named @"indexReindexIfSameOrNewer" (NoContent <$ lift (wrapClient Search.reindexAllIfSameOrNewer)) ---------------------------------------------------------------------------- --- Sitemap (wai-route) - -sitemap :: - ( Member GalleyProvider r - ) => - Routes a (Handler r) () -sitemap = unsafeCallsFed @'Brig @"on-user-deleted-connections" $ do - Provider.routesInternal - --------------------------------------------------------------------------- -- Handlers diff --git a/services/brig/src/Brig/Code.hs b/services/brig/src/Brig/Code.hs index 2d0fc50485c..7840b8e3cb5 100644 --- a/services/brig/src/Brig/Code.hs +++ b/services/brig/src/Brig/Code.hs @@ -40,6 +40,7 @@ module Brig.Code codeForPhone, codeKey, codeValue, + codeToKeyValuePair, codeTTL, codeAccount, scopeFromAction, @@ -114,6 +115,9 @@ scopeFromAction = \case User.Login -> AccountLogin User.DeleteTeam -> DeleteTeam +codeToKeyValuePair :: Code -> KeyValuePair +codeToKeyValuePair code = KeyValuePair code.codeKey code.codeValue + -- | The same 'Key' can exist with different 'Value's in different -- 'Scope's at the same time. data Scope diff --git a/services/brig/src/Brig/Provider/API.hs b/services/brig/src/Brig/Provider/API.hs index 535ad5c9750..c59e561e4cb 100644 --- a/services/brig/src/Brig/Provider/API.hs +++ b/services/brig/src/Brig/Provider/API.hs @@ -17,10 +17,10 @@ module Brig.Provider.API ( -- * Main stuff - routesInternal, botAPI, servicesAPI, providerAPI, + internalProviderAPI, -- * Event handlers finishDeleteService, @@ -58,7 +58,6 @@ import Control.Exception.Enclosed (handleAny) import Control.Lens (view, (^.)) import Control.Monad.Catch (MonadMask) import Control.Monad.Except -import Data.Aeson hiding (json) import Data.ByteString.Conversion import Data.ByteString.Lazy.Char8 qualified as LC8 import Data.CommaSeparatedList (CommaSeparatedList (fromCommaSeparatedList)) @@ -79,14 +78,9 @@ import Data.Text.Encoding qualified as Text import Data.Text.Lazy qualified as Text import GHC.TypeNats import Imports -import Network.HTTP.Types.Status -import Network.Wai (Response) -import Network.Wai.Predicate (accept) -import Network.Wai.Routing +import Network.HTTP.Types import Network.Wai.Utilities.Error ((!>>)) import Network.Wai.Utilities.Error qualified as Wai -import Network.Wai.Utilities.Response (json) -import Network.Wai.Utilities.ZAuth import OpenSSL.EVP.Digest qualified as SSL import OpenSSL.EVP.PKey qualified as SSL import OpenSSL.PEM qualified as SSL @@ -114,6 +108,7 @@ import Wire.API.Provider.External qualified as Ext import Wire.API.Provider.Service import Wire.API.Provider.Service qualified as Public import Wire.API.Provider.Service.Tag qualified as Public +import Wire.API.Routes.Internal.Brig qualified as BrigIRoutes import Wire.API.Routes.Named (Named (Named)) import Wire.API.Routes.Public.Brig.Bot (BotAPI) import Wire.API.Routes.Public.Brig.Provider (ProviderAPI) @@ -177,11 +172,8 @@ providerAPI = :<|> Named @"provider-get-account" getAccount :<|> Named @"provider-get-profile" getProviderProfile -routesInternal :: Member GalleyProvider r => Routes a (Handler r) () -routesInternal = do - get "/i/provider/activation-code" (continue getActivationCodeH) $ - accept "application" "json" - .&> param "email" +internalProviderAPI :: Member GalleyProvider r => ServerT BrigIRoutes.ProviderAPI (Handler r) +internalProviderAPI = Named @"get-provider-activation-code" getActivationCodeH -------------------------------------------------------------------------------- -- Public API (Unauthenticated) @@ -242,26 +234,15 @@ activateAccountKey key val = do lift $ sendApprovalConfirmMail name email pure . Just $ Public.ProviderActivationResponse email -getActivationCodeH :: Member GalleyProvider r => Public.Email -> (Handler r) Response +getActivationCodeH :: Member GalleyProvider r => Public.Email -> (Handler r) Code.KeyValuePair getActivationCodeH e = do guardSecondFactorDisabled Nothing - json <$> getActivationCode e - -getActivationCode :: Public.Email -> (Handler r) FoundActivationCode -getActivationCode e = do email <- case validateEmail e of Right em -> pure em Left _ -> throwStd (errorToWai @'E.InvalidEmail) gen <- Code.mkGen (Code.ForEmail email) code <- wrapClientE $ Code.lookup (Code.genKey gen) Code.IdentityVerification - maybe (throwStd activationKeyNotFound) (pure . FoundActivationCode) code - -newtype FoundActivationCode = FoundActivationCode Code.Code - -instance ToJSON FoundActivationCode where - toJSON (FoundActivationCode vcode) = - toJSON $ - Code.KeyValuePair (Code.codeKey vcode) (Code.codeValue vcode) + maybe (throwStd activationKeyNotFound) (pure . Code.codeToKeyValuePair) code login :: Member GalleyProvider r => ProviderLogin -> Handler r ProviderTokenCookie login l = do diff --git a/services/brig/src/Brig/Run.hs b/services/brig/src/Brig/Run.hs index b74e58081c2..90316762356 100644 --- a/services/brig/src/Brig/Run.hs +++ b/services/brig/src/Brig/Run.hs @@ -22,7 +22,6 @@ module Brig.Run where import AWS.Util (readAuthExpiration) -import Brig.API (sitemap) import Brig.API.Federation import Brig.API.Handler import Brig.API.Internal qualified as IAPI @@ -59,8 +58,6 @@ import Network.HTTP.Types qualified as HTTP import Network.Wai qualified as Wai import Network.Wai.Middleware.Gunzip qualified as GZip import Network.Wai.Middleware.Gzip qualified as GZip -import Network.Wai.Routing (Tree) -import Network.Wai.Routing.Route (App) import Network.Wai.Utilities (lookupRequestId) import Network.Wai.Utilities.Server import Network.Wai.Utilities.Server qualified as Server @@ -72,6 +69,7 @@ import System.Logger qualified as Log import System.Logger.Class (MonadLogger, err) import Util.Options import Wire.API.Routes.API +import Wire.API.Routes.Internal.Brig qualified as IAPI import Wire.API.Routes.Public.Brig import Wire.API.Routes.Version import Wire.API.Routes.Version.Wai @@ -119,22 +117,16 @@ mkApp o = do e <- newEnv o pure (middleware e $ \reqId -> servantApp (e & requestId .~ reqId), e) where - rtree :: Tree (App (Handler BrigCanonicalEffects)) - rtree = compile sitemap - middleware :: Env -> (RequestId -> Wai.Application) -> Wai.Application middleware e = -- this rewrites the request, so it must be at the top (i.e. applied last) versionMiddleware (e ^. disabledVersions) - . Metrics.servantPlusWAIPrometheusMiddleware (sitemap @BrigCanonicalEffects) (Proxy @ServantCombinedAPI) + . Metrics.servantPrometheusMiddleware (Proxy @ServantCombinedAPI) . GZip.gunzip . GZip.gzip GZip.def . catchErrors (e ^. applog) [Right $ e ^. metrics] . lookupRequestIdMiddleware (e ^. applog) - app :: Env -> Wai.Request -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived - app e r k = runHandler e r (Server.route rtree r k) k - -- the servant API wraps the one defined using wai-routing servantApp :: Env -> Wai.Application servantApp e = @@ -147,7 +139,6 @@ mkApp o = do :<|> hoistServerWithDomain @IAPI.API (toServantHandler e) IAPI.servantSitemap :<|> hoistServerWithDomain @FederationAPI (toServantHandler e) federationSitemap :<|> hoistServerWithDomain @VersionAPI (toServantHandler e) versionAPI - :<|> Servant.Tagged (app e) ) type ServantCombinedAPI = @@ -156,7 +147,6 @@ type ServantCombinedAPI = :<|> IAPI.API :<|> FederationAPI :<|> VersionAPI - :<|> Servant.Raw ) lookupRequestIdMiddleware :: Logger -> (RequestId -> Wai.Application) -> Wai.Application diff --git a/services/brig/test/integration/API/Provider.hs b/services/brig/test/integration/API/Provider.hs index a2479df44e8..538453d1aed 100644 --- a/services/brig/test/integration/API/Provider.hs +++ b/services/brig/test/integration/API/Provider.hs @@ -1680,10 +1680,10 @@ testRegisterProvider db' brig = do activateProvider brig (Code.codeKey vcode) (Code.codeValue vcode) !!! const 200 === statusCode Nothing -> do - _rs <- + rs <- getProviderActivationCodeInternal brig email Date: Tue, 16 Apr 2024 15:14:26 +0200 Subject: [PATCH 094/117] Revert "[chore] use `PathInfo` consistently" (#4003) This reverts commit 71c8c596d5b8024847730d2aac6a5eec1edb4b9b. --- hack/bin/Sbom.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/hack/bin/Sbom.hs b/hack/bin/Sbom.hs index 7226a19885d..74a1783a3a4 100644 --- a/hack/bin/Sbom.hs +++ b/hack/bin/Sbom.hs @@ -267,7 +267,7 @@ discoverSBom :: FilePath -> MetaDB -> IO SBom discoverSBom outP metaDb = do canonicalOutP <- canonicalizePath =<< getSymbolicLinkTarget outP info <- pathInfo canonicalOutP - let go :: PathInfo -> IO SBom -> IO SBom + let go :: (Text, (Text, [Text])) -> IO SBom -> IO SBom go (k, (deriver, deps)) = do let proxyToIdentity :: SBomMeta Proxy -> SBomMeta Identity proxyToIdentity (MkSBomMeta {..}) = MkSBomMeta {directDeps = Identity deps, outPath = Identity k, ..} From 02a362652372be2f5db0a8d247f9de25f712f62e Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Tue, 16 Apr 2024 18:23:58 +0200 Subject: [PATCH 095/117] increase 2fa timeout to unflake test (#4004) --- integration/test/Test/Login.hs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/integration/test/Test/Login.hs b/integration/test/Test/Login.hs index 28a23b54d39..6f6b05f6246 100644 --- a/integration/test/Test/Login.hs +++ b/integration/test/Test/Login.hs @@ -62,14 +62,19 @@ testLoginVerify6DigitMissingCodeFails = do testLoginVerify6DigitExpiredCodeFails :: HasCallStack => App () testLoginVerify6DigitExpiredCodeFails = do withModifiedBackend - (def {brigCfg = setField "optSettings.setVerificationTimeout" (Aeson.Number 1)}) + (def {brigCfg = setField "optSettings.setVerificationTimeout" (Aeson.Number 2)}) $ \domain -> do (owner, team, []) <- createTeam domain 0 email <- owner %. "email" setTeamFeatureLockStatus owner team "sndFactorPasswordChallenge" "unlocked" setTeamFeatureStatus owner team "sndFactorPasswordChallenge" "enabled" + bindResponse (getTeamFeature domain "sndFactorPasswordChallenge" team) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. "status" `shouldMatch` "enabled" generateVerificationCode owner email - code <- getVerificationCode owner "login" >>= getJSON 200 >>= asString + code <- bindResponse (getVerificationCode owner "login") $ \resp -> do + resp.status `shouldMatchInt` 200 + asString resp.json liftIO $ threadDelay 2_000_100 bindResponse (loginWith2ndFactor owner email defPassword code) \resp -> do resp.status `shouldMatchInt` 403 From e32b2b3b695ec13d2395639e8c6976f4e229d362 Mon Sep 17 00:00:00 2001 From: Arthur Wolf Date: Wed, 17 Apr 2024 15:05:05 +0200 Subject: [PATCH 096/117] empty commit to re-run ci From a6dff20fa85eac197fa10c036900d567c6363258 Mon Sep 17 00:00:00 2001 From: Matthias Fischmann Date: Wed, 17 Apr 2024 16:37:12 +0200 Subject: [PATCH 097/117] [chore] Move Cql instances to avoid orphans (#4005) * Move around Cql instances. --------- Co-authored-by: Igor Ranieri --- libs/brig-types/src/Brig/Types/Search.hs | 11 + libs/types-common/src/Data/Domain.hs | 7 + libs/types-common/src/Data/Handle.hs | 3 + libs/wire-api/src/Wire/API/Asset.hs | 9 +- libs/wire-api/src/Wire/API/Connection.hs | 33 ++ libs/wire-api/src/Wire/API/MLS/CipherSuite.hs | 11 + libs/wire-api/src/Wire/API/Properties.hs | 9 + libs/wire-api/src/Wire/API/User.hs | 30 ++ libs/wire-api/src/Wire/API/User/Activation.hs | 5 + libs/wire-api/src/Wire/API/User/Client.hs | 34 +- libs/wire-api/src/Wire/API/User/Identity.hs | 23 ++ libs/wire-api/src/Wire/API/User/Password.hs | 5 + libs/wire-api/src/Wire/API/User/Profile.hs | 93 +++++ libs/wire-api/src/Wire/API/User/RichInfo.hs | 7 + libs/wire-api/src/Wire/API/User/Search.hs | 13 + services/brig/brig.cabal | 1 - services/brig/src/Brig/Code.hs | 1 - services/brig/src/Brig/Data/Client.hs | 1 - services/brig/src/Brig/Data/Connection.hs | 1 - services/brig/src/Brig/Data/Instances.hs | 319 ------------------ services/brig/src/Brig/Data/LoginCode.hs | 1 - services/brig/src/Brig/Data/Nonce.hs | 1 - services/brig/src/Brig/Data/Properties.hs | 1 - services/brig/src/Brig/Data/User.hs | 1 - services/brig/src/Brig/Data/UserKey.hs | 1 - .../src/Brig/Effects/CodeStore/Cassandra.hs | 1 - .../FederationConfigStore/Cassandra.hs | 1 - services/brig/src/Brig/Provider/DB.hs | 1 - services/brig/src/Brig/Team/DB.hs | 1 - services/brig/src/Brig/Unique.hs | 1 - services/brig/src/Brig/User/Handle.hs | 1 - services/brig/src/Brig/User/Search/Index.hs | 1 - .../brig/src/Brig/User/Search/SearchIndex.hs | 1 - .../brig/src/Brig/User/Search/TeamSize.hs | 1 - .../src/Brig/User/Search/TeamUserSearch.hs | 1 - .../galley/src/Galley/Cassandra/Instances.hs | 24 -- tools/db/auto-whitelist/auto-whitelist.cabal | 1 - tools/db/auto-whitelist/default.nix | 2 - tools/db/auto-whitelist/src/Work.hs | 5 - tools/db/find-undead/src/Work.hs | 20 -- .../db/inconsistencies/src/DanglingHandles.hs | 1 - .../inconsistencies/src/DanglingUserKeys.hs | 1 - .../db/inconsistencies/src/EmailLessUsers.hs | 1 - .../db/inconsistencies/src/HandleLessUsers.hs | 1 - tools/db/migrate-sso-feature-flag/src/Work.hs | 5 - tools/db/move-team/src/Types.hs | 6 - tools/db/move-team/src/Work.hs | 5 - tools/db/repair-handles/default.nix | 2 - tools/db/repair-handles/repair-handles.cabal | 1 - tools/db/repair-handles/src/Options.hs | 1 - tools/db/repair-handles/src/Types.hs | 1 - tools/db/repair-handles/src/Work.hs | 1 - tools/db/service-backfill/default.nix | 2 - .../service-backfill/service-backfill.cabal | 1 - tools/db/service-backfill/src/Work.hs | 5 - tools/mlsstats/src/MlsStats/Run.hs | 6 - 56 files changed, 285 insertions(+), 438 deletions(-) delete mode 100644 services/brig/src/Brig/Data/Instances.hs diff --git a/libs/brig-types/src/Brig/Types/Search.hs b/libs/brig-types/src/Brig/Types/Search.hs index cbd6eb0a986..2bf55eb1ea8 100644 --- a/libs/brig-types/src/Brig/Types/Search.hs +++ b/libs/brig-types/src/Brig/Types/Search.hs @@ -26,6 +26,7 @@ module Brig.Types.Search ) where +import Cassandra qualified as C import Data.Aeson import Data.Attoparsec.ByteString import Data.ByteString.Builder @@ -77,6 +78,16 @@ instance FromByteString SearchVisibilityInbound where SearchableByOwnTeam <$ string "searchable-by-own-team" <|> SearchableByAllTeams <$ string "searchable-by-all-teams" +instance C.Cql SearchVisibilityInbound where + ctype = C.Tagged C.IntColumn + + toCql SearchableByOwnTeam = C.CqlInt 0 + toCql SearchableByAllTeams = C.CqlInt 1 + + fromCql (C.CqlInt 0) = pure SearchableByOwnTeam + fromCql (C.CqlInt 1) = pure SearchableByAllTeams + fromCql n = Left $ "Unexpected SearchVisibilityInbound: " ++ show n + defaultSearchVisibilityInbound :: SearchVisibilityInbound defaultSearchVisibilityInbound = SearchableByOwnTeam diff --git a/libs/types-common/src/Data/Domain.hs b/libs/types-common/src/Data/Domain.hs index 6f9d0884405..e45d966c85b 100644 --- a/libs/types-common/src/Data/Domain.hs +++ b/libs/types-common/src/Data/Domain.hs @@ -19,6 +19,7 @@ module Data.Domain where +import Cassandra import Control.Lens ((?~)) import Data.Aeson (FromJSON, FromJSONKey, FromJSONKeyFunction (FromJSONKeyTextParser), ToJSON, ToJSONKey (toJSONKey)) import Data.Aeson qualified as Aeson @@ -177,3 +178,9 @@ instance Arbitrary DomainText where [ (1, pure ""), (5, x) -- to get longer labels ] + +instance Cql Domain where + ctype = Tagged TextColumn + toCql = CqlText . domainText + fromCql (CqlText txt) = mkDomain txt + fromCql _ = Left "Domain: Text expected" diff --git a/libs/types-common/src/Data/Handle.hs b/libs/types-common/src/Data/Handle.hs index 29d1570cc32..59854e89ec8 100644 --- a/libs/types-common/src/Data/Handle.hs +++ b/libs/types-common/src/Data/Handle.hs @@ -25,6 +25,7 @@ module Data.Handle ) where +import Cassandra qualified as C import Data.Aeson (FromJSON (..), ToJSON (..)) import Data.Attoparsec.ByteString.Char8 qualified as Atto import Data.Bifunctor (Bifunctor (first)) @@ -50,6 +51,8 @@ newtype Handle = Handle deriving newtype (ToByteString, Hashable, S.ToParamSchema) deriving (FromJSON, ToJSON, S.ToSchema) via Schema Handle +deriving instance C.Cql Handle + instance ToSchema Handle where schema = fromHandle .= parsedText "Handle" p where diff --git a/libs/wire-api/src/Wire/API/Asset.hs b/libs/wire-api/src/Wire/API/Asset.hs index 200fcbe245c..4718fa66ad5 100644 --- a/libs/wire-api/src/Wire/API/Asset.hs +++ b/libs/wire-api/src/Wire/API/Asset.hs @@ -31,7 +31,6 @@ module Wire.API.Asset -- * AssetKey AssetKey (..), assetKeyToText, - nilAssetKey, -- * AssetToken AssetToken (..), @@ -63,6 +62,7 @@ module Wire.API.Asset ) where +import Cassandra qualified as C import Codec.MIME.Type qualified as MIME import Control.Lens (makeLenses, (?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) @@ -186,8 +186,11 @@ instance S.ToParamSchema AssetKey where instance FromHttpApiData AssetKey where parseUrlPiece = first T.pack . runParser parser . T.encodeUtf8 -nilAssetKey :: AssetKey -nilAssetKey = AssetKeyV3 (Id UUID.nil) AssetVolatile +instance C.Cql AssetKey where + ctype = C.Tagged C.TextColumn + toCql = C.CqlText . assetKeyToText + fromCql (C.CqlText txt) = runParser parser . T.encodeUtf8 $ txt + fromCql _ = Left "AssetKey: Text expected" -------------------------------------------------------------------------------- -- AssetToken diff --git a/libs/wire-api/src/Wire/API/Connection.hs b/libs/wire-api/src/Wire/API/Connection.hs index 138b6c3eb4b..7c0fa5bbfdd 100644 --- a/libs/wire-api/src/Wire/API/Connection.hs +++ b/libs/wire-api/src/Wire/API/Connection.hs @@ -40,6 +40,7 @@ module Wire.API.Connection ) where +import Cassandra qualified as C import Control.Applicative (optional) import Control.Lens ((?~)) import Data.Aeson (FromJSON (..), ToJSON (..)) @@ -224,6 +225,38 @@ instance ToHttpApiData Relation where Cancelled -> "cancelled" MissingLegalholdConsent -> "missing-legalhold-consent" +instance C.Cql RelationWithHistory where + ctype = C.Tagged C.IntColumn + + fromCql (C.CqlInt i) = case i of + 0 -> pure AcceptedWithHistory + 1 -> pure BlockedWithHistory + 2 -> pure PendingWithHistory + 3 -> pure IgnoredWithHistory + 4 -> pure SentWithHistory + 5 -> pure CancelledWithHistory + 6 -> pure MissingLegalholdConsentFromAccepted + 7 -> pure MissingLegalholdConsentFromBlocked + 8 -> pure MissingLegalholdConsentFromPending + 9 -> pure MissingLegalholdConsentFromIgnored + 10 -> pure MissingLegalholdConsentFromSent + 11 -> pure MissingLegalholdConsentFromCancelled + n -> Left $ "unexpected RelationWithHistory: " ++ show n + fromCql _ = Left "RelationWithHistory: int expected" + + toCql AcceptedWithHistory = C.CqlInt 0 + toCql BlockedWithHistory = C.CqlInt 1 + toCql PendingWithHistory = C.CqlInt 2 + toCql IgnoredWithHistory = C.CqlInt 3 + toCql SentWithHistory = C.CqlInt 4 + toCql CancelledWithHistory = C.CqlInt 5 + toCql MissingLegalholdConsentFromAccepted = C.CqlInt 6 + toCql MissingLegalholdConsentFromBlocked = C.CqlInt 7 + toCql MissingLegalholdConsentFromPending = C.CqlInt 8 + toCql MissingLegalholdConsentFromIgnored = C.CqlInt 9 + toCql MissingLegalholdConsentFromSent = C.CqlInt 10 + toCql MissingLegalholdConsentFromCancelled = C.CqlInt 11 + ---------------- -- Requests diff --git a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs index fc06a3d708f..f4e24df2989 100644 --- a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs +++ b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs @@ -41,6 +41,7 @@ module Wire.API.MLS.CipherSuite ) where +import Cassandra qualified as C import Cassandra.CQL import Control.Applicative import Control.Error (note) @@ -134,6 +135,16 @@ instance ToSchema CipherSuiteTag where pure (cipherSuiteTag (CipherSuite index)) +instance C.Cql CipherSuiteTag where + ctype = Tagged IntColumn + toCql = CqlInt . fromIntegral . cipherSuiteNumber . tagCipherSuite + + fromCql (CqlInt index) = + case cipherSuiteTag (CipherSuite (fromIntegral index)) of + Just t -> Right t + Nothing -> Left "CipherSuiteTag: unexpected index" + fromCql _ = Left "CipherSuiteTag: int expected" + -- | See https://messaginglayersecurity.rocks/mls-protocol/draft-ietf-mls-protocol.html#table-5. cipherSuiteTag :: CipherSuite -> Maybe CipherSuiteTag cipherSuiteTag cs = listToMaybe $ do diff --git a/libs/wire-api/src/Wire/API/Properties.hs b/libs/wire-api/src/Wire/API/Properties.hs index debcf9016d7..83c8ee1aa50 100644 --- a/libs/wire-api/src/Wire/API/Properties.hs +++ b/libs/wire-api/src/Wire/API/Properties.hs @@ -25,6 +25,7 @@ module Wire.API.Properties ) where +import Cassandra qualified as C import Control.Lens ((?~)) import Data.Aeson (FromJSON (..), ToJSON (..), Value) import Data.Aeson qualified as A @@ -67,9 +68,17 @@ instance S.ToParamSchema PropertyKey where & S.type_ ?~ S.OpenApiString & S.format ?~ "printable" +deriving instance C.Cql PropertyKey + -- | A raw, unparsed property value. newtype RawPropertyValue = RawPropertyValue {rawPropertyBytes :: LByteString} +instance C.Cql RawPropertyValue where + ctype = C.Tagged C.BlobColumn + toCql = C.toCql . C.Blob . rawPropertyBytes + fromCql (C.CqlBlob v) = pure (RawPropertyValue v) + fromCql _ = Left "PropertyValue: Blob expected" + instance {-# OVERLAPPING #-} MimeUnrender JSON RawPropertyValue where mimeUnrender _ = pure . RawPropertyValue diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index a55509da237..39e01c892ce 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -159,6 +159,7 @@ module Wire.API.User ) where +import Cassandra qualified as C import Control.Applicative import Control.Arrow ((&&&)) import Control.Error.Safe (rightMay) @@ -345,6 +346,8 @@ instance ToByteString PhonePrefix where instance FromHttpApiData PhonePrefix where parseUrlPiece = Bifunctor.first cs . phonePrefixParser +deriving instance C.Cql PhonePrefix + phonePrefixParser :: Text -> Either String PhonePrefix phonePrefixParser p = maybe err pure (parsePhonePrefix p) where @@ -1362,6 +1365,8 @@ instance FromHttpApiData InvitationCode where instance ToHttpApiData InvitationCode where toQueryParam = cs . toByteString . fromInvitationCode +deriving instance C.Cql InvitationCode + -------------------------------------------------------------------------------- -- NewTeamUser @@ -1862,6 +1867,24 @@ instance Schema.ToSchema AccountStatus where Schema.element "pending-invitation" PendingInvitation ] +instance C.Cql AccountStatus where + ctype = C.Tagged C.IntColumn + + toCql Active = C.CqlInt 0 + toCql Suspended = C.CqlInt 1 + toCql Deleted = C.CqlInt 2 + toCql Ephemeral = C.CqlInt 3 + toCql PendingInvitation = C.CqlInt 4 + + fromCql (C.CqlInt i) = case i of + 0 -> pure Active + 1 -> pure Suspended + 2 -> pure Deleted + 3 -> pure Ephemeral + 4 -> pure PendingInvitation + n -> Left $ "unexpected account status: " ++ show n + fromCql _ = Left "account status: int expected" + data AccountStatusResp = AccountStatusResp {fromAccountStatusResp :: AccountStatus} deriving (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform AccountStatusResp) @@ -2007,6 +2030,13 @@ data BaseProtocolTag = BaseProtocolProteusTag | BaseProtocolMLSTag deriving (Arbitrary) via (GenericUniform BaseProtocolTag) deriving (FromJSON, ToJSON, S.ToSchema) via (Schema BaseProtocolTag) +instance C.Cql (Imports.Set BaseProtocolTag) where + ctype = C.Tagged C.IntColumn + + toCql = C.CqlInt . fromIntegral . protocolSetBits + fromCql (C.CqlInt bits) = pure $ protocolSetFromBits (fromIntegral bits) + fromCql _ = Left "Protocol set: Int expected" + baseProtocolMask :: BaseProtocolTag -> Word32 baseProtocolMask BaseProtocolProteusTag = 1 baseProtocolMask BaseProtocolMLSTag = 2 diff --git a/libs/wire-api/src/Wire/API/User/Activation.hs b/libs/wire-api/src/Wire/API/User/Activation.hs index e14b30bc326..8998854b2e2 100644 --- a/libs/wire-api/src/Wire/API/User/Activation.hs +++ b/libs/wire-api/src/Wire/API/User/Activation.hs @@ -35,6 +35,7 @@ module Wire.API.User.Activation ) where +import Cassandra qualified as C import Control.Lens ((?~)) import Data.Aeson qualified as A import Data.Aeson.Types (Parser) @@ -82,6 +83,8 @@ instance ToParamSchema ActivationKey where instance FromHttpApiData ActivationKey where parseUrlPiece = fmap ActivationKey . parseUrlPiece +deriving instance C.Cql ActivationKey + -------------------------------------------------------------------------------- -- ActivationCode @@ -100,6 +103,8 @@ instance ToParamSchema ActivationCode where instance FromHttpApiData ActivationCode where parseQueryParam = fmap ActivationCode . parseUrlPiece +deriving instance C.Cql ActivationCode + -------------------------------------------------------------------------------- -- Activate diff --git a/libs/wire-api/src/Wire/API/User/Client.hs b/libs/wire-api/src/Wire/API/User/Client.hs index c26b7c5b9b0..ecdc20531bd 100644 --- a/libs/wire-api/src/Wire/API/User/Client.hs +++ b/libs/wire-api/src/Wire/API/User/Client.hs @@ -68,7 +68,7 @@ module Wire.API.User.Client ) where -import Cassandra qualified as Cql +import Cassandra qualified as C import Control.Applicative import Control.Lens hiding (element, enum, set, (#), (.=)) import Data.Aeson (FromJSON (..), ToJSON (..)) @@ -157,12 +157,12 @@ instance ToSchema ClientCapability where enum @Text "ClientCapability" $ element "legalhold-implicit-consent" ClientSupportsLegalholdImplicitConsent -instance Cql.Cql ClientCapability where - ctype = Cql.Tagged Cql.IntColumn +instance C.Cql ClientCapability where + ctype = C.Tagged C.IntColumn - toCql ClientSupportsLegalholdImplicitConsent = Cql.CqlInt 1 + toCql ClientSupportsLegalholdImplicitConsent = C.CqlInt 1 - fromCql (Cql.CqlInt i) = case i of + fromCql (C.CqlInt i) = case i of 1 -> pure ClientSupportsLegalholdImplicitConsent n -> Left $ "Unexpected ClientCapability value: " ++ show n fromCql _ = Left "ClientCapability value: int expected" @@ -614,6 +614,17 @@ instance ToSchema ClientType where <> element "permanent" PermanentClientType <> element "legalhold" LegalHoldClientType +instance C.Cql ClientType where + ctype = C.Tagged C.IntColumn + toCql TemporaryClientType = C.CqlInt 0 + toCql PermanentClientType = C.CqlInt 1 + toCql LegalHoldClientType = C.CqlInt 2 + + fromCql (C.CqlInt 0) = pure TemporaryClientType + fromCql (C.CqlInt 1) = pure PermanentClientType + fromCql (C.CqlInt 2) = pure LegalHoldClientType + fromCql _ = Left "ClientType: Int [0, 2] expected" + data ClientClass = PhoneClient | TabletClient @@ -631,6 +642,19 @@ instance ToSchema ClientClass where <> element "desktop" DesktopClient <> element "legalhold" LegalHoldClient +instance C.Cql ClientClass where + ctype = C.Tagged C.IntColumn + toCql PhoneClient = C.CqlInt 0 + toCql TabletClient = C.CqlInt 1 + toCql DesktopClient = C.CqlInt 2 + toCql LegalHoldClient = C.CqlInt 3 + + fromCql (C.CqlInt 0) = pure PhoneClient + fromCql (C.CqlInt 1) = pure TabletClient + fromCql (C.CqlInt 2) = pure DesktopClient + fromCql (C.CqlInt 3) = pure LegalHoldClient + fromCql _ = Left "ClientClass: Int [0, 3] expected" + -------------------------------------------------------------------------------- -- NewClient diff --git a/libs/wire-api/src/Wire/API/User/Identity.hs b/libs/wire-api/src/Wire/API/User/Identity.hs index 2b88c1d3bd8..0475554bfeb 100644 --- a/libs/wire-api/src/Wire/API/User/Identity.hs +++ b/libs/wire-api/src/Wire/API/User/Identity.hs @@ -52,6 +52,7 @@ module Wire.API.User.Identity ) where +import Cassandra qualified as C import Control.Applicative (optional) import Control.Lens (dimap, over, (.~), (?~), (^.)) import Data.Aeson (FromJSON (..), ToJSON (..)) @@ -199,6 +200,16 @@ instance Arbitrary Email where domain <- Text.filter (/= '@') <$> arbitrary pure $ Email localPart domain +instance C.Cql Email where + ctype = C.Tagged C.TextColumn + + fromCql (C.CqlText t) = case parseEmail t of + Just e -> pure e + Nothing -> Left "fromCql: Invalid email" + fromCql _ = Left "fromCql: email: CqlText expected" + + toCql = C.toCql . fromEmail + fromEmail :: Email -> Text fromEmail (Email loc dom) = loc <> "@" <> dom @@ -283,6 +294,8 @@ instance Arbitrary Phone where maxi <- mkdigits =<< QC.chooseInt (0, 7) pure $ '+' : mini <> maxi +deriving instance C.Cql Phone + -- | Parses a phone number in E.164 format with a mandatory leading '+'. parsePhone :: Text -> Maybe Phone parsePhone p @@ -315,6 +328,16 @@ data UserSSOId deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform UserSSOId) +instance C.Cql UserSSOId where + ctype = C.Tagged C.TextColumn + + fromCql (C.CqlText t) = case A.eitherDecode $ cs t of + Right i -> pure i + Left msg -> Left $ "fromCql: Invalid UserSSOId: " ++ msg + fromCql _ = Left "fromCql: UserSSOId: CqlText expected" + + toCql = C.toCql . cs @LByteString @Text . A.encode + -- | FUTUREWORK: This schema should ideally be a choice of either tenant+subject, or scim_external_id -- but this is currently not possible to derive in swagger2 -- Maybe this becomes possible with swagger 3? diff --git a/libs/wire-api/src/Wire/API/User/Password.hs b/libs/wire-api/src/Wire/API/User/Password.hs index 4f14e4ca7c6..a4c3f92c2ae 100644 --- a/libs/wire-api/src/Wire/API/User/Password.hs +++ b/libs/wire-api/src/Wire/API/User/Password.hs @@ -31,6 +31,7 @@ module Wire.API.User.Password ) where +import Cassandra qualified as C import Control.Lens ((?~)) import Data.Aeson qualified as A import Data.Aeson.Types (Parser) @@ -180,6 +181,8 @@ instance ToParamSchema PasswordResetKey where instance FromHttpApiData PasswordResetKey where parseQueryParam = fmap PasswordResetKey . parseQueryParam +deriving instance C.Cql PasswordResetKey + -------------------------------------------------------------------------------- -- PasswordResetCode @@ -190,6 +193,8 @@ newtype PasswordResetCode = PasswordResetCode deriving newtype (ToSchema, FromByteString, ToByteString, A.FromJSON, A.ToJSON) deriving (Arbitrary) via (Ranged 6 1024 AsciiBase64Url) +deriving instance C.Cql PasswordResetCode + -------------------------------------------------------------------------------- -- DEPRECATED diff --git a/libs/wire-api/src/Wire/API/User/Profile.hs b/libs/wire-api/src/Wire/API/User/Profile.hs index ae018f20b75..dafefa4b4a1 100644 --- a/libs/wire-api/src/Wire/API/User/Profile.hs +++ b/libs/wire-api/src/Wire/API/User/Profile.hs @@ -49,6 +49,7 @@ module Wire.API.User.Profile ) where +import Cassandra qualified as C import Control.Applicative (optional) import Control.Error (hush, note) import Data.Aeson (FromJSON (..), ToJSON (..)) @@ -85,6 +86,8 @@ mkName txt = Name . fromRange <$> checkedEitherMsg @_ @1 @128 "Name" txt instance ToSchema Name where schema = Name <$> fromName .= untypedRangedSchema 1 128 schema +deriving instance C.Cql Name + -------------------------------------------------------------------------------- -- Colour @@ -96,6 +99,8 @@ newtype ColourId = ColourId {fromColourId :: Int32} defaultAccentId :: ColourId defaultAccentId = ColourId 0 +deriving instance C.Cql ColourId + -------------------------------------------------------------------------------- -- Asset @@ -121,6 +126,45 @@ instance ToSchema Asset where enum @Text @NamedSwaggerDoc "AssetType" $ element "image" () +instance C.Cql Asset where + -- Note: Type name and column names and types must match up with the + -- Cassandra schema definition. New fields may only be added + -- (appended) but no fields may be removed. + ctype = + C.Tagged + ( C.UdtColumn + "asset" + [ ("typ", C.IntColumn), + ("key", C.TextColumn), + ("size", C.MaybeColumn C.IntColumn) + ] + ) + + fromCql (C.CqlUdt fs) = do + t <- required "typ" + k <- required "key" + s <- notrequired "size" + case (t :: Int32) of + 0 -> pure $! ImageAsset k s + _ -> Left $ "unexpected user asset type: " ++ show t + where + required :: C.Cql r => Text -> Either String r + required f = + maybe + (Left ("Asset: Missing required field '" ++ show f ++ "'")) + C.fromCql + (lookup f fs) + notrequired f = maybe (Right Nothing) C.fromCql (lookup f fs) + fromCql _ = Left "UserAsset: UDT expected" + + -- Note: Order must match up with the 'ctype' definition. + toCql (ImageAsset k s) = + C.CqlUdt + [ ("typ", C.CqlInt 0), + ("key", C.toCql k), + ("size", C.toCql s) + ] + data AssetSize = AssetComplete | AssetPreview deriving stock (Eq, Show, Generic) deriving (Arbitrary) via (GenericUniform AssetSize) @@ -134,6 +178,16 @@ instance ToSchema AssetSize where element "complete" AssetComplete ] +instance C.Cql AssetSize where + ctype = C.Tagged C.IntColumn + + fromCql (C.CqlInt 0) = pure AssetPreview + fromCql (C.CqlInt 1) = pure AssetComplete + fromCql n = Left $ "Unexpected asset size: " ++ show n + + toCql AssetPreview = C.CqlInt 0 + toCql AssetComplete = C.CqlInt 1 + -------------------------------------------------------------------------------- -- Locale @@ -172,6 +226,15 @@ newtype Language = Language {fromLanguage :: ISO639_1} deriving stock (Eq, Ord, Show, Generic) deriving newtype (Arbitrary, S.ToSchema) +instance C.Cql Language where + ctype = C.Tagged C.AsciiColumn + toCql = C.toCql . lan2Text + + fromCql (C.CqlAscii l) = case parseLanguage l of + Just l' -> pure l' + Nothing -> Left "Language: ISO 639-1 expected." + fromCql _ = Left "Language: ASCII expected" + languageParser :: Parser Language languageParser = codeParser "language" $ fmap Language . checkAndConvert isLower @@ -188,6 +251,15 @@ newtype Country = Country {fromCountry :: CountryCode} deriving stock (Eq, Ord, Show, Generic) deriving newtype (Arbitrary, S.ToSchema) +instance C.Cql Country where + ctype = C.Tagged C.AsciiColumn + toCql = C.toCql . con2Text + + fromCql (C.CqlAscii c) = case parseCountry c of + Just c' -> pure c' + Nothing -> Left "Country: ISO 3166-1-alpha2 expected." + fromCql _ = Left "Country: ASCII expected" + countryParser :: Parser Country countryParser = codeParser "country" $ fmap Country . checkAndConvert isUpper @@ -243,6 +315,16 @@ instance FromByteString ManagedBy where "scim" -> pure ManagedByScim x -> fail $ "Invalid ManagedBy value: " <> show x +instance C.Cql ManagedBy where + ctype = C.Tagged C.IntColumn + + fromCql (C.CqlInt 0) = pure ManagedByWire + fromCql (C.CqlInt 1) = pure ManagedByScim + fromCql n = Left $ "Unexpected ManagedBy: " ++ show n + + toCql ManagedByWire = C.CqlInt 0 + toCql ManagedByScim = C.CqlInt 1 + defaultManagedBy :: ManagedBy defaultManagedBy = ManagedByWire @@ -262,6 +344,17 @@ instance ToSchema Pict where instance Arbitrary Pict where arbitrary = pure $ Pict [] +instance C.Cql Pict where + ctype = C.Tagged (C.ListColumn C.BlobColumn) + + fromCql (C.CqlList l) = do + vs <- map (\(C.Blob lbs) -> lbs) <$> mapM C.fromCql l + as <- mapM (note "Failed to read asset" . A.decode) vs + pure $ Pict as + fromCql _ = pure noPict + + toCql = C.toCql . map (C.Blob . A.encode) . fromPict + noPict :: Pict noPict = Pict [] diff --git a/libs/wire-api/src/Wire/API/User/RichInfo.hs b/libs/wire-api/src/Wire/API/User/RichInfo.hs index 32a3db8fa19..e6723b9d651 100644 --- a/libs/wire-api/src/Wire/API/User/RichInfo.hs +++ b/libs/wire-api/src/Wire/API/User/RichInfo.hs @@ -43,6 +43,7 @@ module Wire.API.User.RichInfo ) where +import Cassandra qualified as C import Control.Lens ((%~), (?~), _1) import Data.Aeson qualified as A import Data.Aeson.Key qualified as A @@ -319,6 +320,12 @@ instance Arbitrary RichInfoAssocList where arbitrary = mkRichInfoAssocList <$> arbitrary shrink (RichInfoAssocList things) = mkRichInfoAssocList <$> QC.shrink things +instance C.Cql RichInfoAssocList where + ctype = C.Tagged C.BlobColumn + toCql = C.toCql . C.Blob . A.encode + fromCql (C.CqlBlob v) = A.eitherDecode v + fromCql _ = Left "RichInfo: Blob expected" + -------------------------------------------------------------------------------- -- RichField diff --git a/libs/wire-api/src/Wire/API/User/Search.hs b/libs/wire-api/src/Wire/API/User/Search.hs index 375e1f07dc2..8b2fa6fb710 100644 --- a/libs/wire-api/src/Wire/API/User/Search.hs +++ b/libs/wire-api/src/Wire/API/User/Search.hs @@ -33,6 +33,7 @@ module Wire.API.User.Search ) where +import Cassandra qualified as C import Control.Error import Control.Lens (makePrisms, (?~)) import Data.Aeson hiding (object, (.=)) @@ -317,4 +318,16 @@ instance ToSchema FederatedUserSearchPolicy where <> element "exact_handle_search" ExactHandleSearch <> element "full_search" FullSearch +instance C.Cql FederatedUserSearchPolicy where + ctype = C.Tagged C.IntColumn + + toCql NoSearch = C.CqlInt 0 + toCql ExactHandleSearch = C.CqlInt 1 + toCql FullSearch = C.CqlInt 2 + + fromCql (C.CqlInt 0) = pure NoSearch + fromCql (C.CqlInt 1) = pure ExactHandleSearch + fromCql (C.CqlInt 2) = pure FullSearch + fromCql n = Left $ "Unexpected SearchVisibilityInbound: " ++ show n + makePrisms ''FederatedUserSearchPolicy diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index f24644d2d67..a57c21f5e6c 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -110,7 +110,6 @@ library Brig.Data.Activation Brig.Data.Client Brig.Data.Connection - Brig.Data.Instances Brig.Data.LoginCode Brig.Data.MLS.KeyPackage Brig.Data.Nonce diff --git a/services/brig/src/Brig/Code.hs b/services/brig/src/Brig/Code.hs index 7840b8e3cb5..2ea506aa5e7 100644 --- a/services/brig/src/Brig/Code.hs +++ b/services/brig/src/Brig/Code.hs @@ -60,7 +60,6 @@ module Brig.Code ) where -import Brig.Data.Instances () import Brig.Email (emailKeyUniq, mkEmailKey) import Brig.Phone (mkPhoneKey, phoneKeyUniq) import Cassandra hiding (Value) diff --git a/services/brig/src/Brig/Data/Client.hs b/services/brig/src/Brig/Data/Client.hs index 9b864391503..69e9ac0b829 100644 --- a/services/brig/src/Brig/Data/Client.hs +++ b/services/brig/src/Brig/Data/Client.hs @@ -56,7 +56,6 @@ import Amazonka.DynamoDB.Lens qualified as AWS import Bilge.Retry (httpHandlers) import Brig.AWS import Brig.App -import Brig.Data.Instances () import Brig.Data.User (AuthError (..), ReAuthError (..)) import Brig.Data.User qualified as User import Brig.Types.Instances () diff --git a/services/brig/src/Brig/Data/Connection.hs b/services/brig/src/Brig/Data/Connection.hs index b624a7d9447..ec46f3fe406 100644 --- a/services/brig/src/Brig/Data/Connection.hs +++ b/services/brig/src/Brig/Data/Connection.hs @@ -52,7 +52,6 @@ module Brig.Data.Connection where import Brig.App -import Brig.Data.Instances () import Brig.Data.Types as T import Cassandra import Control.Monad.Morph diff --git a/services/brig/src/Brig/Data/Instances.hs b/services/brig/src/Brig/Data/Instances.hs deleted file mode 100644 index 34309315771..00000000000 --- a/services/brig/src/Brig/Data/Instances.hs +++ /dev/null @@ -1,319 +0,0 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} -{-# OPTIONS_GHC -fno-warn-orphans #-} - --- This file is part of the Wire Server implementation. --- --- Copyright (C) 2022 Wire Swiss GmbH --- --- This program is free software: you can redistribute it and/or modify it under --- the terms of the GNU Affero General Public License as published by the Free --- Software Foundation, either version 3 of the License, or (at your option) any --- later version. --- --- This program is distributed in the hope that it will be useful, but WITHOUT --- ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS --- FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more --- details. --- --- You should have received a copy of the GNU Affero General Public License along --- with this program. If not, see . - -module Brig.Data.Instances - ( - ) -where - -import Brig.Types.Common -import Brig.Types.Search -import Cassandra.CQL -import Control.Error (note) -import Data.Aeson (eitherDecode, encode) -import Data.Aeson qualified as JSON -import Data.ByteString.Conversion -import Data.Domain (Domain, domainText, mkDomain) -import Data.Handle (Handle (..)) -import Data.Id () -import Data.Range () -import Data.Text.Ascii () -import Data.Text.Encoding (encodeUtf8) -import Imports -import Wire.API.Asset (AssetKey, assetKeyToText, nilAssetKey) -import Wire.API.Connection (RelationWithHistory (..)) -import Wire.API.MLS.CipherSuite -import Wire.API.Properties -import Wire.API.User -import Wire.API.User.Activation -import Wire.API.User.Client -import Wire.API.User.Password -import Wire.API.User.RichInfo -import Wire.API.User.Search - -deriving instance Cql Name - -deriving instance Cql Handle - -deriving instance Cql ColourId - -deriving instance Cql Phone - -deriving instance Cql InvitationCode - -deriving instance Cql PasswordResetKey - -deriving instance Cql PasswordResetCode - -deriving instance Cql ActivationKey - -deriving instance Cql ActivationCode - -deriving instance Cql PropertyKey - -deriving instance Cql PhonePrefix - -instance Cql Email where - ctype = Tagged TextColumn - - fromCql (CqlText t) = case parseEmail t of - Just e -> pure e - Nothing -> Left "fromCql: Invalid email" - fromCql _ = Left "fromCql: email: CqlText expected" - - toCql = toCql . fromEmail - -instance Cql UserSSOId where - ctype = Tagged TextColumn - - fromCql (CqlText t) = case eitherDecode $ cs t of - Right i -> pure i - Left msg -> Left $ "fromCql: Invalid UserSSOId: " ++ msg - fromCql _ = Left "fromCql: UserSSOId: CqlText expected" - - toCql = toCql . cs @LByteString @Text . encode - -instance Cql RelationWithHistory where - ctype = Tagged IntColumn - - fromCql (CqlInt i) = case i of - 0 -> pure AcceptedWithHistory - 1 -> pure BlockedWithHistory - 2 -> pure PendingWithHistory - 3 -> pure IgnoredWithHistory - 4 -> pure SentWithHistory - 5 -> pure CancelledWithHistory - 6 -> pure MissingLegalholdConsentFromAccepted - 7 -> pure MissingLegalholdConsentFromBlocked - 8 -> pure MissingLegalholdConsentFromPending - 9 -> pure MissingLegalholdConsentFromIgnored - 10 -> pure MissingLegalholdConsentFromSent - 11 -> pure MissingLegalholdConsentFromCancelled - n -> Left $ "unexpected RelationWithHistory: " ++ show n - fromCql _ = Left "RelationWithHistory: int expected" - - toCql AcceptedWithHistory = CqlInt 0 - toCql BlockedWithHistory = CqlInt 1 - toCql PendingWithHistory = CqlInt 2 - toCql IgnoredWithHistory = CqlInt 3 - toCql SentWithHistory = CqlInt 4 - toCql CancelledWithHistory = CqlInt 5 - toCql MissingLegalholdConsentFromAccepted = CqlInt 6 - toCql MissingLegalholdConsentFromBlocked = CqlInt 7 - toCql MissingLegalholdConsentFromPending = CqlInt 8 - toCql MissingLegalholdConsentFromIgnored = CqlInt 9 - toCql MissingLegalholdConsentFromSent = CqlInt 10 - toCql MissingLegalholdConsentFromCancelled = CqlInt 11 - --- DEPRECATED -instance Cql Pict where - ctype = Tagged (ListColumn BlobColumn) - - fromCql (CqlList l) = do - vs <- map (\(Blob lbs) -> lbs) <$> mapM fromCql l - as <- mapM (note "Failed to read asset" . JSON.decode) vs - pure $ Pict as - fromCql _ = pure noPict - - toCql = toCql . map (Blob . JSON.encode) . fromPict - -instance Cql AssetKey where - ctype = Tagged TextColumn - toCql = CqlText . assetKeyToText - - -- if the asset key is invalid we will return the nil asset key (`3-1-00000000-0000-0000-0000-000000000000`) - fromCql (CqlText txt) = pure $ fromRight nilAssetKey $ runParser parser $ encodeUtf8 txt - fromCql _ = Left "AssetKey: Expected CqlText" - -instance Cql AssetSize where - ctype = Tagged IntColumn - - fromCql (CqlInt 0) = pure AssetPreview - fromCql (CqlInt 1) = pure AssetComplete - fromCql n = Left $ "Unexpected asset size: " ++ show n - - toCql AssetPreview = CqlInt 0 - toCql AssetComplete = CqlInt 1 - -instance Cql Asset where - -- Note: Type name and column names and types must match up with the - -- Cassandra schema definition. New fields may only be added - -- (appended) but no fields may be removed. - ctype = - Tagged - ( UdtColumn - "asset" - [ ("typ", IntColumn), - ("key", TextColumn), - ("size", MaybeColumn IntColumn) - ] - ) - - fromCql (CqlUdt fs) = do - t <- required "typ" - k <- required "key" - s <- optional "size" - case (t :: Int32) of - 0 -> pure $! ImageAsset k s - _ -> Left $ "unexpected user asset type: " ++ show t - where - required :: Cql r => Text -> Either String r - required f = - maybe - (Left ("Asset: Missing required field '" ++ show f ++ "'")) - fromCql - (lookup f fs) - optional f = maybe (Right Nothing) fromCql (lookup f fs) - fromCql _ = Left "UserAsset: UDT expected" - - -- Note: Order must match up with the 'ctype' definition. - toCql (ImageAsset k s) = - CqlUdt - [ ("typ", CqlInt 0), - ("key", toCql k), - ("size", toCql s) - ] - -instance Cql AccountStatus where - ctype = Tagged IntColumn - - toCql Active = CqlInt 0 - toCql Suspended = CqlInt 1 - toCql Deleted = CqlInt 2 - toCql Ephemeral = CqlInt 3 - toCql PendingInvitation = CqlInt 4 - - fromCql (CqlInt i) = case i of - 0 -> pure Active - 1 -> pure Suspended - 2 -> pure Deleted - 3 -> pure Ephemeral - 4 -> pure PendingInvitation - n -> Left $ "unexpected account status: " ++ show n - fromCql _ = Left "account status: int expected" - -instance Cql ClientType where - ctype = Tagged IntColumn - toCql TemporaryClientType = CqlInt 0 - toCql PermanentClientType = CqlInt 1 - toCql LegalHoldClientType = CqlInt 2 - - fromCql (CqlInt 0) = pure TemporaryClientType - fromCql (CqlInt 1) = pure PermanentClientType - fromCql (CqlInt 2) = pure LegalHoldClientType - fromCql _ = Left "ClientType: Int [0, 2] expected" - -instance Cql ClientClass where - ctype = Tagged IntColumn - toCql PhoneClient = CqlInt 0 - toCql TabletClient = CqlInt 1 - toCql DesktopClient = CqlInt 2 - toCql LegalHoldClient = CqlInt 3 - - fromCql (CqlInt 0) = pure PhoneClient - fromCql (CqlInt 1) = pure TabletClient - fromCql (CqlInt 2) = pure DesktopClient - fromCql (CqlInt 3) = pure LegalHoldClient - fromCql _ = Left "ClientClass: Int [0, 3] expected" - -instance Cql RawPropertyValue where - ctype = Tagged BlobColumn - toCql = toCql . Blob . rawPropertyBytes - fromCql (CqlBlob v) = pure (RawPropertyValue v) - fromCql _ = Left "PropertyValue: Blob expected" - -instance Cql Country where - ctype = Tagged AsciiColumn - toCql = toCql . con2Text - - fromCql (CqlAscii c) = case parseCountry c of - Just c' -> pure c' - Nothing -> Left "Country: ISO 3166-1-alpha2 expected." - fromCql _ = Left "Country: ASCII expected" - -instance Cql Language where - ctype = Tagged AsciiColumn - toCql = toCql . lan2Text - - fromCql (CqlAscii l) = case parseLanguage l of - Just l' -> pure l' - Nothing -> Left "Language: ISO 639-1 expected." - fromCql _ = Left "Language: ASCII expected" - -instance Cql ManagedBy where - ctype = Tagged IntColumn - - fromCql (CqlInt 0) = pure ManagedByWire - fromCql (CqlInt 1) = pure ManagedByScim - fromCql n = Left $ "Unexpected ManagedBy: " ++ show n - - toCql ManagedByWire = CqlInt 0 - toCql ManagedByScim = CqlInt 1 - -instance Cql RichInfoAssocList where - ctype = Tagged BlobColumn - toCql = toCql . Blob . JSON.encode - fromCql (CqlBlob v) = JSON.eitherDecode v - fromCql _ = Left "RichInfo: Blob expected" - -instance Cql Domain where - ctype = Tagged TextColumn - toCql = CqlText . domainText - fromCql (CqlText txt) = mkDomain txt - fromCql _ = Left "Domain: Text expected" - -instance Cql SearchVisibilityInbound where - ctype = Tagged IntColumn - - toCql SearchableByOwnTeam = CqlInt 0 - toCql SearchableByAllTeams = CqlInt 1 - - fromCql (CqlInt 0) = pure SearchableByOwnTeam - fromCql (CqlInt 1) = pure SearchableByAllTeams - fromCql n = Left $ "Unexpected SearchVisibilityInbound: " ++ show n - -instance Cql FederatedUserSearchPolicy where - ctype = Tagged IntColumn - - toCql NoSearch = CqlInt 0 - toCql ExactHandleSearch = CqlInt 1 - toCql FullSearch = CqlInt 2 - - fromCql (CqlInt 0) = pure NoSearch - fromCql (CqlInt 1) = pure ExactHandleSearch - fromCql (CqlInt 2) = pure FullSearch - fromCql n = Left $ "Unexpected SearchVisibilityInbound: " ++ show n - -instance Cql (Imports.Set BaseProtocolTag) where - ctype = Tagged IntColumn - - toCql = CqlInt . fromIntegral . protocolSetBits - fromCql (CqlInt bits) = pure $ protocolSetFromBits (fromIntegral bits) - fromCql _ = Left "Protocol set: Int expected" - -instance Cql CipherSuiteTag where - ctype = Tagged IntColumn - toCql = CqlInt . fromIntegral . cipherSuiteNumber . tagCipherSuite - - fromCql (CqlInt index) = - case cipherSuiteTag (CipherSuite (fromIntegral index)) of - Just tag -> Right tag - Nothing -> Left "CipherSuiteTag: unexpected index" - fromCql _ = Left "CipherSuiteTag: int expected" diff --git a/services/brig/src/Brig/Data/LoginCode.hs b/services/brig/src/Brig/Data/LoginCode.hs index 197d28aa8d8..1f58dba7cc7 100644 --- a/services/brig/src/Brig/Data/LoginCode.hs +++ b/services/brig/src/Brig/Data/LoginCode.hs @@ -27,7 +27,6 @@ module Brig.Data.LoginCode where import Brig.App (Env, currentTime) -import Brig.Data.Instances () import Brig.User.Auth.DB.Instances () import Cassandra import Control.Lens (view) diff --git a/services/brig/src/Brig/Data/Nonce.hs b/services/brig/src/Brig/Data/Nonce.hs index 83f11d1b78f..8ca18fd1a53 100644 --- a/services/brig/src/Brig/Data/Nonce.hs +++ b/services/brig/src/Brig/Data/Nonce.hs @@ -21,7 +21,6 @@ module Brig.Data.Nonce ) where -import Brig.Data.Instances () import Cassandra import Control.Lens hiding (from) import Data.Id (UserId) diff --git a/services/brig/src/Brig/Data/Properties.hs b/services/brig/src/Brig/Data/Properties.hs index 4519fe9d3ad..b073394584f 100644 --- a/services/brig/src/Brig/Data/Properties.hs +++ b/services/brig/src/Brig/Data/Properties.hs @@ -26,7 +26,6 @@ module Brig.Data.Properties ) where -import Brig.Data.Instances () import Cassandra import Control.Error import Data.Id diff --git a/services/brig/src/Brig/Data/User.hs b/services/brig/src/Brig/Data/User.hs index 053e472a997..5eb86970276 100644 --- a/services/brig/src/Brig/Data/User.hs +++ b/services/brig/src/Brig/Data/User.hs @@ -76,7 +76,6 @@ module Brig.Data.User where import Brig.App (Env, currentTime, settings, viewFederationDomain, zauthEnv) -import Brig.Data.Instances () import Brig.Options import Brig.Types.Intra import Brig.ZAuth qualified as ZAuth diff --git a/services/brig/src/Brig/Data/UserKey.hs b/services/brig/src/Brig/Data/UserKey.hs index 6487a77e730..11128014a24 100644 --- a/services/brig/src/Brig/Data/UserKey.hs +++ b/services/brig/src/Brig/Data/UserKey.hs @@ -33,7 +33,6 @@ module Brig.Data.UserKey ) where -import Brig.Data.Instances () import Brig.Data.User qualified as User import Brig.Email import Brig.Phone diff --git a/services/brig/src/Brig/Effects/CodeStore/Cassandra.hs b/services/brig/src/Brig/Effects/CodeStore/Cassandra.hs index e6cae090996..26d4d2c7f32 100644 --- a/services/brig/src/Brig/Effects/CodeStore/Cassandra.hs +++ b/services/brig/src/Brig/Effects/CodeStore/Cassandra.hs @@ -22,7 +22,6 @@ module Brig.Effects.CodeStore.Cassandra ) where -import Brig.Data.Instances () import Brig.Effects.CodeStore import Cassandra import Data.ByteString.Conversion (toByteString') diff --git a/services/brig/src/Brig/Effects/FederationConfigStore/Cassandra.hs b/services/brig/src/Brig/Effects/FederationConfigStore/Cassandra.hs index 0d348dd4e3b..bc9fcd8f7b6 100644 --- a/services/brig/src/Brig/Effects/FederationConfigStore/Cassandra.hs +++ b/services/brig/src/Brig/Effects/FederationConfigStore/Cassandra.hs @@ -22,7 +22,6 @@ module Brig.Effects.FederationConfigStore.Cassandra ) where -import Brig.Data.Instances () import Brig.Effects.FederationConfigStore import Cassandra import Control.Exception (ErrorCall (ErrorCall)) diff --git a/services/brig/src/Brig/Provider/DB.hs b/services/brig/src/Brig/Provider/DB.hs index e83919f5bb0..77aefc29ea5 100644 --- a/services/brig/src/Brig/Provider/DB.hs +++ b/services/brig/src/Brig/Provider/DB.hs @@ -17,7 +17,6 @@ module Brig.Provider.DB where -import Brig.Data.Instances () import Brig.Email (EmailKey, emailKeyOrig, emailKeyUniq) import Brig.Types.Instances () import Brig.Types.Provider.Tag diff --git a/services/brig/src/Brig/Team/DB.hs b/services/brig/src/Brig/Team/DB.hs index fb62d5edf69..97db9efcded 100644 --- a/services/brig/src/Brig/Team/DB.hs +++ b/services/brig/src/Brig/Team/DB.hs @@ -38,7 +38,6 @@ module Brig.Team.DB where import Brig.App as App -import Brig.Data.Instances () import Brig.Data.Types as T import Brig.Options import Brig.Team.Template diff --git a/services/brig/src/Brig/Unique.hs b/services/brig/src/Brig/Unique.hs index 6c5d5f0a2a8..58c95630a8a 100644 --- a/services/brig/src/Brig/Unique.hs +++ b/services/brig/src/Brig/Unique.hs @@ -29,7 +29,6 @@ module Brig.Unique ) where -import Brig.Data.Instances () import Cassandra as C import Control.Concurrent.Timeout import Data.Id diff --git a/services/brig/src/Brig/User/Handle.hs b/services/brig/src/Brig/User/Handle.hs index 4335f76b4c2..fd62c770c3c 100644 --- a/services/brig/src/Brig/User/Handle.hs +++ b/services/brig/src/Brig/User/Handle.hs @@ -26,7 +26,6 @@ where import Brig.App import Brig.CanonicalInterpreter (runBrigToIO) -import Brig.Data.Instances () import Brig.Data.User qualified as User import Brig.Unique import Cassandra diff --git a/services/brig/src/Brig/User/Search/Index.hs b/services/brig/src/Brig/User/Search/Index.hs index 4428d8bdeed..b3a0b0834e5 100644 --- a/services/brig/src/Brig/User/Search/Index.hs +++ b/services/brig/src/Brig/User/Search/Index.hs @@ -56,7 +56,6 @@ import Bilge.RPC (RPCException (RPCException)) import Bilge.Request qualified as RPC (empty, host, method, port) import Bilge.Response (responseJsonThrow) import Bilge.Retry (rpcHandlers) -import Brig.Data.Instances () import Brig.Index.Types (CreateIndexSettings (..)) import Brig.Types.Search (SearchVisibilityInbound, defaultSearchVisibilityInbound, searchVisibilityInboundFromFeatureStatus) import Brig.User.Search.Index.Types as Types diff --git a/services/brig/src/Brig/User/Search/SearchIndex.hs b/services/brig/src/Brig/User/Search/SearchIndex.hs index b70fcf3deb3..d9803fff6b5 100644 --- a/services/brig/src/Brig/User/Search/SearchIndex.hs +++ b/services/brig/src/Brig/User/Search/SearchIndex.hs @@ -25,7 +25,6 @@ module Brig.User.Search.SearchIndex where import Brig.App (Env, viewFederationDomain) -import Brig.Data.Instances () import Brig.Types.Search import Brig.User.Search.Index import Control.Lens hiding (setting, (#), (.=)) diff --git a/services/brig/src/Brig/User/Search/TeamSize.hs b/services/brig/src/Brig/User/Search/TeamSize.hs index fa13b843bf4..1fd23bbf1c3 100644 --- a/services/brig/src/Brig/User/Search/TeamSize.hs +++ b/services/brig/src/Brig/User/Search/TeamSize.hs @@ -22,7 +22,6 @@ module Brig.User.Search.TeamSize ) where -import Brig.Data.Instances () import Brig.Types.Team (TeamSize (..)) import Brig.User.Search.Index import Control.Monad.Catch (throwM) diff --git a/services/brig/src/Brig/User/Search/TeamUserSearch.hs b/services/brig/src/Brig/User/Search/TeamUserSearch.hs index 1b60532c063..9722be1ad74 100644 --- a/services/brig/src/Brig/User/Search/TeamUserSearch.hs +++ b/services/brig/src/Brig/User/Search/TeamUserSearch.hs @@ -29,7 +29,6 @@ module Brig.User.Search.TeamUserSearch ) where -import Brig.Data.Instances () import Brig.User.Search.Index import Control.Error (lastMay) import Control.Monad.Catch (MonadThrow (throwM)) diff --git a/services/galley/src/Galley/Cassandra/Instances.hs b/services/galley/src/Galley/Cassandra/Instances.hs index 7e9c9f43406..d1911434dd4 100644 --- a/services/galley/src/Galley/Cassandra/Instances.hs +++ b/services/galley/src/Galley/Cassandra/Instances.hs @@ -27,13 +27,11 @@ import Cassandra.CQL import Control.Error (note) import Data.ByteString.Conversion import Data.ByteString.Lazy qualified as LBS -import Data.Domain (Domain, domainText, mkDomain) import Data.Either.Combinators hiding (fromRight) import Data.Text qualified as T import Data.Text.Encoding qualified as T import Galley.Types.Bot () import Imports -import Wire.API.Asset (AssetKey, assetKeyToText) import Wire.API.Conversation import Wire.API.Conversation.Protocol import Wire.API.MLS.CipherSuite @@ -164,12 +162,6 @@ instance Cql TeamSearchVisibility where toCql SearchVisibilityStandard = CqlInt 0 toCql SearchVisibilityNoNameOutsideTeam = CqlInt 1 -instance Cql Domain where - ctype = Tagged TextColumn - toCql = CqlText . domainText - fromCql (CqlText txt) = mkDomain txt - fromCql _ = Left "Domain: Text expected" - instance Cql Public.EnforceAppLock where ctype = Tagged IntColumn toCql (Public.EnforceAppLock False) = CqlInt 0 @@ -214,28 +206,12 @@ instance Cql Icon where fromCql (CqlText txt) = pure . fromRight DefaultIcon . runParser parser . T.encodeUtf8 $ txt fromCql _ = Left "Icon: Text expected" -instance Cql AssetKey where - ctype = Tagged TextColumn - toCql = CqlText . assetKeyToText - fromCql (CqlText txt) = runParser parser . T.encodeUtf8 $ txt - fromCql _ = Left "AssetKey: Text expected" - instance Cql Epoch where ctype = Tagged BigIntColumn toCql = CqlBigInt . fromIntegral . epochNumber fromCql (CqlBigInt n) = pure (Epoch (fromIntegral n)) fromCql _ = Left "epoch: bigint expected" -instance Cql CipherSuiteTag where - ctype = Tagged IntColumn - toCql = CqlInt . fromIntegral . cipherSuiteNumber . tagCipherSuite - - fromCql (CqlInt index) = - case cipherSuiteTag (CipherSuite (fromIntegral index)) of - Just tag -> Right tag - Nothing -> Left "CipherSuiteTag: unexpected index" - fromCql _ = Left "CipherSuiteTag: int expected" - instance Cql ProposalRef where ctype = Tagged BlobColumn toCql = CqlBlob . LBS.fromStrict . unProposalRef diff --git a/tools/db/auto-whitelist/auto-whitelist.cabal b/tools/db/auto-whitelist/auto-whitelist.cabal index f62ee357952..09239aa43c9 100644 --- a/tools/db/auto-whitelist/auto-whitelist.cabal +++ b/tools/db/auto-whitelist/auto-whitelist.cabal @@ -75,6 +75,5 @@ executable auto-whitelist , tinylog , types-common , unliftio - , wire-api default-language: GHC2021 diff --git a/tools/db/auto-whitelist/default.nix b/tools/db/auto-whitelist/default.nix index d749f584c97..e7504cef918 100644 --- a/tools/db/auto-whitelist/default.nix +++ b/tools/db/auto-whitelist/default.nix @@ -14,7 +14,6 @@ , tinylog , types-common , unliftio -, wire-api }: mkDerivation { pname = "auto-whitelist"; @@ -32,7 +31,6 @@ mkDerivation { tinylog types-common unliftio - wire-api ]; description = "Backfill service tables"; license = lib.licenses.agpl3Only; diff --git a/tools/db/auto-whitelist/src/Work.hs b/tools/db/auto-whitelist/src/Work.hs index 709ab1f4b9b..b3976b6447b 100644 --- a/tools/db/auto-whitelist/src/Work.hs +++ b/tools/db/auto-whitelist/src/Work.hs @@ -1,7 +1,5 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE StandaloneDeriving #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -- This file is part of the Wire Server implementation. @@ -33,9 +31,6 @@ import Imports import System.Logger (Logger) import System.Logger qualified as Log import UnliftIO.Async (pooledMapConcurrentlyN_) -import Wire.API.User - -deriving instance Cql Name runCommand :: Logger -> ClientState -> IO () runCommand l brig = runClient brig $ do diff --git a/tools/db/find-undead/src/Work.hs b/tools/db/find-undead/src/Work.hs index 64192cea6d8..4803e7dfb24 100644 --- a/tools/db/find-undead/src/Work.hs +++ b/tools/db/find-undead/src/Work.hs @@ -123,23 +123,3 @@ data WorkError instance Exception WorkError type Name = Text - --- FUTUREWORK: you can avoid this by loading brig-the-service as a library: --- @"services/brig/src/Brig/Data/Instances.hs:165:instance Cql AccountStatus where"@ -instance Cql AccountStatus where - ctype = Tagged IntColumn - - toCql Active = CqlInt 0 - toCql Suspended = CqlInt 1 - toCql Deleted = CqlInt 2 - toCql Ephemeral = CqlInt 3 - toCql PendingInvitation = CqlInt 4 - - fromCql (CqlInt i) = case i of - 0 -> pure Active - 1 -> pure Suspended - 2 -> pure Deleted - 3 -> pure Ephemeral - 4 -> pure PendingInvitation - n -> Left $ "unexpected account status: " ++ show n - fromCql _ = Left "account status: int expected" diff --git a/tools/db/inconsistencies/src/DanglingHandles.hs b/tools/db/inconsistencies/src/DanglingHandles.hs index d8bee670266..cd0bf2b178a 100644 --- a/tools/db/inconsistencies/src/DanglingHandles.hs +++ b/tools/db/inconsistencies/src/DanglingHandles.hs @@ -21,7 +21,6 @@ module DanglingHandles where -import Brig.Data.Instances () import Cassandra import Cassandra.Util import Conduit diff --git a/tools/db/inconsistencies/src/DanglingUserKeys.hs b/tools/db/inconsistencies/src/DanglingUserKeys.hs index 7d04d8348f3..0bf093984dc 100644 --- a/tools/db/inconsistencies/src/DanglingUserKeys.hs +++ b/tools/db/inconsistencies/src/DanglingUserKeys.hs @@ -22,7 +22,6 @@ module DanglingUserKeys where -import Brig.Data.Instances () import Brig.Data.UserKey import Brig.Email (EmailKey (..), mkEmailKey) import Brig.Phone (PhoneKey (..), mkPhoneKey) diff --git a/tools/db/inconsistencies/src/EmailLessUsers.hs b/tools/db/inconsistencies/src/EmailLessUsers.hs index 5ac4cb87389..e1193952194 100644 --- a/tools/db/inconsistencies/src/EmailLessUsers.hs +++ b/tools/db/inconsistencies/src/EmailLessUsers.hs @@ -21,7 +21,6 @@ module EmailLessUsers where -import Brig.Data.Instances () import Brig.Data.UserKey import Brig.Email import Cassandra diff --git a/tools/db/inconsistencies/src/HandleLessUsers.hs b/tools/db/inconsistencies/src/HandleLessUsers.hs index 21901ed02e8..2a6324a3a1d 100644 --- a/tools/db/inconsistencies/src/HandleLessUsers.hs +++ b/tools/db/inconsistencies/src/HandleLessUsers.hs @@ -21,7 +21,6 @@ module HandleLessUsers where -import Brig.Data.Instances () import Cassandra import Cassandra.Util import Conduit diff --git a/tools/db/migrate-sso-feature-flag/src/Work.hs b/tools/db/migrate-sso-feature-flag/src/Work.hs index d64570ce438..36f5d5aba9b 100644 --- a/tools/db/migrate-sso-feature-flag/src/Work.hs +++ b/tools/db/migrate-sso-feature-flag/src/Work.hs @@ -1,7 +1,5 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE StandaloneDeriving #-} {-# OPTIONS_GHC -Wno-orphans -Wno-unused-imports #-} -- This file is part of the Wire Server implementation. @@ -35,9 +33,6 @@ import System.Logger (Logger) import System.Logger qualified as Log import UnliftIO.Async (pooledMapConcurrentlyN) import Wire.API.Team.Feature -import Wire.API.User - -deriving instance Cql Name runCommand :: Logger -> ClientState -> ClientState -> IO () runCommand l spar galley = do diff --git a/tools/db/move-team/src/Types.hs b/tools/db/move-team/src/Types.hs index d10e7786049..3c3cfe4d722 100644 --- a/tools/db/move-team/src/Types.hs +++ b/tools/db/move-team/src/Types.hs @@ -29,7 +29,6 @@ import Cassandra import Data.Aeson (FromJSON (..), ToJSON (..), Value (String), withArray, withText) import Data.Aeson.Types (Value (Array)) import Data.ByteString.Lazy (fromStrict, toStrict) -import Data.Handle import Data.IP (IP (..)) import Data.Id import Data.Text qualified as T @@ -38,7 +37,6 @@ import Data.Vector qualified as V import Galley.Cassandra.Instances () import Imports import System.Logger (Logger) -import Wire.API.User.Password (PasswordResetKey (..)) data Env = Env { envLogger :: Logger, @@ -98,10 +96,6 @@ instance FromJSON IP where Nothing -> fail "not a formatted IP address" Just ip -> pure ip -deriving instance Cql Handle - -deriving instance Cql PasswordResetKey - deriving instance ToJSON Ascii deriving instance FromJSON Ascii diff --git a/tools/db/move-team/src/Work.hs b/tools/db/move-team/src/Work.hs index 0472b082790..b8a75722080 100644 --- a/tools/db/move-team/src/Work.hs +++ b/tools/db/move-team/src/Work.hs @@ -1,9 +1,7 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PartialTypeSignatures #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE StandaloneDeriving #-} {-# OPTIONS_GHC -Wno-orphans -Wno-unused-imports #-} -- This file is part of the Wire Server implementation. @@ -46,9 +44,6 @@ import System.IO qualified as IO import System.Logger qualified as Log import System.Process (system) import Types -import Wire.API.User - -deriving instance Cql Name assertTargetDirEmpty :: Env -> IO () assertTargetDirEmpty Env {..} = do diff --git a/tools/db/repair-handles/default.nix b/tools/db/repair-handles/default.nix index c97deb7b45f..24336b506ea 100644 --- a/tools/db/repair-handles/default.nix +++ b/tools/db/repair-handles/default.nix @@ -4,7 +4,6 @@ # dependencies are added or removed. { mkDerivation , base -, brig , cassandra-util , conduit , containers @@ -27,7 +26,6 @@ mkDerivation { isExecutable = true; executableHaskellDepends = [ base - brig cassandra-util conduit containers diff --git a/tools/db/repair-handles/repair-handles.cabal b/tools/db/repair-handles/repair-handles.cabal index 79393d75a68..bd081ab8c21 100644 --- a/tools/db/repair-handles/repair-handles.cabal +++ b/tools/db/repair-handles/repair-handles.cabal @@ -67,7 +67,6 @@ executable repair-handles build-depends: base - , brig , cassandra-util , conduit , containers diff --git a/tools/db/repair-handles/src/Options.hs b/tools/db/repair-handles/src/Options.hs index 74a4761822b..ba506f8da51 100644 --- a/tools/db/repair-handles/src/Options.hs +++ b/tools/db/repair-handles/src/Options.hs @@ -17,7 +17,6 @@ module Options where -import Brig.Data.Instances () import Cassandra hiding (Set) import Data.Id import Data.UUID diff --git a/tools/db/repair-handles/src/Types.hs b/tools/db/repair-handles/src/Types.hs index 75e64fc272a..7409d1647ec 100644 --- a/tools/db/repair-handles/src/Types.hs +++ b/tools/db/repair-handles/src/Types.hs @@ -19,7 +19,6 @@ module Types where -import Brig.Data.Instances () import Cassandra hiding (Set) import Control.Lens import Data.Id diff --git a/tools/db/repair-handles/src/Work.hs b/tools/db/repair-handles/src/Work.hs index 313c4021179..7d7fcdbde73 100644 --- a/tools/db/repair-handles/src/Work.hs +++ b/tools/db/repair-handles/src/Work.hs @@ -19,7 +19,6 @@ module Work where -import Brig.Data.Instances () import Cassandra hiding (Set) import Cassandra qualified as Cas import Cassandra.Settings qualified as Cas diff --git a/tools/db/service-backfill/default.nix b/tools/db/service-backfill/default.nix index c778afe2db0..b4dbc2d6007 100644 --- a/tools/db/service-backfill/default.nix +++ b/tools/db/service-backfill/default.nix @@ -14,7 +14,6 @@ , tinylog , types-common , unliftio -, wire-api }: mkDerivation { pname = "service-backfill"; @@ -32,7 +31,6 @@ mkDerivation { tinylog types-common unliftio - wire-api ]; description = "Backfill service tables"; license = lib.licenses.agpl3Only; diff --git a/tools/db/service-backfill/service-backfill.cabal b/tools/db/service-backfill/service-backfill.cabal index e725bbb75d3..c6806e06a39 100644 --- a/tools/db/service-backfill/service-backfill.cabal +++ b/tools/db/service-backfill/service-backfill.cabal @@ -75,6 +75,5 @@ executable service-backfill , tinylog , types-common , unliftio - , wire-api default-language: GHC2021 diff --git a/tools/db/service-backfill/src/Work.hs b/tools/db/service-backfill/src/Work.hs index 5846a8e9733..82ae367134e 100644 --- a/tools/db/service-backfill/src/Work.hs +++ b/tools/db/service-backfill/src/Work.hs @@ -1,7 +1,5 @@ -{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} -{-# LANGUAGE StandaloneDeriving #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -- This file is part of the Wire Server implementation. @@ -32,9 +30,6 @@ import Imports import System.Logger (Logger) import System.Logger qualified as Log import UnliftIO.Async (pooledMapConcurrentlyN) -import Wire.API.User - -deriving instance Cql Name runCommand :: Logger -> ClientState -> ClientState -> IO () runCommand l brig galley = diff --git a/tools/mlsstats/src/MlsStats/Run.hs b/tools/mlsstats/src/MlsStats/Run.hs index 211933295b2..803ea8a7bb5 100644 --- a/tools/mlsstats/src/MlsStats/Run.hs +++ b/tools/mlsstats/src/MlsStats/Run.hs @@ -249,9 +249,3 @@ instance Cql GroupId where fromCql (CqlBlob b) = Right . GroupId . LBS.toStrict $ b fromCql _ = Left "group_id: blob expected" - -instance Cql Domain where - ctype = Tagged TextColumn - toCql = CqlText . domainText - fromCql (CqlText txt) = mkDomain txt - fromCql _ = Left "Domain: Text expected" From bb7fab516a5fc16cce88aeb955fd4286a272d6bf Mon Sep 17 00:00:00 2001 From: Leif Battermann Date: Thu, 18 Apr 2024 09:40:11 +0200 Subject: [PATCH 098/117] WPB-8713 Optimize feature configs tests (#4007) --- changelog.d/5-internal/WPB-8713 | 1 + integration/test/API/Galley.hs | 19 +++ integration/test/API/GalleyInternal.hs | 14 +- integration/test/Test/Demo.hs | 4 +- integration/test/Test/FeatureFlags.hs | 156 +++++++++++++++++- integration/test/Test/Login.hs | 2 +- integration/test/Test/User.hs | 8 +- .../src/Galley/API/Teams/Features/Get.hs | 3 +- 8 files changed, 190 insertions(+), 17 deletions(-) create mode 100644 changelog.d/5-internal/WPB-8713 diff --git a/changelog.d/5-internal/WPB-8713 b/changelog.d/5-internal/WPB-8713 new file mode 100644 index 00000000000..cc48758e176 --- /dev/null +++ b/changelog.d/5-internal/WPB-8713 @@ -0,0 +1 @@ +Integration test cases for strangely behaving feature config settings. diff --git a/integration/test/API/Galley.hs b/integration/test/API/Galley.hs index 5def97cc126..424ee03286b 100644 --- a/integration/test/API/Galley.hs +++ b/integration/test/API/Galley.hs @@ -677,3 +677,22 @@ putLegalholdStatus tid usr status = do baseRequest usr Galley Versioned (joinHttpPath ["teams", tidStr, "features", "legalhold"]) >>= submit "PUT" . addJSONObject ["status" .= status, "ttl" .= "unlimited"] + +-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/get_feature_configs +getFeatureConfigs :: (HasCallStack, MakesValue user) => user -> App Response +getFeatureConfigs user = do + req <- baseRequest user Galley Versioned "/feature-configs" + submit "GET" req + +-- | https://staging-nginz-https.zinfra.io/v5/api/swagger-ui/#/default/get_teams__tid__features +getTeamFeatures :: (HasCallStack, MakesValue user, MakesValue tid) => user -> tid -> App Response +getTeamFeatures user tid = do + tidStr <- asString tid + req <- baseRequest user Galley Versioned (joinHttpPath ["teams", tidStr, "features"]) + submit "GET" req + +getTeamFeature :: (HasCallStack, MakesValue user, MakesValue tid) => user -> tid -> String -> App Response +getTeamFeature user tid featureName = do + tidStr <- asString tid + req <- baseRequest user Galley Versioned (joinHttpPath ["teams", tidStr, "features", featureName]) + submit "GET" req diff --git a/integration/test/API/GalleyInternal.hs b/integration/test/API/GalleyInternal.hs index f3b2ef1a135..877c6db2df5 100644 --- a/integration/test/API/GalleyInternal.hs +++ b/integration/test/API/GalleyInternal.hs @@ -33,23 +33,27 @@ putTeamMember user team perms = do req getTeamFeature :: (HasCallStack, MakesValue domain_) => domain_ -> String -> String -> App Response -getTeamFeature domain_ featureName tid = do +getTeamFeature domain_ tid featureName = do req <- baseRequest domain_ Galley Unversioned $ joinHttpPath ["i", "teams", tid, "features", featureName] submit "GET" $ req setTeamFeatureStatus :: (HasCallStack, MakesValue domain, MakesValue team) => domain -> team -> String -> String -> App () setTeamFeatureStatus domain team featureName status = do + setTeamFeatureStatusExpectHttpStatus domain team featureName status 200 + +setTeamFeatureStatusExpectHttpStatus :: (HasCallStack, MakesValue domain, MakesValue team) => domain -> team -> String -> String -> Int -> App () +setTeamFeatureStatusExpectHttpStatus domain team featureName status httpStatus = do tid <- asString team req <- baseRequest domain Galley Unversioned $ joinHttpPath ["i", "teams", tid, "features", featureName] - res <- submit "PATCH" $ req & addJSONObject ["status" .= status] - res.status `shouldMatchInt` 200 + bindResponse (submit "PATCH" $ req & addJSONObject ["status" .= status]) $ \res -> + res.status `shouldMatchInt` httpStatus setTeamFeatureLockStatus :: (HasCallStack, MakesValue domain, MakesValue team) => domain -> team -> String -> String -> App () setTeamFeatureLockStatus domain team featureName status = do tid <- asString team req <- baseRequest domain Galley Unversioned $ joinHttpPath ["i", "teams", tid, "features", featureName, status] - res <- submit "PUT" $ req - res.status `shouldMatchInt` 200 + bindResponse (submit "PUT" $ req) $ \res -> + res.status `shouldMatchInt` 200 getFederationStatus :: ( HasCallStack, diff --git a/integration/test/Test/Demo.hs b/integration/test/Test/Demo.hs index 824af5a7d2c..8b255f1c0d2 100644 --- a/integration/test/Test/Demo.hs +++ b/integration/test/Test/Demo.hs @@ -37,7 +37,7 @@ testModifiedGalley = do let getFeatureStatus :: (MakesValue domain) => domain -> String -> App Value getFeatureStatus domain team = do - bindResponse (GalleyI.getTeamFeature domain "searchVisibility" team) $ \res -> do + bindResponse (GalleyI.getTeamFeature domain team "searchVisibility") $ \res -> do res.status `shouldMatchInt` 200 res.json %. "status" @@ -75,7 +75,7 @@ testModifiedServices = do withModifiedBackend serviceMap $ \domain -> do (_user, tid, _) <- createTeam domain 1 - bindResponse (GalleyI.getTeamFeature domain "searchVisibility" tid) $ \res -> do + bindResponse (GalleyI.getTeamFeature domain tid "searchVisibility") $ \res -> do res.status `shouldMatchInt` 200 res.json %. "status" `shouldMatch` "enabled" diff --git a/integration/test/Test/FeatureFlags.hs b/integration/test/Test/FeatureFlags.hs index f31e1ed4250..7d522b61320 100644 --- a/integration/test/Test/FeatureFlags.hs +++ b/integration/test/Test/FeatureFlags.hs @@ -17,19 +17,167 @@ module Test.FeatureFlags where -import API.GalleyInternal +import qualified API.Galley as Public +import qualified API.GalleyInternal as Internal +import Control.Monad.Codensity (Codensity (runCodensity)) +import Control.Monad.Reader import SetupHelpers import Testlib.Prelude +import Testlib.ResourcePool (acquireResources) testLimitedEventFanout :: HasCallStack => App () testLimitedEventFanout = do let featureName = "limitedEventFanout" (_alice, team, _) <- createTeam OwnDomain 1 -- getTeamFeatureStatus OwnDomain team "limitedEventFanout" "enabled" - bindResponse (getTeamFeature OwnDomain featureName team) $ \resp -> do + bindResponse (Internal.getTeamFeature OwnDomain team featureName) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "status" `shouldMatch` "disabled" - setTeamFeatureStatus OwnDomain team featureName "enabled" - bindResponse (getTeamFeature OwnDomain featureName team) $ \resp -> do + Internal.setTeamFeatureStatus OwnDomain team featureName "enabled" + bindResponse (Internal.getTeamFeature OwnDomain team featureName) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "status" `shouldMatch` "enabled" + +disabled :: Value +disabled = object ["lockStatus" .= "unlocked", "status" .= "disabled", "ttl" .= "unlimited"] + +disabledLocked :: Value +disabledLocked = object ["lockStatus" .= "locked", "status" .= "disabled", "ttl" .= "unlimited"] + +enabled :: Value +enabled = object ["lockStatus" .= "unlocked", "status" .= "enabled", "ttl" .= "unlimited"] + +-- always disabled +testLegalholdDisabledPermanently :: HasCallStack => App () +testLegalholdDisabledPermanently = do + let cfgLhDisabledPermanently = + def + { galleyCfg = setField "settings.featureFlags.legalhold" "disabled-permanently" + } + cfgLhDisabledByDefault = + def + { galleyCfg = setField "settings.featureFlags.legalhold" "disabled-by-default" + } + resourcePool <- asks (.resourcePool) + runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do + let domain = testBackend.berDomain + + -- Happy case: DB has no config for the team + runCodensity (startDynamicBackend testBackend cfgLhDisabledPermanently) $ \_ -> do + (owner, tid, _) <- createTeam domain 1 + checkFeature "legalhold" owner tid disabled + Internal.setTeamFeatureStatusExpectHttpStatus domain tid "legalhold" "enabled" 403 + + -- Inteteresting case: The team had LH enabled before backend config was + -- changed to disabled-permanently + (owner, tid) <- runCodensity (startDynamicBackend testBackend cfgLhDisabledByDefault) $ \_ -> do + (owner, tid, _) <- createTeam domain 1 + checkFeature "legalhold" owner tid disabled + Internal.setTeamFeatureStatusExpectHttpStatus domain tid "legalhold" "enabled" 200 + checkFeature "legalhold" owner tid enabled + pure (owner, tid) + + runCodensity (startDynamicBackend testBackend cfgLhDisabledPermanently) $ \_ -> do + checkFeature "legalhold" owner tid disabled + +-- can be enabled for a team, disabled if unset +testLegalholdDisabledByDefault :: HasCallStack => App () +testLegalholdDisabledByDefault = do + withModifiedBackend + (def {galleyCfg = setField "settings.featureFlags.legalhold" "disabled-by-default"}) + $ \domain -> do + (owner, tid, _) <- createTeam domain 1 + checkFeature "legalhold" owner tid disabled + Internal.setTeamFeatureStatus domain tid "legalhold" "enabled" + checkFeature "legalhold" owner tid enabled + Internal.setTeamFeatureStatus domain tid "legalhold" "disabled" + checkFeature "legalhold" owner tid disabled + +-- enabled if team is allow listed, disabled in any other case +testLegalholdWhitelistTeamsAndImplicitConsent :: HasCallStack => App () +testLegalholdWhitelistTeamsAndImplicitConsent = do + let cfgLhWhitelistTeamsAndImplicitConsent = + def + { galleyCfg = setField "settings.featureFlags.legalhold" "whitelist-teams-and-implicit-consent" + } + cfgLhDisabledByDefault = + def + { galleyCfg = setField "settings.featureFlags.legalhold" "disabled-by-default" + } + resourcePool <- asks (.resourcePool) + runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do + let domain = testBackend.berDomain + + -- Happy case: DB has no config for the team + (owner, tid) <- runCodensity (startDynamicBackend testBackend cfgLhWhitelistTeamsAndImplicitConsent) $ \_ -> do + (owner, tid, _) <- createTeam domain 1 + checkFeature "legalhold" owner tid disabled + Internal.legalholdWhitelistTeam tid owner >>= assertSuccess + checkFeature "legalhold" owner tid enabled + + -- Disabling it doesn't work + Internal.setTeamFeatureStatusExpectHttpStatus domain tid "legalhold" "disabled" 403 + checkFeature "legalhold" owner tid enabled + pure (owner, tid) + + -- Interesting case: The team had LH disabled before backend config was + -- changed to "whitelist-teams-and-implicit-consent". It should still show + -- enabled when the config gets changed. + runCodensity (startDynamicBackend testBackend cfgLhDisabledByDefault) $ \_ -> do + checkFeature "legalhold" owner tid disabled + Internal.setTeamFeatureStatusExpectHttpStatus domain tid "legalhold" "disabled" 200 + checkFeature "legalhold" owner tid disabled + + runCodensity (startDynamicBackend testBackend cfgLhWhitelistTeamsAndImplicitConsent) $ \_ -> do + checkFeature "legalhold" owner tid enabled + +testExposeInvitationURLsToTeamAdminConfig :: HasCallStack => App () +testExposeInvitationURLsToTeamAdminConfig = do + let cfgExposeInvitationURLsTeamAllowlist tids = + def + { galleyCfg = setField "settings.exposeInvitationURLsTeamAllowlist" tids + } + resourcePool <- asks (.resourcePool) + runCodensity (acquireResources 1 resourcePool) $ \[testBackend] -> do + let domain = testBackend.berDomain + + -- Happy case: DB has no config for the team + let testNoAllowlistEntry = runCodensity (startDynamicBackend testBackend $ cfgExposeInvitationURLsTeamAllowlist ([] :: [String])) $ \_ -> do + (owner, tid, _) <- createTeam domain 1 + checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabledLocked + -- here we get a response with HTTP status 200 and feature status unchanged (disabled), which we find weird, but we're just testing the current behavior + Internal.setTeamFeatureStatusExpectHttpStatus domain tid "exposeInvitationURLsToTeamAdmin" "enabled" 200 + Internal.setTeamFeatureStatusExpectHttpStatus domain tid "exposeInvitationURLsToTeamAdmin" "disabled" 200 + pure (owner, tid) + + (owner, tid) <- testNoAllowlistEntry + + -- Interesting case: The team is in the allow list + runCodensity (startDynamicBackend testBackend $ cfgExposeInvitationURLsTeamAllowlist [tid]) $ \_ -> do + checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabled + Internal.setTeamFeatureStatusExpectHttpStatus domain tid "exposeInvitationURLsToTeamAdmin" "enabled" 200 + checkFeature "exposeInvitationURLsToTeamAdmin" owner tid enabled + Internal.setTeamFeatureStatusExpectHttpStatus domain tid "exposeInvitationURLsToTeamAdmin" "disabled" 200 + checkFeature "exposeInvitationURLsToTeamAdmin" owner tid disabled + Internal.setTeamFeatureStatusExpectHttpStatus domain tid "exposeInvitationURLsToTeamAdmin" "enabled" 200 + checkFeature "exposeInvitationURLsToTeamAdmin" owner tid enabled + + -- Interesting case: The team had the feature enabled but is not in allow list + void testNoAllowlistEntry + +checkFeature :: (HasCallStack, MakesValue user, MakesValue tid) => String -> user -> tid -> Value -> App () +checkFeature feature user tid expected = do + tidStr <- asString tid + domain <- objDomain user + bindResponse (Internal.getTeamFeature domain tidStr feature) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatch` expected + bindResponse (Public.getFeatureConfigs user) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. feature `shouldMatch` expected + bindResponse (Public.getTeamFeatures user tid) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json %. feature `shouldMatch` expected + bindResponse (Public.getTeamFeature user tid feature) $ \resp -> do + resp.status `shouldMatchInt` 200 + resp.json `shouldMatch` expected diff --git a/integration/test/Test/Login.hs b/integration/test/Test/Login.hs index 6f6b05f6246..1617e8b3a0f 100644 --- a/integration/test/Test/Login.hs +++ b/integration/test/Test/Login.hs @@ -68,7 +68,7 @@ testLoginVerify6DigitExpiredCodeFails = do email <- owner %. "email" setTeamFeatureLockStatus owner team "sndFactorPasswordChallenge" "unlocked" setTeamFeatureStatus owner team "sndFactorPasswordChallenge" "enabled" - bindResponse (getTeamFeature domain "sndFactorPasswordChallenge" team) $ \resp -> do + bindResponse (getTeamFeature owner team "sndFactorPasswordChallenge") $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "status" `shouldMatch` "enabled" generateVerificationCode owner email diff --git a/integration/test/Test/User.hs b/integration/test/Test/User.hs index 2c5df564377..89af540d2eb 100644 --- a/integration/test/Test/User.hs +++ b/integration/test/Test/User.hs @@ -63,11 +63,11 @@ testUpdateHandle = do mem1id <- asString $ mem1 %. "id" let featureName = "mlsE2EId" - bindResponse (getTeamFeature owner featureName team) $ \resp -> do + bindResponse (getTeamFeature owner team featureName) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "status" `shouldMatch` "disabled" setTeamFeatureStatus owner team featureName "enabled" - bindResponse (getTeamFeature owner featureName team) $ \resp -> do + bindResponse (getTeamFeature owner team featureName) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "status" `shouldMatch` "enabled" @@ -126,11 +126,11 @@ testUpdateSelf (MkTagged mode) = do (owner, team, [mem1]) <- createTeam OwnDomain 2 let featureName = "mlsE2EId" - bindResponse (getTeamFeature owner featureName team) $ \resp -> do + bindResponse (getTeamFeature owner team featureName) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "status" `shouldMatch` "disabled" setTeamFeatureStatus owner team featureName "enabled" - bindResponse (getTeamFeature owner featureName team) $ \resp -> do + bindResponse (getTeamFeature owner team featureName) $ \resp -> do resp.status `shouldMatchInt` 200 resp.json %. "status" `shouldMatch` "enabled" diff --git a/services/galley/src/Galley/API/Teams/Features/Get.hs b/services/galley/src/Galley/API/Teams/Features/Get.hs index c0ca2b2c9c4..727d6646ae2 100644 --- a/services/galley/src/Galley/API/Teams/Features/Get.hs +++ b/services/galley/src/Galley/API/Teams/Features/Get.hs @@ -473,7 +473,8 @@ instance GetFeatureConfig ExposeInvitationURLsToTeamAdminConfig where computeConfigForTeam teamAllowed teamDbStatus = if teamAllowed then makeConfig LockStatusUnlocked teamDbStatus - else makeConfig LockStatusLocked FeatureStatusDisabled + else -- FUTUREWORK: use default feature status instead + makeConfig LockStatusLocked FeatureStatusDisabled makeConfig :: LockStatus -> FeatureStatus -> WithStatus ExposeInvitationURLsToTeamAdminConfig makeConfig lockStatus status = From 011fd4b38d88589ae529cecb2c0c26e03094e979 Mon Sep 17 00:00:00 2001 From: Mango The Fourth <40720523+MangoIV@users.noreply.github.com> Date: Thu, 18 Apr 2024 09:43:27 +0200 Subject: [PATCH 099/117] [WPB-7222] (part 2) add coding-conventions.md to developer docs (#4006) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [feat] add coding-conventions.md to developer docs * [chore] changelog entry * Slight wording improvement --------- Co-authored-by: Marko Dimjašević --- changelog.d/4-docs/start-coding-conventions | 1 + .../developer/developer/coding-conventions.md | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 changelog.d/4-docs/start-coding-conventions create mode 100644 docs/src/developer/developer/coding-conventions.md diff --git a/changelog.d/4-docs/start-coding-conventions b/changelog.d/4-docs/start-coding-conventions new file mode 100644 index 00000000000..968e54abc7c --- /dev/null +++ b/changelog.d/4-docs/start-coding-conventions @@ -0,0 +1 @@ +adds new coding-conventions.md and talks about the decision we made for `cs` diff --git a/docs/src/developer/developer/coding-conventions.md b/docs/src/developer/developer/coding-conventions.md new file mode 100644 index 00000000000..b050c4533de --- /dev/null +++ b/docs/src/developer/developer/coding-conventions.md @@ -0,0 +1,25 @@ +# Coding conventions + +## On the topic of `cs` + +**TL;DR**: +use `cs` only in test-suites, *don't* use it in production code + +In wire we use all types of Strings; +- `String ~ [Char]` (`base` itself still does many things with `String`, also we use it in the `/integration` test suite) +- `Text` in both its strict and lazy versions +- `ByteString` in both its strict and lazy versions + +`ByteString` is literally a pointer to an Array of Bytes; there's no inherent encoding that makes it safe to +convert from and to `String` and `Text` which are nowadays typically `utf8` encoded; that means that using +`cs :: ConvertibleStrings a b => a -> b` is not a safe operation; the encoding between a given `ByteString` +and a `String` or `Text` can be different; e.g. we could decode a `ByteString` as ASCII-Chars or as utf8, just +to name a few. + +There's another inherent problem to `cs` in that context, namely **readability**; a `TL.fromStict` immediately tells +you what the code does; `cs`, however, says nothing; you know there's *some* conversion going on but not which. + +We have hence decided to not use the error-prone and hard-to-read `cs` in production code, i.e., in all libraries +and services, and instead only allow for use in test suites in general and `integration/` more specifically. + +As a consequence we also decided to drop `cs` from `Imports`. From 9fff6f97a6ce139abb4e918aa80fa00d72a886f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marko=20Dimja=C5=A1evi=C4=87?= Date: Thu, 18 Apr 2024 10:35:30 +0200 Subject: [PATCH 100/117] [WPB-7222] Drop depencency on `convertible-strings` in production code (#4001) * Drop 'cs' from types-common * Drop 'cs' from metrics-wai * Drop 'cs' from extended * Drop 'cs' from wai-utilities * Drop 'cs' from wire-api * Drop 'cs' from polysemy-wire-zoo * Drop 'cs' from gundeck-types * Drop 'cs' from wire-api-federation * Drop 'cs' from galley-types * Drop 'cs' from bilge * Drop 'cs' from jwt-tools * Drop 'cs' from federator * Drop 'cs' from spar * Drop 'cs' from brig * Drop 'cs' from galley * Drop 'cs' from gundeck * Drop 'cs' from cannon * Drop 'cs' from move-team * Drop 'cs' from inconsistencies * Drop 'cs' from stern * Drop 'cs' from cargohold * Drop 'cs' from wire-subsystems * Drop 'cs' from rabbitmq-consumer * [feat] cs: rest of the owl --------- Co-authored-by: Magnus Viernickel --- changelog.d/5-internal/WPB-7222 | 1 + libs/bilge/src/Bilge/Request.hs | 2 +- libs/extended/default.nix | 10 ++- libs/extended/extended.cabal | 1 + libs/extended/src/Servant/API/Extended.hs | 3 +- libs/extended/src/System/Logger/Extended.hs | 23 +++++- .../test/Test/System/Logger/ExtendedSpec.hs | 1 + libs/galley-types/default.nix | 2 + libs/galley-types/galley-types.cabal | 1 + libs/galley-types/src/Galley/Types.hs | 2 +- libs/galley-types/src/Galley/Types/Teams.hs | 8 ++- .../src/Gundeck/Types/Push/V2.hs | 2 +- libs/imports/default.nix | 2 - libs/imports/imports.cabal | 1 - libs/imports/src/Imports.hs | 2 - libs/jwt-tools/default.nix | 11 ++- libs/jwt-tools/jwt-tools.cabal | 2 + libs/jwt-tools/src/Data/Jwt/Tools.hs | 9 +-- libs/jwt-tools/test/Spec.hs | 1 + libs/metrics-wai/default.nix | 2 + libs/metrics-wai/metrics-wai.cabal | 1 + libs/metrics-wai/src/Data/Metrics/Servant.hs | 11 +-- libs/metrics-wai/src/Data/Metrics/Test.hs | 9 ++- libs/polysemy-wire-zoo/src/Wire/Sem/Jwk.hs | 7 +- libs/types-common/default.nix | 4 ++ libs/types-common/src/Data/Code.hs | 7 +- libs/types-common/src/Data/Domain.hs | 2 +- libs/types-common/src/Data/Json/Util.hs | 3 +- libs/types-common/src/Data/Misc.hs | 10 ++- libs/types-common/src/Data/Nonce.hs | 21 ++++-- libs/types-common/src/Util/Logging.hs | 6 +- libs/types-common/test/Test/Data/PEMKeys.hs | 1 + libs/types-common/test/Test/Properties.hs | 1 + libs/types-common/types-common.cabal | 2 + .../src/Network/Wai/Utilities/Headers.hs | 11 ++- .../src/Network/Wai/Utilities/Server.hs | 8 ++- .../src/Wire/API/Federation/Error.hs | 8 ++- libs/wire-api/default.nix | 2 + libs/wire-api/src/Wire/API/Call/Config.hs | 10 ++- libs/wire-api/src/Wire/API/Conversation.hs | 3 +- .../src/Wire/API/Internal/Notification.hs | 2 +- .../src/Wire/API/MLS/AuthenticatedContent.hs | 2 +- libs/wire-api/src/Wire/API/MLS/CipherSuite.hs | 2 +- .../src/Wire/API/MLS/Group/Serialisation.hs | 2 +- libs/wire-api/src/Wire/API/MLS/KeyPackage.hs | 2 +- libs/wire-api/src/Wire/API/MLS/Message.hs | 2 +- libs/wire-api/src/Wire/API/MLS/Proposal.hs | 2 +- .../src/Wire/API/MLS/SubConversation.hs | 2 +- libs/wire-api/src/Wire/API/MLS/Validation.hs | 2 +- libs/wire-api/src/Wire/API/MLS/Welcome.hs | 2 +- libs/wire-api/src/Wire/API/Notification.hs | 3 +- libs/wire-api/src/Wire/API/OAuth.hs | 52 +++++++++----- .../Wire/API/Routes/MultiTablePaging/State.hs | 4 +- .../wire-api/src/Wire/API/Routes/MultiVerb.hs | 2 +- libs/wire-api/src/Wire/API/Routes/Named.hs | 5 +- libs/wire-api/src/Wire/API/Routes/Public.hs | 16 ++++- .../src/Wire/API/Routes/Version/Wai.hs | 3 +- libs/wire-api/src/Wire/API/Team/Export.hs | 12 +++- libs/wire-api/src/Wire/API/Team/Feature.hs | 14 ++-- libs/wire-api/src/Wire/API/User.hs | 27 +++++-- .../Wire/API/User/Client/DPoPAccessToken.hs | 20 ++++-- libs/wire-api/src/Wire/API/User/Identity.hs | 31 +++++--- .../src/Wire/API/User/IdentityProvider.hs | 19 +++-- libs/wire-api/src/Wire/API/User/RichInfo.hs | 6 +- libs/wire-api/src/Wire/API/User/Saml.hs | 14 +++- libs/wire-api/src/Wire/API/User/Scim.hs | 7 +- libs/wire-api/src/Wire/API/User/Search.hs | 2 +- .../golden/Test/Wire/API/Golden/Runner.hs | 1 + .../test/unit/Test/Wire/API/Routes/Version.hs | 1 + .../unit/Test/Wire/API/Routes/Version/Wai.hs | 1 + .../test/unit/Test/Wire/API/User/Search.hs | 1 + libs/wire-api/wire-api.cabal | 2 + libs/wire-subsystems/default.nix | 2 + .../NotificationSubsystem/InterpreterSpec.hs | 1 + libs/wire-subsystems/wire-subsystems.cabal | 1 + .../background-worker/background-worker.cabal | 1 + services/background-worker/default.nix | 2 + .../src/Wire/BackgroundWorker.hs | 3 +- .../src/Wire/BackgroundWorker/Health.hs | 3 +- services/brig/brig.cabal | 2 + services/brig/default.nix | 4 ++ services/brig/src/Brig/API/Client.hs | 18 ++++- services/brig/src/Brig/API/Error.hs | 3 +- services/brig/src/Brig/API/Internal.hs | 48 ++++++++++--- .../Brig/API/MLS/KeyPackages/Validation.hs | 2 +- services/brig/src/Brig/API/OAuth.hs | 36 ++++++++-- services/brig/src/Brig/API/Public.hs | 11 ++- services/brig/src/Brig/API/User.hs | 2 +- services/brig/src/Brig/API/Util.hs | 3 +- services/brig/src/Brig/Calling.hs | 11 ++- services/brig/src/Brig/Effects/JwtTools.hs | 4 +- services/brig/src/Brig/Effects/SFT.hs | 5 +- services/brig/src/Brig/Index/Eval.hs | 3 +- services/brig/src/Brig/Run.hs | 8 ++- services/brig/src/Brig/Team/API.hs | 8 ++- services/brig/src/Brig/User/Auth/Cookie.hs | 2 +- .../brig/src/Brig/User/Auth/Cookie/Limit.hs | 2 +- services/brig/src/Brig/User/Auth/DB/Cookie.hs | 2 +- services/brig/src/Brig/User/EJPD.hs | 3 +- services/brig/src/Brig/User/Search/Index.hs | 17 ++++- .../src/Brig/User/Search/TeamUserSearch.hs | 5 +- services/brig/test/integration/API/Calling.hs | 1 + .../test/integration/API/Internal/Util.hs | 1 + services/brig/test/integration/API/OAuth.hs | 1 + services/brig/test/integration/API/Search.hs | 1 + .../brig/test/integration/API/Search/Util.hs | 1 + .../test/integration/API/SystemSettings.hs | 1 + .../test/integration/API/TeamUserSearch.hs | 1 + .../brig/test/integration/API/User/Account.hs | 1 + .../brig/test/integration/API/User/Auth.hs | 2 +- .../brig/test/integration/API/User/Client.hs | 1 + .../integration/API/User/PasswordReset.hs | 2 +- .../test/integration/API/User/Property.hs | 1 + .../brig/test/integration/API/User/Util.hs | 1 + .../integration/API/UserPendingActivation.hs | 1 + .../test/integration/Federation/End2end.hs | 2 +- services/brig/test/integration/Util.hs | 1 + services/cannon/src/Cannon/Types.hs | 2 +- services/cannon/src/Cannon/WS.hs | 2 +- .../cargohold/src/CargoHold/API/Public.hs | 8 ++- services/cargohold/src/CargoHold/Run.hs | 2 +- services/federator/default.nix | 5 ++ services/federator/federator.cabal | 3 + services/federator/src/Federator/Discovery.hs | 2 +- .../federator/src/Federator/ExternalServer.hs | 2 +- services/federator/src/Federator/Health.hs | 11 ++- .../federator/src/Federator/InternalServer.hs | 3 +- services/federator/src/Federator/Response.hs | 3 +- services/federator/src/Federator/Service.hs | 2 +- .../integration/Test/Federator/IngressSpec.hs | 1 + .../test/integration/Test/Federator/Util.hs | 1 + .../unit/Test/Federator/ExternalServer.hs | 1 + services/galley/default.nix | 4 ++ services/galley/galley.cabal | 2 + services/galley/src/Galley/API/Internal.hs | 3 +- .../Galley/API/MLS/Commit/ExternalCommit.hs | 2 +- services/galley/src/Galley/API/MLS/One2One.hs | 2 +- .../galley/src/Galley/API/MLS/Proposal.hs | 2 +- services/galley/src/Galley/API/MLS/Removal.hs | 2 +- .../src/Galley/API/MLS/SubConversation.hs | 2 +- services/galley/src/Galley/API/MLS/Types.hs | 2 +- services/galley/src/Galley/API/MLS/Welcome.hs | 2 +- services/galley/src/Galley/API/Query.hs | 2 +- .../galley/src/Galley/API/Teams/Features.hs | 3 +- .../src/Galley/Cassandra/Conversation.hs | 2 +- .../Galley/Cassandra/Conversation/Members.hs | 2 +- services/galley/src/Galley/Cassandra/Store.hs | 2 +- .../src/Galley/Cassandra/SubConversation.hs | 2 +- services/galley/src/Galley/Intra/User.hs | 5 +- services/galley/src/Galley/Monad.hs | 2 +- services/galley/src/Galley/Run.hs | 5 +- .../test/integration/API/Federation/Util.hs | 1 + .../integration/API/Teams/LegalHold/Util.hs | 1 + services/galley/test/integration/API/Util.hs | 1 + services/gundeck/default.nix | 2 + services/gundeck/gundeck.cabal | 1 + services/gundeck/src/Gundeck/Monad.hs | 2 +- .../gundeck/src/Gundeck/Notification/Data.hs | 2 +- services/gundeck/src/Gundeck/Push.hs | 2 +- .../gundeck/src/Gundeck/Push/Websocket.hs | 2 +- services/gundeck/src/Gundeck/Run.hs | 2 +- services/gundeck/test/unit/MockGundeck.hs | 1 + services/gundeck/test/unit/ThreadBudget.hs | 1 + services/proxy/src/Proxy/Proxy.hs | 10 +-- services/spar/default.nix | 6 ++ .../src/Spar/DataMigration/V2_UserV2.hs | 8 ++- services/spar/spar.cabal | 4 ++ services/spar/src/Spar/API.hs | 15 +++- services/spar/src/Spar/App.hs | 71 +++++++++++++------ services/spar/src/Spar/Data/Instances.hs | 21 ++++-- services/spar/src/Spar/Error.hs | 47 +++++++++--- services/spar/src/Spar/Intra/BrigApp.hs | 4 +- services/spar/src/Spar/Intra/Galley.hs | 3 +- services/spar/src/Spar/Orphans.hs | 3 +- services/spar/src/Spar/Run.hs | 7 +- services/spar/src/Spar/Scim.hs | 10 ++- services/spar/src/Spar/Scim/Auth.hs | 9 +-- services/spar/src/Spar/Scim/User.hs | 45 ++++++++---- services/spar/src/Spar/Sem/SAML2/Library.hs | 9 ++- services/spar/src/Spar/Sem/Utils.hs | 9 ++- .../spar/test-integration/Test/LoggingSpec.hs | 1 + .../spar/test-integration/Test/MetricsSpec.hs | 1 + .../test-integration/Test/Spar/APISpec.hs | 1 + .../test-integration/Test/Spar/AppSpec.hs | 1 + .../Test/Spar/Scim/AuthSpec.hs | 1 + .../Test/Spar/Scim/UserSpec.hs | 1 + services/spar/test-integration/Util/Core.hs | 1 + services/spar/test-integration/Util/Scim.hs | 1 + services/spar/test/Arbitrary.hs | 1 + .../spar/test/Test/Spar/Intra/BrigSpec.hs | 1 + .../db/inconsistencies/src/DanglingHandles.hs | 4 +- .../inconsistencies/src/DanglingUserKeys.hs | 4 +- .../db/inconsistencies/src/EmailLessUsers.hs | 4 +- .../db/inconsistencies/src/HandleLessUsers.hs | 2 +- tools/db/move-team/src/ParseSchema.hs | 2 +- tools/db/repair-handles/src/Options.hs | 3 +- tools/db/repair-handles/src/Work.hs | 6 +- .../src/RabbitMQConsumer/Lib.hs | 10 ++- tools/stern/default.nix | 4 ++ tools/stern/src/Stern/API.hs | 41 +++++++++-- tools/stern/src/Stern/Intra.hs | 53 +++++++++++--- tools/stern/stern.cabal | 2 + tools/stern/test/integration/API.hs | 1 + 203 files changed, 925 insertions(+), 323 deletions(-) create mode 100644 changelog.d/5-internal/WPB-7222 diff --git a/changelog.d/5-internal/WPB-7222 b/changelog.d/5-internal/WPB-7222 new file mode 100644 index 00000000000..a434ed6bf87 --- /dev/null +++ b/changelog.d/5-internal/WPB-7222 @@ -0,0 +1 @@ +drop cs in all production code and from Imports diff --git a/libs/bilge/src/Bilge/Request.hs b/libs/bilge/src/Bilge/Request.hs index 30c15eee8e0..1acd96aa03e 100644 --- a/libs/bilge/src/Bilge/Request.hs +++ b/libs/bilge/src/Bilge/Request.hs @@ -82,7 +82,7 @@ import Data.ByteString.Lazy qualified as Lazy import Data.ByteString.Lazy.Char8 qualified as LC import Data.CaseInsensitive (original) import Data.Id (RequestId (..)) -import Imports hiding (cs, intercalate) +import Imports hiding (intercalate) import Network.HTTP.Client (Cookie, GivesPopper, Request, RequestBody (..)) import Network.HTTP.Client qualified as Rq import Network.HTTP.Client.Internal (CookieJar (..), brReadSome, throwHttp) diff --git a/libs/extended/default.nix b/libs/extended/default.nix index ad03254ed71..66687c40075 100644 --- a/libs/extended/default.nix +++ b/libs/extended/default.nix @@ -29,6 +29,7 @@ , servant-client-core , servant-openapi3 , servant-server +, string-conversions , temporary , text , time @@ -69,7 +70,14 @@ mkDerivation { unliftio wai ]; - testHaskellDepends = [ aeson base hspec imports temporary ]; + testHaskellDepends = [ + aeson + base + hspec + imports + string-conversions + temporary + ]; testToolDepends = [ hspec-discover ]; description = "Extended versions of common modules"; license = lib.licenses.agpl3Only; diff --git a/libs/extended/extended.cabal b/libs/extended/extended.cabal index 2bfb4d92022..087fb75843a 100644 --- a/libs/extended/extended.cabal +++ b/libs/extended/extended.cabal @@ -172,6 +172,7 @@ test-suite extended-tests , extended , hspec , imports + , string-conversions , temporary default-language: GHC2021 diff --git a/libs/extended/src/Servant/API/Extended.hs b/libs/extended/src/Servant/API/Extended.hs index c1e87f38beb..a531f141bd7 100644 --- a/libs/extended/src/Servant/API/Extended.hs +++ b/libs/extended/src/Servant/API/Extended.hs @@ -19,6 +19,7 @@ -- errors instead of plaintext. module Servant.API.Extended where +import Data.ByteString import Data.ByteString.Lazy qualified as BL import Data.EitherR (fmapL) import Data.Kind @@ -92,7 +93,7 @@ instance fromMaybe "application/octet-stream" $ lookup hContentType $ requestHeaders request - case canHandleCTypeH (Proxy :: Proxy list) (cs contentTypeH) :: Maybe (BL.ByteString -> Either String a) of + case canHandleCTypeH (Proxy :: Proxy list) (fromStrict contentTypeH) :: Maybe (BL.ByteString -> Either String a) of Nothing -> delayedFail err415 Just f -> pure f -- Body check, we get a body parsing functions as the first argument. diff --git a/libs/extended/src/System/Logger/Extended.hs b/libs/extended/src/System/Logger/Extended.hs index e360da4e852..2b45c3f746c 100644 --- a/libs/extended/src/System/Logger/Extended.hs +++ b/libs/extended/src/System/Logger/Extended.hs @@ -37,9 +37,12 @@ import Control.Monad.Catch import Data.Aeson as Aeson import Data.Aeson.Encoding (list, pair, text) import Data.Aeson.Key qualified as Key +import Data.ByteString (toStrict) import Data.ByteString.Builder qualified as B import Data.ByteString.Lazy.Char8 qualified as L import Data.Map.Lazy qualified as Map +import Data.Text.Encoding +import Data.Text.Encoding.Error import GHC.Generics import Imports import System.Logger as Log @@ -65,7 +68,14 @@ elementToEncoding :: Element' -> Encoding elementToEncoding (Element' fields msgs) = pairs $ fields <> msgsToSeries msgs where msgsToSeries :: [Builder] -> Series - msgsToSeries = pair "msgs" . list (text . cs . eval) + msgsToSeries = + pair "msgs" + . list + ( text + . decodeUtf8With lenientDecode + . toStrict + . eval + ) collect :: [Element] -> Element' collect = foldr go (Element' mempty []) @@ -74,7 +84,14 @@ collect = foldr go (Element' mempty []) go (Bytes b) (Element' f m) = Element' f (b : m) go (Field k v) (Element' f m) = - Element' (f <> pair (Key.fromText . cs . eval $ k) (text . cs . eval $ v)) m + Element' + ( f + <> pair + (Key.fromText . dec . toStrict . eval $ k) + (text . dec . toStrict . eval $ v) + ) + m + dec = decodeUtf8With lenientDecode jsonRenderer :: Renderer jsonRenderer _sep _dateFormat _logLevel = fromEncoding . elementToEncoding . collect @@ -105,7 +122,7 @@ structuredJSONRenderer _sep _dateFmt _lvlThreshold logElems = renderTextList xs = toJSON xs builderToText :: Builder -> Text - builderToText = cs . eval + builderToText = decodeUtf8With lenientDecode . toStrict . eval -- We need to do this to work around https://gitlab.com/twittner/tinylog/-/issues/5 parseLevel :: Text -> Maybe Level diff --git a/libs/extended/test/Test/System/Logger/ExtendedSpec.hs b/libs/extended/test/Test/System/Logger/ExtendedSpec.hs index 7516a6f7014..753ba59ada7 100644 --- a/libs/extended/test/Test/System/Logger/ExtendedSpec.hs +++ b/libs/extended/test/Test/System/Logger/ExtendedSpec.hs @@ -19,6 +19,7 @@ module Test.System.Logger.ExtendedSpec where import Data.Aeson ((.=)) import Data.Aeson qualified as Aeson +import Data.String.Conversions import Imports import System.IO.Temp import System.Logger.Extended hiding ((.=)) diff --git a/libs/galley-types/default.nix b/libs/galley-types/default.nix index 2cbe283392e..5a6070c01a4 100644 --- a/libs/galley-types/default.nix +++ b/libs/galley-types/default.nix @@ -21,6 +21,7 @@ , tasty-quickcheck , text , types-common +, utf8-string , uuid , wire-api }: @@ -43,6 +44,7 @@ mkDerivation { schema-profunctor text types-common + utf8-string uuid wire-api ]; diff --git a/libs/galley-types/galley-types.cabal b/libs/galley-types/galley-types.cabal index 947ca36cd70..4953776a8ae 100644 --- a/libs/galley-types/galley-types.cabal +++ b/libs/galley-types/galley-types.cabal @@ -85,6 +85,7 @@ library , schema-profunctor , text >=0.11 , types-common >=0.16 + , utf8-string , uuid , wire-api diff --git a/libs/galley-types/src/Galley/Types.hs b/libs/galley-types/src/Galley/Types.hs index 8e035a40e02..b08103a22cd 100644 --- a/libs/galley-types/src/Galley/Types.hs +++ b/libs/galley-types/src/Galley/Types.hs @@ -26,7 +26,7 @@ where import Data.Aeson import Data.Id (ClientId, UserId) import Data.Map.Strict qualified as Map -import Imports hiding (cs) +import Imports import Wire.API.Message -------------------------------------------------------------------------------- diff --git a/libs/galley-types/src/Galley/Types/Teams.hs b/libs/galley-types/src/Galley/Types/Teams.hs index 07a2d755b4c..715377e42bb 100644 --- a/libs/galley-types/src/Galley/Types/Teams.hs +++ b/libs/galley-types/src/Galley/Types/Teams.hs @@ -62,6 +62,8 @@ where import Control.Lens (makeLenses, view) import Data.Aeson import Data.Aeson.Types qualified as A +import Data.ByteString (toStrict) +import Data.ByteString.UTF8 qualified as UTF8 import Data.Id (UserId) import Data.Schema qualified as Schema import Data.Set qualified as Set @@ -199,7 +201,7 @@ instance ToJSON FeatureFlags where instance FromJSON FeatureSSO where parseJSON (String "enabled-by-default") = pure FeatureSSOEnabledByDefault parseJSON (String "disabled-by-default") = pure FeatureSSODisabledByDefault - parseJSON bad = fail $ "FeatureSSO: " <> cs (encode bad) + parseJSON bad = fail $ "FeatureSSO: " <> (UTF8.toString . toStrict . encode $ bad) instance ToJSON FeatureSSO where toJSON FeatureSSOEnabledByDefault = String "enabled-by-default" @@ -209,7 +211,7 @@ instance FromJSON FeatureLegalHold where parseJSON (String "disabled-permanently") = pure $ FeatureLegalHoldDisabledPermanently parseJSON (String "disabled-by-default") = pure $ FeatureLegalHoldDisabledByDefault parseJSON (String "whitelist-teams-and-implicit-consent") = pure FeatureLegalHoldWhitelistTeamsAndImplicitConsent - parseJSON bad = fail $ "FeatureLegalHold: " <> cs (encode bad) + parseJSON bad = fail $ "FeatureLegalHold: " <> (UTF8.toString . toStrict . encode $ bad) instance ToJSON FeatureLegalHold where toJSON FeatureLegalHoldDisabledPermanently = String "disabled-permanently" @@ -219,7 +221,7 @@ instance ToJSON FeatureLegalHold where instance FromJSON FeatureTeamSearchVisibilityAvailability where parseJSON (String "enabled-by-default") = pure FeatureTeamSearchVisibilityAvailableByDefault parseJSON (String "disabled-by-default") = pure FeatureTeamSearchVisibilityUnavailableByDefault - parseJSON bad = fail $ "FeatureSearchVisibility: " <> cs (encode bad) + parseJSON bad = fail $ "FeatureSearchVisibility: " <> (UTF8.toString . toStrict . encode $ bad) instance ToJSON FeatureTeamSearchVisibilityAvailability where toJSON FeatureTeamSearchVisibilityAvailableByDefault = String "enabled-by-default" diff --git a/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs b/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs index aedfc7f0164..c087d911135 100644 --- a/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs +++ b/libs/gundeck-types/src/Gundeck/Types/Push/V2.hs @@ -83,7 +83,7 @@ import Data.List1 qualified as List1 import Data.Range import Data.Range qualified as Range import Data.Set qualified as Set -import Imports hiding (cs) +import Imports import Wire.API.Message (Priority (..)) import Wire.API.Push.V2.Token import Wire.Arbitrary diff --git a/libs/imports/default.nix b/libs/imports/default.nix index b1b77f2c86e..728fca8f3b5 100644 --- a/libs/imports/default.nix +++ b/libs/imports/default.nix @@ -11,7 +11,6 @@ , gitignoreSource , lib , mtl -, string-conversions , text , transformers , unliftio @@ -29,7 +28,6 @@ mkDerivation { deepseq extra mtl - string-conversions text transformers unliftio diff --git a/libs/imports/imports.cabal b/libs/imports/imports.cabal index 845228c8f10..a1ddc13d9bb 100644 --- a/libs/imports/imports.cabal +++ b/libs/imports/imports.cabal @@ -75,7 +75,6 @@ library , deepseq , extra , mtl - , string-conversions , text , transformers , unliftio diff --git a/libs/imports/src/Imports.hs b/libs/imports/src/Imports.hs index d44ab47c404..91841bbdd8c 100644 --- a/libs/imports/src/Imports.hs +++ b/libs/imports/src/Imports.hs @@ -114,7 +114,6 @@ module Imports -- * Extra Helpers whenM, unlessM, - cs, -- * Functor (<$$>), @@ -165,7 +164,6 @@ import Data.Ord import Data.Semigroup hiding (diff) import Data.Set (Set) import Data.String -import Data.String.Conversions (cs) import Data.Text (Text) import Data.Text.Lazy qualified import Data.Traversable diff --git a/libs/jwt-tools/default.nix b/libs/jwt-tools/default.nix index a6cdf09b241..1314bde5186 100644 --- a/libs/jwt-tools/default.nix +++ b/libs/jwt-tools/default.nix @@ -12,7 +12,9 @@ , imports , lib , rusty_jwt_tools_ffi +, string-conversions , transformers +, utf8-string }: mkDerivation { pname = "jwt-tools"; @@ -24,9 +26,16 @@ mkDerivation { http-types imports transformers + utf8-string ]; librarySystemDepends = [ rusty_jwt_tools_ffi ]; - testHaskellDepends = [ bytestring hspec imports transformers ]; + testHaskellDepends = [ + bytestring + hspec + imports + string-conversions + transformers + ]; description = "FFI to rusty-jwt-tools"; license = lib.licenses.agpl3Only; } diff --git a/libs/jwt-tools/jwt-tools.cabal b/libs/jwt-tools/jwt-tools.cabal index 57f815466ab..e2f12a9b352 100644 --- a/libs/jwt-tools/jwt-tools.cabal +++ b/libs/jwt-tools/jwt-tools.cabal @@ -68,6 +68,7 @@ library , http-types , imports , transformers + , utf8-string default-language: GHC2021 other-extensions: ForeignFunctionInterface @@ -83,6 +84,7 @@ test-suite jwt-tools-tests , hspec , imports , jwt-tools + , string-conversions , transformers hs-source-dirs: test diff --git a/libs/jwt-tools/src/Data/Jwt/Tools.hs b/libs/jwt-tools/src/Data/Jwt/Tools.hs index a38cc02c9fd..e9c3ce549de 100644 --- a/libs/jwt-tools/src/Data/Jwt/Tools.hs +++ b/libs/jwt-tools/src/Data/Jwt/Tools.hs @@ -42,6 +42,7 @@ where import Control.Exception hiding (handle) import Control.Monad.Trans.Except import Data.ByteString.Conversion +import Data.ByteString.UTF8 qualified as UTF8 import Foreign.C.String (CString, newCString, peekCString) import Foreign.Ptr (Ptr, nullPtr) import Imports @@ -163,7 +164,7 @@ generateDpopToken dpopProof uid cid handle displayName tid domain nonce uri meth domainCStr <- toCStr domain nonceCStr <- toCStr nonce uriCStr <- toCStr uri - methodCStr <- liftIO $ newCString $ cs $ methodToBS method + methodCStr <- liftIO $ newCString $ UTF8.toString $ methodToBS method backendPubkeyBundleCStr <- toCStr backendPubkeyBundle -- log all variable inputs (can comment in if need to generate new test data) @@ -205,7 +206,7 @@ generateDpopToken dpopProof uid cid handle displayName tid domain nonce uri meth toCStr = liftIO . newCString . toStr where toStr :: a -> String - toStr = cs . toByteString' + toStr = UTF8.toString . toByteString' methodToBS :: StdMethod -> ByteString methodToBS = \case @@ -221,8 +222,8 @@ generateDpopToken dpopProof uid cid handle displayName tid domain nonce uri meth toResult :: Maybe Word8 -> Maybe String -> Either DPoPTokenGenerationError ByteString -- the only valid cases are when the error=0 (meaning no error) or nothing and the token is not null -toResult (Just 0) (Just token) = Right $ cs token -toResult Nothing (Just token) = Right $ cs token +toResult (Just 0) (Just token) = Right $ UTF8.fromString token +toResult Nothing (Just token) = Right $ UTF8.fromString token -- errors toResult (Just errNo) _ = Left $ fromInt (fromIntegral errNo) where diff --git a/libs/jwt-tools/test/Spec.hs b/libs/jwt-tools/test/Spec.hs index 2e0afb3dc13..664c18d3874 100644 --- a/libs/jwt-tools/test/Spec.hs +++ b/libs/jwt-tools/test/Spec.hs @@ -18,6 +18,7 @@ import Control.Monad.Trans.Except import Data.ByteString.Char8 (split) import Data.Jwt.Tools +import Data.String.Conversions import Imports import Test.Hspec diff --git a/libs/metrics-wai/default.nix b/libs/metrics-wai/default.nix index 616c581b7fd..eb65cf447ae 100644 --- a/libs/metrics-wai/default.nix +++ b/libs/metrics-wai/default.nix @@ -16,6 +16,7 @@ , servant , servant-multipart , text +, utf8-string , wai , wai-middleware-prometheus , wai-route @@ -35,6 +36,7 @@ mkDerivation { servant servant-multipart text + utf8-string wai wai-middleware-prometheus wai-route diff --git a/libs/metrics-wai/metrics-wai.cabal b/libs/metrics-wai/metrics-wai.cabal index 5ea237e1964..3d9725348fe 100644 --- a/libs/metrics-wai/metrics-wai.cabal +++ b/libs/metrics-wai/metrics-wai.cabal @@ -79,6 +79,7 @@ library , servant , servant-multipart , text >=0.11 + , utf8-string , wai >=3 , wai-middleware-prometheus , wai-route >=0.3 diff --git a/libs/metrics-wai/src/Data/Metrics/Servant.hs b/libs/metrics-wai/src/Data/Metrics/Servant.hs index 372cdc95055..b8ec0984997 100644 --- a/libs/metrics-wai/src/Data/Metrics/Servant.hs +++ b/libs/metrics-wai/src/Data/Metrics/Servant.hs @@ -26,11 +26,14 @@ -- | Given a servant API type, this module gives you a 'Paths' for 'withPathTemplate'. module Data.Metrics.Servant where +import Data.ByteString.UTF8 qualified as UTF8 import Data.Metrics.Middleware.Prometheus (normalizeWaiRequestRoute) import Data.Metrics.Types import Data.Metrics.Types qualified as Metrics import Data.Metrics.WaiRoute (treeToPaths) import Data.Proxy +import Data.Text.Encoding +import Data.Text.Encoding.Error import Data.Tree import GHC.TypeLits import Imports @@ -48,8 +51,8 @@ servantPrometheusMiddleware _ = Promth.prometheus conf . instrument promthNormal promthNormalize :: Wai.Request -> Text promthNormalize req = pathInfo where - mPathInfo = Metrics.treeLookup (routesToPaths @api) $ cs <$> Wai.pathInfo req - pathInfo = cs $ fromMaybe "N/A" mPathInfo + mPathInfo = Metrics.treeLookup (routesToPaths @api) $ encodeUtf8 <$> Wai.pathInfo req + pathInfo = decodeUtf8With lenientDecode $ fromMaybe "N/A" mPathInfo -- See Note [Raw Response] instrument = Promth.instrumentHandlerValueWithFilter Promth.ignoreRawResponses @@ -85,14 +88,14 @@ instance (KnownSymbol seg, RoutesToPaths segs) => RoutesToPaths (seg :> segs) where - getRoutes = [Node (Right . cs $ symbolVal (Proxy @seg)) (getRoutes @segs)] + getRoutes = [Node (Right . UTF8.fromString $ symbolVal (Proxy @seg)) (getRoutes @segs)] -- :> routes instance (KnownSymbol capture, RoutesToPaths segs) => RoutesToPaths (Capture' mods capture a :> segs) where - getRoutes = [Node (Left (cs (":" <> symbolVal (Proxy @capture)))) (getRoutes @segs)] + getRoutes = [Node (Left (UTF8.fromString (":" <> symbolVal (Proxy @capture)))) (getRoutes @segs)] instance (RoutesToPaths rest) => diff --git a/libs/metrics-wai/src/Data/Metrics/Test.hs b/libs/metrics-wai/src/Data/Metrics/Test.hs index 308dc18193f..95016f23a5f 100644 --- a/libs/metrics-wai/src/Data/Metrics/Test.hs +++ b/libs/metrics-wai/src/Data/Metrics/Test.hs @@ -19,6 +19,8 @@ module Data.Metrics.Test where import Data.Metrics.Types import Data.Text qualified as Text +import Data.Text.Encoding +import Data.Text.Encoding.Error import Data.Tree qualified as Tree import Imports @@ -50,9 +52,12 @@ pathsConsistencyCheck (Paths forest) = mconcat $ go [] <$> forest findSiteConsistencyError prefix subtrees = case mapMaybe captureVars subtrees of [] -> Nothing [_] -> Nothing - bad@(_ : _ : _) -> Just $ SiteConsistencyError (either cs cs <$> prefix) bad + bad@(_ : _ : _) -> + Just $ + SiteConsistencyError (either decode decode <$> prefix) bad captureVars :: Tree.Tree (Either ByteString any) -> Maybe (Text, Int) - captureVars (Tree.Node (Left root) trees) = Just (cs root, weight trees) + captureVars (Tree.Node (Left root) trees) = Just (decode root, weight trees) captureVars (Tree.Node (Right _) _) = Nothing weight :: Tree.Forest a -> Int weight = sum . fmap (length . Tree.flatten) + decode = decodeUtf8With lenientDecode diff --git a/libs/polysemy-wire-zoo/src/Wire/Sem/Jwk.hs b/libs/polysemy-wire-zoo/src/Wire/Sem/Jwk.hs index c0edb3d94c4..913e5cbf7b7 100644 --- a/libs/polysemy-wire-zoo/src/Wire/Sem/Jwk.hs +++ b/libs/polysemy-wire-zoo/src/Wire/Sem/Jwk.hs @@ -5,6 +5,7 @@ module Wire.Sem.Jwk where import Control.Exception import Crypto.JOSE.JWK import Data.Aeson +import Data.ByteString (fromStrict) import qualified Data.ByteString as BS import Imports import Polysemy @@ -18,4 +19,8 @@ interpretJwk :: Members '[Embed IO] r => Sem (Jwk ': r) a -> Sem r a interpretJwk = interpret $ \(Get fp) -> liftIO $ readJwk fp readJwk :: FilePath -> IO (Maybe JWK) -readJwk fp = try @IOException (BS.readFile fp) <&> either (const Nothing) (decode . cs) +readJwk fp = + try @IOException (BS.readFile fp) + <&> either + (const Nothing) + (decode . fromStrict) diff --git a/libs/types-common/default.nix b/libs/types-common/default.nix index abf2ee2f27d..7421aae499c 100644 --- a/libs/types-common/default.nix +++ b/libs/types-common/default.nix @@ -41,6 +41,7 @@ , random , schema-profunctor , servant-server +, string-conversions , tagged , tasty , tasty-hunit @@ -52,6 +53,7 @@ , unix , unordered-containers , uri-bytestring +, utf8-string , uuid , yaml }: @@ -105,6 +107,7 @@ mkDerivation { unix unordered-containers uri-bytestring + utf8-string uuid yaml ]; @@ -116,6 +119,7 @@ mkDerivation { cereal imports protobuf + string-conversions tasty tasty-hunit tasty-quickcheck diff --git a/libs/types-common/src/Data/Code.hs b/libs/types-common/src/Data/Code.hs index c745b752caa..ef70a0aeb52 100644 --- a/libs/types-common/src/Data/Code.hs +++ b/libs/types-common/src/Data/Code.hs @@ -35,7 +35,8 @@ import Data.Range import Data.Schema import Data.Text (pack) import Data.Text.Ascii -import Data.Text.Encoding (encodeUtf8) +import Data.Text.Encoding +import Data.Text.Encoding.Error import Data.Time.Clock import Imports import Servant (FromHttpApiData (..), ToHttpApiData (..)) @@ -62,7 +63,7 @@ instance FromHttpApiData Key where first pack $ runParser parser (encodeUtf8 s) instance ToHttpApiData Key where - toQueryParam key = cs (toByteString' key) + toQueryParam key = decodeUtf8With lenientDecode (toByteString' key) -- | A secret value bound to a 'Key' and a 'Timeout'. newtype Value = Value {asciiValue :: Range 6 20 AsciiBase64Url} @@ -85,7 +86,7 @@ instance FromHttpApiData Value where first pack $ runParser parser (encodeUtf8 s) instance ToHttpApiData Value where - toQueryParam key = cs (toByteString' key) + toQueryParam key = decodeUtf8With lenientDecode (toByteString' key) -- | A 'Timeout' is rendered in/parsed from JSON as an integer representing the -- number of seconds remaining. diff --git a/libs/types-common/src/Data/Domain.hs b/libs/types-common/src/Data/Domain.hs index e45d966c85b..ed74cd230a3 100644 --- a/libs/types-common/src/Data/Domain.hs +++ b/libs/types-common/src/Data/Domain.hs @@ -82,7 +82,7 @@ instance FromByteString Domain where parser = domainParser instance ToByteString Domain where - builder = Builder.lazyByteString . cs @Text @LByteString . _domainText + builder = Builder.lazyByteString . BS.Char8.fromStrict . Text.E.encodeUtf8 . _domainText instance FromHttpApiData Domain where parseUrlPiece = first Text.pack . mkDomain diff --git a/libs/types-common/src/Data/Json/Util.hs b/libs/types-common/src/Data/Json/Util.hs index f6c990081ec..91f0e420fe6 100644 --- a/libs/types-common/src/Data/Json/Util.hs +++ b/libs/types-common/src/Data/Json/Util.hs @@ -61,6 +61,7 @@ import Data.ByteString.Base64.URL qualified as B64U import Data.ByteString.Builder qualified as BB import Data.ByteString.Conversion qualified as BS import Data.ByteString.Lazy qualified as L +import Data.ByteString.UTF8 qualified as UTF8 import Data.Fixed import Data.OpenApi qualified as S import Data.Schema @@ -141,7 +142,7 @@ instance Show UTCTimeMillis where showsPrec d = showParen (d > 10) . showString . Text.unpack . showUTCTimeMillis instance BS.ToByteString UTCTimeMillis where - builder = BB.byteString . cs . show + builder = BB.byteString . UTF8.fromString . show instance BS.FromByteString UTCTimeMillis where parser = maybe (fail "UTCTimeMillis") pure . readUTCTimeMillis =<< BS.parser diff --git a/libs/types-common/src/Data/Misc.hs b/libs/types-common/src/Data/Misc.hs index 23a404e2c02..837c24d18c2 100644 --- a/libs/types-common/src/Data/Misc.hs +++ b/libs/types-common/src/Data/Misc.hs @@ -79,7 +79,8 @@ import Data.OpenApi qualified as S import Data.Range import Data.Schema import Data.Text qualified as Text -import Data.Text.Encoding (decodeUtf8, encodeUtf8) +import Data.Text.Encoding +import Data.Text.Encoding.Error import GHC.TypeLits (Nat) import GHC.TypeNats (KnownNat) import Imports @@ -139,10 +140,13 @@ instance ToSchema IpAddr where schema = toText .= parsedText "IpAddr" fromText where toText :: IpAddr -> Text - toText = cs . toByteString + toText = decodeUtf8With lenientDecode . toStrict . toByteString fromText :: Text -> Either String IpAddr - fromText = maybe (Left "Failed parsing IP address.") Right . fromByteString . cs + fromText = + maybe (Left "Failed parsing IP address.") Right + . fromByteString + . encodeUtf8 instance ToSchema Port where schema = Port <$> portNumber .= schema diff --git a/libs/types-common/src/Data/Nonce.hs b/libs/types-common/src/Data/Nonce.hs index 1f094bab764..50d84f7c655 100644 --- a/libs/types-common/src/Data/Nonce.hs +++ b/libs/types-common/src/Data/Nonce.hs @@ -35,6 +35,8 @@ import Data.OpenApi qualified as S import Data.OpenApi.ParamSchema import Data.Proxy (Proxy (Proxy)) import Data.Schema +import Data.Text.Encoding +import Data.Text.Encoding.Error import Data.UUID as UUID (UUID, fromByteString, toByteString) import Data.UUID.V4 (nextRandom) import Imports @@ -48,10 +50,15 @@ newtype Nonce = Nonce {unNonce :: UUID} deriving (A.FromJSON, A.ToJSON, S.ToSchema) via (Schema Nonce) instance ToSchema Nonce where - schema = (cs . toByteString') .= parsedText "Nonce" p + schema = + (decodeUtf8With lenientDecode . toByteString') .= parsedText "Nonce" p where p :: Text -> Either String Nonce - p = maybe (Left "Invalid Nonce") Right . fromByteString' . cs + p = + maybe (Left "Invalid Nonce") Right + . fromByteString' + . fromStrict + . encodeUtf8 instance ToByteString Nonce where builder = builder . Base64.encodeUnpadded . toStrict . UUID.toByteString . unNonce @@ -68,16 +75,20 @@ instance ToParamSchema Nonce where toParamSchema _ = toParamSchema (Proxy @Text) instance ToHttpApiData Nonce where - toQueryParam = cs . toByteString' + toQueryParam = decodeUtf8With lenientDecode . toByteString' instance FromHttpApiData Nonce where - parseQueryParam = maybe (Left "Invalid Nonce") Right . fromByteString' . cs + parseQueryParam = + maybe (Left "Invalid Nonce") Right + . fromByteString' + . fromStrict + . encodeUtf8 randomNonce :: MonadIO m => m Nonce randomNonce = Nonce <$> liftIO nextRandom isValidBase64UrlEncodedUUID :: ByteString -> Bool -isValidBase64UrlEncodedUUID = isJust . fromByteString' @Nonce . cs +isValidBase64UrlEncodedUUID = isJust . fromByteString' @Nonce . fromStrict instance Cql Nonce where ctype = Tagged UuidColumn diff --git a/libs/types-common/src/Util/Logging.hs b/libs/types-common/src/Util/Logging.hs index 0b4a3e7c5a2..318785c7578 100644 --- a/libs/types-common/src/Util/Logging.hs +++ b/libs/types-common/src/Util/Logging.hs @@ -29,7 +29,7 @@ import System.Logger.Message (Msg) sha256String :: Text -> Text sha256String t = let digest = hash @ByteString @SHA256 (encodeUtf8 t) - in cs . show $ digest + in T.pack . show $ digest logHandle :: Handle -> (Msg -> Msg) logHandle handl = @@ -44,7 +44,7 @@ logFunction fn = Log.field "fn" fn . Log.field "module" (getModule fn) x -> T.intercalate "." (init x) logUser :: UserId -> (Msg -> Msg) -logUser uid = Log.field "user" (cs @_ @Text . show $ uid) +logUser uid = Log.field "user" (T.pack . show $ uid) logTeam :: TeamId -> (Msg -> Msg) -logTeam tid = Log.field "team" (cs @_ @Text . show $ tid) +logTeam tid = Log.field "team" (T.pack . show $ tid) diff --git a/libs/types-common/test/Test/Data/PEMKeys.hs b/libs/types-common/test/Test/Data/PEMKeys.hs index 013688d7d70..c7a727f23d1 100644 --- a/libs/types-common/test/Test/Data/PEMKeys.hs +++ b/libs/types-common/test/Test/Data/PEMKeys.hs @@ -22,6 +22,7 @@ where import Data.ByteString.Conversion import Data.PEMKeys +import Data.String.Conversions import Imports import Test.Tasty import Test.Tasty.HUnit diff --git a/libs/types-common/test/Test/Properties.hs b/libs/types-common/test/Test/Properties.hs index 8e0556df41d..fbd1de60122 100644 --- a/libs/types-common/test/Test/Properties.hs +++ b/libs/types-common/test/Test/Properties.hs @@ -39,6 +39,7 @@ import Data.Json.Util qualified as Util import Data.Nonce (Nonce) import Data.ProtocolBuffers.Internal import Data.Serialize +import Data.String.Conversions import Data.Text.Ascii import Data.Text.Ascii qualified as Ascii import Data.Time diff --git a/libs/types-common/types-common.cabal b/libs/types-common/types-common.cabal index 14cb8cb6a6a..5fb1c0ca72c 100644 --- a/libs/types-common/types-common.cabal +++ b/libs/types-common/types-common.cabal @@ -136,6 +136,7 @@ library , unix , unordered-containers >=0.2 , uri-bytestring >=0.2 + , utf8-string , uuid >=1.3.11 , yaml >=0.8.22 @@ -209,6 +210,7 @@ test-suite tests , cereal , imports , protobuf + , string-conversions , tasty , tasty-hunit , tasty-quickcheck diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Headers.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Headers.hs index f1673e7de13..56049d0ecdf 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Headers.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Headers.hs @@ -17,9 +17,12 @@ module Network.Wai.Utilities.Headers where +import Data.ByteString import Data.ByteString.Conversion (FromByteString (..), ToByteString (..), fromByteString', toByteString') import Data.OpenApi.ParamSchema (ToParamSchema (..)) import Data.Text as T +import Data.Text.Encoding +import Data.Text.Encoding.Error import Imports import Servant (FromHttpApiData (..), Proxy (Proxy), ToHttpApiData (..)) @@ -37,10 +40,14 @@ instance FromByteString CacheControl where _ -> fail $ "Invalid CacheControl type: " ++ show t instance ToHttpApiData CacheControl where - toQueryParam = cs . toByteString' + toQueryParam = decodeUtf8With lenientDecode . toByteString' instance FromHttpApiData CacheControl where - parseQueryParam = maybe (Left "Invalid CacheControl") Right . fromByteString' . cs + parseQueryParam = + maybe (Left "Invalid CacheControl") Right + . fromByteString' + . fromStrict + . encodeUtf8 instance ToParamSchema CacheControl where toParamSchema _ = toParamSchema (Proxy @Text) diff --git a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs index e611a3e34fd..80ee4329c13 100644 --- a/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs +++ b/libs/wai-utilities/src/Network/Wai/Utilities/Server.hs @@ -52,6 +52,7 @@ import Control.Error.Util ((?:)) import Control.Exception (throw) import Control.Monad.Catch hiding (onError, onException) import Data.Aeson (decode, encode) +import Data.ByteString (toStrict) import Data.ByteString qualified as BS import Data.ByteString.Builder import Data.ByteString.Char8 qualified as C @@ -61,6 +62,7 @@ import Data.Metrics.GC (spawnGCMetricsCollector) import Data.Metrics.Middleware import Data.Streaming.Zlib (ZlibException (..)) import Data.Text.Encoding.Error (lenientDecode) +import Data.Text.Lazy qualified as LT import Data.Text.Lazy.Encoding qualified as LT import Imports import Network.HTTP.Types.Status @@ -222,7 +224,7 @@ errorHandlers = Wai.mkError status500 "server-error" "Server Error", Handler $ \(e :: SomeException) -> pure . Left $ - Wai.mkError status500 "server-error" ("Server Error. " <> cs (displayException e)) + Wai.mkError status500 "server-error" ("Server Error. " <> LT.pack (displayException e)) ] {-# INLINE errorHandlers #-} @@ -290,7 +292,7 @@ heavyDebugLogging sanitizeReq lvl lgr app = \req cont -> do -- >>> pure $ fromMaybe "" nextChunk emitLByteString :: LByteString -> IO (IO ByteString) emitLByteString lbs = do - tvar <- newTVarIO (cs lbs) + tvar <- newTVarIO (toStrict lbs) -- Emit the bytestring on the first read, then always return "" on subsequent reads pure . atomically $ swapTVar tvar mempty @@ -323,7 +325,7 @@ rethrow5xx logger app req k = app req k' wrapError :: Status -> LByteString -> Wai.Error wrapError st body = decode body ?: - Wai.mkError st "server-error" (cs body) + Wai.mkError st "server-error" (LT.decodeUtf8With lenientDecode body) -- | This flushes the response! If you want to keep using the response, you need to construct -- a new one with a fresh body stream. diff --git a/libs/wire-api-federation/src/Wire/API/Federation/Error.hs b/libs/wire-api-federation/src/Wire/API/Federation/Error.hs index 2303d744abb..830a9f062fc 100644 --- a/libs/wire-api-federation/src/Wire/API/Federation/Error.hs +++ b/libs/wire-api-federation/src/Wire/API/Federation/Error.hs @@ -245,7 +245,9 @@ federationRemoteHTTP2Error target path = \case addErrData err = err { Wai.errorData = - ((mkDomain . cs . srvTargetDomain $ target) :: Either String Domain) + ( (mkDomain . T.decodeUtf8With T.lenientDecode . srvTargetDomain $ target) :: + Either String Domain + ) & either (const Nothing) (\dom -> Just (Wai.FederationErrorData dom path)) } @@ -271,7 +273,9 @@ federationRemoteResponseError target path status body = ) ) { Wai.errorData = - ((mkDomain . cs . srvTargetDomain $ target) :: Either String Domain) + ( (mkDomain . T.decodeUtf8With T.lenientDecode . srvTargetDomain $ target) :: + Either String Domain + ) & either (const Nothing) (\dom -> Just (Wai.FederationErrorData dom path)), Wai.innerError = Just $ diff --git a/libs/wire-api/default.nix b/libs/wire-api/default.nix index 581ac8b9612..b3c02ca5099 100644 --- a/libs/wire-api/default.nix +++ b/libs/wire-api/default.nix @@ -86,6 +86,7 @@ , singletons-base , singletons-th , sop-core +, string-conversions , tagged , tasty , tasty-hspec @@ -244,6 +245,7 @@ mkDerivation { schema-profunctor servant servant-server + string-conversions tasty tasty-hspec tasty-hunit diff --git a/libs/wire-api/src/Wire/API/Call/Config.hs b/libs/wire-api/src/Wire/API/Call/Config.hs index 442f81fd4af..a4eb530ae5c 100644 --- a/libs/wire-api/src/Wire/API/Call/Config.hs +++ b/libs/wire-api/src/Wire/API/Call/Config.hs @@ -93,6 +93,7 @@ import Data.Aeson qualified as A hiding (()) import Data.Aeson.Types qualified as A import Data.Attoparsec.Text hiding (Parser, parse) import Data.Attoparsec.Text qualified as Text +import Data.ByteString (toStrict) import Data.ByteString.Builder import Data.ByteString.Conversion (toByteString) import Data.ByteString.Conversion qualified as BC @@ -104,6 +105,7 @@ import Data.Schema import Data.Text qualified as Text import Data.Text.Ascii import Data.Text.Encoding qualified as TE +import Data.Text.Encoding.Error import Data.Text.Strict.Lens (utf8) import Data.Time.Clock.POSIX import Imports @@ -264,7 +266,9 @@ data TurnURI = TurnURI deriving (A.ToJSON, A.FromJSON, S.ToSchema) via (Schema TurnURI) instance ToSchema TurnURI where - schema = (cs . toByteString) .= parsedText "TurnURI" parseTurnURI + schema = + (TE.decodeUtf8With lenientDecode . toStrict . toByteString) + .= parsedText "TurnURI" parseTurnURI turnURI :: Scheme -> TurnHost -> Port -> Maybe Transport -> TurnURI turnURI = TurnURI @@ -478,7 +482,7 @@ instance ToSchema SFTUsername where fromText = parseOnly (parseSFTUsername <* endOfInput) toText :: SFTUsername -> Text - toText = cs . toByteString + toText = TE.decodeUtf8With lenientDecode . toStrict . toByteString instance BC.ToByteString SFTUsername where builder su = @@ -555,7 +559,7 @@ instance ToSchema TurnUsername where fromText = parseOnly (parseTurnUsername <* endOfInput) toText :: TurnUsername -> Text - toText = cs . toByteString + toText = TE.decodeUtf8With lenientDecode . toStrict . toByteString instance BC.ToByteString TurnUsername where builder tu = diff --git a/libs/wire-api/src/Wire/API/Conversation.hs b/libs/wire-api/src/Wire/API/Conversation.hs index 84cd7d2390b..120ffe6a921 100644 --- a/libs/wire-api/src/Wire/API/Conversation.hs +++ b/libs/wire-api/src/Wire/API/Conversation.hs @@ -104,6 +104,7 @@ import Data.Range (Range, fromRange, rangedSchema) import Data.SOP import Data.Schema import Data.Set qualified as Set +import Data.Text qualified as Text import Data.UUID qualified as UUID import Data.UUID.V5 qualified as UUIDV5 import Imports @@ -724,7 +725,7 @@ newConvSchema sch = \to be part of this conversation" usersRoleDesc :: Text usersRoleDesc = - cs $ + Text.pack $ "The conversation permissions the users \ \added in this request should have. \ \Optional, defaults to '" diff --git a/libs/wire-api/src/Wire/API/Internal/Notification.hs b/libs/wire-api/src/Wire/API/Internal/Notification.hs index 849c8125460..7d43ef30962 100644 --- a/libs/wire-api/src/Wire/API/Internal/Notification.hs +++ b/libs/wire-api/src/Wire/API/Internal/Notification.hs @@ -47,7 +47,7 @@ import Data.Id import Data.List1 import Data.OpenApi qualified as Swagger import Data.Schema qualified as S -import Imports hiding (cs) +import Imports import Wire.API.Notification ------------------------------------------------------------------------------- diff --git a/libs/wire-api/src/Wire/API/MLS/AuthenticatedContent.hs b/libs/wire-api/src/Wire/API/MLS/AuthenticatedContent.hs index 521217f7c53..394a18ede1a 100644 --- a/libs/wire-api/src/Wire/API/MLS/AuthenticatedContent.hs +++ b/libs/wire-api/src/Wire/API/MLS/AuthenticatedContent.hs @@ -25,7 +25,7 @@ module Wire.API.MLS.AuthenticatedContent where import Crypto.PubKey.Ed25519 -import Imports hiding (cs) +import Imports import Wire.API.MLS.CipherSuite import Wire.API.MLS.Context import Wire.API.MLS.Epoch diff --git a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs index f4e24df2989..32073718892 100644 --- a/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs +++ b/libs/wire-api/src/Wire/API/MLS/CipherSuite.hs @@ -68,7 +68,7 @@ import Data.Text.Lazy qualified as LT import Data.Text.Lazy.Builder qualified as LT import Data.Text.Lazy.Builder.Int qualified as LT import Data.Word -import Imports hiding (cs) +import Imports import Web.HttpApiData import Wire.API.MLS.Serialisation import Wire.Arbitrary diff --git a/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs b/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs index 9a52f1a0879..3250af2f81a 100644 --- a/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs +++ b/libs/wire-api/src/Wire/API/MLS/Group/Serialisation.hs @@ -35,7 +35,7 @@ import Data.Qualified import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.UUID qualified as UUID -import Imports hiding (cs) +import Imports import Web.HttpApiData (FromHttpApiData (parseHeader)) import Wire.API.Conversation import Wire.API.MLS.Group diff --git a/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs b/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs index 906ec74fc58..eb736de6ea5 100644 --- a/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs +++ b/libs/wire-api/src/Wire/API/MLS/KeyPackage.hs @@ -49,7 +49,7 @@ import Data.Text qualified as T import Data.Text.Encoding qualified as T import Data.X509 qualified as X509 import GHC.Records -import Imports hiding (cs) +import Imports import Test.QuickCheck import Web.HttpApiData import Wire.API.MLS.CipherSuite diff --git a/libs/wire-api/src/Wire/API/MLS/Message.hs b/libs/wire-api/src/Wire/API/MLS/Message.hs index c13dcc0d96f..342bb739e23 100644 --- a/libs/wire-api/src/Wire/API/MLS/Message.hs +++ b/libs/wire-api/src/Wire/API/MLS/Message.hs @@ -45,7 +45,7 @@ import Data.Json.Util import Data.OpenApi qualified as S import Data.Schema hiding (HasField) import GHC.Records -import Imports hiding (cs) +import Imports import Test.QuickCheck hiding (label) import Wire.API.Event.Conversation import Wire.API.MLS.CipherSuite diff --git a/libs/wire-api/src/Wire/API/MLS/Proposal.hs b/libs/wire-api/src/Wire/API/MLS/Proposal.hs index 125364b8362..1ae2ef989ac 100644 --- a/libs/wire-api/src/Wire/API/MLS/Proposal.hs +++ b/libs/wire-api/src/Wire/API/MLS/Proposal.hs @@ -25,7 +25,7 @@ import Control.Lens (makePrisms) import Data.Binary import Data.ByteString as B import GHC.Records -import Imports hiding (cs) +import Imports import Test.QuickCheck import Wire.API.MLS.CipherSuite import Wire.API.MLS.Extension diff --git a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs index c01aa3e2366..043984800ac 100644 --- a/libs/wire-api/src/Wire/API/MLS/SubConversation.hs +++ b/libs/wire-api/src/Wire/API/MLS/SubConversation.hs @@ -35,7 +35,7 @@ import Data.Schema hiding (HasField) import Data.Text qualified as T import Data.Time.Clock import GHC.Records -import Imports hiding (cs) +import Imports import Servant (FromHttpApiData (..), ToHttpApiData (toQueryParam)) import Test.QuickCheck import Wire.API.MLS.CipherSuite diff --git a/libs/wire-api/src/Wire/API/MLS/Validation.hs b/libs/wire-api/src/Wire/API/MLS/Validation.hs index 2f98d969426..b256f846632 100644 --- a/libs/wire-api/src/Wire/API/MLS/Validation.hs +++ b/libs/wire-api/src/Wire/API/MLS/Validation.hs @@ -29,7 +29,7 @@ import Data.Text.Lazy qualified as LT import Data.Text.Lazy.Builder qualified as LT import Data.Text.Lazy.Builder.Int qualified as LT import Data.X509 qualified as X509 -import Imports hiding (cs) +import Imports import Wire.API.MLS.Capabilities import Wire.API.MLS.CipherSuite import Wire.API.MLS.Credential diff --git a/libs/wire-api/src/Wire/API/MLS/Welcome.hs b/libs/wire-api/src/Wire/API/MLS/Welcome.hs index 08028e31219..8cf1839e9d5 100644 --- a/libs/wire-api/src/Wire/API/MLS/Welcome.hs +++ b/libs/wire-api/src/Wire/API/MLS/Welcome.hs @@ -18,7 +18,7 @@ module Wire.API.MLS.Welcome where import Data.OpenApi qualified as S -import Imports hiding (cs) +import Imports import Wire.API.MLS.CipherSuite import Wire.API.MLS.Commit import Wire.API.MLS.KeyPackage diff --git a/libs/wire-api/src/Wire/API/Notification.hs b/libs/wire-api/src/Wire/API/Notification.hs index 1b7601bce10..83317eb5259 100644 --- a/libs/wire-api/src/Wire/API/Notification.hs +++ b/libs/wire-api/src/Wire/API/Notification.hs @@ -51,6 +51,7 @@ import Data.OpenApi (ToParamSchema (..)) import Data.OpenApi qualified as S import Data.SOP import Data.Schema +import Data.Text.Encoding import Data.Time.Clock (UTCTime) import Data.UUID qualified as UUID import Imports @@ -150,7 +151,7 @@ newtype RawNotificationId = RawNotificationId {unRawNotificationId :: ByteString deriving stock (Eq, Show, Generic) instance FromHttpApiData RawNotificationId where - parseUrlPiece = pure . RawNotificationId . cs + parseUrlPiece = pure . RawNotificationId . encodeUtf8 instance ToParamSchema RawNotificationId where toParamSchema _ = toParamSchema (Proxy @Text) diff --git a/libs/wire-api/src/Wire/API/OAuth.hs b/libs/wire-api/src/Wire/API/OAuth.hs index 8b0a8617ad3..dfbd5987201 100644 --- a/libs/wire-api/src/Wire/API/OAuth.hs +++ b/libs/wire-api/src/Wire/API/OAuth.hs @@ -26,7 +26,7 @@ import Data.Aeson.KeyMap qualified as M import Data.Aeson.Types qualified as A import Data.ByteArray (convert) import Data.ByteString.Conversion -import Data.ByteString.Lazy (toStrict) +import Data.ByteString.Lazy (fromStrict, toStrict) import Data.Either.Combinators (mapLeft) import Data.HashMap.Strict qualified as HM import Data.Id as Id @@ -115,8 +115,8 @@ instance ToSchema OAuthClientConfig where where applicationNameDescription = description ?~ "The name of the application. This will be shown to the user when they are asked to authorize the application. The name must be between " <> minL <> " and " <> maxL <> " characters long." redirectUrlDescription = description ?~ "The URL to redirect to after the user has authorized the application." - minL = cs @String @Text $ symbolVal $ Proxy @(Show_ OAuthApplicationNameMinLength) - maxL = cs @String @Text $ symbolVal $ Proxy @(Show_ OAuthApplicationNameMaxLength) + minL = T.pack $ symbolVal $ Proxy @(Show_ OAuthApplicationNameMinLength) + maxL = T.pack $ symbolVal $ Proxy @(Show_ OAuthApplicationNameMaxLength) newtype OAuthClientPlainTextSecret = OAuthClientPlainTextSecret {unOAuthClientPlainTextSecret :: AsciiBase16} deriving (Eq, Generic, Arbitrary) @@ -130,7 +130,7 @@ instance ToSchema OAuthClientPlainTextSecret where schema = (toText . unOAuthClientPlainTextSecret) .= parsedText "OAuthClientPlainTextSecret" (fmap OAuthClientPlainTextSecret . validateBase16) instance FromHttpApiData OAuthClientPlainTextSecret where - parseQueryParam = bimap cs OAuthClientPlainTextSecret . validateBase16 . cs + parseQueryParam = bimap T.pack OAuthClientPlainTextSecret . validateBase16 instance ToHttpApiData OAuthClientPlainTextSecret where toQueryParam = toText . unOAuthClientPlainTextSecret @@ -236,11 +236,17 @@ instance ToSchema OAuthScopes where schema = OAuthScopes <$> (oauthScopesToText . unOAuthScopes) .= withParser schema oauthScopeParser where oauthScopesToText :: Set OAuthScope -> Text - oauthScopesToText = T.intercalate " " . fmap (cs . toByteString') . Set.toList + oauthScopesToText = + T.intercalate " " + . fmap (TE.decodeUtf8With lenientDecode . toByteString') + . Set.toList oauthScopeParser :: Text -> A.Parser (Set OAuthScope) oauthScopeParser scope = - pure $ (not . T.null) `filter` T.splitOn " " scope & maybe Set.empty Set.fromList . mapM (fromByteString' . cs) + pure $ + (not . T.null) `filter` T.splitOn " " scope + & maybe Set.empty Set.fromList + . mapM (fromByteString' . fromStrict . TE.encodeUtf8) data CodeChallengeMethod = S256 deriving (Eq, Show, Generic) @@ -265,7 +271,7 @@ instance ToSchema OAuthCodeVerifier where schema = OAuthCodeVerifier <$> unOAuthCodeVerifier .= schema instance FromHttpApiData OAuthCodeVerifier where - parseQueryParam = fmap OAuthCodeVerifier . mapLeft cs . checkedEither + parseQueryParam = fmap OAuthCodeVerifier . mapLeft T.pack . checkedEither instance ToHttpApiData OAuthCodeVerifier where toQueryParam = fromRange . unOAuthCodeVerifier @@ -294,7 +300,7 @@ mkChallenge = . encodeBase64UrlUnpadded . convert . Crypto.hash @ByteString @Crypto.SHA256 - . cs + . TE.encodeUtf8 . fromRange . unOAuthCodeVerifier @@ -347,7 +353,7 @@ instance FromByteString OAuthAuthorizationCode where parser = OAuthAuthorizationCode <$> parser instance FromHttpApiData OAuthAuthorizationCode where - parseQueryParam = bimap cs OAuthAuthorizationCode . validateBase16 . cs + parseQueryParam = bimap T.pack OAuthAuthorizationCode . validateBase16 instance ToHttpApiData OAuthAuthorizationCode where toQueryParam = toText . unOAuthAuthorizationCode @@ -379,10 +385,10 @@ instance ToByteString OAuthGrantType where OAuthGrantTypeRefreshToken -> "refresh_token" instance FromHttpApiData OAuthGrantType where - parseQueryParam = maybe (Left "invalid OAuthGrantType") pure . fromByteString . cs + parseQueryParam = maybe (Left "invalid OAuthGrantType") pure . fromByteString . TE.encodeUtf8 instance ToHttpApiData OAuthGrantType where - toQueryParam = cs . toByteString + toQueryParam = TE.decodeUtf8With lenientDecode . toStrict . toByteString data OAuthAccessTokenRequest = OAuthAccessTokenRequest { grantType :: OAuthGrantType, @@ -454,20 +460,27 @@ instance ToByteString (OAuthToken a) where instance FromByteString (OAuthToken a) where parser = do t <- parser @Text - case decodeCompact (cs (TE.encodeUtf8 t)) of + case decodeCompact (fromStrict (TE.encodeUtf8 t)) of Left (err :: JWTError) -> fail $ show err Right jwt -> pure $ OAuthToken jwt instance ToHttpApiData (OAuthToken a) where toHeader = toByteString' - toUrlPiece = cs . toHeader + toUrlPiece = TE.decodeUtf8With lenientDecode . toHeader instance FromHttpApiData (OAuthToken a) where - parseHeader = either (Left . cs) pure . runParser parser . cs - parseUrlPiece = parseHeader . cs + parseHeader = either (Left . T.pack) pure . runParser parser + parseUrlPiece = parseHeader . TE.encodeUtf8 instance ToSchema (OAuthToken a) where - schema = (TE.decodeUtf8 . toByteString') .= withParser schema (either fail pure . runParser parser . cs) + schema = + (TE.decodeUtf8 . toByteString') + .= withParser + schema + ( either fail pure + . runParser parser + . TE.encodeUtf8 + ) type OAuthAccessToken = OAuthToken 'Access @@ -686,8 +699,11 @@ instance Cql OAuthAuthorizationCode where instance Cql OAuthScope where ctype = Tagged TextColumn - toCql = CqlText . cs . toByteString' - fromCql (CqlText t) = maybe (Left "invalid oauth scope") Right $ fromByteString' (cs t) + toCql = CqlText . TE.decodeUtf8With lenientDecode . toByteString' + fromCql (CqlText t) = + maybe (Left "invalid oauth scope") Right $ + fromByteString' . fromStrict . TE.encodeUtf8 $ + t fromCql _ = Left "OAuthScope: Text expected" instance Cql OAuthCodeChallenge where diff --git a/libs/wire-api/src/Wire/API/Routes/MultiTablePaging/State.hs b/libs/wire-api/src/Wire/API/Routes/MultiTablePaging/State.hs index 7d43b3009be..1fae94b78b4 100644 --- a/libs/wire-api/src/Wire/API/Routes/MultiTablePaging/State.hs +++ b/libs/wire-api/src/Wire/API/Routes/MultiTablePaging/State.hs @@ -64,7 +64,9 @@ instance PagingTable tables => ToHttpApiData (MultiTablePagingState name tables) toQueryParam = (Text.decodeUtf8 . Base64Url.encode) . encodePagingState instance PagingTable tables => FromHttpApiData (MultiTablePagingState name tables) where - parseQueryParam = mapLeft cs . (parsePagingState <=< (Base64Url.decode . Text.encodeUtf8)) + parseQueryParam = + mapLeft Text.pack + . (parsePagingState <=< (Base64Url.decode . Text.encodeUtf8)) -- | A class for values that can be encoded with a single byte. Used to add a -- byte of extra information to the paging state in order to recover the table diff --git a/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs b/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs index ed24bbfdbe5..7c4e6dcd5ab 100644 --- a/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs +++ b/libs/wire-api/src/Wire/API/Routes/MultiVerb.hs @@ -72,7 +72,7 @@ import Data.Text.Encoding qualified as Text import Data.Typeable import GHC.TypeLits import Generics.SOP as GSOP -import Imports hiding (cs) +import Imports import Network.HTTP.Media qualified as M import Network.HTTP.Types (hContentType) import Network.HTTP.Types qualified as HTTP diff --git a/libs/wire-api/src/Wire/API/Routes/Named.hs b/libs/wire-api/src/Wire/API/Routes/Named.hs index f76ada19664..5e8220818b5 100644 --- a/libs/wire-api/src/Wire/API/Routes/Named.hs +++ b/libs/wire-api/src/Wire/API/Routes/Named.hs @@ -25,6 +25,7 @@ import Data.Metrics.Servant import Data.OpenApi.Lens hiding (HasServer) import Data.OpenApi.Operation import Data.Proxy +import Data.Text qualified as T import GHC.TypeLits import Imports import Servant @@ -42,7 +43,7 @@ class RenderableSymbol a where renderSymbol :: Text instance {-# OVERLAPPABLE #-} KnownSymbol a => RenderableSymbol a where - renderSymbol = cs . show $ symbolVal (Proxy @a) + renderSymbol = T.pack . show $ symbolVal (Proxy @a) instance {-# OVERLAPPING #-} (RenderableSymbol a, RenderableSymbol b) => RenderableSymbol '(a, b) where renderSymbol = "(" <> (renderSymbol @a) <> ", " <> (renderSymbol @b) <> ")" @@ -55,7 +56,7 @@ instance (HasOpenApi api, RenderableSymbol name) => HasOpenApi (Named name api) dscr :: Text dscr = " [internal route ID: " - <> cs (renderSymbol @name) + <> renderSymbol @name <> "]" instance HasServer api ctx => HasServer (Named name api) ctx where diff --git a/libs/wire-api/src/Wire/API/Routes/Public.hs b/libs/wire-api/src/Wire/API/Routes/Public.hs index f5dd0fd40fa..73b6de1b3f6 100644 --- a/libs/wire-api/src/Wire/API/Routes/Public.hs +++ b/libs/wire-api/src/Wire/API/Routes/Public.hs @@ -39,6 +39,7 @@ module Wire.API.Routes.Public where import Control.Lens ((%~), (<>~)) +import Data.ByteString (toStrict) import Data.ByteString.Conversion (toByteString) import Data.Domain import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap @@ -48,6 +49,8 @@ import Data.Metrics.Servant import Data.OpenApi hiding (HasServer, Header, Server) import Data.OpenApi qualified as S import Data.Qualified +import Data.Text.Encoding +import Data.Text.Encoding.Error import GHC.Base (Symbol) import GHC.TypeLits (KnownSymbol) import Imports hiding (All, head) @@ -339,7 +342,18 @@ instance toOpenApi _ = addScopeDescription @scope (toOpenApi (Proxy @api)) addScopeDescription :: forall scope. OAuth.IsOAuthScope scope => OpenApi -> OpenApi -addScopeDescription = allOperations . description %~ Just . (<> "\nOAuth scope: `" <> cs (toByteString (OAuth.toOAuthScope @scope)) <> "`") . fold +addScopeDescription = + allOperations + . description + %~ Just + . ( <> + "\nOAuth scope: `" + <> ( decodeUtf8With lenientDecode . toStrict . toByteString $ + OAuth.toOAuthScope @scope + ) + <> "`" + ) + . fold instance (HasServer api ctx) => HasServer (DescriptionOAuthScope scope :> api) ctx where type ServerT (DescriptionOAuthScope scope :> api) m = ServerT api m diff --git a/libs/wire-api/src/Wire/API/Routes/Version/Wai.hs b/libs/wire-api/src/Wire/API/Routes/Version/Wai.hs index 939b91c5b75..cd797101f11 100644 --- a/libs/wire-api/src/Wire/API/Routes/Version/Wai.hs +++ b/libs/wire-api/src/Wire/API/Routes/Version/Wai.hs @@ -21,6 +21,7 @@ import Control.Monad.Except (throwError) import Data.ByteString.Conversion import Data.EitherR (fmapL) import Data.Text qualified as T +import Data.Text.Lazy (fromStrict) import Imports import Network.HTTP.Types qualified as HTTP import Network.Wai @@ -44,7 +45,7 @@ versionMiddleware disabledAPIVersions app req k = case parseVersion (removeVersi err :: Text -> IO ResponseReceived err v = k . errorRs' . mkError HTTP.status404 "unsupported-version" $ - "Version " <> cs v <> " is not supported" + "Version " <> fromStrict v <> " is not supported" errint :: IO ResponseReceived errint = diff --git a/libs/wire-api/src/Wire/API/Team/Export.hs b/libs/wire-api/src/Wire/API/Team/Export.hs index f5bfbbe08bb..1f1d4b1462a 100644 --- a/libs/wire-api/src/Wire/API/Team/Export.hs +++ b/libs/wire-api/src/Wire/API/Team/Export.hs @@ -67,7 +67,7 @@ instance ToNamedRecord TeamExportUser where ("managed_by", secureCsvFieldToByteString (tExportManagedBy row)), ("saml_name_id", secureCsvFieldToByteString (tExportSAMLNamedId row)), ("scim_external_id", secureCsvFieldToByteString (tExportSCIMExternalId row)), - ("scim_rich_info", maybe "" (cs . Aeson.encode) (tExportSCIMRichInfo row)), + ("scim_rich_info", maybe "" (C.toStrict . Aeson.encode) (tExportSCIMRichInfo row)), ("user_id", secureCsvFieldToByteString (tExportUserId row)), ("num_devices", secureCsvFieldToByteString (tExportNumDevices row)) ] @@ -100,7 +100,7 @@ allowEmpty p str = Just <$> p str parseByteString :: forall a. FromByteString a => ByteString -> Parser a parseByteString bstr = - case parseOnly (parser @a) (cs (unquoted bstr)) of + case parseOnly (parser @a) (C.fromStrict (unquoted bstr)) of Left err -> fail err Right thing -> pure thing @@ -117,7 +117,13 @@ instance FromNamedRecord TeamExportUser where <*> (nrec .: "managed_by" >>= parseByteString) <*> (nrec .: "saml_name_id" >>= parseByteString) <*> (nrec .: "scim_external_id" >>= parseByteString) - <*> (nrec .: "scim_rich_info" >>= allowEmpty (maybe (fail "failed to decode RichInfo") pure . Aeson.decode . cs)) + <*> ( nrec .: "scim_rich_info" + >>= allowEmpty + ( maybe (fail "failed to decode RichInfo") pure + . Aeson.decode + . C.fromStrict + ) + ) <*> (nrec .: "user_id" >>= parseByteString) <*> (nrec .: "num_devices" >>= parseByteString) diff --git a/libs/wire-api/src/Wire/API/Team/Feature.hs b/libs/wire-api/src/Wire/API/Team/Feature.hs index 8dd48682f7a..55319e388d4 100644 --- a/libs/wire-api/src/Wire/API/Team/Feature.hs +++ b/libs/wire-api/src/Wire/API/Team/Feature.hs @@ -94,6 +94,7 @@ import Control.Lens (makeLenses, (?~)) import Data.Aeson qualified as A import Data.Aeson.Types qualified as A import Data.Attoparsec.ByteString qualified as Parser +import Data.ByteString (fromStrict) import Data.ByteString.Conversion import Data.ByteString.UTF8 qualified as UTF8 import Data.Domain (Domain) @@ -108,6 +109,7 @@ import Data.Schema import Data.Scientific (toBoundedInteger) import Data.Text qualified as T import Data.Text.Encoding qualified as T +import Data.Text.Encoding.Error import Data.Text.Lazy qualified as TL import Data.Time import Deriving.Aeson @@ -509,7 +511,7 @@ data LockStatus = LockStatusLocked | LockStatusUnlocked deriving (ToJSON, FromJSON, S.ToSchema) via (Schema LockStatus) instance FromHttpApiData LockStatus where - parseUrlPiece = maybeToEither "Invalid lock status" . fromByteString . cs + parseUrlPiece = maybeToEither "Invalid lock status" . fromByteString . T.encodeUtf8 instance ToSchema LockStatus where schema = @@ -1106,7 +1108,7 @@ instance RenderableSymbol EnforceFileDownloadLocationConfig where renderSymbol = "EnforceFileDownloadLocationConfig" instance Arbitrary EnforceFileDownloadLocationConfig where - arbitrary = EnforceFileDownloadLocationConfig . fmap (cs . getPrintableString) <$> arbitrary + arbitrary = EnforceFileDownloadLocationConfig . fmap (T.pack . getPrintableString) <$> arbitrary instance ToSchema EnforceFileDownloadLocationConfig where schema = @@ -1164,10 +1166,14 @@ instance S.ToParamSchema FeatureStatus where } instance FromHttpApiData FeatureStatus where - parseUrlPiece = maybe (Left "must be 'enabled' or 'disabled'") Right . fromByteString' . cs + parseUrlPiece = + maybe (Left "must be 'enabled' or 'disabled'") Right + . fromByteString' + . fromStrict + . T.encodeUtf8 instance ToHttpApiData FeatureStatus where - toUrlPiece = cs . toByteString' + toUrlPiece = T.decodeUtf8With lenientDecode . toByteString' instance ToSchema FeatureStatus where schema = diff --git a/libs/wire-api/src/Wire/API/User.hs b/libs/wire-api/src/Wire/API/User.hs index 39e01c892ce..e51b6949746 100644 --- a/libs/wire-api/src/Wire/API/User.hs +++ b/libs/wire-api/src/Wire/API/User.hs @@ -170,6 +170,7 @@ import Data.Attoparsec.ByteString qualified as Parser import Data.Attoparsec.Text qualified as TParser import Data.Bifunctor qualified as Bifunctor import Data.Bits +import Data.ByteString (toStrict) import Data.ByteString.Builder (toLazyByteString) import Data.ByteString.Conversion import Data.CaseInsensitive qualified as CI @@ -194,6 +195,7 @@ import Data.Set qualified as Set import Data.Text qualified as T import Data.Text.Ascii import Data.Text.Encoding qualified as T +import Data.Text.Encoding.Error import Data.Time.Clock (NominalDiffTime) import Data.UUID (UUID, nil) import Data.UUID qualified as UUID @@ -329,7 +331,7 @@ newtype PhonePrefix = PhonePrefix {fromPhonePrefix :: Text} instance Arbitrary PhonePrefix where arbitrary = do digits <- take 8 <$> QC.listOf1 (QC.elements ['0' .. '9']) - pure . PhonePrefix . cs $ "+" <> digits + pure . PhonePrefix . T.pack $ "+" <> digits instance ToSchema PhonePrefix where schema = fromPhonePrefix .= parsedText "PhonePrefix" phonePrefixParser @@ -344,7 +346,7 @@ instance ToByteString PhonePrefix where builder = builder . fromPhonePrefix instance FromHttpApiData PhonePrefix where - parseUrlPiece = Bifunctor.first cs . phonePrefixParser + parseUrlPiece = Bifunctor.first T.pack . phonePrefixParser deriving instance C.Cql PhonePrefix @@ -766,7 +768,11 @@ scimExternalId ManagedByWire (UserSSOId _) = Nothing ssoIssuerAndNameId :: UserSSOId -> Maybe (Text, Text) ssoIssuerAndNameId (UserSSOId (SAML.UserRef (SAML.Issuer uri) nameIdXML)) = Just (fromUri uri, fromNameId nameIdXML) where - fromUri = cs . toLazyByteString . serializeURIRef + fromUri = + T.decodeUtf8With lenientDecode + . toStrict + . toLazyByteString + . serializeURIRef fromNameId = CI.original . SAML.unsafeShowNameID ssoIssuerAndNameId (UserScimExternalId _) = Nothing @@ -1360,10 +1366,14 @@ instance S.ToParamSchema InvitationCode where toParamSchema _ = S.toParamSchema (Proxy @Text) instance FromHttpApiData InvitationCode where - parseQueryParam = bimap cs InvitationCode . validateBase64Url + parseQueryParam = bimap T.pack InvitationCode . validateBase64Url instance ToHttpApiData InvitationCode where - toQueryParam = cs . toByteString . fromInvitationCode + toQueryParam = + T.decodeUtf8With lenientDecode + . toStrict + . toByteString + . fromInvitationCode deriving instance C.Cql InvitationCode @@ -1996,10 +2006,13 @@ instance S.ToParamSchema VerificationAction where } instance FromHttpApiData VerificationAction where - parseUrlPiece = maybeToEither "Invalid verification action" . fromByteString . cs + parseUrlPiece = + maybeToEither "Invalid verification action" + . fromByteString + . T.encodeUtf8 instance ToHttpApiData VerificationAction where - toQueryParam a = cs (toByteString' a) + toQueryParam a = T.decodeUtf8With lenientDecode (toByteString' a) data SendVerificationCode = SendVerificationCode { svcAction :: VerificationAction, diff --git a/libs/wire-api/src/Wire/API/User/Client/DPoPAccessToken.hs b/libs/wire-api/src/Wire/API/User/Client/DPoPAccessToken.hs index 99ed6e13d92..980e376e7fd 100644 --- a/libs/wire-api/src/Wire/API/User/Client/DPoPAccessToken.hs +++ b/libs/wire-api/src/Wire/API/User/Client/DPoPAccessToken.hs @@ -21,13 +21,15 @@ module Wire.API.User.Client.DPoPAccessToken where import Data.Aeson (FromJSON, ToJSON) +import Data.ByteString (fromStrict) import Data.ByteString.Conversion (FromByteString (..), ToByteString (..), fromByteString', toByteString') import Data.OpenApi qualified as S import Data.OpenApi.ParamSchema (ToParamSchema (..)) import Data.SOP import Data.Schema import Data.Text as T -import Data.Text.Encoding (decodeUtf8, encodeUtf8) +import Data.Text.Encoding +import Data.Text.Encoding.Error import Imports import Servant (FromHttpApiData (..), ToHttpApiData (..)) @@ -36,10 +38,14 @@ newtype Proof = Proof {unProof :: ByteString} deriving newtype (FromByteString, ToByteString) instance ToHttpApiData Proof where - toQueryParam = cs . toByteString' + toQueryParam = decodeUtf8With lenientDecode . toByteString' instance FromHttpApiData Proof where - parseQueryParam = maybe (Left "Invalid Proof") Right . fromByteString' . cs + parseQueryParam = + maybe (Left "Invalid Proof") Right + . fromByteString' + . fromStrict + . encodeUtf8 instance ToParamSchema Proof where toParamSchema _ = toParamSchema (Proxy @Text) @@ -56,10 +62,14 @@ instance ToParamSchema DPoPAccessToken where toParamSchema _ = toParamSchema (Proxy @Text) instance ToHttpApiData DPoPAccessToken where - toQueryParam = cs . toByteString' + toQueryParam = decodeUtf8With lenientDecode . toByteString' instance FromHttpApiData DPoPAccessToken where - parseQueryParam = maybe (Left "Invalid DPoPAccessToken") Right . fromByteString' . cs + parseQueryParam = + maybe (Left "Invalid DPoPAccessToken") Right + . fromByteString' + . fromStrict + . encodeUtf8 data AccessTokenType = DPoP deriving (Eq, Show, Generic) diff --git a/libs/wire-api/src/Wire/API/User/Identity.hs b/libs/wire-api/src/Wire/API/User/Identity.hs index 0475554bfeb..88435e20602 100644 --- a/libs/wire-api/src/Wire/API/User/Identity.hs +++ b/libs/wire-api/src/Wire/API/User/Identity.hs @@ -60,13 +60,17 @@ import Data.Aeson qualified as A import Data.Aeson.Types qualified as A import Data.Attoparsec.Text import Data.Bifunctor (first) +import Data.ByteString (fromStrict, toStrict) import Data.ByteString.Conversion +import Data.ByteString.UTF8 qualified as UTF8 import Data.CaseInsensitive qualified as CI import Data.OpenApi (ToParamSchema (..)) import Data.OpenApi qualified as S import Data.Schema import Data.Text qualified as Text -import Data.Text.Encoding (decodeUtf8', encodeUtf8) +import Data.Text.Encoding +import Data.Text.Encoding.Error +import Data.Text.Lazy qualified as LT import Data.Time.Clock import Data.Tuple.Extra (fst3, snd3, thd3) import Imports @@ -189,10 +193,10 @@ instance FromByteString Email where parser = parser >>= maybe (fail "Invalid email") pure . parseEmail instance S.FromHttpApiData Email where - parseUrlPiece = maybe (Left "Invalid email") Right . fromByteString . cs + parseUrlPiece = maybe (Left "Invalid email") Right . fromByteString . encodeUtf8 instance S.ToHttpApiData Email where - toUrlPiece = cs . toByteString' + toUrlPiece = decodeUtf8With lenientDecode . toByteString' instance Arbitrary Email where arbitrary = do @@ -281,10 +285,10 @@ instance FromByteString Phone where parser = parser >>= maybe (fail "Invalid phone") pure . parsePhone instance S.FromHttpApiData Phone where - parseUrlPiece = maybe (Left "Invalid phone") Right . fromByteString . cs + parseUrlPiece = maybe (Left "Invalid phone") Right . fromByteString . encodeUtf8 instance S.ToHttpApiData Phone where - toUrlPiece = cs . toByteString' + toUrlPiece = decodeUtf8With lenientDecode . toByteString' instance Arbitrary Phone where arbitrary = @@ -331,12 +335,12 @@ data UserSSOId instance C.Cql UserSSOId where ctype = C.Tagged C.TextColumn - fromCql (C.CqlText t) = case A.eitherDecode $ cs t of + fromCql (C.CqlText t) = case A.eitherDecode $ fromStrict (encodeUtf8 t) of Right i -> pure i Left msg -> Left $ "fromCql: Invalid UserSSOId: " ++ msg fromCql _ = Left "fromCql: UserSSOId: CqlText expected" - toCql = C.toCql . cs @LByteString @Text . A.encode + toCql = C.toCql . decodeUtf8With lenientDecode . toStrict . A.encode -- | FUTUREWORK: This schema should ideally be a choice of either tenant+subject, or scim_external_id -- but this is currently not possible to derive in swagger2 @@ -394,7 +398,7 @@ lenientlyParseSAMLIssuer mbtxt = forM mbtxt $ \txt -> do asurl :: Either String SAML.Issuer asurl = bimap show SAML.Issuer $ - URI.parseURI URI.laxURIParserOptions (cs txt) + URI.parseURI URI.laxURIParserOptions (encodeUtf8 . LT.toStrict $ txt) err :: String err = "lenientlyParseSAMLIssuer: " <> show (asxml, asurl, mbtxt) @@ -412,11 +416,11 @@ lenientlyParseSAMLNameID (Just txt) = do maybe (Left "not an email") (fmap emailToSAMLNameID . validateEmail) - (parseEmail (cs txt)) + (parseEmail . LT.toStrict $ txt) astxt :: Either String SAML.NameID astxt = do - nm <- mkName (cs txt) + nm <- mkName . LT.toStrict $ txt SAML.mkNameID (SAML.mkUNameIDUnspecified (fromName nm)) Nothing Nothing Nothing err :: String @@ -449,7 +453,12 @@ mkSampleUref :: Text -> Text -> SAML.UserRef mkSampleUref iseed nseed = SAML.UserRef issuer nameid where issuer :: SAML.Issuer - issuer = SAML.Issuer ([uri|http://example.com/|] & URI.pathL .~ cs ("/" cs iseed)) + issuer = + SAML.Issuer + ( [uri|http://example.com/|] + & URI.pathL + .~ UTF8.fromString ("/" Text.unpack iseed) + ) nameid :: SAML.NameID nameid = fromRight (error "impossible") $ do diff --git a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs index e954f15c2e6..544e8718685 100644 --- a/libs/wire-api/src/Wire/API/User/IdentityProvider.hs +++ b/libs/wire-api/src/Wire/API/User/IdentityProvider.hs @@ -28,11 +28,15 @@ import Data.Aeson.Types (parseMaybe) import Data.Attoparsec.ByteString qualified as AP import Data.Binary.Builder qualified as BSB import Data.ByteString.Conversion qualified as BSC +import Data.ByteString.Lazy (fromStrict, toStrict) import Data.HashMap.Strict.InsOrd (InsOrdHashMap) import Data.HashMap.Strict.InsOrd qualified as InsOrdHashMap import Data.Id (TeamId) import Data.OpenApi import Data.Proxy (Proxy (Proxy)) +import Data.Text.Encoding +import Data.Text.Encoding.Error +import Data.Text.Lazy qualified as LT import Imports import Network.HTTP.Media ((//)) import SAML2.WebSSO (IdPConfig) @@ -98,12 +102,14 @@ instance BSC.FromByteString WireIdPAPIVersion where <|> (AP.string "v2" >> pure WireIdPAPIV2) instance FromHttpApiData WireIdPAPIVersion where - parseQueryParam txt = maybe err Right $ BSC.fromByteString' (cs txt) + parseQueryParam txt = + maybe err Right $ + (BSC.fromByteString' . fromStrict . encodeUtf8) txt where err = Left $ "FromHttpApiData WireIdPAPIVersion: " <> txt instance ToHttpApiData WireIdPAPIVersion where - toQueryParam = cs . BSC.toByteString' + toQueryParam = decodeUtf8With lenientDecode . BSC.toByteString' instance ToParamSchema WireIdPAPIVersion where toParamSchema Proxy = @@ -153,10 +159,13 @@ instance Accept RawXML where contentType Proxy = "application" // "xml" instance MimeUnrender RawXML IdPMetadataInfo where - mimeUnrender Proxy raw = IdPMetadataValue (cs raw) <$> mimeUnrender (Proxy @SAML.XML) raw + mimeUnrender Proxy raw = + IdPMetadataValue + (decodeUtf8With lenientDecode . toStrict $ raw) + <$> mimeUnrender (Proxy @SAML.XML) raw instance MimeRender RawXML RawIdPMetadata where - mimeRender Proxy (RawIdPMetadata raw) = cs raw + mimeRender Proxy (RawIdPMetadata raw) = fromStrict . encodeUtf8 $ raw newtype RawIdPMetadata = RawIdPMetadata Text deriving (Eq, Show, Generic) @@ -164,7 +173,7 @@ newtype RawIdPMetadata = RawIdPMetadata Text instance FromJSON IdPMetadataInfo where parseJSON = withObject "IdPMetadataInfo" $ \obj -> do raw <- obj .: "value" - either fail (pure . IdPMetadataValue raw) (SAML.decode (cs raw)) + either fail (pure . IdPMetadataValue raw) (SAML.decode (LT.fromStrict raw)) instance ToJSON IdPMetadataInfo where toJSON (IdPMetadataValue _ x) = diff --git a/libs/wire-api/src/Wire/API/User/RichInfo.hs b/libs/wire-api/src/Wire/API/User/RichInfo.hs index e6723b9d651..d84ba4f3c2f 100644 --- a/libs/wire-api/src/Wire/API/User/RichInfo.hs +++ b/libs/wire-api/src/Wire/API/User/RichInfo.hs @@ -133,7 +133,7 @@ ciObject name sch = mkSchema s r w desc = S.description ?~ ("json object with case-insensitive fields." :: Text) r :: A.Value -> A.Parser b - r = A.withObject (cs name) f + r = A.withObject (Text.unpack name) f where f :: A.Object -> A.Parser b f = schemaIn sch . g @@ -350,8 +350,8 @@ instance ToSchema RichField where instance Arbitrary RichField where arbitrary = RichField - <$> (CI.mk . cs . QC.getPrintableString <$> arbitrary) - <*> (cs . QC.getPrintableString <$> arbitrary) + <$> (CI.mk . Text.pack . QC.getPrintableString <$> arbitrary) + <*> (Text.pack . QC.getPrintableString <$> arbitrary) shrink (RichField k v) = RichField <$> QC.shrink k <*> QC.shrink v -------------------------------------------------------------------------------- diff --git a/libs/wire-api/src/Wire/API/User/Saml.hs b/libs/wire-api/src/Wire/API/User/Saml.hs index 09ad0d24367..f165a17f76c 100644 --- a/libs/wire-api/src/Wire/API/User/Saml.hs +++ b/libs/wire-api/src/Wire/API/User/Saml.hs @@ -28,11 +28,14 @@ import Control.Lens (makeLenses) import Control.Monad.Except import Data.Aeson hiding (fieldLabelModifier) import Data.Aeson.TH hiding (fieldLabelModifier) +import Data.ByteString (toStrict) import Data.ByteString.Builder qualified as Builder import Data.Id (UserId) import Data.OpenApi import Data.Proxy (Proxy (Proxy)) import Data.Text qualified as T +import Data.Text.Encoding +import Data.Text.Encoding.Error import Data.Time import GHC.TypeLits (KnownSymbol, symbolVal) import GHC.Types (Symbol) @@ -66,8 +69,15 @@ deriveJSON deriveJSONOptions ''VerdictFormat mkVerdictGrantedFormatMobile :: MonadError String m => URI -> SetCookie -> UserId -> m URI mkVerdictGrantedFormatMobile before cky uid = parseURI' - . substituteVar "cookie" (cs . Builder.toLazyByteString . renderSetCookie $ cky) - . substituteVar "userid" (cs . show $ uid) + . substituteVar + "cookie" + ( decodeUtf8With lenientDecode + . toStrict + . Builder.toLazyByteString + . renderSetCookie + $ cky + ) + . substituteVar "userid" (T.pack . show $ uid) $ renderURI before mkVerdictDeniedFormatMobile :: MonadError String m => URI -> Text -> m URI diff --git a/libs/wire-api/src/Wire/API/User/Scim.hs b/libs/wire-api/src/Wire/API/User/Scim.hs index 1b3e4e50b5e..d8482447523 100644 --- a/libs/wire-api/src/Wire/API/User/Scim.hs +++ b/libs/wire-api/src/Wire/API/User/Scim.hs @@ -61,6 +61,7 @@ import Data.Map qualified as Map import Data.Misc (PlainTextPassword6) import Data.OpenApi hiding (Operation) import Data.Proxy +import Data.Text qualified as T import Data.Text.Encoding (decodeUtf8, encodeUtf8) import Data.Time.Clock (UTCTime) import Imports @@ -252,8 +253,8 @@ instance QC.Arbitrary (Scim.User SparTag) where where addFields :: Scim.User.User tag -> QC.Gen (Scim.User.User tag) addFields usr = do - gexternalId <- cs . QC.getPrintableString <$$> QC.arbitrary - gdisplayName <- cs . QC.getPrintableString <$$> QC.arbitrary + gexternalId <- T.pack . QC.getPrintableString <$$> QC.arbitrary + gdisplayName <- T.pack . QC.getPrintableString <$$> QC.arbitrary gactive <- Just . Scim.ScimBool <$> QC.arbitrary -- (`Nothing` maps on `Just True` and was in the way of a unit test.) gemails <- catMaybes <$> (A.decode <$$> QC.listOf (QC.elements ["a@b.c", "x@y,z", "roland@st.uv"])) pure @@ -268,7 +269,7 @@ instance QC.Arbitrary (Scim.User SparTag) where genSchemas = QC.listOf1 $ QC.elements Scim.fakeEnumSchema genUserName :: QC.Gen Text - genUserName = cs . QC.getPrintableString <$> QC.arbitrary + genUserName = T.pack . QC.getPrintableString <$> QC.arbitrary genExtra :: QC.Gen ScimUserExtra genExtra = QC.arbitrary diff --git a/libs/wire-api/src/Wire/API/User/Search.hs b/libs/wire-api/src/Wire/API/User/Search.hs index 8b2fa6fb710..435c7cf3998 100644 --- a/libs/wire-api/src/Wire/API/User/Search.hs +++ b/libs/wire-api/src/Wire/API/User/Search.hs @@ -73,7 +73,7 @@ instance ToParamSchema PagingState where toParamSchema _ = toParamSchema (Proxy @Text) instance FromHttpApiData PagingState where - parseQueryParam s = mapLeft cs $ PagingState <$> validateBase64Url s + parseQueryParam s = mapLeft T.pack $ PagingState <$> validateBase64Url s instance ToHttpApiData PagingState where toQueryParam = toText . unPagingState diff --git a/libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs b/libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs index f4c9a736005..6a458413d84 100644 --- a/libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs +++ b/libs/wire-api/test/golden/Test/Wire/API/Golden/Runner.hs @@ -33,6 +33,7 @@ import Data.ByteString.Lazy qualified as LBS import Data.ProtoLens.Encoding (decodeMessage, encodeMessage) import Data.ProtoLens.Message (Message) import Data.ProtoLens.TextFormat (readMessage, showMessage) +import Data.String.Conversions import Data.Text.Lazy.IO qualified as LText import Imports import Test.Tasty (TestTree) diff --git a/libs/wire-api/test/unit/Test/Wire/API/Routes/Version.hs b/libs/wire-api/test/unit/Test/Wire/API/Routes/Version.hs index 603e14776af..b6224933398 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Routes/Version.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Routes/Version.hs @@ -4,6 +4,7 @@ import Data.Aeson as Aeson import Data.Binary.Builder import Data.ByteString.Conversion import Data.Set as Set +import Data.String.Conversions import Imports import Servant.API import Test.Tasty diff --git a/libs/wire-api/test/unit/Test/Wire/API/Routes/Version/Wai.hs b/libs/wire-api/test/unit/Test/Wire/API/Routes/Version/Wai.hs index 573904b8600..2e87537f60b 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/Routes/Version/Wai.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/Routes/Version/Wai.hs @@ -2,6 +2,7 @@ module Test.Wire.API.Routes.Version.Wai where import Data.Proxy import Data.Set qualified as Set +import Data.String.Conversions import Data.Text as T import Imports import Network.HTTP.Types.Status (status200, status400) diff --git a/libs/wire-api/test/unit/Test/Wire/API/User/Search.hs b/libs/wire-api/test/unit/Test/Wire/API/User/Search.hs index ad437d6d06f..a1fd13f4252 100644 --- a/libs/wire-api/test/unit/Test/Wire/API/User/Search.hs +++ b/libs/wire-api/test/unit/Test/Wire/API/User/Search.hs @@ -20,6 +20,7 @@ module Test.Wire.API.User.Search where import Data.Aeson (encode, toJSON) import Data.Aeson qualified as Aeson import Data.Aeson.KeyMap qualified as KeyMap +import Data.String.Conversions import Imports import Test.Tasty qualified as T import Test.Tasty.QuickCheck (counterexample, testProperty) diff --git a/libs/wire-api/wire-api.cabal b/libs/wire-api/wire-api.cabal index a5ad166cf26..e17d212cde5 100644 --- a/libs/wire-api/wire-api.cabal +++ b/libs/wire-api/wire-api.cabal @@ -614,6 +614,7 @@ test-suite wire-api-golden-tests , lens , pem , proto-lens + , string-conversions , tasty , tasty-hunit , text @@ -689,6 +690,7 @@ test-suite wire-api-tests , schema-profunctor , servant , servant-server + , string-conversions , tasty , tasty-hspec , tasty-hunit diff --git a/libs/wire-subsystems/default.nix b/libs/wire-subsystems/default.nix index bd3ef35a59c..dc35149eb50 100644 --- a/libs/wire-subsystems/default.nix +++ b/libs/wire-subsystems/default.nix @@ -26,6 +26,7 @@ , QuickCheck , quickcheck-instances , retry +, string-conversions , text , tinylog , types-common @@ -73,6 +74,7 @@ mkDerivation { polysemy-wire-zoo QuickCheck quickcheck-instances + string-conversions types-common wire-api ]; diff --git a/libs/wire-subsystems/test/unit/Wire/NotificationSubsystem/InterpreterSpec.hs b/libs/wire-subsystems/test/unit/Wire/NotificationSubsystem/InterpreterSpec.hs index 4677a0d1bfd..1b5aee83b2b 100644 --- a/libs/wire-subsystems/test/unit/Wire/NotificationSubsystem/InterpreterSpec.hs +++ b/libs/wire-subsystems/test/unit/Wire/NotificationSubsystem/InterpreterSpec.hs @@ -8,6 +8,7 @@ import Data.List.NonEmpty (NonEmpty ((:|)), fromList) import Data.List1 qualified as List1 import Data.Range (fromRange, toRange) import Data.Set qualified as Set +import Data.String.Conversions import Data.Time.Clock.DiffTime import Gundeck.Types.Push.V2 qualified as V2 import Imports diff --git a/libs/wire-subsystems/wire-subsystems.cabal b/libs/wire-subsystems/wire-subsystems.cabal index a631c0e2737..1c7676e7140 100644 --- a/libs/wire-subsystems/wire-subsystems.cabal +++ b/libs/wire-subsystems/wire-subsystems.cabal @@ -127,6 +127,7 @@ test-suite wire-subsystems-tests , polysemy-wire-zoo , QuickCheck , quickcheck-instances + , string-conversions , types-common , wire-api , wire-subsystems diff --git a/services/background-worker/background-worker.cabal b/services/background-worker/background-worker.cabal index 36e566299da..31657b00ca5 100644 --- a/services/background-worker/background-worker.cabal +++ b/services/background-worker/background-worker.cabal @@ -49,6 +49,7 @@ library , transformers-base , types-common , unliftio + , utf8-string , wai-utilities , wire-api , wire-api-federation diff --git a/services/background-worker/default.nix b/services/background-worker/default.nix index 31ce1fae0eb..3698011087d 100644 --- a/services/background-worker/default.nix +++ b/services/background-worker/default.nix @@ -37,6 +37,7 @@ , transformers-base , types-common , unliftio +, utf8-string , wai , wai-utilities , wire-api @@ -71,6 +72,7 @@ mkDerivation { transformers-base types-common unliftio + utf8-string wai-utilities wire-api wire-api-federation diff --git a/services/background-worker/src/Wire/BackgroundWorker.hs b/services/background-worker/src/Wire/BackgroundWorker.hs index 31a9c769034..b5e745d6558 100644 --- a/services/background-worker/src/Wire/BackgroundWorker.hs +++ b/services/background-worker/src/Wire/BackgroundWorker.hs @@ -5,6 +5,7 @@ module Wire.BackgroundWorker where import Data.Domain import Data.Map.Strict qualified as Map import Data.Metrics.Servant qualified as Metrics +import Data.Text qualified as T import Imports import Network.AMQP qualified as Q import Network.Wai.Utilities.Server @@ -47,7 +48,7 @@ run opts = do -- Close the channel. `extended` will then close the connection, flushing messages to the server. Log.info l $ Log.msg $ Log.val "Closing RabbitMQ channel" Q.closeChannel chan - let server = defaultServer (cs $ opts.backgroundWorker._host) opts.backgroundWorker._port env.logger env.metrics + let server = defaultServer (T.unpack $ opts.backgroundWorker._host) opts.backgroundWorker._port env.logger env.metrics settings <- newSettings server -- Additional cleanup when shutting down via signals. runSettingsWithCleanup cleanup settings (servantApp env) Nothing diff --git a/services/background-worker/src/Wire/BackgroundWorker/Health.hs b/services/background-worker/src/Wire/BackgroundWorker/Health.hs index dc0cc0a97d7..f56c0404c4d 100644 --- a/services/background-worker/src/Wire/BackgroundWorker/Health.hs +++ b/services/background-worker/src/Wire/BackgroundWorker/Health.hs @@ -1,5 +1,6 @@ module Wire.BackgroundWorker.Health where +import Data.ByteString.Lazy.UTF8 qualified as UTF8 import Data.Map.Strict qualified as Map import Imports import Servant @@ -17,7 +18,7 @@ statusWorkersImpl = do notWorkingWorkers <- Map.keys . Map.filter not <$> (readIORef =<< asks statuses) if null notWorkingWorkers then pure NoContent - else lift $ throwError err500 {errBody = "These workers are not working: " <> cs (show notWorkingWorkers)} + else lift $ throwError err500 {errBody = "These workers are not working: " <> UTF8.fromString (show notWorkingWorkers)} api :: Env -> HealthAPI AsServer api env = fromServant $ hoistServer (Proxy @(ToServant HealthAPI AsApi)) (runAppT env) (toServant apiInAppT) diff --git a/services/brig/brig.cabal b/services/brig/brig.cabal index a57c21f5e6c..9ad17d98040 100644 --- a/services/brig/brig.cabal +++ b/services/brig/brig.cabal @@ -351,6 +351,7 @@ library , unliftio >=0.2 , unordered-containers >=0.2 , uri-bytestring >=0.2 + , utf8-string , uuid >=1.3.5 , vector >=0.11 , wai >=3.0 @@ -496,6 +497,7 @@ executable brig-integration , servant-client-core , spar , streaming-commons + , string-conversions , tasty >=1.0 , tasty-ant-xml , tasty-cannon >=0.3.4 diff --git a/services/brig/default.nix b/services/brig/default.nix index 37c5d355190..fc3eff7812f 100644 --- a/services/brig/default.nix +++ b/services/brig/default.nix @@ -122,6 +122,7 @@ , statistics , stomp-queue , streaming-commons +, string-conversions , tasty , tasty-ant-xml , tasty-cannon @@ -144,6 +145,7 @@ , unliftio , unordered-containers , uri-bytestring +, utf8-string , uuid , vector , wai @@ -277,6 +279,7 @@ mkDerivation { unliftio unordered-containers uri-bytestring + utf8-string uuid vector wai @@ -352,6 +355,7 @@ mkDerivation { servant-client-core spar streaming-commons + string-conversions tasty tasty-ant-xml tasty-cannon diff --git a/services/brig/src/Brig/API/Client.hs b/services/brig/src/Brig/API/Client.hs index 65bb1ab8b77..2fbd3390af5 100644 --- a/services/brig/src/Brig/API/Client.hs +++ b/services/brig/src/Brig/API/Client.hs @@ -76,6 +76,7 @@ import Cassandra (MonadClient) import Control.Error import Control.Lens (view) import Control.Monad.Trans.Except (except) +import Data.ByteString (toStrict) import Data.ByteString.Conversion import Data.Code as Code import Data.Domain @@ -86,6 +87,8 @@ import Data.Map.Strict qualified as Map import Data.Misc (PlainTextPassword6) import Data.Qualified import Data.Set qualified as Set +import Data.Text.Encoding qualified as T +import Data.Text.Encoding.Error import Data.Time.Clock (UTCTime) import Imports import Network.HTTP.Types.Method (StdMethod) @@ -539,8 +542,19 @@ createAccessToken luid cid method link proof = do <$> note NotATeamUser (userTeam =<< mUser) <*> note MissingHandle (userHandle =<< mUser) <*> note MissingName (userDisplayName <$> mUser) - nonce <- ExceptT $ note NonceNotFound <$> wrapClient (Nonce.lookupAndDeleteNonce uid (cs $ toByteString cid)) - httpsUrl <- except $ note MisconfiguredRequestUrl $ fromByteString $ "https://" <> toByteString' domain <> "/" <> cs (toUrlPiece link) + nonce <- + ExceptT $ + note NonceNotFound + <$> wrapClient + ( Nonce.lookupAndDeleteNonce + uid + (T.decodeUtf8With lenientDecode . toStrict $ toByteString cid) + ) + httpsUrl <- + except $ + note MisconfiguredRequestUrl $ + fromByteString $ + "https://" <> toByteString' domain <> "/" <> T.encodeUtf8 (toUrlPiece link) maxSkewSeconds <- Opt.setDpopMaxSkewSecs <$> view settings expiresIn <- Opt.setDpopTokenExpirationTimeSecs <$> view settings now <- fromUTCTime <$> lift (liftSem Now.get) diff --git a/services/brig/src/Brig/API/Error.hs b/services/brig/src/Brig/API/Error.hs index 14cef9c4be8..23a24bad4e2 100644 --- a/services/brig/src/Brig/API/Error.hs +++ b/services/brig/src/Brig/API/Error.hs @@ -25,6 +25,7 @@ import Data.Aeson.KeyMap qualified as KeyMap import Data.ByteString.Conversion import Data.Domain (Domain) import Data.Jwt.Tools (DPoPTokenGenerationError (..)) +import Data.Text.Lazy as LT import Data.ZAuth.Validation qualified as ZAuth import Imports import Network.HTTP.Types.Header @@ -447,7 +448,7 @@ customerExtensionBlockedDomain domain = Wai.mkError (mkStatus 451 "Unavailable F where msg = "[Customer extension] the email domain " - <> cs (show domain) + <> LT.pack (show domain) <> " that you are attempting to register a user with has been \ \blocked for creating wire users. Please contact your IT department." diff --git a/services/brig/src/Brig/API/Internal.hs b/services/brig/src/Brig/API/Internal.hs index 28d004f2ec8..12e529d9886 100644 --- a/services/brig/src/Brig/API/Internal.hs +++ b/services/brig/src/Brig/API/Internal.hs @@ -69,6 +69,8 @@ import Data.Id as Id import Data.Map.Strict qualified as Map import Data.Qualified import Data.Set qualified as Set +import Data.Text qualified as T +import Data.Text.Lazy qualified as LT import Data.Time.Clock (UTCTime) import Data.Time.Clock.System import Imports hiding (head) @@ -296,24 +298,24 @@ addFederationRemote fedDomConf = do "keeping track of remote domains in the brig config file is deprecated, but as long as we \ \do that, adding a domain with different settings than in the config file is not allowed. want " <> ( "Just " - <> cs (show fedDomConf) + <> T.pack (show fedDomConf) <> "or Nothing, " ) <> ( "got " - <> cs (show (Map.lookup (domain fedDomConf) cfg)) + <> T.pack (show (Map.lookup (domain fedDomConf) cfg)) ) updateFederationRemote :: (Member FederationConfigStore r) => Domain -> FederationDomainConfig -> (Handler r) () updateFederationRemote dom fedcfg = do if (dom /= fedcfg.domain) then - throwError . fedError . FederationUnexpectedError . cs $ + throwError . fedError . FederationUnexpectedError . T.pack $ "federation domain of a given peer cannot be changed from " <> show (domain fedcfg) <> " to " <> show dom <> "." else lift (liftSem (E.updateFederationConfig fedcfg)) >>= \case UpdateFederationSuccess -> pure () UpdateFederationRemoteNotFound -> - throwError . fedError . FederationUnexpectedError . cs $ + throwError . fedError . FederationUnexpectedError . T.pack $ "federation domain does not exist and cannot be updated: " <> show (dom, fedcfg) UpdateFederationRemoteDivergingConfig -> throwError . fedError . FederationUnexpectedError $ @@ -571,7 +573,13 @@ listActivatedAccounts elh includePendingInvitations = do getActivationCodeH :: Maybe Email -> Maybe Phone -> (Handler r) GetActivationCodeResp getActivationCodeH (Just email) Nothing = getActivationCode (Left email) getActivationCodeH Nothing (Just phone) = getActivationCode (Right phone) -getActivationCodeH bade badp = throwStd (badRequest ("need exactly one of email, phone: " <> Imports.cs (show (bade, badp)))) +getActivationCodeH bade badp = + throwStd + ( badRequest + ( "need exactly one of email, phone: " + <> LT.pack (show (bade, badp)) + ) + ) getActivationCode :: Either Email Phone -> (Handler r) GetActivationCodeResp getActivationCode emailOrPhone = do @@ -587,7 +595,11 @@ getPasswordResetCodeH :: (Handler r) GetPasswordResetCodeResp getPasswordResetCodeH (Just email) Nothing = getPasswordResetCode (Left email) getPasswordResetCodeH Nothing (Just phone) = getPasswordResetCode (Right phone) -getPasswordResetCodeH bade badp = throwStd (badRequest ("need exactly one of email, phone: " <> Imports.cs (show (bade, badp)))) +getPasswordResetCodeH bade badp = + throwStd + ( badRequest + ("need exactly one of email, phone: " <> LT.pack (show (bade, badp))) + ) getPasswordResetCode :: ( Member CodeStore r, @@ -659,7 +671,11 @@ revokeIdentityH :: (Handler r) NoContent revokeIdentityH (Just email) Nothing = lift $ NoContent <$ API.revokeIdentity (Left email) revokeIdentityH Nothing (Just phone) = lift $ NoContent <$ API.revokeIdentity (Right phone) -revokeIdentityH bade badp = throwStd (badRequest ("need exactly one of email, phone: " <> Imports.cs (show (bade, badp)))) +revokeIdentityH bade badp = + throwStd + ( badRequest + ("need exactly one of email, phone: " <> LT.pack (show (bade, badp))) + ) updateConnectionInternalH :: ( Member GalleyProvider r, @@ -676,7 +692,11 @@ updateConnectionInternalH updateConn = do checkBlacklistH :: Member BlacklistStore r => Maybe Email -> Maybe Phone -> (Handler r) CheckBlacklistResponse checkBlacklistH (Just email) Nothing = checkBlacklist (Left email) checkBlacklistH Nothing (Just phone) = checkBlacklist (Right phone) -checkBlacklistH bade badp = throwStd (badRequest ("need exactly one of email, phone: " <> Imports.cs (show (bade, badp)))) +checkBlacklistH bade badp = + throwStd + ( badRequest + ("need exactly one of email, phone: " <> LT.pack (show (bade, badp))) + ) checkBlacklist :: Member BlacklistStore r => Either Email Phone -> (Handler r) CheckBlacklistResponse checkBlacklist emailOrPhone = lift $ bool NotBlacklisted YesBlacklisted <$> API.isBlacklisted emailOrPhone @@ -684,7 +704,11 @@ checkBlacklist emailOrPhone = lift $ bool NotBlacklisted YesBlacklisted <$> API. deleteFromBlacklistH :: Member BlacklistStore r => Maybe Email -> Maybe Phone -> (Handler r) NoContent deleteFromBlacklistH (Just email) Nothing = deleteFromBlacklist (Left email) deleteFromBlacklistH Nothing (Just phone) = deleteFromBlacklist (Right phone) -deleteFromBlacklistH bade badp = throwStd (badRequest ("need exactly one of email, phone: " <> Imports.cs (show (bade, badp)))) +deleteFromBlacklistH bade badp = + throwStd + ( badRequest + ("need exactly one of email, phone: " <> LT.pack (show (bade, badp))) + ) deleteFromBlacklist :: Member BlacklistStore r => Either Email Phone -> (Handler r) NoContent deleteFromBlacklist emailOrPhone = lift $ NoContent <$ API.blacklistDelete emailOrPhone @@ -692,7 +716,11 @@ deleteFromBlacklist emailOrPhone = lift $ NoContent <$ API.blacklistDelete email addBlacklistH :: Member BlacklistStore r => Maybe Email -> Maybe Phone -> (Handler r) NoContent addBlacklistH (Just email) Nothing = addBlacklist (Left email) addBlacklistH Nothing (Just phone) = addBlacklist (Right phone) -addBlacklistH bade badp = throwStd (badRequest ("need exactly one of email, phone: " <> Imports.cs (show (bade, badp)))) +addBlacklistH bade badp = + throwStd + ( badRequest + ("need exactly one of email, phone: " <> LT.pack (show (bade, badp))) + ) addBlacklist :: Member BlacklistStore r => Either Email Phone -> (Handler r) NoContent addBlacklist emailOrPhone = lift $ NoContent <$ API.blacklistInsert emailOrPhone diff --git a/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs b/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs index 0783663e807..16707b15ff4 100644 --- a/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs +++ b/services/brig/src/Brig/API/MLS/KeyPackages/Validation.hs @@ -34,7 +34,7 @@ import Data.ByteString qualified as LBS import Data.Qualified import Data.Time.Clock import Data.Time.Clock.POSIX -import Imports hiding (cs) +import Imports import Wire.API.Error import Wire.API.Error.Brig import Wire.API.MLS.CipherSuite diff --git a/services/brig/src/Brig/API/OAuth.hs b/services/brig/src/Brig/API/OAuth.hs index 43453f45548..8643e8eff6d 100644 --- a/services/brig/src/Brig/API/OAuth.hs +++ b/services/brig/src/Brig/API/OAuth.hs @@ -39,6 +39,7 @@ import Data.Id import Data.Misc import Data.Set qualified as Set import Data.Text.Ascii +import Data.Text.Encoding qualified as T import Data.Time import Imports hiding (exp) import OpenSSL.Random (randBytes) @@ -125,13 +126,40 @@ createNewOAuthAuthorizationCode :: UserId -> CreateOAuthAuthorizationCodeRequest createNewOAuthAuthorizationCode uid code = do runExceptT (validateAndCreateAuthorizationCode uid code) >>= \case Right oauthCode -> - pure $ CreateOAuthCodeSuccess $ code.redirectUri & addParams [("code", toByteString' oauthCode), ("state", cs code.state)] + pure $ + CreateOAuthCodeSuccess $ + code.redirectUri + & addParams + [ ("code", toByteString' oauthCode), + ("state", T.encodeUtf8 code.state) + ] Left CreateNewOAuthCodeErrorFeatureDisabled -> - pure $ CreateOAuthCodeFeatureDisabled $ code.redirectUri & addParams [("error", "access_denied"), ("error_description", "OAuth is not enabled"), ("state", cs code.state)] + pure $ + CreateOAuthCodeFeatureDisabled $ + code.redirectUri + & addParams + [ ("error", "access_denied"), + ("error_description", "OAuth is not enabled"), + ("state", T.encodeUtf8 code.state) + ] Left CreateNewOAuthCodeErrorClientNotFound -> - pure $ CreateOAuthCodeClientNotFound $ code.redirectUri & addParams [("error", "access_denied"), ("error_description", "The client ID was not found"), ("state", cs code.state)] + pure $ + CreateOAuthCodeClientNotFound $ + code.redirectUri + & addParams + [ ("error", "access_denied"), + ("error_description", "The client ID was not found"), + ("state", T.encodeUtf8 code.state) + ] Left CreateNewOAuthCodeErrorUnsupportedResponseType -> - pure $ CreateOAuthCodeUnsupportedResponseType $ code.redirectUri & addParams [("error", "access_denied"), ("error_description", "The client ID was not found"), ("state", cs code.state)] + pure $ + CreateOAuthCodeUnsupportedResponseType $ + code.redirectUri + & addParams + [ ("error", "access_denied"), + ("error_description", "The client ID was not found"), + ("state", T.encodeUtf8 code.state) + ] Left CreateNewOAuthCodeErrorRedirectUrlMissMatch -> pure CreateOAuthCodeRedirectUrlMissMatch diff --git a/services/brig/src/Brig/API/Public.hs b/services/brig/src/Brig/API/Public.hs index c72ee3d2db8..f4d77b27a52 100644 --- a/services/brig/src/Brig/API/Public.hs +++ b/services/brig/src/Brig/API/Public.hs @@ -78,8 +78,10 @@ import Control.Monad.Catch (throwM) import Control.Monad.Except import Data.Aeson hiding (json) import Data.Bifunctor +import Data.ByteString (fromStrict, toStrict) import Data.ByteString.Lazy qualified as Lazy import Data.ByteString.Lazy.Char8 qualified as LBS +import Data.ByteString.UTF8 qualified as UTF8 import Data.CommaSeparatedList import Data.Domain import Data.FileEmbed @@ -95,6 +97,7 @@ import Data.Range import Data.Schema () import Data.Text qualified as Text import Data.Text.Ascii qualified as Ascii +import Data.Text.Encoding qualified as Text import Data.Text.Lazy (pack) import Data.Time.Clock (UTCTime) import Data.ZAuth.Token qualified as ZAuth @@ -217,7 +220,7 @@ versionedSwaggerDocsAPI Nothing = allroutes (throwError listAllVersionsResp) Servant.Server (SwaggerSchemaUI "swagger-ui" "swagger.json") allroutes action = -- why? see 'SwaggerSchemaUI' type. - action :<|> action :<|> action :<|> error (cs listAllVersionsHTML) + action :<|> action :<|> action :<|> error (UTF8.toString . toStrict $ listAllVersionsHTML) listAllVersionsResp :: ServerError listAllVersionsResp = ServerError 200 mempty listAllVersionsHTML [("Content-Type", "text/html;charset=utf-8")] @@ -227,7 +230,11 @@ versionedSwaggerDocsAPI Nothing = allroutes (throwError listAllVersionsResp) "