From 6c49801b4d8d2306ef63883f0b28f88f9f087911 Mon Sep 17 00:00:00 2001 From: amanda-her <912amandahernando@gmail.com> Date: Wed, 5 Jul 2023 16:36:24 +0200 Subject: [PATCH 01/12] feat(auth): platform instance ownership added in policies --- .../test/resources/public/favicon.ico | Bin 0 -> 14189 bytes .../test/resources/public/index.html | 38 +++ .../test/resources/public/logo.png | Bin 0 -> 53563 bytes .../test/resources/public/manifest.json | 15 ++ .../test/resources/public/meta-favicon.ico | Bin 0 -> 242942 bytes .../test/resources/public/robots.txt | 3 + .../authorization/ResolvedResourceSpec.java | 14 + .../authorization/ResourceFieldType.java | 6 +- .../linkedin/policy/DataHubActorFilter.pdl | 14 +- .../datahub/authorization/PolicyEngine.java | 22 +- ...PlatformInstanceFieldResolverProvider.java | 69 +++++ .../authorization/PolicyEngineTest.java | 2 + schema | 250 ++++++++++++++++++ 13 files changed, 423 insertions(+), 10 deletions(-) create mode 100644 datahub-frontend/test/resources/public/favicon.ico create mode 100644 datahub-frontend/test/resources/public/index.html create mode 100644 datahub-frontend/test/resources/public/logo.png create mode 100644 datahub-frontend/test/resources/public/manifest.json create mode 100644 datahub-frontend/test/resources/public/meta-favicon.ico create mode 100644 datahub-frontend/test/resources/public/robots.txt create mode 100644 metadata-service/auth-impl/src/main/java/com/datahub/authorization/fieldresolverprovider/PlatformInstanceFieldResolverProvider.java create mode 100644 schema diff --git a/datahub-frontend/test/resources/public/favicon.ico b/datahub-frontend/test/resources/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..35687ad4fba404a1c1f00c58f29b10beb7bfbff4 GIT binary patch literal 14189 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 + + + + + + + + + + + DataHub + + + +
+ + diff --git a/datahub-frontend/test/resources/public/logo.png b/datahub-frontend/test/resources/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..5e34e6425d23fa1a19ca3c89dae7acdfb2902e86 GIT binary patch literal 53563 zcmYhiWmJ^g`!Iad9R?vapeRTibc2YX0xCnNA~lqB4oIp8RPLAqb+=cyRA21d%p_f0ro9!C!)B;4I)j zR4xw;-5|&yk?@ab)KPK>g18}#d&;`r>1&fcuIx*xifa^^-@In6rY?9jurE_yg}u(p zE8=Bl>O9ZO`%7dXL#(0BeFPbNl6-aJi>km`bKRu?QtRmEbWYD1-N&~RVvY`tC+#0_ zQxRkD2OfIBYpjf+W6KcI0mX^KQ&NBege{M=VS%zIKTZGCSEa<12O=-1{D?nw3vilk z=LsEhJ3q}Dt!o%sYZ?m7!eK#XMJjWl>XRRN?(!d-rdLQc775`#V%B7v zBpnlTx7z#-B#!P$kwd!L5INKzRJ5}CQev)9!O{EB>lK~4A^$;sjm7V^TTu30!UG=x z*3O}f^!DG5jXp1X1+BsY(NAx(l0XtyArk28lf9RC?)QUTsKttMYN439wXE}wg7#>> ztoK^0XBVNM4FVp2c#m%**;Kp-L!g2vL<-&Hxg~RKswg@1o3a^#>=^^DDE4{!`m0jv zwX<2b9fUoH#9Rn(zsqxsu~Z(CeaR-IT&uM2GOc z^UPuI%Tou}>FfXq%T8dn+9%PuLY`2V{>A}+kbOJb6lY+g$BwOQp!*+eEff?*1s)c- z7;vSDo@c^fb%0FAQISG@N#&Mf0@Tk#c$@m3`LE9AlcgFiLufcpoWmw3MTr)YQ35>E zDZ_S4+4y2r8O6T%G0h~VhC{waOi_=?g|RP@+`{vIG1JhdPHzN9@1eTP@+NI zcUJozvgcp2P@t79!P`P&N(2Ps0fHI}nC_>2`H;OW;z2J|sIzi1y4NvAO;@xfrm4!=Ag<9siNY&WHDnRoD{_-mW zLUfUwx}G9rI%t;zT)VSi+eRuoIaEjxnI=JG3_NlLdnVM`W8cFMGD1ctN<~90q0*Fp zpa+t-R~cKQy87~<0#1bOrZi(XQ8O_C?TY~ISB^_hpaqViYH~|f$m$<^zKTOM#Qcbk z2Ew002)rBtB2KSRXe`;6qs}hM!Lh|dYmp8?jg!1YtfYW3&EafR&B0I?o2H@a#qfa4 zFw6Wr6cuFjS6-VGQc@otcdtniudt!}X~Mu$Ps{Tt_Y}uH7PINT9SH@&bpg*EzJ8}J zy#hB_H&6}iZLyOOU31%Qg|IaLgUtY77cjMNF0pBqF?Y%f$3yO^%nzU-1*{W|nNNj^ z-uG~zUrR>3L|7RnAYk@)NOnHA4@h02mvkhCs!yq^k}DsKsa*`xB3Qr&?qf#|8nYtH zAcPb(kN>S2$+1HG1E`b;Jf=kTY5JF9-um-k7KH5(+lp*sff!`eLkpPYT3>^ejObz; z``qZ}ay@j5KXmI^1*|FhV2;96N*36_u^asv~q8z$db&>GLl>wLG28X^}>Ap%D-vDMpm>#%|C z90E(tY>P2j71W9}JRDlcJ2H;m;t=f{YJk!XhnY98V^Be9k>wdqLw3HqLSStb=d)LN z@Bmu*r#gLwdYKPrW5g@$=$xa^oovrA+sglbF32Oh2xZ?JaGuLjp=2pEs+ciUO+)wh zy18(u;QV*e2F$Ll5@lol=>y=VVuw+^$i;dCRpV%w(P=~MZ>p24ps_3%I0 z8<(;O=Ck_XOs{ofhBSxii_LfOnuwj#@f zIX*<^8)3C?PnRAA6Jp`F3xs z<~OmBT0%nC0S#Yuss!g(_4-K4!#z0^y}2|ER4;}GTu|}8b<~5c_#;l#Z}-m+zT9n& zXI$&bQ@`k6@q(^bSr%P-w4JcE>ZZ&J-65P{qim6J#n1TI_ONwHH-fi65H#b4A>~2Y z@Ote36st=X{9rLdKuWyJI+)W$hSeNvNr%>1*e0`tNC^1YD(#Kq>?mhDTdx6S&3TN# zE=fOVMYVfIk+RYgs8t2jN|q&c6td`Xdw&nF;!xZ>_Tw29-$~`B%|0i9rt2H3$K}ED?SYj_o1$bLXMF+z)DMxqYcEyk?761G7(y0WJQ3bcR zp6yTcLEJ!fvR$!DpcQaRo}g8>-+*!s-z8-aivdBGrI)%ea;u$ftRnn4Ixqzun(@c; z9wb0Gh<$_Yu6VrTd(c<(c!$O9T;b%zo)XX^+|E#$71AI?DOkI5YlMQ4tyb`L2?#FMhvr_$wDUby7sfc5B zIy`>(QZyCtX`Joa#HA7B3nXc88fBkFHEcZUOIXYLZ+ZRzJCficO#qts7U^Z_avni5 zE)aQ9$&g?Rt&xnacXnHXx_^h6NG>V}32b`(l~p&aJIE>vS~HL$_|9w|!{L#l=WFR*7_VVX;;@rbs`4u8kmh z_g)iz&#I@7pqF%xhHZ6od@PAs+8fcC617~}cSnU#ynDw*b2*ro)|MkI3FD{VteAj9 z?WeIjndfg{JaQ4?D9qZ*2SH|9TB@_4#dq48G$65O?^uYYX>4lBAZfmGb(gW#3}X{c zi22xJ?DUZGiR7XY-)fD;1*OUtbn9{y8sa}y9pm~r2fUUy0Kt~{Q!Q;bfSPBatHG#B4*^O-OjcSn2{VuuP5{Bqpdq|Xh;nt$pA#!n938fvcZc3s z;o2j>L@2p~_`9szYXcH;UzthI&nKivl6tpKg!MJ!#cB%Nw=@X%%{!2`J=tjs-wf-?VXM4oU2il#)OB+ ztajTre)ljKYeB=&;_gcvax^^kfWF~3jZvtGC*G-!Z&Z%Vtae+BcG(D;6-1X&NMGll z(nw$hwpUt=7bd_Mkfs+PuK_a@c$%jN5Ai7=g0ihf*Gjd^b=Dj_UjI9ZmLBOIy9y6! zA^+q&)HNA$2i5&FPNqI(f=k}X_d=#U1byu{DV)!D7Of@K=~X`aXIc@7B^15_#}Zm* z5#}OA{fY1Y@T4Ms$k)ir*x3_hT7x?pXaOI-_fu7x66(8_RgS`Nxr_3Adw84U z+-{O$e~%baYGVWHsbi`@5i#me`pf8DcJZJo*Yxj`+K-60PvdLrQymTFMB*}yAjmfE zeybRdL2_S<9dqz{Vj~y!D|Q@rpBX7U73amIpuwspC;fPNtxK1GIRI>V%ZRA+$uth{k^iFFrIoa{$qx9 zqD4EL3;hg~Nt~BX6*3%m4fQznihkN$AksBQXS2O;xhanS7?<+r+YaOJAu4c+`Bb3+ zE9h*NLXg7<9#_!9`d+gYb>*8zh5%iQ94MvwNkHPGF#bdBDKwn#wgrOhBXQOMxE)UX zn)J74FL_e(FF=s*mk-PuJO)|4EgE`sp1NJR$`AisMD{$;UinaO9iVft5u2$3%z{!2a`z(%By+V4Y{Wu#Z^WrAy1fGJWnRnc|i(|mINn`~#X zS!wY`VZ`0$f%X*$+S1!Qao4ZprL223Tl53Fwks{houwDqC|y05i^}$*H~kPZ2-rl()z_ZacG=@3o@C8 z?Px$y_T>S68uCSq+JZVKqW_|XaG5DAP{>^fo}(I%(0`y!13@|kzk-2H{rh)qJO&vEsV<|d+6Za35XOK@=elF7p^Ogv1WnNE&DnE@ zAgJibrkF~D{;ie0r?U6h=;raW<@G@`n()8dAf*2+L+v-@Wbpv{kqIZc4V|ZZmrmLZ znV!@<*S7Cjnt8bwfqPl3{@Ixeep|4V4n<}bv$8c>@S=l<2nxCnmk}~Qc{T~`WHC77 zkFTx_>d&ZozPU0#>1rL}kS{m{ zUkong%PZvqO=x=^`zEbRrNl56butR2(i~L8sQ5MmsG zBV*!tv+G8%j3cFcgxI@xUxHuJF4Dgq?GIFa$hvJiqUO?!QiEe)Iv?ap-*vc+zeWvm zmY}KB30OnNV#so6L@P zu{KYPo@FxJs8;Vh5zW60VZSU((dAjozUetX5nVi!pJ#hBN!RY?Xxf<-_|bTOy!4R^ zIg6Tk9@G84zJ~kh3$1l@2r1g!EtfJ!DI^T=&Feh91V0#lR?c61v%9==-M!rcfVE9t zmTC~oATuiODm47+k@uf{QI1q9JLQ?`!P44!hSQ`bi8$+fY~kMDlg`x1r9~=2hv7+; z>Sx9xuTP3!cBkmk$F<>HDjE)5E_Z7(oMQjF6GQtSX?~PgpX0|Tn#}jz@BVc!DO=Ot zJs0NAMU_=`B-5yR5K{I%=wDnn(0OP6>AN@g-&#Fb>ho@!vX#6+_mR;2jkB)n^ox{Lq!)KQKByiE(K;R z**tMAuW_eO8KQ(POR%_`_6gLKWTmyMopajEj$~P%m)O5A=^VRiZZ?wItLB`Bj{Qw) z+>pyx%yN3Gx2Z2DOO0p5noHX$#!M;UohiJ{N|@> zPK37?zM=B~YnhA4i8UeF61}s>_~_lU2e&DwzmWtHrA7}`WzK)vXj{`(UAD_jIMQ)g z)f-cxit?7_4g3?FP=3BPJH^+5@|=M|HaVucz~$mm6t@7H$!`}&OFOp3|bD`!+mSf+M?xAT0n=i7gw#w)1)y_f-IQWogJU=wOQ ze=(FxriA|)(r;fP22DF%5pXGQ-MFW*s&o?^yOfY%S6~3CM~AkesiE22MLa;BRr8JK&fUkfY37kzC{v<8io3Vn$)|>7 zc++M*)WcYc+~sKgt8&UK56;#dK+28ZEK9{$vP>;uTt32ssalK#j$fqLJNuX(uY_8t zly994qIK&RXT1^HUWMk)KWDd+r+1-5@o-9DKb_wE*vsij8^ov<0a% zT0pP^FTD#vL`v+B+U<;U1w_Ag?R>jazEa9dd*@M>3R^MxrC?zh(u~2vUVrx>C6Y4qFGK80Z+YFD(;W*5MKWu{XmvX=@*1mQi#~)RLZm*-LRjAi zVh*(^mO@5Ki$gjIHADwl;jvZH#PD&4rtd>FVvl5*5RnIStA^qR0jPZuyr^*qW6^bc zLVvD69AV3oPX-C_?gfW`s2e~shCjJi0pm-wh_RRT5vl)ol+2!{Ihe5s9K8YpyJk|D zS=LK8i%>J2>YX;!ZoJsquGK@2J8cHn$F#i~{S3$b)mf&JInPIdB3B$#Zs`>h888M{ zTGI*tbqDPQjebT1 zNslyKF#0;Wg_wFpX=!Xsc;nU1Y^Sm?Ed2-2zse2^l2VBx<(&jy6d3{()M?Re8Ou_R z=11Sq63jEMuL@gHv1DwDT1^ZLMy1R~_O8zEw`(MixkK3m$kWb_Y}@*l{))@Jq5qp) zi3+o>SUtRVCq0DF(-DcQkkV?OJ#tSEH|xybXRx<3CelM}rX9JXhxSe^MkF3S-iYFg zV05w;t~k9;noAej62j0z7%TK@8gb;$ZCS|Bqsr}3>XqvgWJUSx&VREWH8F^k7!pvp zOi1FS3f3OgY?)P&gG7(Ag&Y`a-^m$iSw904$D9`Y924^Ni5fgNsOIV-M5{B2Jxrt~|QS_$oYD zShi8KwDxQ8)3<0w&~v4|e|uaobnDx}ee9dF2IX^$)m)c}V2^&H0c5(v=dI82iS!M} zFOS%CgWA7ErAF3XgW)8gJOXBP*p6v9~+4%!{wB*?-dKb((v3 z0O`+Hs4H@Km`rmHM=<`oo)zE$^M&SZzH9YWC=vHR9D4QR7G@E54@bs15R=s2ZPMqN z6ju3YBZ@wut3^moI7Y7nj_9X1%t*KsBe$ogq!{TT^Mr_831#IN9X_VYLbvo7S6Lj> zYf9QmJ%D8E_#pdGYrnH#PBG0PCKFwiMD)h&CyFKd6RYp_q^zGZ6T0B;()BuNApB)_ zhp+PP=u+~X0wJF2&W4KXsEI1MVZOrXb6%M@juuZ!eh$A^Mg3QHONlcYXYb zh$QYda|s)E6}+Ya}&2BPOQ2r5dy5;7c?1dQ>+^yYHd?<{J*{ z`=l{b1%1ST>bVu%dY@i{9`RcFA1ntLcJ5kvct{89yjTe-50-cILNaOvm$zY z%ZLYK*tWmc&@;oUx{<5Dz3V^{Za+Ov*KqN7-}4LVyN4i#>D1KBta^OAzO5un*(i}0 z@UDxuZ98G*b5!_T-IiH<29}#kmy)^R-KN!gG>9J9epPVA=05&A5$v*B)vj{(l0KpQ~F#d0Hcx{cTvIe*2d za3CG_PBS)D+s*rCkBotz1WD%YYvN^0@P`hP~o%s#RK5=FPIGG`tG zcUJQl`Z-v67fO&gg?WpkXlDgKjd)F(eHpaq`g+PBxZq$!9 zY_qGnW3^33X~Zq_9ypP5ZR+%p6=~$|SgJtA=;^EbpyFWfB%e4>Sx55XgBu5TC>!F} z8GVZT>Ej63Rd+J69`1}@p2--jn3jad@%G?WAzio`*@DgXZF8aHb71P!8srn&?X%n` zdn3$e{Wig(*t{Cft6>(yMLbk3^q)e0JcBXbfSV6k%K61q*Q<>mzMiX|F2iL zJZ9sVkv3mR5q*cvOUb8UK;sinf7XEx(f@V+B(6 zB*Xj>;pIP1+X8tHO?`lcAH5v127a$W1r&Oc0T?h)h3tM@X?Al%3&7+WLVt*-2b(I-u^y ziqTDHq9!I^J!@p(XgYJ-(t*ecH3VnYS6ndB1%L5%m(|=-mwW+R8Xkj}?TS=9{yLc6 z*v{A!AzT6K_&1Ou#*020xO-SpZ5(i5AP3TtSbB&fQ{}eWe-6=2VuOp3=-D45fAE>iE{v~4Y+s?u=Z$P7&=9dv|19c%C0`w0PGGNXnZf$`j z?pL4+w0pXqpM2-=Ak^j&e_H-_HR`;cs%h-rJ_{)DC1rqqrPYw`r(!)(}3LcIe~iP5;p>WV5YNkpe-{p?WR>v8FFe3D*SLb;y&%R;>PJV9lI%+{90Z zRa{*{|Kic-8C|@z)b@Q>F&EZ*>USpg7&m;Q^KcpjYS)Sn$`9|I*!?I&QhqtMo)ni$ zcMyfmG(=;$8{-=KWM${4c4u&>crI^=ng*n|5JbWnyDY`q#I5jbU#-2T;CuYmjP_`< zYx?QoGP;Fp<0 z$SUxA<5!LpT=1@aEoo7_PN=51P>9w7DqD$9u37X{a*$)7%YUmwRw}2TFKq3Qj*J~=Un-7@1%NmB&x=x=qUB&sk_^1ZtN9r&VopuM0%&UE;$`0=t4m;HD&!`ocFZ*L9S{(5mFClsbhnz*Yp8EX2JOH5&svX`M^an5|_H<`ANKM|Kp{DSFv#;G&Eo9t*U zWpL~Jwj43!u#tS^N!vQcld|Tmfx`{0K+exfXV#-#e^2Ol9+VG(+#fi(!QWTt2A(6jNjm{aNiZOSN z4J$Wp-GJ|J4QKXMnY}Zl(*9ge#`g|nRB08(PKPyC{ zC?}&mPzvF1v_qJ&jYb`Ca)4Iq_C+Q>y+!_@2|G1Rp~% z!47x-s`sal>H?2h#)HObeom6C?^_#ODn$1Y9CC|t5y6a?V;885DMcVc!Teuru zvkk=!k=Vk7W2~$b&CDqqvQ0*Nsarxt*A3eG zV_@ka@nAJX4t+d}0-RAEdgXmt#d|}V^}S)_+>&wiAgDFTwSo2#JWGo-|12(lwDe?b znuewsVvDVmyU~InSqfx~F0(u^;vlkkZL6p?b*aI`*C`a%tIQq^Wbb~6OWfT1@g6|8 z24&XAo|CRRCdJ8k{PXGVf`+xL6p$cg3rC>i|M-A!M|n(P2n4b{ZeNZ)A=z#8eO$=` zUB0~DUf8Zm3FDZSwryua%OZDjwhjP|xm}oYkR@l|SPa-7bEv}H88(ATQ*9NIHLj0B zx7ws_+t@%lU%JD=VcoW2{Cze7oFyN6WpH{wT12z8q;Gwm5?V025=>}+f|D@lTs?C8T; zjjTG=Zfn0q-5_k8tzu(bm_;&BEUYhi)h4XWx>d2q3I|erhnbjmgsWmoHu$co?r2(LmJ_pYbR#4lYtsooi)y-)2hZLe3~8o?`&AjG;*cv z8iF5*Z!22q_LHLqToSs)nV*AEUJp2)_eiOf71$;ZdJU3e*gEa#&RoRA(hM(6G@d`)Y=u01@KG+sl(| z5Cze!(T+yW!EE5>Ars$XSL&B0h|@vP-@O)UzMkmM=KUB)2Bk~ZB!}OKNzP39L6%>< zEMBpTE9W_cQ8s-IrWOm5HpJ}@O55eRcO%>y!MY&`P@_yc1A}RF|8iW=SI-5soX81W zgGf3kn~YWgzQ2!%AvE)PyN23%)I(>{ATXfOYU8JCq-QrJ3&|$CsDvBV)3|~!FE+NT z%iW@ZIuNoa+>W=3V;`et)o!CzaB8N2h!8%!{q^-|sQDM|Pv?d0p_MZ*?x0Bj16Ffr zLGGZ}7m=;SH@Xq6Kn41&7l4OUT)P}R?18sQSRWWNiY8s6Yz}GxkzP`T!JneyS7gz` zwmXOy5WhZO_}^+vx?#?9{=(y@liDvxEtKF1dnRm?)1<6d%^584nxeSK>f}Zk1@vPWCc!FOc5^^=% zFkAQp2~-J8mVHzQ-)$E*;Z7GmF{HWaV-%Fbh^%b2LvMIL0|NNt=qgJ0GE-RIVInv` zj}F0q!_EV*gi~Yx2H+^;hN%kx; zSHR5sR7w}tXnjW6qRR?IH+QjMAGP23{|kgyoZfSC5e4Z1685J=saqDogn-OEBC3SK z70e|Y?sDdX+DN`G3N%y#Qi+J5icBAi8qkq4$J`jt9bzsbcl}Po+b`RSx_{lR(TTbV z3ZftFE%+#6W79FpR#HSulamMJ+;A0d|JzP)KkqUZgiQxAR3!r=h$+4(B#peiyGjDL zaE}6rO9I3-$g|BZA&DSUanK2x>XS3B@lUkq1Vz@Jth@Ep9goRaK|c1%z?lWsbpYl} zV_>KY2Dixc`mFlKomCww=0Sndx@#{)v9wGUN=q+iF)Q&NT_2JfX#i%KhVl$T6`wB_ z`FI-VKO?nJ%I^s?*OyDEl>Xf+nuQ~R8bFPvsW9(1@)q^&h;idS7;!7l6$Xjhy(X1G z$Y?Dw;Q8?diAm~E_93v5RKYr#i2>?V-EPyg330}(gJ(qK?t_F@H$^LI%+`k#3ZH*< zaAqT|x}9+NH-^oWs7M2#Pp~%MVxqhcJa#_*KRggoK4ADN50rb=x$N^rp>M>-oUAG# zXQQ1IGT6MWLWw@$e)36%B2!uy|eg{OM88uV{LN4#2O=odccL-uxw=2D@Ht+@f~p z?);{g*G4F#0@(VzlHq*VyG`%3w_&bT=9$TR`6OnoZ!U0b8a_!?3dSJkKMo>LkIgM5 z{qXBxInOdzFz7m9qYaqlTpwJ~o*DV`iWzzHMa0L`(~Vw-q^^9P&+Gx|uV$l+zjLqE z8;TvYneZqmY(QM9^b$TJRSB}J~w#PuYQ8dz^}nC~bBWv^}VvQ&T@*nTa1x)`8La#oc=|07WP z{Md6jt?ZMsf%I+*T5&P${DIjsvplBf=HD1ej`_x7c0F#;N|K&fxO%xybi~bGBo@m~ zEZbV$ACukCxmWhEPZgkK22iST|1vu$X>Cf`Dl)YxmUb&pT|R};Q5a?>lm zQnOd!ztBx3KDCoqXCh&i8?-g@HL@}UvVs6vU_LZyxfEyf<+jE!#!S0O!nU}27E>=m zYWZ_1q_}o-)`ay79Xs}NaV$bQb%N8h@bs$-6%eujNRcbV-$Y&UWS~1*&B##C?XK!G zpPMvT-fej|QC-U|WL4hQ_(0{BC2Kzu=)&&&Tb2RngFHW59JFltDlnOGn(0=v$yelb z%A2M}rJr3d)kqmmP(CW8YzxxDS_xX6A|5CNHAvLR4jyRD0RBJo(UKSmdkI>xT2X$Xey;~I;;8bu7kO)Ylyt|d?Xrqr zP>v6$UL|scdid#_-EPl)f|K(xWj~#Q0t|*t*K68e@Lykh&DUsb_Yt_sC%|>Duspw>={QNP*^|R#w>no= zwefK3b)~(6LEMR+EdTl1>C}Wm&CDau%G2k2C#HAkU;hFlzp{{{zdc$>cE|7Pr{<>9 zCpbsnzx@St{nyC$_uCrXa|AJBL(h-AmlpUxqh57;@K7R_vhiM$fnF!P(9l(?yY~g> zHPsX9a?>g~>3Tt?6qN#|>e-m^sbqng89yO>u=QzQ+6pSmI?`aZN3T#aiFe2|8=irP-4R&7+?Hx*80 z(6j2r3^tW`D;P*A!i3t>)94mM!laP9+6Q&lzcrrRIG&4cCh#0j{mpi`*^Twl=h zeNmvoNd5e$JQ+2l{O}BJ7(r)2( z5^mFc)8tB+m}%|DPmXbF8wj1VpC=-X+A`%fKp)nnl25a*iDso390<>^bqA0pHa-!qo)tP4!d!9hoF@rO#wT{$2G5y zl&*mHJ$8sRaw1j&uN&;J`8fhcS0)(|z<}(vC3&lm1F)xD$|URjzxw!s5++G8e#Q1v zX8n+`A|vTk{Z!phd0;0`8fYCpjVR&8-e{Hz?!q{fhe5iLm>R8G){RCK$K)|0mAe5d zp1o!_(*2U*K;i=@CEM9)dZFSnu%eCtcqkFm+k+v!roZgU`*jx|F|k&Hxu@=P87);5 zD9fHwHf2Zl2H2bZ?_$jhtIQY0^@hEgWh!vfC7Z~FQYSToMs>0W4IL5_{9mSJRSSr7 zC4lB>rkudpW?9^u1JCW3?(1#W8cf-jUY8Uw(x}=4l^0BCX}gs?(aO?CEyeWO>j}UO zt*V>4w+fmqUVz(!ssC>8{jAaaqFGRj{-Hbo^lUMN=^N$v*8)Tv=QYt=O^Epn{}^i8 zVK8|R3w6=Ob#ETZiNP)Ys6gZ&>_H2OHdD5c9f%^LDB!WqT}tCq9mI5VHlE|Ku3q>y zSjbqwHE+4Og6aJCIZ<$G#w)?L%54RDIdL+9nHosZS%=0P!fym3Z?nLO#)Q`6}q4V z_yDA!NObtE#~NS(-~P#My>4RNLidoN9PPdJNqOXkaALB;{kWtq5J=-R%NCSJa)l^& zWiR}&a24g;JM*5qGt|cjB$`-8!&~|t*vQNM1ax~85NX@lPIt!VvvG ze-EbwR{bsRq>)=DEB@(vu^?lurfl&4*WZv*jwDjc&Yvc?xzF2y6xolsn6Te^DE>y6 zg_}v&)p)p&7xUzEUYRYU-skwW9c~9S zvt9KA)*qb>Pj*^5m-5oX&cXm3{tSSel?=W1!z&ZgCJbTb{j#eJGJVE?S9Ank&5^4W z)=D@=ns^UgO|&8#yDCZ5DiWtF$vbQT!c%Dje;&2ao(O5(^WW4j%V~IHjnT)r+&lUP z7m?PNv@zHU%%pc?v3RmBA=g(HZx4sc`YIvvg0Od z-TAmqlHXjMmG)W9Cd&y3k#;v@T4$)5gFNg4gl{G>rrVGyfSxNNMnH3w5Qgh*RWUc3N_MS}rUwueTjrRdV9slTcHbPHw|86>PW)#nhkw@-uEh7PU8Xt( zU5QSLUd6;SHP0qa@DVLYe=91*9uSLJd@I`DRv$JG^XJ9~&mQltXUtdb9_R68lYuO} z);Th`G%s*6jN4)N$fAAJL9EF*yB+vtAIjM$%jT9t`sZn|^RfAjuZndqpUMLC1dL#j zKK%IL7URb4pI<=wy2fhwInQ;7^BN|wJaVh-0bIZ3B(soQ_IE)`#XS`U(CIsFQry8J1wRXjQG)_hXm?_?(@`5o_8Y%e zH#twdXj^>gxz5>GyOGbdC2dz0U56wq+wrm*$W$j`@6NL;JjA2`XL6RM;GAmu>c0AB0&etK*;&w%NA~-KAGt$yeVVS?`40^ zz<512VtGOV-HPfHJbG8l863cT%G3CC#fX&_aEol{)mQ7oRHSTA_>^ELxd;vw;~CFz z*(~Nh&vcq6&@4r45XA!GZD=Owve`@Kt~klgw}B$lY#EVr2x5Aq&{S>KOVD8m>c-HbRzes+1!0u`-q2P zBVPi@f?AOp=j;EMY8rwlb>j3d>nPtWOg=w6i?(;d#*}`J%=^Dx0HpqFCgV1nJXFZk z9g_cnhXvTzH_m}URG5&G)A{~@R@u--?&#@#pXtOX<}<{`i3qOGb#wgFkww~+*KE&9 zIz!TVquubVpLr?`xha@ZDZ(Xagxo#3Mk+fe<1X+Pffh1&ti_X%JpspsUXiz zH|7iUuCWY_c&JRpjjqrZE9MmVpV)Bg+zeEEG?tPZoof z8Fcl@m}QKqzPN>%0116tmeM`>_VnD}VuM8|{QTHeZ8l{ zmA~jTr~m13?}?MnFuWpu4WPPiKN>+_SWhkFUUty|;p+9dE3p#T1Q_jq+Y(F$9ywb` z@f>-$w=}(Z1J#Bp)5Z-+?tpFeK$4IhO#E*$zs$`CO5M7~03AcB1KX%^B~3C`+6i~O zb^F(&0+J-)iWS}81r=B^S$dVdE3NPl_7XQ3$-Y_yEMKC=xe#PlHO{Y>WR?OL%I(FN zF7+!{>b?$R<|SK&om&A1Qao7M&rU4H`eRv&!-~bID2e{kR4tRqU8s_v#wFIM0a!Sr z)BF3o*E)%-GnIq-jUSo%$SzPi5mL7{+#$k2EN& zPN($i)Q`A0ZJ#>AEpndm5Y%BnV*!yfL~0?XGdkmEOo3{j5X~+>^Y>f&1-@~-M)yUXO~*4L!l2rERyO55+AUe%4a+B!A* zeM;=R0G#zy#iQs!v9%tD@Ac-{%12=ve#<>$7eeLzHVfS)pJtQT7ZcNDP_}553y5uw z)_`qMa$6a#KtqMC36#|aix_3#*+TFB6K|F`#ivAXm5<0+{){Of`E3SN7XG~=1HO%4 zJy)t*-m}=>hG}g?>z2FT1X#RpIjoTbk}-3)ZGiG{-J(N2&%G9jzc0{siT=peqwit~ zR#pdO`ddWY}iXxQ( zFyaS)A^pi+nzj=2AFlxk%!KOq3Ezs@}5*f}9; z+)7fkm2AFY(qnX8v4JuC$_I07WAao{D)%Ewa$#W8`sD(pzLk=scWAR70u|u?GN*H% zuR6I0R@zHxn3c4W!!5}dSWI*f|L#;ePv2(j~BlGWB5DeqkO~bDK#?vDpBy)Vd!v+@Y!+#u3#;< zPn9x84)ZX>x;SQPg5(2Mv}vLy4j$Id& zM?y(jrRsb-hM~4&RbcrFg>Zo`pm|U$YC&u09VukLdsbc7ZU;BuC<&92qv!cdf_A&C zx`3h-1b*#rW16mqqtOMBsrv|ax@K%uA2$3yuHHJX={EWs|B$1HK}%V16M|9#O4g$|yo%25N z*@=p?Zoh(2EzVBLJD`?5e9l%ey)d3jc@m5f)s26QeUAMu6`0-0(vBx=J5F5c&R<^N zh}S3QNW4rn3#G=MQ+9}R(4|eyjqkhYV1A&dV|4yZ0?LiEDlH>_I**t_`oLG>8ZjrB zhqiX7E#tC}-^&dJYsxhG>b*`!)EIYl+HD3*O{J7~m=?*ip6`<~61~Bllg_qt3)hPY zoXc3-krpvXx7*mHy8TQi6Ad_&UJ#wamMyg*(dV!~6%O?cPMxW>ziDtnJ%e{HWb%Haq>EYw-9bY%jn|`jY~NqVury zGP});q&$S>(KZHK0#GokE&Tsie2ecV0bdkyNr5%+Gi$27c=_FZ_DKLf*{`RJOICT>H5gAOV|L z%2YG1_Pgbdey|VfpX7xB7N}1@wv=>e4R}kLV^`WcE|d*7bJBk*?f>5h9ozWo$|B+L zq|NekjCZ)T&)YoP;bU-EySgy)2XdY%U8_8WbSn#m2gUFNsml`7!j|*s3l+%;GdZl8 zj5IdcaQB~Y<+2YR7$zovH`fQ;vRx^Q5Z}4Q(XP^q-|o$Q4!QRwpv$pH3m4WDc6IcynW@W9Hfo3qNih?0OP2!Mo@%h0XO!6`%(c zP;z|R&(pQ)1*#$vbNxsiz1Xu^#;&wZ+WgYiY4*mQK>~!73fdU;YVHM(db;&;;&bBL z`w*Wmp0VZhX_R;44=_rIC?to!DE@KlltpFnnXqN*ySz~J7xqvO8sERyFG6{>|4aNUoKoU=l z1oe-GHo9Az%QzCs@SNpHDA$!3Y>nxYLILzLFcuqK$5V-tN&vP#o|4DTZQ6>mTpnT( zdr4oGB=mMfvaXc|3e)fgc=-bZ!s^g7%Ey#YcR) zcCwF|ACXk>!`RB|{IXwC-hMqCZVlTy1GCw}Y^Py1d0mwHJ`>|+TM;m4JC zOHM!Mt<|;i$N$#>s&0q_G*hPIANJ+dv=VYI!8B}u=dN#jS(Xzm64cMqy!1`u)G^Sf z4oF+9nS}5{5K!sKHR83x*FsT_o?6+zpN@_R7iT2An$Uzf9^|U@|iQBxqjj2A1&&tN{>(_NVh4ECNt3h-CTve*SMm#BFSyE#KyRE`A zc6*m&I1G9=gE7t1h=v}DW((|^dA!mVYX9}Q=BmZl5I@v-PwB{}M2OidNI72*69G0p zG&$Gex=qdXvo?-vpkBUG;h!`!Owrs;cU<)ftax$yqbbm5aH`+aLr42c|Nm&0Z=)1g8rhVu_x)wPKLZVqa@2ViopuIr+Egd_gSvqvJshb7)1W6t1 z{ll9pJdJH%UzPCDTqT4}J|ylt*@&+8{LKUW+Bf%nVj}eY){N{#5R#_Ozy)9N)?@aB zio8tqI6QSquPs4W%lI3}dsU+HHaWHbV_aF}46x9}djCrZ``pLf8+*GjNTHSb9(v!0 z6)%`5fP}&l%506*O#YarpPs{TjH`Ww5;g5tK!;+U{r5qE36`l{q``eVd*|%3bSL}R z#Mg!MIWfi$M+&?s0--_I`}TF5gS{x#E+}20vgu_sM_4ya%nH9Gv_hEgjCI$}`+@oD zKSm0w`uIcR{+wF%QnJ$%a8~Dw?={C*1A05H1ZO?}ROFL! z8TPDmZr7&6^-eLR}gv@Zw6Yb^6(;VwI z|2#7Q+a)bo!q=+R7}lh8OS;Eja$5K$wimNs@tl&mBKMD+O$B^$&oLm2Au&Bb}3Kb!Z+>P=Z%+~mxr{~f1v#;xn5`d|7x z#{z@Itde9{xuMJA=tBQB>2!q;h5C_J;nunJ{vh=fZ*EczvIxlJht%8B7V|TzVaxV~ z1_tEDJ7Kfytug;KnuA7H8I6kevvObMh@bKANZWq>Uf^f5glyY59LIq8Bkb`lEwdYa zhOlfQx1mURX&?@1K>_vKCbIf>7#(=4P42IOUVRM!?^-1Ybnti! zn@r#A^?~tkWD7V(@0m~AmcPre1#y|NL z!s`p#Wx%)RJc)^kSVCGlslGzoiSIz+^ zrjTn?^7^GCJ=E%c+o!GDpDWpXEGUOhou_-`P_l!X_ik8jC&o&8~ zKLp=Ml?@P!AT0&S~qL2X|uWxy)M~Y*nHEmm)eWcb7bejg$&#t*2 zgX#R)KfR3QVKyT-vNp)_nzNRIEXYMluTL4MKgD0P{5z#)M+|d(2zvqcV`YHiq~8-` zSqGTpO*`3p1!pQ89K&XJzyM&<>RwjP7)fsH&*w)RqT{43lx4B^ao93jDIMp`zOUs2 z!jJMS(oX}SD%38JH2X@6(&|`{GY0rkpV9S36&qt(c^l2!zJ6Om7U}`M^t3bSPwt@c z&w^mfwoaE}^h=d3UpZ+h$c(tG7A$zj`W-`?A8JY47GYVk@#tD;9i$~ow=m6s_g(eY zU-~*%c{ksyX)yq?7_%qM&F56ufN}c}g)J+vEpd^}P0_TTlX&FDXJG`VrYWht;ReJN zqDBQgG3u+gz1U{jwuON3ZuPIaTGzC&_zPf8nuM%@L>Gl7S$m6_l2&?pI&xOtQET-uU{OON`6sO|@@(@F}2Ms}$;17L<75J{rX#X)?jIpi-65 z+M|3KcL%c`+VC}EzjkZgKN*}kmtE*1TghL6g8lN$JzH?VvW77KZGB5?w=QS(JvN(K zv6tAjJRi_h%PtH(_Lr~f6A69I(#^uGocVgaUgc?t>)0zBW+Z88Wx*$wyVJgaZm!B9 z2HJ_ti~{mo2;mN0;cYZ$V`uBqMLB;Ihkzta>zWn5T_O4vJXP3QAcf4<2tVs+oEk+==wPM0ESY8_+rJM{3umPl*4DlI4*wbnt~ zj2zxTjKfl~ghZZix~(4X@BdvTncDus?AyodZd>Owj=}=0wJu>~^R$S=o-<}govXDF zYgq|FvKC5J79$`$x-wo72qGf#1m&|F{we6d$IH)DN^diVh%&bR=O|y>y%G&OcICf~ zmC3g}-<0GR9%+ei&z;i(nZLIA%kNSzbqC_OuJmUhqk+vST@zy{{8AFf9F{cL%p~S2 zKdL0EDJKSp1#$JOu_r4=q?4Qn92+uDBFe|yETuI*vCoX9ZNKMIP{#?}+Z>*|7$p+= z37L^)5;kcQ%hF(6L*#aouLhZax_5T+B(9okphPTeId$}gC{IssvQ(5%=#7W#wkyfn zprUqB+BGlD6)e7!tHC&qrePh9FNzjiz-iH#`|$@ML6VFe`)u zJUslKZ3YQRxBMbM`cdDaQoGRGm}c*nz{29~IecEyjB!6GxF3VgyIPbcW7D~3NITbr zrF0vEn`h`NGvcP=_!LRO!7Na=jyLRd1 zmnQd^I^jpu2mv-V zaDFPEZL_uq9~@;}8pN2p>oU_vSnRbqSgg^XFFjGF$0c9V_Y=3QLk>a}*zT;oa|^98 z%=o7}LiE`nT8hU=P0y6F5Am%2a}8JeXs`rOEps}>TwA9(ysNKFY_$B5!50VJb*&C? zG-O5!88A+klfFPt;QMBzbkiwkTF#%e#Xg38voUNFLMvQvv+B&c8YYM5g9+4hJgu~s z@9-o1`S&;Jx&a&AH%|MHM^zNZ=birTJuphp=dAK{*^Yus~oeupCeLiB#Hut_3WEo4{ z?GY<;T+k|a>sly@{}`~JS=Qt(aTwFKQvUi>ZMHnda-Q2!@+E;xAJE{D6qF5aBen3) z<}?!a6~Q`>n}ockvmqUuPq1HB1*pwI9SuVedKWM-Z4y|k1VSgwZlZfVn3_3afQ!p=jchK zA3XvCn zLP@3Y5Mq;9@#07oS1==ny{gqL{QBGLS?|J@H4^%dOh)9H(GL9KtKOpR5rzOtlUvKU z(fD1dpsi2FP|p`sonyBV-@{nQIL?kgxLn?FovNt}b(k}sk}>00CO!N2rq-SN$)kNM z4p2`J7wB>kyeYKV3Pto+<=S&$i-rZi$J+U{BNC4{%}fff8p|6 zT=-=k7tc6u{Hj!|*_pP`9q^ocZRsF%_s!&+=RdL_cQ@C274SrPgJO|zwj;7w+pmft zR++h63YGj95J4y=Ym%{`7{ibPiN#7S%s6EZ%TrBZ%SHNd9_Si^Y%c$7e4JD*%+7To z_`nho=wsle`TdsblXl~Q_<3|Ak)5-^lpq>-UHX-MO%;eY(IDO4WIh7KsfX|dXf2p0 zzV?HNy-M{nuWYsx!~9cFBb;zr7la{gz~tS=3W2$Y9qNvfXs5MhR6Zw?(Q;M5;-NAQt`@fMyjCfU)@I!m!UiTGZidqGjB%$A8KVxWp1p*j7qiu7KjmY* z=)^7(vxqx~`qsNU3(@-w0I}+-=v)bMpa~(XO@sNSH^c$4slG?zEp>tuu(Jz^5DOh} z4Aho|7Z?ZrVo3DqX8(H5)ciF27>R5|>i@aSN>p13XJUUZYD1Rr zWUM7N-{amAlTVy74qR_V+SOhIH#nEGW+q}v`Udxp?+U5rI!Krfs&NAaWwkTI96Mtg zhEF8)hUU4sG4A2ZeB$@@v%WboSq2Prt!wo-@-LMQIqo@Q67s>V`gQGi>c?Y8%2?(IzrAPLoZ-g zD{`+7=1u2LKLXEP)QvB>$wFf`;SjsRd;keMa4|S8xo0czo(i2CIf`R;l4|Di1q_b9 zIiQFxcfT5-)Yy-%Zf4>JCssUhV^_a9lQRp_o=YX_R@~3Z1-G`F!iIQy!Pa*6;v7aH-DeypNvCCDDO z4$p9*NtZx-TA)YO(AzsmL1;fWL;*1Hx+Vx7*S!eI<7Y3i^g!9-Ac{~)+LNqY1W+n! z)Pi%4;)ZxyESF-;fEH}5jw=vEN0X4)B2eQ)dV9~;_}n-oJu zrV#5uokM>P^oxFaISr3jh}JN%JV{6(#60ViX!Xap+`Cd1>h_9)6%ilF>TFGtOESGUmp-WDv`daNX_s5rGsWBzS9u-kT5*@V~JDw`3}ejwUBc zvVpteGXF>3or7PoQ);lmnM&axL_qBss|(s|IdXBF4Ww* z=}4t#Xv>^^k<`rnC=2la<+N=qT~{<5{dHs|jJGbsjxlKNer!>EJ&tiOC(HdNlRYGV zF6ooybge}|2N=wn)xHb{>PsObHj6c(m{?FekoJ=ptF2^Lj#pY*vLva%qyN&_{C%?U zJk@AG4$i&WmU|IvQDlnA<0Z&LsaLMo1M7uS9P$)34ccdQOSue*Kxs8vwN$I=Xpw>U z3xubUlWzC1GPYJVv3oT)`b@j%)S!bRbxd)kjz)WXH~=6zjy*42OI&Ve&WS7zci!pWlYPX z&&SItXTC5yV+ON^9wazwZabR$4E7<&MXlH5f_UDV5wVv_NLbeCPaM@2aN>`gG9C{! z2p|uBVKy;#=o)MeM9(!`#1LBLh%_!a8m_Ah!9hmdt*CW>L2-QTff~HufW?qv!&5cYx&E9$!4{b1F5Y_4vP2Fw%PfDUJ zIK=AIc7;Uv?MyvRU_q=uyB(rEyatgtwDb{DJ_HhE=?-1+vFG9*X6R(b;T4VVAO+Z> zRrR$dJp-m>#->H+lkGzE2Wng+%KIn1-S;sG@7^$Tp~!8Quy^JDW=2*UaHnvkT-)~# zqQ+8bXv?gsG*`38_XxBz58}Sn^Oq*IDJ_af)n0n9kak8M=A+uxuA(8d>QROjx}W!$ z5uA`7!tQeS9xJl-XSvob*7OExfK<2$(L^I{Hp)p?Q=jHB(pHXRz}Q}Fwa(8(ipqx+Ma%P5SB=5ZbXf-v^Y`> zC-?Kimk3nr&hjIZd9@J)KOfW>p{urlg!(vEsc@23@%Dz+?VqO(}k0 z_m6?d!8sJ-YwJ(Oa1Y3N*&5TPJ#O6}E23W?-@^}H1Cy4~f02-@e0hWWOa;OfSBhNt z8lxl}E7_L!rZx?phY3HsrdSA}bJ!4B$5UraD1msf;4{0zmSyPF?t6b!)AoJW6)x0e zg_?8$FrVFBQt>W1r0Qa2`6}TC=R=0pg|Me2I6#W+qt~R8oL}OIwI7b%#)CHV14@`* zT)aA~2I|6FNtt$U${HX%N53v@Fmw-FhDd>mv=d{=At!Niv{}w!isLb^zpM-;toF#98eC zw)La0U7gfh@bnw!aN$b#ioPTf1I}7pKu#J$KI286Dio5n3aWjC>5b2PjVR_}l|F7z z(~z`(h~}Z$_$>KJeX`zEo9Vm25N*ciQ;@>Ew^_MEKoTpu$6Qv1=TsS$MEN6OY-T{u zGA2Lj<$0;FTH>~~^LPLKD}Ek|hq6lA73sD8tYB#EaCJ~B^ue7Mfmf#|JROX6Vgmp5 z720e!26BZz9#6ec$=5m}EBW9-+g-@cOcVAHLy#=x8;3c{mg(P*%1X*^_BtiDxo5zC zuPR$zWQiZ4eD1G&eGBkzvorrlF2a>BUEkYXk%I?eb!D_!6PGtf=5JcSdnK6ze8$~)Lt2d^b9%nd2gNXU4$vBYioVR411 zF1cw9)taEHE=a{3;{Em)4aV89=+T^0-uL+*YyMs3zum3a*3~}3%UwWWlb?&e2YaPz z+Cd@{g5+2fi7Gs_H?H){Z!au1#B1o%@m+I6K>l!HS?XbUaAj_IoxD4#5$B)}Nj(Un zpDD*7zrm)+(};RwBOxpVc`J#(Kl>mdTmy>BIsPiLl9{RK{ZR3LcPBA_&GN6dD{w&a zL;&NT1hnnK0od^@&T3vxhzm(GXfpBgx@BA`z1gnbrd|OB*K$`HcOscRbPCDk_B(<2 zoask_@V+z!YkIb-hRpA5J6Zw7+YfnMMv&>Htzj@WH6lhM($^a^?hAr0#Xs znY1v2GtUwSB7<9F`4~JlH9vGEPl+i+FMCgT9gyx;E>>EY35?mp1M-%pWoSfoCg-hg z4tOTg+rq6?!Eg#PnLpviE{xzy@8oxzn3{)#EtmCQMA#nFXrA8Oh5V1%fw$cnQ-@lB zG_vsVLC@H0vyIuu=yggyJk^nYFbd-#OFBVF{WZpdD85NSS)VO1y&(+;{wT-_U`Yw! zv)#(uuA@vnjzTZp%!|blJte9xea=iU_(oH1A4kobRJ0x2nFsno(n*4odcaLHX1dQ4 z!Jx}_oEssU3qXu{!U%@w%g>&z_?zXL^e2#nrRSRVA^cBYqX|S;Q_<{cM|b1k?RToy zUePT06C9^1fF7?O`+)08Ddeq2A!ceD!#;MjUpe|@(o1@7yRCt;<6d}YY9y^JjJgt_ z^pdq&=VnO|H#a9Qylm9 zM;heo8vo7<>o;%VErmi2$a%64y`_8YF4uuD{B45z)x*tMdpRgdG@`hGHC|z*Dif~u z=GX}y~{_U?eS61?Tnho7n%V9Yv{w8pLG|#5nGJZJt|s^3-5jV3(w^z+6l=sQ#ZFHf~u4 zcL^dPDn$E`ftQkAkaA@{Rw?h-fB=9_E+n9}JH4w?xUn_fdoaPum#?Q|r9KL_H zgagE0T6Q+uz22cJDvKAlE~|$G2gu|`a(*#YAeb`;#Ix)yiI$vWDYPJI_G$VN zSn(*#(SH{nE%!5rC&b&T%Z=V1)^ zY5k(rbvLd-E^9F)#P@f_o%80rHH|@?Y}jmo4&|zbE>M{)IP8*8paC33GgQJclh~p9 zf7iU8Gqm6@ca~kB>oVTj>a;*~rMsP-n!{M3K|VO=8?RDN9UP3ksUma;Nt2(?Red)z zLgy!?ZFka_v80YI){ETotR8$E4EsVj*rzVm;

?*5y(jBpY@{v`pxiG=JUUwS>Js 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 JC{{KL{C{3veEk3b literal 0 HcmV?d00001 diff --git a/datahub-frontend/test/resources/public/manifest.json b/datahub-frontend/test/resources/public/manifest.json new file mode 100644 index 0000000000000..1ef8d60978d6c --- /dev/null +++ b/datahub-frontend/test/resources/public/manifest.json @@ -0,0 +1,15 @@ +{ + "short_name": "DataHub", + "name": "DataHub", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/datahub-frontend/test/resources/public/meta-favicon.ico b/datahub-frontend/test/resources/public/meta-favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..1587e379073865767be6c13c239fd950d0f29f25 GIT binary patch literal 242942 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 sCI?Iom>e)UU~<6ZfXM-q111Md4wxJ;Ibd?Y platformInstance = fieldResolvers.get(ResourceFieldType.PLATFORM_INSTANCE).getFieldValuesFuture().join().getValues(); + assert platformInstance.size() == 1; // There should be a single platform instance. + return platformInstance.stream().findFirst().get(); + } } diff --git a/metadata-auth/auth-api/src/main/java/com/datahub/authorization/ResourceFieldType.java b/metadata-auth/auth-api/src/main/java/com/datahub/authorization/ResourceFieldType.java index ee54d2bfbba1d..0b8fcc871d019 100644 --- a/metadata-auth/auth-api/src/main/java/com/datahub/authorization/ResourceFieldType.java +++ b/metadata-auth/auth-api/src/main/java/com/datahub/authorization/ResourceFieldType.java @@ -19,5 +19,9 @@ public enum ResourceFieldType { /** * Domains of resource */ - DOMAIN + DOMAIN, + /** + * Platform instance of resource + */ + PLATFORM_INSTANCE } diff --git a/metadata-models/src/main/pegasus/com/linkedin/policy/DataHubActorFilter.pdl b/metadata-models/src/main/pegasus/com/linkedin/policy/DataHubActorFilter.pdl index 5fcb0819d9f69..fe13b065dacb1 100644 --- a/metadata-models/src/main/pegasus/com/linkedin/policy/DataHubActorFilter.pdl +++ b/metadata-models/src/main/pegasus/com/linkedin/policy/DataHubActorFilter.pdl @@ -1,7 +1,6 @@ namespace com.linkedin.policy import com.linkedin.common.Urn -import com.linkedin.ownership.OwnershipTypeInfo /** * Information used to filter DataHub actors. @@ -25,10 +24,21 @@ record DataHubActorFilter { resourceOwners: boolean = false /** - * Define type of ownership for the policy + * Define type of resource ownership for the policy */ resourceOwnersTypes: optional array[Urn] + /** + * Whether the filter should return true for owners of the platform instance to which a particular resource belongs to. + * Only applies to policies of type 'Metadata', which have a resource associated with them. + */ + platformInstanceOwners: boolean = false + + /** + * Define type of platform instance ownership for the policy + */ + platformInstanceOwnersTypes: optional array[Urn] + /** * Whether the filter should apply to all users. */ diff --git a/metadata-service/auth-impl/src/main/java/com/datahub/authorization/PolicyEngine.java b/metadata-service/auth-impl/src/main/java/com/datahub/authorization/PolicyEngine.java index 6a36fac7de4e0..e5951eb779692 100644 --- a/metadata-service/auth-impl/src/main/java/com/datahub/authorization/PolicyEngine.java +++ b/metadata-service/auth-impl/src/main/java/com/datahub/authorization/PolicyEngine.java @@ -2,6 +2,7 @@ import com.datahub.authentication.Authentication; import com.google.common.collect.ImmutableSet; +import com.linkedin.common.DataPlatformInstance; import com.linkedin.common.Owner; import com.linkedin.common.Ownership; import com.linkedin.common.urn.Urn; @@ -309,15 +310,15 @@ private boolean isOwnerMatch( final Optional requestResource, final PolicyEvaluationContext context) { // If the policy does not apply to owners, or there is no resource to own, return false immediately. - if (!actorFilter.isResourceOwners() || !requestResource.isPresent()) { + if (!actorFilter.isResourceOwners() || !actorFilter.isPlatformInstanceOwners() || !requestResource.isPresent() ) { return false; } - List ownershipTypes = actorFilter.getResourceOwnersTypes(); - return isActorOwner(actor, requestResource.get(), ownershipTypes, context); + List resourceOwnershipTypes = actorFilter.getResourceOwnersTypes(); + List platformInstanceOwnershipTypes = actorFilter.getPlatformInstanceOwnersTypes(); + return isActorOwner(actor, requestResource.get(), resourceOwnershipTypes, platformInstanceOwnershipTypes, context); } - private Set getOwnersForType(ResourceSpec resourceSpec, List ownershipTypes) { - Urn entityUrn = UrnUtils.getUrn(resourceSpec.getResource()); + private Set getEntityOwnersForType(Urn entityUrn, List ownershipTypes) { EnvelopedAspect ownershipAspect; try { EntityResponse response = _entityClient.getV2(entityUrn.getEntityType(), entityUrn, @@ -338,8 +339,15 @@ private Set getOwnersForType(ResourceSpec resourceSpec, List owners return ownersStream.map(owner -> owner.getOwner().toString()).collect(Collectors.toSet()); } - private boolean isActorOwner(Urn actor, ResolvedResourceSpec resourceSpec, List ownershipTypes, PolicyEvaluationContext context) { - Set owners = this.getOwnersForType(resourceSpec.getSpec(), ownershipTypes); + private boolean isActorOwner(Urn actor, ResolvedResourceSpec resourceSpec, List resourceOwnershipTypes, List platformInstanceOwnershipTypes, PolicyEvaluationContext context) { + Urn entityUrn = UrnUtils.getUrn(resourceSpec.getSpec().getResource()); + Set owners = this.getEntityOwnersForType(entityUrn, resourceOwnershipTypes); + String platformInstance = resourceSpec.getPlatformInstance(); + Set platformInstanceOwners = null; + if (platformInstance != null) { + platformInstanceOwners = this.getEntityOwnersForType(UrnUtils.getUrn(platformInstance), resourceOwnershipTypes); + owners.addAll(platformInstanceOwners); + } if (isUserOwner(actor, owners)) { return true; } diff --git a/metadata-service/auth-impl/src/main/java/com/datahub/authorization/fieldresolverprovider/PlatformInstanceFieldResolverProvider.java b/metadata-service/auth-impl/src/main/java/com/datahub/authorization/fieldresolverprovider/PlatformInstanceFieldResolverProvider.java new file mode 100644 index 0000000000000..cc62788e32dc1 --- /dev/null +++ b/metadata-service/auth-impl/src/main/java/com/datahub/authorization/fieldresolverprovider/PlatformInstanceFieldResolverProvider.java @@ -0,0 +1,69 @@ +package com.datahub.authorization.fieldresolverprovider; + +import com.datahub.authentication.Authentication; +import com.datahub.authorization.FieldResolver; +import com.datahub.authorization.ResourceFieldType; +import com.datahub.authorization.ResourceSpec; +import com.linkedin.common.DataPlatformInstance; +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.domain.Domains; +import com.linkedin.entity.EntityResponse; +import com.linkedin.entity.EnvelopedAspect; +import com.linkedin.entity.client.EntityClient; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.Collections; +import java.util.Objects; +import java.util.stream.Collectors; + +import static com.linkedin.metadata.Constants.*; + +/** + * Provides field resolver for domain given resourceSpec + */ +@Slf4j +@RequiredArgsConstructor +public class PlatformInstanceFieldResolverProvider implements ResourceFieldResolverProvider { + + private final EntityClient _entityClient; + private final Authentication _systemAuthentication; + + @Override + public ResourceFieldType getFieldType() { + return ResourceFieldType.PLATFORM_INSTANCE; + } + + @Override + public FieldResolver getFieldResolver(ResourceSpec resourceSpec) { + return FieldResolver.getResolverFromFunction(resourceSpec, this::getPlatformInstance); + } + + private FieldResolver.FieldValue getPlatformInstance(ResourceSpec resourceSpec) { + Urn entityUrn = UrnUtils.getUrn(resourceSpec.getResource()); + // In the case that the entity is a platform instance, the associated domain is the domain itself + if (entityUrn.getEntityType().equals(DATA_PLATFORM_ENTITY_NAME)) { + return FieldResolver.FieldValue.builder() + .values(Collections.singleton(entityUrn.toString())) + .build(); + } + + EnvelopedAspect platformInstanceAspect; + try { + EntityResponse response = _entityClient.getV2(entityUrn.getEntityType(), entityUrn, + Collections.singleton(DATA_PLATFORM_INSTANCE_ASPECT_NAME), _systemAuthentication); + if (response == null || !response.getAspects().containsKey(DATA_PLATFORM_INSTANCE_ASPECT_NAME)) { + return FieldResolver.emptyFieldValue(); + } + platformInstanceAspect = response.getAspects().get(DATA_PLATFORM_INSTANCE_ASPECT_NAME); + } catch (Exception e) { + log.error("Error while retrieving platform instance aspect for urn {}", entityUrn, e); + return FieldResolver.emptyFieldValue(); + } + DataPlatformInstance platformInstance = new DataPlatformInstance(platformInstanceAspect.getValue().data()); + return FieldResolver.FieldValue.builder() + .values(Collections.singleton(Objects.requireNonNull(platformInstance.getInstance()).toString())) + .build(); + } +} diff --git a/metadata-service/auth-impl/src/test/java/com/datahub/authorization/PolicyEngineTest.java b/metadata-service/auth-impl/src/test/java/com/datahub/authorization/PolicyEngineTest.java index 99d8fee309d91..70e408961fd9b 100644 --- a/metadata-service/auth-impl/src/test/java/com/datahub/authorization/PolicyEngineTest.java +++ b/metadata-service/auth-impl/src/test/java/com/datahub/authorization/PolicyEngineTest.java @@ -52,6 +52,8 @@ public class PolicyEngineTest { private static final String DOMAIN_URN = "urn:li:domain:domain1"; + private static final String PLATFORM_INSTANCE_URN = "urn:li:dataPlatformInstance:(urn:li:dataPlatform:file,test-platform-instance)"; + private static final String OWNERSHIP_TYPE_URN = "urn:li:ownershipType:__system__technical_owner"; private static final String OTHER_OWNERSHIP_TYPE_URN = "urn:li:ownershipType:__system__data_steward"; diff --git a/schema b/schema new file mode 100644 index 0000000000000..a14e89bca565f --- /dev/null +++ b/schema @@ -0,0 +1,250 @@ +{ + "type": "record", + "name": "MetadataChangeProposal", + "namespace": "com.linkedin.pegasus2avro.mxe", + "fields": [ + { + "type": [ + "null", + { + "type": "record", + "name": "KafkaAuditHeader", + "namespace": "com.linkedin.events", + "fields": [ + { + "compliance": [ + { + "policy": "EVENT_TIME" + } + ], + "type": "long", + "name": "time", + "doc": "The time at which the event was emitted into kafka." + }, + { + "compliance": "NONE", + "type": "string", + "name": "server", + "doc": "The fully qualified name of the host from which the event is being emitted." + }, + { + "compliance": "NONE", + "type": [ + "null", + "string" + ], + "name": "instance", + "default": null, + "doc": "The instance on the server from which the event is being emitted. e.g. i001" + }, + { + "compliance": "NONE", + "type": "string", + "name": "appName", + "doc": "The name of the application from which the event is being emitted. see go/appname" + }, + { + "compliance": "NONE", + "type": { + "type": "fixed", + "name": "UUID", + "namespace": "com.linkedin.events", + "size": 16 + }, + "name": "messageId", + "doc": "A unique identifier for the message" + }, + { + "compliance": "NONE", + "type": [ + "null", + "int" + ], + "name": "auditVersion", + "default": null, + "doc": "The version that is being used for auditing. In version 0, the audit trail buckets events into 10 minute audit windows based on the EventHeader timestamp. In version 1, the audit trail buckets events as follows: if the schema has an outer KafkaAuditHeader, use the outer audit header timestamp for bucketing; else if the EventHeader has an inner KafkaAuditHeader use that inner audit header's timestamp for bucketing" + }, + { + "compliance": "NONE", + "type": [ + "null", + "string" + ], + "name": "fabricUrn", + "default": null, + "doc": "The fabricUrn of the host from which the event is being emitted. Fabric Urn in the format of urn:li:fabric:{fabric_name}. See go/fabric." + }, + { + "compliance": "NONE", + "type": [ + "null", + "string" + ], + "name": "clusterConnectionString", + "default": null, + "doc": "This is a String that the client uses to establish some kind of connection with the Kafka cluster. The exact format of it depends on specific versions of clients and brokers. This information could potentially identify the fabric and cluster with which the client is producing to or consuming from." + } + ], + "doc": "This header records information about the context of an event as it is emitted into kafka and is intended to be used by the kafka audit application. For more information see go/kafkaauditheader" + } + ], + "name": "auditHeader", + "default": null, + "doc": "Kafka audit header. Currently remains unused in the open source." + }, + { + "type": "string", + "name": "entityType", + "doc": "Type of the entity being written to" + }, + { + "java": { + "class": "com.linkedin.pegasus2avro.common.urn.Urn" + }, + "Urn": "Urn", + "type": [ + "null", + "string" + ], + "name": "entityUrn", + "default": null, + "doc": "Urn of the entity being written" + }, + { + "type": [ + "null", + { + "type": "record", + "name": "GenericAspect", + "namespace": "com.linkedin.pegasus2avro.mxe", + "fields": [ + { + "type": "bytes", + "name": "value", + "doc": "The value of the aspect, serialized as bytes." + }, + { + "type": "string", + "name": "contentType", + "doc": "The content type, which represents the fashion in which the aspect was serialized.\nThe only type currently supported is application/json." + } + ], + "doc": "Generic record structure for serializing an Aspect" + } + ], + "name": "entityKeyAspect", + "default": null, + "doc": "Key aspect of the entity being written" + }, + { + "type": { + "type": "enum", + "symbolDocs": { + "CREATE": "NOT SUPPORTED YET\ninsert if not exists. otherwise fail", + "DELETE": "NOT SUPPORTED YET\ndelete action", + "PATCH": "NOT SUPPORTED YET\npatch the changes instead of full replace", + "RESTATE": "Restate an aspect, eg. in a index refresh.", + "UPDATE": "NOT SUPPORTED YET\nupdate if exists. otherwise fail", + "UPSERT": "insert if not exists. otherwise update" + }, + "name": "ChangeType", + "namespace": "com.linkedin.pegasus2avro.events.metadata", + "symbols": [ + "UPSERT", + "CREATE", + "UPDATE", + "DELETE", + "PATCH", + "RESTATE" + ], + "doc": "Descriptor for a change action" + }, + "name": "changeType", + "doc": "Type of change being proposed" + }, + { + "type": [ + "null", + "string" + ], + "name": "aspectName", + "default": null, + "doc": "Aspect of the entity being written to\nNot filling this out implies that the writer wants to affect the entire entity\nNote: This is only valid for CREATE, UPSERT, and DELETE operations." + }, + { + "type": [ + "null", + "com.linkedin.pegasus2avro.mxe.GenericAspect" + ], + "name": "aspect", + "default": null, + "doc": "The value of the new aspect." + }, + { + "type": [ + "null", + { + "type": "record", + "name": "SystemMetadata", + "namespace": "com.linkedin.pegasus2avro.mxe", + "fields": [ + { + "type": [ + "long", + "null" + ], + "name": "lastObserved", + "default": 0, + "doc": "The timestamp the metadata was observed at" + }, + { + "type": [ + "string", + "null" + ], + "name": "runId", + "default": "no-run-id-provided", + "doc": "The run id that produced the metadata. Populated in case of batch-ingestion." + }, + { + "type": [ + "null", + "string" + ], + "name": "registryName", + "default": null, + "doc": "The model registry name that was used to process this event" + }, + { + "type": [ + "null", + "string" + ], + "name": "registryVersion", + "default": null, + "doc": "The model registry version that was used to process this event" + }, + { + "type": [ + "null", + { + "type": "map", + "values": "string" + } + ], + "name": "properties", + "default": null, + "doc": "Additional properties" + } + ], + "doc": "Metadata associated with each metadata change that is processed by the system" + } + ], + "name": "systemMetadata", + "default": null, + "doc": "A string->string map of custom properties that one might want to attach to an event" + } + ], + "doc": "Kafka event for proposing a metadata change for an entity. A corresponding MetadataChangeLog is emitted when the change is accepted and committed, otherwise a FailedMetadataChangeProposal will be emitted instead." + } + From cc258700e9e1c305684f16f3994d334ca3ed3e81 Mon Sep 17 00:00:00 2001 From: amanda-her <912amandahernando@gmail.com> Date: Thu, 6 Jul 2023 16:46:41 +0200 Subject: [PATCH 02/12] tests and front-end --- .../datahub/graphql/GmsGraphQLEngine.java | 5 +- .../ownership/ListOwnershipTypesResolver.java | 4 +- .../mappers/PolicyInfoPolicyMapper.java | 5 + .../mappers/PolicyUpdateInputInfoMapper.java | 4 + .../types/policy/DataHubPolicyMapper.java | 5 + .../src/main/resources/entity.graphql | 29 +++- .../app/permissions/policy/ManagePolicies.tsx | 6 +- .../permissions/policy/PolicyActorForm.tsx | 85 ++++++++-- .../permissions/policy/PolicyDetailsModal.tsx | 27 +++- datahub-web-react/src/graphql/policy.graphql | 10 +- .../graphql/graphql-endpoint-development.md | 2 +- .../com/linkedin/common/urn/UrnUtils.java | 1 + .../datahub/authorization/PolicyEngine.java | 57 +++---- .../authorization/PolicyEngineTest.java | 153 +++++++++++++++++- 14 files changed, 344 insertions(+), 49 deletions(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java index 198c9d86c2117..0513ccab21c63 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java @@ -1634,9 +1634,12 @@ private void configurePolicyResolvers(final RuntimeWiring.Builder builder) { })).dataFetcher("resolvedRoles", new LoadableTypeBatchResolver<>(dataHubRoleType, (env) -> { final ActorFilter filter = env.getSource(); return filter.getRoles(); - })).dataFetcher("resolvedOwnershipTypes", new LoadableTypeBatchResolver<>(ownershipType, (env) -> { + })).dataFetcher("resolvedResourceOwnershipTypes", new LoadableTypeBatchResolver<>(ownershipType, (env) -> { final ActorFilter filter = env.getSource(); return filter.getResourceOwnersTypes(); + })).dataFetcher("resolvedPlatformInstanceOwnershipTypes", new LoadableTypeBatchResolver<>(ownershipType, (env) -> { + final ActorFilter filter = env.getSource(); + return filter.getPlatformInstanceOwnersTypes(); }))); } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/ownership/ListOwnershipTypesResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/ownership/ListOwnershipTypesResolver.java index 70441815f0a74..07017b1286864 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/ownership/ListOwnershipTypesResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/ownership/ListOwnershipTypesResolver.java @@ -72,7 +72,7 @@ public CompletableFuture get(DataFetchingEnvironment e result.setStart(gmsResult.getFrom()); result.setCount(gmsResult.getPageSize()); result.setTotal(gmsResult.getNumEntities()); - result.setOwnershipTypes(mapUnresolvedOwnershipTypes(gmsResult.getEntities().stream() + result.setOwnershipTypes(mapUnresolvedResourceOwnershipTypes(gmsResult.getEntities().stream() .map(SearchEntity::getEntity) .collect(Collectors.toList()))); return result; @@ -83,7 +83,7 @@ public CompletableFuture get(DataFetchingEnvironment e }); } - private List mapUnresolvedOwnershipTypes(List entityUrns) { + private List mapUnresolvedResourceOwnershipTypes(List entityUrns) { final List results = new ArrayList<>(); for (final Urn urn : entityUrns) { final OwnershipTypeEntity unresolvedView = new OwnershipTypeEntity(); diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/policy/mappers/PolicyInfoPolicyMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/policy/mappers/PolicyInfoPolicyMapper.java index b9a6bf07be8c8..6291e23347c02 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/policy/mappers/PolicyInfoPolicyMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/policy/mappers/PolicyInfoPolicyMapper.java @@ -54,10 +54,15 @@ private ActorFilter mapActors(final DataHubActorFilter actorFilter) { result.setAllGroups(actorFilter.isAllGroups()); result.setAllUsers(actorFilter.isAllUsers()); result.setResourceOwners(actorFilter.isResourceOwners()); + result.setPlatformInstanceOwners(actorFilter.isPlatformInstanceOwners()); UrnArray resourceOwnersTypes = actorFilter.getResourceOwnersTypes(); if (resourceOwnersTypes != null) { result.setResourceOwnersTypes(resourceOwnersTypes.stream().map(Urn::toString).collect(Collectors.toList())); } + UrnArray platformInstanceOwnersTypes = actorFilter.getPlatformInstanceOwnersTypes(); + if (resourceOwnersTypes != null) { + result.setPlatformInstanceOwnersTypes(platformInstanceOwnersTypes.stream().map(Urn::toString).collect(Collectors.toList())); + } if (actorFilter.hasGroups()) { result.setGroups(actorFilter.getGroups().stream().map(Urn::toString).collect(Collectors.toList())); } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/policy/mappers/PolicyUpdateInputInfoMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/policy/mappers/PolicyUpdateInputInfoMapper.java index cb323b60dd465..e84040b23deeb 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/policy/mappers/PolicyUpdateInputInfoMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/policy/mappers/PolicyUpdateInputInfoMapper.java @@ -51,9 +51,13 @@ private DataHubActorFilter mapActors(final ActorFilterInput actorInput) { result.setAllGroups(actorInput.getAllGroups()); result.setAllUsers(actorInput.getAllUsers()); result.setResourceOwners(actorInput.getResourceOwners()); + result.setPlatformInstanceOwners(actorInput.getPlatformInstanceOwners()); if (actorInput.getResourceOwnersTypes() != null) { result.setResourceOwnersTypes(new UrnArray(actorInput.getResourceOwnersTypes().stream().map(this::createUrn).collect(Collectors.toList()))); } + if (actorInput.getPlatformInstanceOwnersTypes() != null) { + result.setPlatformInstanceOwnersTypes(new UrnArray(actorInput.getPlatformInstanceOwnersTypes().stream().map(this::createUrn).collect(Collectors.toList()))); + } if (actorInput.getGroups() != null) { result.setGroups(new UrnArray(actorInput.getGroups().stream().map(this::createUrn).collect(Collectors.toList()))); } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/policy/DataHubPolicyMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/policy/DataHubPolicyMapper.java index 167e1615fc4cc..862d1321b20a5 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/policy/DataHubPolicyMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/policy/DataHubPolicyMapper.java @@ -68,11 +68,16 @@ private ActorFilter mapActors(final DataHubActorFilter actorFilter) { result.setAllGroups(actorFilter.isAllGroups()); result.setAllUsers(actorFilter.isAllUsers()); result.setResourceOwners(actorFilter.isResourceOwners()); + result.setPlatformInstanceOwners(actorFilter.isPlatformInstanceOwners()); // Change here is not executed at the moment - leaving it for the future UrnArray resourceOwnersTypes = actorFilter.getResourceOwnersTypes(); if (resourceOwnersTypes != null) { result.setResourceOwnersTypes(resourceOwnersTypes.stream().map(Urn::toString).collect(Collectors.toList())); } + UrnArray platformInstanceOwnersTypes = actorFilter.getPlatformInstanceOwnersTypes(); + if (platformInstanceOwnersTypes != null) { + result.setPlatformInstanceOwnersTypes(platformInstanceOwnersTypes.stream().map(Urn::toString).collect(Collectors.toList())); + } if (actorFilter.hasGroups()) { result.setGroups(actorFilter.getGroups().stream().map(Urn::toString).collect(Collectors.toList())); } diff --git a/datahub-graphql-core/src/main/resources/entity.graphql b/datahub-graphql-core/src/main/resources/entity.graphql index e45ecfe519d8b..80bc9844b0e82 100644 --- a/datahub-graphql-core/src/main/resources/entity.graphql +++ b/datahub-graphql-core/src/main/resources/entity.graphql @@ -8015,7 +8015,23 @@ type ActorFilter { """ Set of OwnershipTypes to apply the policy to (if resourceOwners field is set to True), resolved. """ - resolvedOwnershipTypes: [OwnershipTypeEntity!] + resolvedResourceOwnershipTypes: [OwnershipTypeEntity!] + + """ + Whether the filter should return TRUE for platform instance owners of a particular resource + Only applies to policies of type METADATA, which have a resource associated with them + """ + platformInstanceOwners: Boolean! + + """ + Set of OwnershipTypes to apply the policy to (if platformInstanceOwners field is set to True) + """ + platformInstanceOwnersTypes: [String!] + + """ + Set of OwnershipTypes to apply the policy to (if platformInstanceOwners field is set to True), resolved. + """ + resolvedPlatformInstanceOwnershipTypes: [OwnershipTypeEntity!] """ Whether the filter should apply to all users @@ -8150,6 +8166,17 @@ input ActorFilterInput { """ resourceOwnersTypes: [String!] + """ + Whether the filter should return TRUE for platform instance owners of a particular resource + Only applies to policies of type METADATA, which have a resource associated with them + """ + platformInstanceOwners: Boolean! + + """ + Set of OwnershipTypes to apply the policy to (if platformInstanceOwners field is set to True) + """ + platformInstanceOwnersTypes: [String!] + """ Whether the filter should apply to all users """ diff --git a/datahub-web-react/src/app/permissions/policy/ManagePolicies.tsx b/datahub-web-react/src/app/permissions/policy/ManagePolicies.tsx index 08327d40a7165..d877fee86071b 100644 --- a/datahub-web-react/src/app/permissions/policy/ManagePolicies.tsx +++ b/datahub-web-react/src/app/permissions/policy/ManagePolicies.tsx @@ -110,6 +110,8 @@ const toPolicyInput = (policy: Omit): PolicyUpdateInput => { allGroups: policy.actors.allGroups, resourceOwners: policy.actors.resourceOwners, resourceOwnersTypes: policy.actors.resourceOwnersTypes, + platformInstanceOwners: policy.actors.platformInstanceOwners, + platformInstanceOwnersTypes: policy.actors.platformInstanceOwnersTypes, }, }; if (policy.resources !== null && policy.resources !== undefined) { @@ -360,7 +362,8 @@ export const ManagePolicies = () => { /> {record?.allUsers ? All Users : null} {record?.allGroups ? All Groups : null} - {record?.resourceOwners ? All Owners : null} + {record?.resourceOwners ? All Resource Owners : null} + {record?.platformInstanceOwners ? All Platform Instance Owners : null} ); }, @@ -430,6 +433,7 @@ export const ManagePolicies = () => { allGroups: policy?.actors?.allGroups, allUsers: policy?.actors?.allUsers, resourceOwners: policy?.actors?.resourceOwners, + platformInstanceOwners: policy?.actors?.platformInstanceOwners, description: policy?.description, editable: policy?.editable, name: policy?.name, diff --git a/datahub-web-react/src/app/permissions/policy/PolicyActorForm.tsx b/datahub-web-react/src/app/permissions/policy/PolicyActorForm.tsx index 31b9472a7e53b..711c41bdf9214 100644 --- a/datahub-web-react/src/app/permissions/policy/PolicyActorForm.tsx +++ b/datahub-web-react/src/app/permissions/policy/PolicyActorForm.tsx @@ -61,8 +61,8 @@ export default function PolicyActorForm({ policyType, actors, setActors }: Props ownershipData?.listOwnershipTypes?.ownershipTypes.filter((type) => type.urn !== 'urn:li:ownershipType:none') || []; const ownershipTypesMap = Object.fromEntries(ownershipTypes.map((type) => [type.urn, type.info?.name])); - // Toggle the "Owners" switch - const onToggleAppliesToOwners = (value: boolean) => { + // Toggle the "Resource Owners" switch + const onToggleAppliesToResourceOwners = (value: boolean) => { setActors({ ...actors, resourceOwners: value, @@ -70,7 +70,7 @@ export default function PolicyActorForm({ policyType, actors, setActors }: Props }); }; - const onSelectOwnershipTypeActor = (newType: string) => { + const onSelectResourceOwnershipTypeActor = (newType: string) => { const newResourceOwnersTypes: Maybe = [...(actors.resourceOwnersTypes || []), newType]; setActors({ ...actors, @@ -78,13 +78,37 @@ export default function PolicyActorForm({ policyType, actors, setActors }: Props }); }; - const onDeselectOwnershipTypeActor = (type: string) => { + const onDeselectResourceOwnershipTypeActor = (type: string) => { const newResourceOwnersTypes: Maybe = actors.resourceOwnersTypes?.filter((u: string) => u !== type); setActors({ ...actors, resourceOwnersTypes: newResourceOwnersTypes?.length ? newResourceOwnersTypes : null, }); }; + // Toggle the "Platform Instance Owners" switch + const onToggleAppliesToPlatformInstanceOwners = (value: boolean) => { + setActors({ + ...actors, + platformInstanceOwners: value, + platformInstanceOwnersTypes: value ? actors.platformInstanceOwnersTypes : null, + }); + }; + + const onSelectPlatformInstanceOwnershipTypeActor = (newType: string) => { + const newPlatformInstanceOwnersTypes: Maybe = [...(actors.platformInstanceOwnersTypes || []), newType]; + setActors({ + ...actors, + platformInstanceOwnersTypes: newPlatformInstanceOwnersTypes, + }); + }; + + const onDeselectPlatformInstanceOwnershipTypeActor = (type: string) => { + const newPlatformInstanceOwnersTypes: Maybe = actors.platformInstanceOwnersTypes?.filter((u: string) => u !== type); + setActors({ + ...actors, + platformInstanceOwnersTypes: newPlatformInstanceOwnersTypes?.length ? newPlatformInstanceOwnersTypes : null, + }); + }; // When a user search result is selected, add the urn to the ActorFilter const onSelectUserActor = (newUser: string) => { @@ -206,7 +230,8 @@ export default function PolicyActorForm({ policyType, actors, setActors }: Props // Select dropdown values. const usersSelectValue = actors.allUsers ? ['All'] : actors.users || []; const groupsSelectValue = actors.allGroups ? ['All'] : actors.groups || []; - const ownershipTypesSelectValue = actors.resourceOwnersTypes || []; + const resourceOwnershipTypesSelectValue = actors.resourceOwnersTypes || []; + const platformInstanceOwnershipTypesSelectValue = actors.platformInstanceOwnershipTypes || []; const tagRender = (props) => { // eslint-disable-next-line react/prop-types @@ -240,13 +265,13 @@ export default function PolicyActorForm({ policyType, actors, setActors }: Props Select the users & groups that this policy should apply to. {showAppliesToOwners && ( - Owners} labelAlign="right"> + Resource Owners} labelAlign="right"> - Whether this policy should be apply to owners of the Metadata asset. If true, those who are + Whether this policy should apply to owners of the Metadata asset. If true, those who are marked as owners of a Metadata Asset, either directly or indirectly via a Group, will have the selected privileges. - + {actors.resourceOwners && ( @@ -254,11 +279,49 @@ export default function PolicyActorForm({ policyType, actors, setActors }: Props will applied to any type of ownership. + + )} + + Platform Instance Owners} labelAlign="right"> + + Whether this policy should apply to platform instance owners of the Metadata asset. If true, those who are + marked as owners of the platform instance to which a Metadata Asset belongs to, either directly or indirectly + via a Group, will have the selected privileges. + + + {actors.platformInstanceOwners && ( + + + List of types of ownership which will be used to match plafrom instance owners. If empty, + the policies will applied to any type of ownership. + + Date: Mon, 10 Jul 2023 16:53:47 +0200 Subject: [PATCH 06/12] feat(ingest): revert some changes and fix policyUtils --- .../resolvers/ownership/ListOwnershipTypesResolver.java | 4 ++-- datahub-web-react/src/app/permissions/policy/policyUtils.ts | 1 + docs/api/graphql/graphql-endpoint-development.md | 2 +- .../main/javaPegasus/com/linkedin/common/urn/UrnUtils.java | 1 - 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/ownership/ListOwnershipTypesResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/ownership/ListOwnershipTypesResolver.java index 07017b1286864..70441815f0a74 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/ownership/ListOwnershipTypesResolver.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/ownership/ListOwnershipTypesResolver.java @@ -72,7 +72,7 @@ public CompletableFuture get(DataFetchingEnvironment e result.setStart(gmsResult.getFrom()); result.setCount(gmsResult.getPageSize()); result.setTotal(gmsResult.getNumEntities()); - result.setOwnershipTypes(mapUnresolvedResourceOwnershipTypes(gmsResult.getEntities().stream() + result.setOwnershipTypes(mapUnresolvedOwnershipTypes(gmsResult.getEntities().stream() .map(SearchEntity::getEntity) .collect(Collectors.toList()))); return result; @@ -83,7 +83,7 @@ public CompletableFuture get(DataFetchingEnvironment e }); } - private List mapUnresolvedResourceOwnershipTypes(List entityUrns) { + private List mapUnresolvedOwnershipTypes(List entityUrns) { final List results = new ArrayList<>(); for (final Urn urn : entityUrns) { final OwnershipTypeEntity unresolvedView = new OwnershipTypeEntity(); diff --git a/datahub-web-react/src/app/permissions/policy/policyUtils.ts b/datahub-web-react/src/app/permissions/policy/policyUtils.ts index c7af7342f6efa..b858547724976 100644 --- a/datahub-web-react/src/app/permissions/policy/policyUtils.ts +++ b/datahub-web-react/src/app/permissions/policy/policyUtils.ts @@ -28,6 +28,7 @@ export const EMPTY_POLICY = { allUsers: false, allGroups: false, resourceOwners: false, + platformInstanceOwners: false, }, editable: true, }; diff --git a/docs/api/graphql/graphql-endpoint-development.md b/docs/api/graphql/graphql-endpoint-development.md index 1b1c0d891af48..fb36fb0503d99 100644 --- a/docs/api/graphql/graphql-endpoint-development.md +++ b/docs/api/graphql/graphql-endpoint-development.md @@ -29,7 +29,7 @@ GraphQL queries are handled by `Resolver` classes located in the `datahub-graphq > **listOwnershipTypes example:** The [`ListOwnershipTypesResolver`](https://github.com/datahub-project/datahub/commit/ea92b86e6ab4cbb18742fb8db6bc11fae8970cdb#diff-d2ad02d0ec286017d032640cfdb289fbdad554ef5f439355104766fa068513ac) class implements `DataFetcher>` since this is the return type of the endpoint. It contains an `EntityClient` instance variable to handle the ownership type fetching. Often the structure of the `Resolver` classes is to call a service to receive a response, then use a method to transform the result from the service into the GraphQL type returned. -> **listOwnershipTypes example:** The [`ListOwnershipTypesResolver`](https://github.com/datahub-project/datahub/commit/ea92b86e6ab4cbb18742fb8db6bc11fae8970cdb#diff-d2ad02d0ec286017d032640cfdb289fbdad554ef5f439355104766fa068513ac) calls the `search` method in its `EntityClient` to get the ownership types, then calls the defined `mapUnresolvedResourceOwnershipTypes` function to transform the response into a `ListOwnershipTypesResult`. +> **listOwnershipTypes example:** The [`ListOwnershipTypesResolver`](https://github.com/datahub-project/datahub/commit/ea92b86e6ab4cbb18742fb8db6bc11fae8970cdb#diff-d2ad02d0ec286017d032640cfdb289fbdad554ef5f439355104766fa068513ac) calls the `search` method in its `EntityClient` to get the ownership types, then calls the defined `mapUnresolvedOwnershipTypes` function to transform the response into a `ListOwnershipTypesResult`. Tip: Resolver classes can be tested with unit tests! > **listOwnershipTypes example:** The reference commit adds the [`ListOwnershipTypeResolverTest` class](https://github.com/datahub-project/datahub/commit/ea92b86e6ab4cbb18742fb8db6bc11fae8970cdb#diff-9443d70b221e36e9d47bfa9244673d1cd553a92ae496d03622932ad0a4832045). diff --git a/li-utils/src/main/javaPegasus/com/linkedin/common/urn/UrnUtils.java b/li-utils/src/main/javaPegasus/com/linkedin/common/urn/UrnUtils.java index 60c77b9aa269a..b68e429a5202c 100644 --- a/li-utils/src/main/javaPegasus/com/linkedin/common/urn/UrnUtils.java +++ b/li-utils/src/main/javaPegasus/com/linkedin/common/urn/UrnUtils.java @@ -52,7 +52,6 @@ public static Urn getUrn(String urnStr) { try { return Urn.createFromString(urnStr); } catch (URISyntaxException e) { - System.out.println(e.toString()); throw new RuntimeException(String.format("Failed to retrieve entity with urn %s, invalid urn", urnStr)); } } From f68cee6134352293d019f28d6c36c80258b319d0 Mon Sep 17 00:00:00 2001 From: amanda-her <912amandahernando@gmail.com> Date: Tue, 11 Jul 2023 14:58:22 +0200 Subject: [PATCH 07/12] feat(ingest): adding PlatformInstanceFieldResolverProvider --- .../authorization/DefaultResourceSpecResolver.java | 9 +++------ .../PlatformInstanceFieldResolverProvider.java | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/metadata-service/auth-impl/src/main/java/com/datahub/authorization/DefaultResourceSpecResolver.java b/metadata-service/auth-impl/src/main/java/com/datahub/authorization/DefaultResourceSpecResolver.java index cd4e0b0967829..3ce558d6ef7f7 100644 --- a/metadata-service/auth-impl/src/main/java/com/datahub/authorization/DefaultResourceSpecResolver.java +++ b/metadata-service/auth-impl/src/main/java/com/datahub/authorization/DefaultResourceSpecResolver.java @@ -1,11 +1,7 @@ package com.datahub.authorization; -import com.datahub.authorization.fieldresolverprovider.EntityTypeFieldResolverProvider; -import com.datahub.authorization.fieldresolverprovider.OwnerFieldResolverProvider; +import com.datahub.authorization.fieldresolverprovider.*; import com.datahub.authentication.Authentication; -import com.datahub.authorization.fieldresolverprovider.DomainFieldResolverProvider; -import com.datahub.authorization.fieldresolverprovider.EntityUrnFieldResolverProvider; -import com.datahub.authorization.fieldresolverprovider.ResourceFieldResolverProvider; import com.google.common.collect.ImmutableList; import com.linkedin.entity.client.EntityClient; import java.util.List; @@ -20,7 +16,8 @@ public DefaultResourceSpecResolver(Authentication systemAuthentication, EntityCl _resourceFieldResolverProviders = ImmutableList.of(new EntityTypeFieldResolverProvider(), new EntityUrnFieldResolverProvider(), new DomainFieldResolverProvider(entityClient, systemAuthentication), - new OwnerFieldResolverProvider(entityClient, systemAuthentication)); + new OwnerFieldResolverProvider(entityClient, systemAuthentication), + new PlatformInstanceFieldResolverProvider(entityClient, systemAuthentication)); } @Override diff --git a/metadata-service/auth-impl/src/main/java/com/datahub/authorization/fieldresolverprovider/PlatformInstanceFieldResolverProvider.java b/metadata-service/auth-impl/src/main/java/com/datahub/authorization/fieldresolverprovider/PlatformInstanceFieldResolverProvider.java index cc62788e32dc1..2be43334c207f 100644 --- a/metadata-service/auth-impl/src/main/java/com/datahub/authorization/fieldresolverprovider/PlatformInstanceFieldResolverProvider.java +++ b/metadata-service/auth-impl/src/main/java/com/datahub/authorization/fieldresolverprovider/PlatformInstanceFieldResolverProvider.java @@ -42,7 +42,7 @@ public FieldResolver getFieldResolver(ResourceSpec resourceSpec) { private FieldResolver.FieldValue getPlatformInstance(ResourceSpec resourceSpec) { Urn entityUrn = UrnUtils.getUrn(resourceSpec.getResource()); - // In the case that the entity is a platform instance, the associated domain is the domain itself + // In the case that the entity is a platform instance, the associated platform instance entity is the instance itself if (entityUrn.getEntityType().equals(DATA_PLATFORM_ENTITY_NAME)) { return FieldResolver.FieldValue.builder() .values(Collections.singleton(entityUrn.toString())) From 173a397f093c801f29be9638210afdc8bebb6cf0 Mon Sep 17 00:00:00 2001 From: amanda-her <912amandahernando@gmail.com> Date: Tue, 11 Jul 2023 16:28:43 +0200 Subject: [PATCH 08/12] feat(ingest): fix format in PolicyUpdateInputInfoMapper --- .../policy/mappers/PolicyUpdateInputInfoMapper.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/policy/mappers/PolicyUpdateInputInfoMapper.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/policy/mappers/PolicyUpdateInputInfoMapper.java index e84040b23deeb..69c3913bc9da6 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/policy/mappers/PolicyUpdateInputInfoMapper.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/policy/mappers/PolicyUpdateInputInfoMapper.java @@ -53,10 +53,14 @@ private DataHubActorFilter mapActors(final ActorFilterInput actorInput) { result.setResourceOwners(actorInput.getResourceOwners()); result.setPlatformInstanceOwners(actorInput.getPlatformInstanceOwners()); if (actorInput.getResourceOwnersTypes() != null) { - result.setResourceOwnersTypes(new UrnArray(actorInput.getResourceOwnersTypes().stream().map(this::createUrn).collect(Collectors.toList()))); + result.setResourceOwnersTypes( + new UrnArray(actorInput.getResourceOwnersTypes().stream().map(this::createUrn).collect(Collectors.toList())) + ); } if (actorInput.getPlatformInstanceOwnersTypes() != null) { - result.setPlatformInstanceOwnersTypes(new UrnArray(actorInput.getPlatformInstanceOwnersTypes().stream().map(this::createUrn).collect(Collectors.toList()))); + result.setPlatformInstanceOwnersTypes( + new UrnArray(actorInput.getPlatformInstanceOwnersTypes().stream().map(this::createUrn).collect(Collectors.toList())) + ); } if (actorInput.getGroups() != null) { result.setGroups(new UrnArray(actorInput.getGroups().stream().map(this::createUrn).collect(Collectors.toList()))); From 5fd57ce39c2d94e8b19718f8524318282215853d Mon Sep 17 00:00:00 2001 From: amanda-her <912amandahernando@gmail.com> Date: Wed, 12 Jul 2023 10:52:22 +0200 Subject: [PATCH 09/12] feat(auth): fix formatting and tests --- .../authorization/DefaultResourceSpecResolver.java | 7 ++++++- .../java/com/datahub/authorization/PolicyEngine.java | 10 ++++++++-- .../PlatformInstanceFieldResolverProvider.java | 2 -- smoke-test/tests/policies/test_policies.py | 1 + 4 files changed, 15 insertions(+), 5 deletions(-) diff --git a/metadata-service/auth-impl/src/main/java/com/datahub/authorization/DefaultResourceSpecResolver.java b/metadata-service/auth-impl/src/main/java/com/datahub/authorization/DefaultResourceSpecResolver.java index 3ce558d6ef7f7..8eaf97e985bb6 100644 --- a/metadata-service/auth-impl/src/main/java/com/datahub/authorization/DefaultResourceSpecResolver.java +++ b/metadata-service/auth-impl/src/main/java/com/datahub/authorization/DefaultResourceSpecResolver.java @@ -1,7 +1,12 @@ package com.datahub.authorization; -import com.datahub.authorization.fieldresolverprovider.*; +import com.datahub.authorization.fieldresolverprovider.EntityTypeFieldResolverProvider; +import com.datahub.authorization.fieldresolverprovider.OwnerFieldResolverProvider; import com.datahub.authentication.Authentication; +import com.datahub.authorization.fieldresolverprovider.DomainFieldResolverProvider; +import com.datahub.authorization.fieldresolverprovider.EntityUrnFieldResolverProvider; +import com.datahub.authorization.fieldresolverprovider.PlatformInstanceFieldResolverProvider; +import com.datahub.authorization.fieldresolverprovider.ResourceFieldResolverProvider; import com.google.common.collect.ImmutableList; import com.linkedin.entity.client.EntityClient; import java.util.List; diff --git a/metadata-service/auth-impl/src/main/java/com/datahub/authorization/PolicyEngine.java b/metadata-service/auth-impl/src/main/java/com/datahub/authorization/PolicyEngine.java index 5f3f456378563..6e92bd2720516 100644 --- a/metadata-service/auth-impl/src/main/java/com/datahub/authorization/PolicyEngine.java +++ b/metadata-service/auth-impl/src/main/java/com/datahub/authorization/PolicyEngine.java @@ -309,7 +309,7 @@ private boolean isOwnerMatch( final Optional requestResource, final PolicyEvaluationContext context) { // If the policy does not apply to owners, or there is no resource to own, return false immediately. - if (!(actorFilter.isResourceOwners() || actorFilter.isPlatformInstanceOwners()) || !requestResource.isPresent() ) { + if (!(actorFilter.isResourceOwners() || actorFilter.isPlatformInstanceOwners()) || !requestResource.isPresent()) { return false; } List resourceOwnershipTypes = actorFilter.getResourceOwnersTypes(); @@ -317,7 +317,13 @@ private boolean isOwnerMatch( return isActorOwner(actor, requestResource.get(), resourceOwnershipTypes, platformInstanceOwnershipTypes, context); } - private boolean isActorOwner(Urn actor, ResolvedResourceSpec resourceSpec, List resourceOwnershipTypes, List platformInstanceOwnershipTypes, PolicyEvaluationContext context) { + private boolean isActorOwner( + Urn actor, + ResolvedResourceSpec resourceSpec, + List resourceOwnershipTypes, + List platformInstanceOwnershipTypes, + PolicyEvaluationContext context + ) { Urn entityUrn = UrnUtils.getUrn(resourceSpec.getSpec().getResource()); Set resourceOwners = this.resolveEntityOwnersForType(entityUrn, resourceOwnershipTypes); Set owners = new HashSet<>(new ArrayList<>(resourceOwners)); diff --git a/metadata-service/auth-impl/src/main/java/com/datahub/authorization/fieldresolverprovider/PlatformInstanceFieldResolverProvider.java b/metadata-service/auth-impl/src/main/java/com/datahub/authorization/fieldresolverprovider/PlatformInstanceFieldResolverProvider.java index 2be43334c207f..162e14efbe314 100644 --- a/metadata-service/auth-impl/src/main/java/com/datahub/authorization/fieldresolverprovider/PlatformInstanceFieldResolverProvider.java +++ b/metadata-service/auth-impl/src/main/java/com/datahub/authorization/fieldresolverprovider/PlatformInstanceFieldResolverProvider.java @@ -7,7 +7,6 @@ import com.linkedin.common.DataPlatformInstance; import com.linkedin.common.urn.Urn; import com.linkedin.common.urn.UrnUtils; -import com.linkedin.domain.Domains; import com.linkedin.entity.EntityResponse; import com.linkedin.entity.EnvelopedAspect; import com.linkedin.entity.client.EntityClient; @@ -16,7 +15,6 @@ import java.util.Collections; import java.util.Objects; -import java.util.stream.Collectors; import static com.linkedin.metadata.Constants.*; diff --git a/smoke-test/tests/policies/test_policies.py b/smoke-test/tests/policies/test_policies.py index b7091541894dd..d9d497e1c40ed 100644 --- a/smoke-test/tests/policies/test_policies.py +++ b/smoke-test/tests/policies/test_policies.py @@ -109,6 +109,7 @@ def test_frontend_policy_operations(frontend_session): "actors": { "users": [get_root_urn()], "resourceOwners": False, + "platformInstanceOwners": False, "allUsers": False, "allGroups": False, }, From ba437381205966e6640e7aafc0527f9fb571b9cc Mon Sep 17 00:00:00 2001 From: amanda-her <912amandahernando@gmail.com> Date: Wed, 12 Jul 2023 12:51:15 +0200 Subject: [PATCH 10/12] feat(auth): fix tests --- smoke-test/tests/policies/test_policies.py | 1 + 1 file changed, 1 insertion(+) diff --git a/smoke-test/tests/policies/test_policies.py b/smoke-test/tests/policies/test_policies.py index d9d497e1c40ed..65c42bbca70ec 100644 --- a/smoke-test/tests/policies/test_policies.py +++ b/smoke-test/tests/policies/test_policies.py @@ -140,6 +140,7 @@ def test_frontend_policy_operations(frontend_session): "privileges": ["EDIT_ENTITY_TAGS", "EDIT_ENTITY_GLOSSARY_TERMS"], "actors": { "resourceOwners": False, + "platformInstanceOwners": False, "allUsers": True, "allGroups": False, }, From fb37de26a7a9269624a6ab01f8f19f6a8f3b6630 Mon Sep 17 00:00:00 2001 From: amanda-her <912amandahernando@gmail.com> Date: Wed, 12 Jul 2023 17:09:25 +0200 Subject: [PATCH 11/12] feat(auth): update doc --- docs/authorization/access-policies-guide.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/authorization/access-policies-guide.md b/docs/authorization/access-policies-guide.md index 5820e513a83e3..0696fc6765a82 100644 --- a/docs/authorization/access-policies-guide.md +++ b/docs/authorization/access-policies-guide.md @@ -255,6 +255,13 @@ This advanced functionality allows of Admins of DataHub to closely control which

+We can also grant the Privileges to the *owners* of the platform instance to which Entities (or *Resources*) belong to that are in scope for the Policy. +This advanced functionality allows of Admins of DataHub to closely control which actions can or cannot be performed by owners. + +

+ +

+ ### Updating an Existing Policy To update an existing Policy, simply click the **Edit** on the Policy you wish to change. From 5654a6b40311ca6b3ddf141a3297ef8ecdacfdc9 Mon Sep 17 00:00:00 2001 From: amanda-her <912amandahernando@gmail.com> Date: Thu, 13 Jul 2023 12:50:02 +0200 Subject: [PATCH 12/12] feat(auth): remove doc --- docs/authorization/access-policies-guide.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/docs/authorization/access-policies-guide.md b/docs/authorization/access-policies-guide.md index 0696fc6765a82..5820e513a83e3 100644 --- a/docs/authorization/access-policies-guide.md +++ b/docs/authorization/access-policies-guide.md @@ -255,13 +255,6 @@ This advanced functionality allows of Admins of DataHub to closely control which

-We can also grant the Privileges to the *owners* of the platform instance to which Entities (or *Resources*) belong to that are in scope for the Policy. -This advanced functionality allows of Admins of DataHub to closely control which actions can or cannot be performed by owners. - -

- -

- ### Updating an Existing Policy To update an existing Policy, simply click the **Edit** on the Policy you wish to change.