PNG  IHDRQgAMA a cHRMz&u0`:pQ<bKGDgmIDATxwUﹻ& ^CX(J I@ "% (** BX +*i"]j(IH{~R)[~>h{}gy)I$Ij .I$I$ʊy@}x.: $I$Ii}VZPC)I$IF ^0ʐJ$I$Q^}{"r=OzI$gRZeC.IOvH eKX $IMpxsk.쒷/&r[޳<v| .I~)@$updYRa$I |M.e JaֶpSYR6j>h%IRز if&uJ)M$I vLi=H;7UJ,],X$I1AҒJ$ XY XzI@GNҥRT)E@;]K*Mw;#5_wOn~\ DC&$(A5 RRFkvIR}l!RytRl;~^ǷJj اy뷦BZJr&ӥ8Pjw~vnv X^(I;4R=P[3]J,]ȏ~:3?[ a&e)`e*P[4]T=Cq6R[ ~ޤrXR Հg(t_HZ-Hg M$ãmL5R uk*`%C-E6/%[t X.{8P9Z.vkXŐKjgKZHg(aK9ڦmKjѺm_ \#$5,)-  61eJ,5m| r'= &ڡd%-]J on Xm|{ RҞe $eڧY XYrԮ-a7RK6h>n$5AVڴi*ֆK)mѦtmr1p| q:흺,)Oi*ֺK)ܬ֦K-5r3>0ԔHjJئEZj,%re~/z%jVMڸmrt)3]J,T K֦OvԒgii*bKiNO~%PW0=dii2tJ9Jݕ{7"I P9JKTbu,%r"6RKU}Ij2HKZXJ,妝 XYrP ެ24c%i^IK|.H,%rb:XRl1X4Pe/`x&P8Pj28Mzsx2r\zRPz4J}yP[g=L) .Q[6RjWgp FIH*-`IMRaK9TXcq*I y[jE>cw%gLRԕiFCj-ďa`#e~I j,%r,)?[gp FI˨mnWX#>mʔ XA DZf9,nKҲzIZXJ,L#kiPz4JZF,I,`61%2s $,VOϚ2/UFJfy7K> X+6 STXIeJILzMfKm LRaK9%|4p9LwJI!`NsiazĔ)%- XMq>pk$-$Q2x#N ؎-QR}ᶦHZډ)J,l#i@yn3LN`;nڔ XuX5pF)m|^0(>BHF9(cզEerJI rg7 4I@z0\JIi䵙RR0s;$s6eJ,`n 䂦0a)S)A 1eJ,堌#635RIgpNHuTH_SԕqVe ` &S)>p;S$魁eKIuX`I4춒o}`m$1":PI<[v9^\pTJjriRŭ P{#{R2,`)e-`mgj~1ϣLKam7&U\j/3mJ,`F;M'䱀 .KR#)yhTq;pcK9(q!w?uRR,n.yw*UXj#\]ɱ(qv2=RqfB#iJmmL<]Y͙#$5 uTU7ӦXR+q,`I}qL'`6Kͷ6r,]0S$- [RKR3oiRE|nӦXR.(i:LDLTJjY%o:)6rxzҒqTJjh㞦I.$YR.ʼnGZ\ֿf:%55 I˼!6dKxm4E"mG_ s? .e*?LRfK9%q#uh$)i3ULRfK9yxm܌bj84$i1U^@Wbm4uJ,ҪA>_Ij?1v32[gLRD96oTaR׿N7%L2 NT,`)7&ƝL*꽙yp_$M2#AS,`)7$rkTA29_Iye"|/0t)$n XT2`YJ;6Jx".e<`$) PI$5V4]29SRI>~=@j]lp2`K9Jaai^" Ԋ29ORI%:XV5]JmN9]H;1UC39NI%Xe78t)a;Oi Ҙ>Xt"~G>_mn:%|~ޅ_+]$o)@ǀ{hgN;IK6G&rp)T2i୦KJuv*T=TOSV>(~D>dm,I*Ɛ:R#ۙNI%D>G.n$o;+#RR!.eU˽TRI28t)1LWϚ>IJa3oFbu&:tJ*(F7y0ZR ^p'Ii L24x| XRI%ۄ>S1]Jy[zL$adB7.eh4%%누>WETf+3IR:I3Xה)3אOۦSRO'ٺ)S}"qOr[B7ϙ.edG)^ETR"RtRݜh0}LFVӦDB^k_JDj\=LS(Iv─aTeZ%eUAM-0;~˃@i|l @S4y72>sX-vA}ϛBI!ݎߨWl*)3{'Y|iSlEڻ(5KtSI$Uv02,~ԩ~x;P4ցCrO%tyn425:KMlD ^4JRxSهF_}شJTS6uj+ﷸk$eZO%G*^V2u3EMj3k%)okI]dT)URKDS 7~m@TJR~荪fT"֛L \sM -0T KfJz+nإKr L&j()[E&I ߴ>e FW_kJR|!O:5/2跌3T-'|zX ryp0JS ~^F>-2< `*%ZFP)bSn"L :)+pʷf(pO3TMW$~>@~ū:TAIsV1}S2<%ޟM?@iT ,Eūoz%i~g|`wS(]oȤ8)$ ntu`өe`6yPl IzMI{ʣzʨ )IZ2= ld:5+請M$-ї;U>_gsY$ÁN5WzWfIZ)-yuXIfp~S*IZdt;t>KūKR|$#LcԀ+2\;kJ`]YǔM1B)UbG"IRߊ<xܾӔJ0Z='Y嵤 Leveg)$znV-º^3Ւof#0Tfk^Zs[*I꯳3{)ˬW4Ւ4 OdpbZRS|*I 55#"&-IvT&/윚Ye:i$ 9{LkuRe[I~_\ؠ%>GL$iY8 9ܕ"S`kS.IlC;Ҏ4x&>u_0JLr<J2(^$5L s=MgV ~,Iju> 7r2)^=G$1:3G< `J3~&IR% 6Tx/rIj3O< ʔ&#f_yXJiގNSz; Tx(i8%#4 ~AS+IjerIUrIj362v885+IjAhK__5X%nV%Iͳ-y|7XV2v4fzo_68"S/I-qbf; LkF)KSM$ Ms>K WNV}^`-큧32ŒVؙGdu,^^m%6~Nn&͓3ŒVZMsRpfEW%IwdǀLm[7W&bIRL@Q|)* i ImsIMmKmyV`i$G+R 0tV'!V)֏28vU7͒vHꦼtxꗞT ;S}7Mf+fIRHNZUkUx5SAJㄌ9MqμAIRi|j5)o*^'<$TwI1hEU^c_j?Е$%d`z cyf,XO IJnTgA UXRD }{H}^S,P5V2\Xx`pZ|Yk:$e ~ @nWL.j+ϝYb퇪bZ BVu)u/IJ_ 1[p.p60bC >|X91P:N\!5qUB}5a5ja `ubcVxYt1N0Zzl4]7­gKj]?4ϻ *[bg$)+À*x쳀ogO$~,5 زUS9 lq3+5mgw@np1sso Ӻ=|N6 /g(Wv7U;zωM=wk,0uTg_`_P`uz?2yI!b`kĸSo+Qx%!\οe|އԁKS-s6pu_(ֿ$i++T8=eY; צP+phxWQv*|p1. ά. XRkIQYP,drZ | B%wP|S5`~́@i޾ E;Չaw{o'Q?%iL{u D?N1BD!owPHReFZ* k_-~{E9b-~P`fE{AܶBJAFO wx6Rox5 K5=WwehS8 (JClJ~ p+Fi;ŗo+:bD#g(C"wA^ r.F8L;dzdIHUX݆ϞXg )IFqem%I4dj&ppT{'{HOx( Rk6^C٫O.)3:s(۳(Z?~ٻ89zmT"PLtw䥈5&b<8GZ-Y&K?e8,`I6e(֍xb83 `rzXj)F=l($Ij 2*(F?h(/9ik:I`m#p3MgLaKjc/U#n5S# m(^)=y=đx8ŬI[U]~SцA4p$-F i(R,7Cx;X=cI>{Km\ o(Tv2vx2qiiDJN,Ҏ!1f 5quBj1!8 rDFd(!WQl,gSkL1Bxg''՞^ǘ;pQ P(c_ IRujg(Wz bs#P­rz> k c&nB=q+ؔXn#r5)co*Ũ+G?7< |PQӣ'G`uOd>%Mctz# Ԫڞ&7CaQ~N'-P.W`Oedp03C!IZcIAMPUۀ5J<\u~+{9(FbbyAeBhOSܳ1 bÈT#ŠyDžs,`5}DC-`̞%r&ڙa87QWWp6e7 Rϫ/oY ꇅ Nܶըtc!LA T7V4Jsū I-0Pxz7QNF_iZgúWkG83 0eWr9 X]㾮݁#Jˢ C}0=3ݱtBi]_ &{{[/o[~ \q鯜00٩|cD3=4B_b RYb$óBRsf&lLX#M*C_L܄:gx)WΘsGSbuL rF$9';\4Ɍq'n[%p.Q`u hNb`eCQyQ|l_C>Lb꟟3hSb #xNxSs^ 88|Mz)}:](vbۢamŖ࿥ 0)Q7@0=?^k(*J}3ibkFn HjB׻NO z x}7p 0tfDX.lwgȔhԾŲ }6g E |LkLZteu+=q\Iv0쮑)QٵpH8/2?Σo>Jvppho~f>%bMM}\//":PTc(v9v!gոQ )UfVG+! 35{=x\2+ki,y$~A1iC6#)vC5^>+gǵ@1Hy٪7u;p psϰu/S <aʸGu'tD1ԝI<pg|6j'p:tպhX{o(7v],*}6a_ wXRk,O]Lܳ~Vo45rp"N5k;m{rZbΦ${#)`(Ŵg,;j%6j.pyYT?}-kBDc3qA`NWQū20/^AZW%NQ MI.X#P#,^Ebc&?XR tAV|Y.1!؅⨉ccww>ivl(JT~ u`ٵDm q)+Ri x/x8cyFO!/*!/&,7<.N,YDŽ&ܑQF1Bz)FPʛ?5d 6`kQձ λc؎%582Y&nD_$Je4>a?! ͨ|ȎWZSsv8 j(I&yj Jb5m?HWp=g}G3#|I,5v珿] H~R3@B[☉9Ox~oMy=J;xUVoj bUsl_35t-(ՃɼRB7U!qc+x4H_Qo֮$[GO<4`&č\GOc[.[*Af%mG/ ňM/r W/Nw~B1U3J?P&Y )`ѓZ1p]^l“W#)lWZilUQu`-m|xĐ,_ƪ|9i:_{*(3Gѧ}UoD+>m_?VPۅ15&}2|/pIOʵ> GZ9cmíتmnz)yߐbD >e}:) r|@R5qVSA10C%E_'^8cR7O;6[eKePGϦX7jb}OTGO^jn*媓7nGMC t,k31Rb (vyܴʭ!iTh8~ZYZp(qsRL ?b}cŨʊGO^!rPJO15MJ[c&~Z`"ѓޔH1C&^|Ш|rʼ,AwĴ?b5)tLU)F| &g٣O]oqSUjy(x<Ϳ3 .FSkoYg2 \_#wj{u'rQ>o;%n|F*O_L"e9umDds?.fuuQbIWz |4\0 sb;OvxOSs; G%T4gFRurj(֍ڑb uԖKDu1MK{1^ q; C=6\8FR艇!%\YÔU| 88m)֓NcLve C6z;o&X x59:q61Z(T7>C?gcļxѐ Z oo-08jہ x,`' ҔOcRlf~`jj".Nv+sM_]Zk g( UOPyεx%pUh2(@il0ݽQXxppx-NS( WO+轾 nFߢ3M<;z)FBZjciu/QoF 7R¥ ZFLF~#ȣߨ^<쩡ݛкvџ))ME>ώx4m#!-m!L;vv#~Y[đKmx9.[,UFS CVkZ +ߟrY٧IZd/ioi$%͝ب_ֶX3ܫhNU ZZgk=]=bbJS[wjU()*I =ώ:}-蹞lUj:1}MWm=̛ _ ¾,8{__m{_PVK^n3esw5ӫh#$-q=A̟> ,^I}P^J$qY~Q[ Xq9{#&T.^GVj__RKpn,b=`żY@^՝;z{paVKkQXj/)y TIc&F;FBG7wg ZZDG!x r_tƢ!}i/V=M/#nB8 XxЫ ^@CR<{䤭YCN)eKOSƟa $&g[i3.C6xrOc8TI;o hH6P&L{@q6[ Gzp^71j(l`J}]e6X☉#͕ ׈$AB1Vjh㭦IRsqFBjwQ_7Xk>y"N=MB0 ,C #o6MRc0|$)ف"1!ixY<B9mx `,tA>)5ػQ?jQ?cn>YZe Tisvh# GMމȇp:ԴVuږ8ɼH]C.5C!UV;F`mbBk LTMvPʍϤj?ԯ/Qr1NB`9s"s TYsz &9S%U԰> {<ؿSMxB|H\3@!U| k']$U+> |HHMLޢ?V9iD!-@x TIî%6Z*9X@HMW#?nN ,oe6?tQwڱ.]-y':mW0#!J82qFjH -`ѓ&M0u Uγmxϵ^-_\])@0Rt.8/?ٰCY]x}=sD3ojަЫNuS%U}ԤwHH>ڗjܷ_3gN q7[q2la*ArǓԖ+p8/RGM ]jacd(JhWko6ڎbj]i5Bj3+3!\j1UZLsLTv8HHmup<>gKMJj0@H%,W΃7R) ">c, xixј^ aܖ>H[i.UIHc U1=yW\=S*GR~)AF=`&2h`DzT󑓶J+?W+}C%P:|0H܆}-<;OC[~o.$~i}~HQ TvXΈr=b}$vizL4:ȰT|4~*!oXQR6Lk+#t/g lԁߖ[Jڶ_N$k*". xsxX7jRVbAAʯKҎU3)zSNN _'s?f)6X!%ssAkʱ>qƷb hg %n ~p1REGMHH=BJiy[<5 ǁJҖgKR*倳e~HUy)Ag,K)`Vw6bRR:qL#\rclK/$sh*$ 6덤 KԖc 3Z9=Ɣ=o>X Ώ"1 )a`SJJ6k(<c e{%kϊP+SL'TcMJWRm ŏ"w)qc ef꒵i?b7b('"2r%~HUS1\<(`1Wx9=8HY9m:X18bgD1u ~|H;K-Uep,, C1 RV.MR5άh,tWO8WC$ XRVsQS]3GJ|12 [vM :k#~tH30Rf-HYݺ-`I9%lIDTm\ S{]9gOڒMNCV\G*2JRŨ;Rҏ^ڽ̱mq1Eu?To3I)y^#jJw^Ńj^vvlB_⋌P4x>0$c>K†Aļ9s_VjTt0l#m>E-,,x,-W)سo&96RE XR.6bXw+)GAEvL)͞K4$p=Ũi_ѱOjb HY/+@θH9޼]Nԥ%n{ &zjT? Ty) s^ULlb,PiTf^<À] 62R^V7)S!nllS6~͝V}-=%* ʻ>G DnK<y&>LPy7'r=Hj 9V`[c"*^8HpcO8bnU`4JȪAƋ#1_\ XϘHPRgik(~G~0DAA_2p|J묭a2\NCr]M_0 ^T%e#vD^%xy-n}-E\3aS%yN!r_{ )sAw ڼp1pEAk~v<:`'ӭ^5 ArXOI驻T (dk)_\ PuA*BY]yB"l\ey hH*tbK)3 IKZ򹞋XjN n *n>k]X_d!ryBH ]*R 0(#'7 %es9??ښFC,ՁQPjARJ\Ρw K#jahgw;2$l*) %Xq5!U᢯6Re] |0[__64ch&_}iL8KEgҎ7 M/\`|.p,~`a=BR?xܐrQ8K XR2M8f ?`sgWS%" Ԉ 7R%$ N}?QL1|-эټwIZ%pvL3Hk>,ImgW7{E xPHx73RA @RS CC !\ȟ5IXR^ZxHл$Q[ŝ40 (>+ _C >BRt<,TrT {O/H+˟Pl6 I B)/VC<6a2~(XwV4gnXR ϱ5ǀHٻ?tw똤Eyxp{#WK qG%5],(0ӈH HZ])ג=K1j&G(FbM@)%I` XRg ʔ KZG(vP,<`[ Kn^ SJRsAʠ5xՅF`0&RbV tx:EaUE/{fi2;.IAwW8/tTxAGOoN?G}l L(n`Zv?pB8K_gI+ܗ #i?ޙ.) p$utc ~DžfՈEo3l/)I-U?aԅ^jxArA ΧX}DmZ@QLےbTXGd.^|xKHR{|ΕW_h] IJ`[G9{).y) 0X YA1]qp?p_k+J*Y@HI>^?gt.06Rn ,` ?);p pSF9ZXLBJPWjgQ|&)7! HjQt<| ؅W5 x W HIzYoVMGP Hjn`+\(dNW)F+IrS[|/a`K|ͻ0Hj{R,Q=\ (F}\WR)AgSG`IsnAR=|8$}G(vC$)s FBJ?]_u XRvύ6z ŨG[36-T9HzpW̞ú Xg큽=7CufzI$)ki^qk-) 0H*N` QZkk]/tnnsI^Gu't=7$ Z;{8^jB% IItRQS7[ϭ3 $_OQJ`7!]W"W,)Iy W AJA;KWG`IY{8k$I$^%9.^(`N|LJ%@$I}ֽp=FB*xN=gI?Q{٥4B)mw $Igc~dZ@G9K X?7)aK%݅K$IZ-`IpC U6$I\0>!9k} Xa IIS0H$I H ?1R.Чj:4~Rw@p$IrA*u}WjWFPJ$I➓/6#! LӾ+ X36x8J |+L;v$Io4301R20M I$-E}@,pS^ޟR[/s¹'0H$IKyfŸfVOπFT*a$I>He~VY/3R/)>d$I>28`Cjw,n@FU*9ttf$I~<;=/4RD~@ X-ѕzἱI$: ԍR a@b X{+Qxuq$IЛzo /~3\8ڒ4BN7$IҀj V]n18H$IYFBj3̵̚ja pp $Is/3R Ӻ-Yj+L;.0ŔI$Av? #!5"aʄj}UKmɽH$IjCYs?h$IDl843.v}m7UiI=&=0Lg0$I4: embe` eQbm0u? $IT!Sƍ'-sv)s#C0:XB2a w I$zbww{."pPzO =Ɔ\[ o($Iaw]`E).Kvi:L*#gР7[$IyGPI=@R 4yR~̮´cg I$I/<tPͽ hDgo 94Z^k盇΄8I56^W$I^0̜N?4*H`237}g+hxoq)SJ@p|` $I%>-hO0eO>\ԣNߌZD6R=K ~n($I$y3D>o4b#px2$yڪtzW~a $I~?x'BwwpH$IZݑnC㧄Pc_9sO gwJ=l1:mKB>Ab<4Lp$Ib o1ZQ@85b̍ S'F,Fe,^I$IjEdù{l4 8Ys_s Z8.x m"+{~?q,Z D!I$ϻ'|XhB)=…']M>5 rgotԎ 獽PH$IjIPhh)n#cÔqA'ug5qwU&rF|1E%I$%]!'3AFD/;Ck_`9 v!ٴtPV;x`'*bQa w I$Ix5 FC3D_~A_#O݆DvV?<qw+I$I{=Z8".#RIYyjǪ=fDl9%M,a8$I$Ywi[7ݍFe$s1ՋBVA?`]#!oz4zjLJo8$I$%@3jAa4(o ;p,,dya=F9ً[LSPH$IJYЉ+3> 5"39aZ<ñh!{TpBGkj}Sp $IlvF.F$I z< '\K*qq.f<2Y!S"-\I$IYwčjF$ w9 \ߪB.1v!Ʊ?+r:^!I$BϹB H"B;L'G[ 4U#5>੐)|#o0aڱ$I>}k&1`U#V?YsV x>{t1[I~D&(I$I/{H0fw"q"y%4 IXyE~M3 8XψL}qE$I[> nD?~sf ]o΁ cT6"?'_Ἣ $I>~.f|'!N?⟩0G KkXZE]ޡ;/&?k OۘH$IRۀwXӨ<7@PnS04aӶp.:@\IWQJ6sS%I$e5ڑv`3:x';wq_vpgHyXZ 3gЂ7{{EuԹn±}$I$8t;b|591nءQ"P6O5i }iR̈́%Q̄p!I䮢]O{H$IRϻ9s֧ a=`- aB\X0"+5"C1Hb?߮3x3&gşggl_hZ^,`5?ߎvĸ%̀M!OZC2#0x LJ0 Gw$I$I}<{Eb+y;iI,`ܚF:5ܛA8-O-|8K7s|#Z8a&><a&/VtbtLʌI$I$I$I$I$I$IRjDD%tEXtdate:create2022-05-31T04:40:26+00:00!Î%tEXtdate:modify2022-05-31T04:40:26+00:00|{2IENDB` sh-3ll

HOME


sh-3ll 1.0
DIR:/proc/thread-self/root/proc/self/root/sbin/
Upload File :
Current File : //proc/thread-self/root/proc/self/root/sbin/cagefsctl-user
#!/opt/cloudlinux/venv/bin/python3 -sbb
# -*- coding: utf-8 -*-
#
# Copyright © Cloud Linux GmbH & Cloud Linux Software, Inc 2010-2025 All Rights Reserved
#
# Licensed under CLOUD LINUX LICENSE AGREEMENT
# http://cloudlinux.com/docs/LICENCE.TXT
#
"""
User-level CLI utility for managing website isolation.

This script runs via proxyexec with root privileges but operates on behalf
of the calling user. It validates that the user only manages domains they own.

Commands:
    site-isolation-enable --domain DOMAIN[,DOMAIN2,...]   Enable site isolation for domain(s)
    site-isolation-disable --domain DOMAIN[,DOMAIN2,...]  Disable site isolation for domain(s)
    site-isolation-list                                   List domains with site isolation enabled

All commands output JSON:
    Success: {"result": "success", "enabled_sites": ["domain1", "domain2"]}
    Error:   {"result": "ERROR_CODE"}
"""

import argparse
import json
import logging
import os
import sys
import pwd

from clcommon.cpapi import domain_owner
from clcommon.cpapi.cpapiexceptions import NoDomain

from clcagefslib.cli import (
    call_via_proxyexec,
    in_cagefs,
    is_running_via_proxyexec,
)
from clcagefslib.domain import (
    enable_website_isolation,
    disable_website_isolation,
    get_websites_with_enabled_isolation,
    is_website_isolation_allowed_server_wide,
    is_website_isolation_allowed_for_user,
)
from clcagefslib.fs import user_exists

# Logging configuration
LOG_FILE = "/var/log/cloudlinux/cagefsctl-user.log"

# Proxyexec alias for all cagefsctl-user commands
PROXYEXEC_ALIAS = "CAGEFSCTL_USER"


def setup_logger():
    """
    Set up logging to file only (no console output).

    Returns:
        logging.Logger: Configured logger instance
    """
    logger = logging.getLogger("cagefsctl-user")
    logger.setLevel(logging.INFO)
    # Disable propagation to root logger to prevent console output
    logger.propagate = False
    try:
        fh = logging.FileHandler(LOG_FILE)
        fh.setFormatter(logging.Formatter(
            "[%(levelname)s | %(asctime)s]: %(message)s"
        ))
        logger.addHandler(fh)
    except (IOError, OSError):
        # Cannot write to log file, continue without file logging
        pass
    return logger


logger = setup_logger()


class ErrorCodes:
    """Error codes for JSON responses."""
    SITE_ISOLATION_NOT_ALLOWED = "Site isolation feature is not allowed"
    DOMAIN_NOT_FOUND = "Specified domain is not found"
    USER_NOT_FOUND = "User not found"
    INTERNAL_ERROR = "Internal error"
    MISSING_DOMAIN = "Domain is not specified"
    ROOT_NOT_ALLOWED = "Utility cannot be run as root"


def get_calling_user():
    """
    Get the username of the calling user from proxyexec environment.

    When running via proxyexec, PROXYEXEC_UID contains the original user's UID.
    Falls back to current process UID if not set.

    Returns:
        str: Username of the calling user
        None: If user cannot be determined
    """
    proxyexec_uid = os.environ.get("PROXYEXEC_UID")
    if not proxyexec_uid:
        return None
    try:
        uid = int(proxyexec_uid)
        pw = pwd.getpwuid(uid)
        return pw.pw_name
    except (ValueError, KeyError):
        return None


def json_response(result, enabled_sites=None, message=None):
    """
    Create a JSON response dictionary.

    Args:
        result: "success" or error code
        enabled_sites: Optional list of enabled sites (for success responses)
        message: Optional error message with additional details

    Returns:
        dict: Response dictionary
    """
    response = {"result": result}
    if enabled_sites is not None:
        response["enabled_sites"] = enabled_sites
    if message is not None:
        response["message"] = message
    return response


def output_json(response):
    """Print JSON response to stdout."""
    print(json.dumps(response))


def validate_domain_ownership(username, domain):
    """
    Validate that a domain belongs to the specified user.

    Args:
        username: The username to check ownership for
        domain: The domain to validate

    Returns:
        tuple: (is_valid, error_code)
            is_valid: True if domain belongs to user
            error_code: Error code if validation fails, None otherwise
    """
    try:
        owner = domain_owner(domain)
        if owner is None:
            return False, ErrorCodes.DOMAIN_NOT_FOUND
        if owner != username:
            return False, ErrorCodes.DOMAIN_NOT_FOUND
        return True, None
    except NoDomain:
        return False, ErrorCodes.DOMAIN_NOT_FOUND
    except Exception:
        return False, ErrorCodes.INTERNAL_ERROR


def get_validated_user():
    """
    Get the calling user and validate they exist.

    Returns:
        tuple: (username, error_code)
            username: The validated username, or None if validation failed
            error_code: Error code if validation failed, or None if successful
    """
    username = get_calling_user()
    if not username:
        logger.error("User not found")
        return None, ErrorCodes.USER_NOT_FOUND

    if not user_exists(username):
        logger.error("User %s does not exist", username)
        return None, ErrorCodes.USER_NOT_FOUND

    return username, None


def validate_domain_for_user(username, domain):
    """
    Validate domain argument and ownership for a user.

    Args:
        username: The username to check ownership for
        domain: The domain to validate

    Returns:
        tuple: (is_valid, error_code)
            is_valid: True if domain is valid and belongs to user
            error_code: Error code if validation failed, None otherwise
    """
    if not domain:
        logger.error("Missing domain argument")
        return False, ErrorCodes.MISSING_DOMAIN

    is_valid, error_code = validate_domain_ownership(username, domain)
    if not is_valid:
        logger.error("Domain validation failed: user=%s, domain=%s, error=%s",
                     username, domain, error_code)
        return False, error_code

    return True, None


def parse_domains(domain_arg):
    """
    Parse comma-separated domain argument into a list of domains.

    Args:
        domain_arg: Comma-separated domain string (e.g., "domain1.com,domain2.com")

    Returns:
        list: List of domain names, with whitespace stripped
    """
    if not domain_arg:
        return []
    return [d.strip() for d in domain_arg.split(",") if d.strip()]


def cmd_site_isolation_enable(args):
    """Handle site-isolation-enable command."""
    domains = parse_domains(args.domain)
    logger.info("site-isolation-enable called: domains=%s", domains)

    if not domains:
        logger.error("No domains specified")
        output_json(json_response(ErrorCodes.MISSING_DOMAIN))
        return 1

    username, error = get_validated_user()
    if error:
        output_json(json_response(error))
        return 1

    if not is_website_isolation_allowed_server_wide():
        logger.error("Site isolation not allowed server-wide")
        output_json(json_response(ErrorCodes.SITE_ISOLATION_NOT_ALLOWED))
        return 1

    if not is_website_isolation_allowed_for_user(username):
        logger.error("Site isolation not allowed for user %s", username)
        output_json(json_response(ErrorCodes.SITE_ISOLATION_NOT_ALLOWED))
        return 1

    # Validate all domains first
    for domain in domains:
        is_valid, error = validate_domain_for_user(username, domain)
        if not is_valid:
            output_json(json_response(error))
            return 1

    try:
        for domain in domains:
            enable_website_isolation(username, domain)
        enabled_sites = get_websites_with_enabled_isolation(username)
        logger.info("Site isolation enabled: user=%s, domains=%s, enabled_sites=%s",
                    username, domains, enabled_sites)
        output_json(json_response("success", enabled_sites))
        return 0
    except Exception as e:
        logger.exception("Failed to enable site isolation: user=%s, domains=%s, error=%s",
                         username, domains, e)
        output_json(json_response(ErrorCodes.INTERNAL_ERROR, message=str(e)))
        return 1


def cmd_site_isolation_disable(args):
    """Handle site-isolation-disable command."""
    domains = parse_domains(args.domain)
    logger.info("site-isolation-disable called: domains=%s", domains)

    if not domains:
        logger.error("No domains specified")
        output_json(json_response(ErrorCodes.MISSING_DOMAIN))
        return 1

    username, error = get_validated_user()
    if error:
        output_json(json_response(error))
        return 1

    # Validate all domains first
    for domain in domains:
        is_valid, error = validate_domain_for_user(username, domain)
        if not is_valid:
            output_json(json_response(error))
            return 1

    try:
        for domain in domains:
            disable_website_isolation(username, domain)
        enabled_sites = get_websites_with_enabled_isolation(username)
        logger.info("Site isolation disabled: user=%s, domains=%s, enabled_sites=%s",
                    username, domains, enabled_sites)
        output_json(json_response("success", enabled_sites))
        return 0
    except Exception as e:
        logger.exception("Failed to disable site isolation: user=%s, domains=%s, error=%s",
                         username, domains, e)
        output_json(json_response(ErrorCodes.INTERNAL_ERROR, message=str(e)))
        return 1


def cmd_site_isolation_list(args):
    """Handle site-isolation-list command."""
    logger.info("site-isolation-list called")

    username, error = get_validated_user()
    if error:
        output_json(json_response(error))
        return 1

    try:
        enabled_sites = get_websites_with_enabled_isolation(username)
        logger.info("Site isolation list: user=%s, enabled_sites=%s", username, enabled_sites)
        output_json(json_response("success", enabled_sites))
        return 0
    except Exception as e:
        logger.exception("Failed to list site isolation: user=%s, error=%s", username, e)
        output_json(json_response(ErrorCodes.INTERNAL_ERROR, message=str(e)))
        return 1


def create_parser():
    """Create argument parser for cagefsctl-user."""
    parser = argparse.ArgumentParser(
        prog="cagefsctl-user",
        description="User-level CLI utility for managing website isolation.",
    )

    subparsers = parser.add_subparsers(
        title="commands",
        dest="command",
        help="Available commands",
    )

    # site-isolation-enable command
    enable_parser = subparsers.add_parser(
        "site-isolation-enable",
        help="Enable site isolation for domain(s)",
    )
    enable_parser.add_argument(
        "--domain",
        required=True,
        help="Domain name(s) to enable site isolation for (comma-separated)",
    )
    enable_parser.set_defaults(func=cmd_site_isolation_enable)

    # site-isolation-disable command
    disable_parser = subparsers.add_parser(
        "site-isolation-disable",
        help="Disable site isolation for domain(s)",
    )
    disable_parser.add_argument(
        "--domain",
        required=True,
        help="Domain name(s) to disable site isolation for (comma-separated)",
    )
    disable_parser.set_defaults(func=cmd_site_isolation_disable)

    # site-isolation-list command
    list_parser = subparsers.add_parser(
        "site-isolation-list",
        help="List domains with site isolation enabled",
    )
    list_parser.set_defaults(func=cmd_site_isolation_list)

    return parser


def main(argv=None):
    """Main entry point."""
    parser = create_parser()
    args = parser.parse_args(argv)

    # Guard: do not allow running as root unless via proxyexec
    # When running via proxyexec, PROXYEXEC_UID is set
    if os.getuid() == 0 and not is_running_via_proxyexec():
        logger.error("Direct root invocation not allowed")
        output_json(json_response(ErrorCodes.ROOT_NOT_ALLOWED))
        return 1

    # If running as user (not root via proxyexec)
    if os.getuid() != 0:
        if not in_cagefs():
            print("This utility is only available inside CageFS.\n"
                  "Please run it via: cagefs_enter cagefsctl-user <command>",
                  file=sys.stderr)
            return 1

        # Inside CageFS - call via proxyexec to get root privileges
        if not args.command:
            parser.print_help()
            return 1

        # Build args list for proxyexec
        args_list = sys.argv[1:]  # Pass all original args
        result = call_via_proxyexec(PROXYEXEC_ALIAS, args_list)
        if result is None:
            output_json(json_response(
                ErrorCodes.INTERNAL_ERROR,
                message="Failed to execute via proxyexec"
            ))
            return 1
        return result

    # Running as root via proxyexec - execute the command
    if not hasattr(args, "func"):
        parser.print_help()
        return 1

    return args.func(args)


if __name__ == "__main__":
    sys.exit(main())