PNG  IHDRX cHRMz&u0`:pQ<bKGD pHYsodtIME MeqIDATxw]Wug^Qd˶ 6`!N:!@xI~)%7%@Bh&`lnjVF29gΨ4E$|>cɚ{gk= %,a KX%,a KX%,a KX%,a KX%,a KX%,a KX%, b` ǟzeאfp]<!SJmɤY޲ڿ,%c ~ع9VH.!Ͳz&QynֺTkRR.BLHi٪:l;@(!MԴ=žI,:o&N'Kù\vRmJ雵֫AWic H@" !: Cé||]k-Ha oݜ:y F())u]aG7*JV@J415p=sZH!=!DRʯvɱh~V\}v/GKY$n]"X"}t@ xS76^[bw4dsce)2dU0 CkMa-U5tvLƀ~mlMwfGE/-]7XAƟ`׮g ewxwC4\[~7@O-Q( a*XGƒ{ ՟}$_y3tĐƤatgvێi|K=uVyrŲlLӪuܿzwk$m87k( `múcE)"@rK( z4$D; 2kW=Xb$V[Ru819קR~qloѱDyįݎ*mxw]y5e4K@ЃI0A D@"BDk_)N\8͜9dz"fK0zɿvM /.:2O{ Nb=M=7>??Zuo32 DLD@D| &+֎C #B8ַ`bOb $D#ͮҪtx]%`ES`Ru[=¾!@Od37LJ0!OIR4m]GZRJu$‡c=%~s@6SKy?CeIh:[vR@Lh | (BhAMy=݃  G"'wzn޺~8ԽSh ~T*A:xR[ܹ?X[uKL_=fDȊ؂p0}7=D$Ekq!/t.*2ʼnDbŞ}DijYaȲ(""6HA;:LzxQ‘(SQQ}*PL*fc\s `/d'QXW, e`#kPGZuŞuO{{wm[&NBTiiI0bukcA9<4@SӊH*؎4U/'2U5.(9JuDfrޱtycU%j(:RUbArLֺN)udA':uGQN"-"Is.*+k@ `Ojs@yU/ H:l;@yyTn}_yw!VkRJ4P)~y#)r,D =ě"Q]ci'%HI4ZL0"MJy 8A{ aN<8D"1#IJi >XjX֔#@>-{vN!8tRݻ^)N_╗FJEk]CT՟ YP:_|H1@ CBk]yKYp|og?*dGvzنzӴzjֺNkC~AbZƷ`.H)=!QͷVTT(| u78y֮}|[8-Vjp%2JPk[}ԉaH8Wpqhwr:vWª<}l77_~{s۴V+RCģ%WRZ\AqHifɤL36: #F:p]Bq/z{0CU6ݳEv_^k7'>sq*+kH%a`0ԣisqにtү04gVgW΂iJiS'3w.w}l6MC2uԯ|>JF5`fV5m`Y**Db1FKNttu]4ccsQNnex/87+}xaUW9y>ͯ骵G{䩓Գ3+vU}~jJ.NFRD7<aJDB1#ҳgSb,+CS?/ VG J?|?,2#M9}B)MiE+G`-wo߫V`fio(}S^4e~V4bHOYb"b#E)dda:'?}׮4繏`{7Z"uny-?ǹ;0MKx{:_pÚmFמ:F " .LFQLG)Q8qN q¯¯3wOvxDb\. BKD9_NN &L:4D{mm o^tֽ:q!ƥ}K+<"m78N< ywsard5+вz~mnG)=}lYݧNj'QJS{S :UYS-952?&O-:W}(!6Mk4+>A>j+i|<<|;ر^߉=HE|V#F)Emm#}/"y GII웻Jі94+v뾧xu~5C95~ūH>c@덉pʃ1/4-A2G%7>m;–Y,cyyaln" ?ƻ!ʪ<{~h~i y.zZB̃/,雋SiC/JFMmBH&&FAbϓO^tubbb_hZ{_QZ-sύodFgO(6]TJA˯#`۶ɟ( %$&+V'~hiYy>922 Wp74Zkq+Ovn錄c>8~GqܲcWꂎz@"1A.}T)uiW4="jJ2W7mU/N0gcqܗOO}?9/wìXžΏ0 >֩(V^Rh32!Hj5`;O28؇2#ݕf3 ?sJd8NJ@7O0 b־?lldщ̡&|9C.8RTWwxWy46ah嘦mh٤&l zCy!PY?: CJyв]dm4ǜҐR޻RլhX{FƯanшQI@x' ao(kUUuxW_Ñ줮[w8 FRJ(8˼)_mQ _!RJhm=!cVmm ?sFOnll6Qk}alY}; "baӌ~M0w,Ggw2W:G/k2%R,_=u`WU R.9T"v,<\Ik޽/2110Ӿxc0gyC&Ny޽JҢrV6N ``یeA16"J³+Rj*;BϜkZPJaÍ<Jyw:NP8/D$ 011z֊Ⱳ3ι֘k1V_"h!JPIΣ'ɜ* aEAd:ݺ>y<}Lp&PlRfTb1]o .2EW\ͮ]38؋rTJsǏP@芎sF\> P^+dYJLbJ C-xϐn> ι$nj,;Ǖa FU *择|h ~izť3ᤓ`K'-f tL7JK+vf2)V'-sFuB4i+m+@My=O҈0"|Yxoj,3]:cо3 $#uŘ%Y"y죯LebqtҢVzq¼X)~>4L׶m~[1_k?kxֺQ`\ |ٛY4Ѯr!)N9{56(iNq}O()Em]=F&u?$HypWUeB\k]JɩSع9 Zqg4ZĊo oMcjZBU]B\TUd34ݝ~:7ڶSUsB0Z3srx 7`:5xcx !qZA!;%͚7&P H<WL!džOb5kF)xor^aujƍ7 Ǡ8/p^(L>ὴ-B,{ۇWzֺ^k]3\EE@7>lYBȝR.oHnXO/}sB|.i@ɥDB4tcm,@ӣgdtJ!lH$_vN166L__'Z)y&kH;:,Y7=J 9cG) V\hjiE;gya~%ks_nC~Er er)muuMg2;֫R)Md) ,¶ 2-wr#F7<-BBn~_(o=KO㭇[Xv eN_SMgSҐ BS헃D%g_N:/pe -wkG*9yYSZS.9cREL !k}<4_Xs#FmҶ:7R$i,fi!~' # !6/S6y@kZkZcX)%5V4P]VGYq%H1!;e1MV<!ϐHO021Dp= HMs~~a)ަu7G^];git!Frl]H/L$=AeUvZE4P\.,xi {-~p?2b#amXAHq)MWǾI_r`S Hz&|{ +ʖ_= (YS(_g0a03M`I&'9vl?MM+m~}*xT۲(fY*V4x@29s{DaY"toGNTO+xCAO~4Ϳ;p`Ѫ:>Ҵ7K 3}+0 387x\)a"/E>qpWB=1 ¨"MP(\xp߫́A3+J] n[ʼnӼaTbZUWb={~2ooKױӰp(CS\S筐R*JغV&&"FA}J>G֐p1ٸbk7 ŘH$JoN <8s^yk_[;gy-;߉DV{c B yce% aJhDȶ 2IdйIB/^n0tNtџdcKj4϶v~- CBcgqx9= PJ) dMsjpYB] GD4RDWX +h{y`,3ꊕ$`zj*N^TP4L:Iz9~6s) Ga:?y*J~?OrMwP\](21sZUD ?ܟQ5Q%ggW6QdO+\@ ̪X'GxN @'4=ˋ+*VwN ne_|(/BDfj5(Dq<*tNt1х!MV.C0 32b#?n0pzj#!38}޴o1KovCJ`8ŗ_"]] rDUy޲@ Ȗ-;xџ'^Y`zEd?0„ DAL18IS]VGq\4o !swV7ˣι%4FѮ~}6)OgS[~Q vcYbL!wG3 7띸*E Pql8=jT\꘿I(z<[6OrR8ºC~ډ]=rNl[g|v TMTղb-o}OrP^Q]<98S¤!k)G(Vkwyqyr޽Nv`N/e p/~NAOk \I:G6]4+K;j$R:Mi #*[AȚT,ʰ,;N{HZTGMoּy) ]%dHء9Պ䠬|<45,\=[bƟ8QXeB3- &dҩ^{>/86bXmZ]]yޚN[(WAHL$YAgDKp=5GHjU&99v簪C0vygln*P)9^͞}lMuiH!̍#DoRBn9l@ xA/_v=ȺT{7Yt2N"4!YN`ae >Q<XMydEB`VU}u]嫇.%e^ánE87Mu\t`cP=AD/G)sI"@MP;)]%fH9'FNsj1pVhY&9=0pfuJ&gޤx+k:!r˭wkl03׼Ku C &ѓYt{.O.zҏ z}/tf_wEp2gvX)GN#I ݭ߽v/ .& и(ZF{e"=V!{zW`, ]+LGz"(UJp|j( #V4, 8B 0 9OkRrlɱl94)'VH9=9W|>PS['G(*I1==C<5"Pg+x'K5EMd؞Af8lG ?D FtoB[je?{k3zQ vZ;%Ɠ,]E>KZ+T/ EJxOZ1i #T<@ I}q9/t'zi(EMqw`mYkU6;[t4DPeckeM;H}_g pMww}k6#H㶏+b8雡Sxp)&C $@'b,fPߑt$RbJ'vznuS ~8='72_`{q纶|Q)Xk}cPz9p7O:'|G~8wx(a 0QCko|0ASD>Ip=4Q, d|F8RcU"/KM opKle M3#i0c%<7׿p&pZq[TR"BpqauIp$ 8~Ĩ!8Սx\ւdT>>Z40ks7 z2IQ}ItԀ<-%S⍤};zIb$I 5K}Q͙D8UguWE$Jh )cu4N tZl+[]M4k8֦Zeq֮M7uIqG 1==tLtR,ƜSrHYt&QP윯Lg' I,3@P'}'R˪e/%-Auv·ñ\> vDJzlӾNv5:|K/Jb6KI9)Zh*ZAi`?S {aiVDԲuy5W7pWeQJk֤#5&V<̺@/GH?^τZL|IJNvI:'P=Ϛt"¨=cud S Q.Ki0 !cJy;LJR;G{BJy޺[^8fK6)=yʊ+(k|&xQ2`L?Ȓ2@Mf 0C`6-%pKpm')c$׻K5[J*U[/#hH!6acB JA _|uMvDyk y)6OPYjœ50VT K}cǻP[ $:]4MEA.y)|B)cf-A?(e|lɉ#P9V)[9t.EiQPDѠ3ϴ;E:+Օ t ȥ~|_N2,ZJLt4! %ա]u {+=p.GhNcŞQI?Nd'yeh n7zi1DB)1S | S#ًZs2|Ɛy$F SxeX{7Vl.Src3E℃Q>b6G ўYCmtկ~=K0f(=LrAS GN'ɹ9<\!a`)֕y[uՍ[09` 9 +57ts6}b4{oqd+J5fa/,97J#6yν99mRWxJyѡyu_TJc`~W>l^q#Ts#2"nD1%fS)FU w{ܯ R{ ˎ󅃏џDsZSQS;LV;7 Od1&1n$ N /.q3~eNɪ]E#oM~}v֯FڦwyZ=<<>Xo稯lfMFV6p02|*=tV!c~]fa5Y^Q_WN|Vs 0ҘދU97OI'N2'8N֭fgg-}V%y]U4 峧p*91#9U kCac_AFңĪy뚇Y_AiuYyTTYЗ-(!JFLt›17uTozc. S;7A&&<ԋ5y;Ro+:' *eYJkWR[@F %SHWP 72k4 qLd'J "zB6{AC0ƁA6U.'F3:Ȅ(9ΜL;D]m8ڥ9}dU "v!;*13Rg^fJyShyy5auA?ɩGHRjo^]׽S)Fm\toy 4WQS@mE#%5ʈfFYDX ~D5Ϡ9tE9So_aU4?Ѽm%&c{n>.KW1Tlb}:j uGi(JgcYj0qn+>) %\!4{LaJso d||u//P_y7iRJ߬nHOy) l+@$($VFIQ9%EeKʈU. ia&FY̒mZ=)+qqoQn >L!qCiDB;Y<%} OgBxB!ØuG)WG9y(Ą{_yesuZmZZey'Wg#C~1Cev@0D $a@˲(.._GimA:uyw֬%;@!JkQVM_Ow:P.s\)ot- ˹"`B,e CRtaEUP<0'}r3[>?G8xU~Nqu;Wm8\RIkբ^5@k+5(By'L&'gBJ3ݶ!/㮻w҅ yqPWUg<e"Qy*167΃sJ\oz]T*UQ<\FԎ`HaNmڜ6DysCask8wP8y9``GJ9lF\G g's Nn͵MLN֪u$| /|7=]O)6s !ĴAKh]q_ap $HH'\1jB^s\|- W1:=6lJBqjY^LsPk""`]w)󭃈,(HC ?䔨Y$Sʣ{4Z+0NvQkhol6C.婧/u]FwiVjZka&%6\F*Ny#8O,22+|Db~d ~Çwc N:FuuCe&oZ(l;@ee-+Wn`44AMK➝2BRՈt7g*1gph9N) *"TF*R(#'88pm=}X]u[i7bEc|\~EMn}P瘊J)K.0i1M6=7'_\kaZ(Th{K*GJyytw"IO-PWJk)..axӝ47"89Cc7ĐBiZx 7m!fy|ϿF9CbȩV 9V-՛^pV̌ɄS#Bv4-@]Vxt-Z, &ֺ*diؠ2^VXbs֔Ìl.jQ]Y[47gj=幽ex)A0ip׳ W2[ᎇhuE^~q흙L} #-b۸oFJ_QP3r6jr+"nfzRJTUqoaۍ /$d8Mx'ݓ= OՃ| )$2mcM*cЙj}f };n YG w0Ia!1Q.oYfr]DyISaP}"dIӗթO67jqR ҊƐƈaɤGG|h;t]䗖oSv|iZqX)oalv;۩meEJ\!8=$4QU4Xo&VEĊ YS^E#d,yX_> ۘ-e\ "Wa6uLĜZi`aD9.% w~mB(02G[6y.773a7 /=o7D)$Z 66 $bY^\CuP. (x'"J60׿Y:Oi;F{w佩b+\Yi`TDWa~|VH)8q/=9!g߆2Y)?ND)%?Ǐ`k/sn:;O299yB=a[Ng 3˲N}vLNy;*?x?~L&=xyӴ~}q{qE*IQ^^ͧvü{Huu=R|>JyUlZV, B~/YF!Y\u_ݼF{_C)LD]m {H 0ihhadd nUkf3oٺCvE\)QJi+֥@tDJkB$1!Đr0XQ|q?d2) Ӣ_}qv-< FŊ߫%roppVBwü~JidY4:}L6M7f٬F "?71<2#?Jyy4뷢<_a7_=Q E=S1И/9{+93֮E{ǂw{))?maÆm(uLE#lïZ  ~d];+]h j?!|$F}*"4(v'8s<ŏUkm7^7no1w2ؗ}TrͿEk>p'8OB7d7R(A 9.*Mi^ͳ; eeUwS+C)uO@ =Sy]` }l8^ZzRXj[^iUɺ$tj))<sbDJfg=Pk_{xaKo1:-uyG0M ԃ\0Lvuy'ȱc2Ji AdyVgVh!{]/&}}ċJ#%d !+87<;qN޼Nفl|1N:8ya  8}k¾+-$4FiZYÔXk*I&'@iI99)HSh4+2G:tGhS^繿 Kتm0 вDk}֚+QT4;sC}rՅE,8CX-e~>G&'9xpW,%Fh,Ry56Y–hW-(v_,? ; qrBk4-V7HQ;ˇ^Gv1JVV%,ik;D_W!))+BoS4QsTM;gt+ndS-~:11Sgv!0qRVh!"Ȋ(̦Yl.]PQWgٳE'`%W1{ndΗBk|Ž7ʒR~,lnoa&:ü$ 3<a[CBݮwt"o\ePJ=Hz"_c^Z.#ˆ*x z̝grY]tdkP*:97YľXyBkD4N.C_[;F9`8& !AMO c `@BA& Ost\-\NX+Xp < !bj3C&QL+*&kAQ=04}cC!9~820G'PC9xa!w&bo_1 Sw"ܱ V )Yl3+ס2KoXOx]"`^WOy :3GO0g;%Yv㐫(R/r (s } u B &FeYZh0y> =2<Ϟc/ -u= c&׭,.0"g"7 6T!vl#sc>{u/Oh Bᾈ)۴74]x7 gMӒ"d]U)}" v4co[ ɡs 5Gg=XR14?5A}D "b{0$L .\4y{_fe:kVS\\O]c^W52LSBDM! C3Dhr̦RtArx4&agaN3Cf<Ԉp4~ B'"1@.b_/xQ} _߃҉/gٓ2Qkqp0շpZ2fԫYz< 4L.Cyυι1t@鎫Fe sYfsF}^ V}N<_`p)alٶ "(XEAVZ<)2},:Ir*#m_YӼ R%a||EƼIJ,,+f"96r/}0jE/)s)cjW#w'Sʯ5<66lj$a~3Kʛy 2:cZ:Yh))+a߭K::N,Q F'qB]={.]h85C9cr=}*rk?vwV렵ٸW Rs%}rNAkDv|uFLBkWY YkX מ|)1!$#3%y?pF<@<Rr0}: }\J [5FRxY<9"SQdE(Q*Qʻ)q1E0B_O24[U'],lOb ]~WjHޏTQ5Syu wq)xnw8~)c 쫬gٲߠ H% k5dƝk> kEj,0% b"vi2Wس_CuK)K{n|>t{P1򨾜j>'kEkƗBg*H%'_aY6Bn!TL&ɌOb{c`'d^{t\i^[uɐ[}q0lM˕G:‚4kb祔c^:?bpg… +37stH:0}en6x˟%/<]BL&* 5&fK9Mq)/iyqtA%kUe[ڛKN]Ě^,"`/ s[EQQm?|XJ߅92m]G.E΃ח U*Cn.j_)Tѧj̿30ڇ!A0=͜ar I3$C^-9#|pk!)?7.x9 @OO;WƝZBFU keZ75F6Tc6"ZȚs2y/1 ʵ:u4xa`C>6Rb/Yм)^=+~uRd`/|_8xbB0?Ft||Z\##|K 0>>zxv8۴吅q 8ĥ)"6>~\8:qM}#͚'ĉ#p\׶ l#bA?)|g g9|8jP(cr,BwV (WliVxxᡁ@0Okn;ɥh$_ckCgriv}>=wGzβ KkBɛ[˪ !J)h&k2%07δt}!d<9;I&0wV/ v 0<H}L&8ob%Hi|޶o&h1L|u֦y~󛱢8fٲUsւ)0oiFx2}X[zVYr_;N(w]_4B@OanC?gĦx>мgx>ΛToZoOMp>40>V Oy V9iq!4 LN,ˢu{jsz]|"R޻&'ƚ{53ўFu(<٪9:΋]B;)B>1::8;~)Yt|0(pw2N%&X,URBK)3\zz&}ax4;ǟ(tLNg{N|Ǽ\G#C9g$^\}p?556]/RP.90 k,U8/u776s ʪ_01چ|\N 0VV*3H鴃J7iI!wG_^ypl}r*jɤSR 5QN@ iZ#1ٰy;_\3\BQQ x:WJv츟ٯ$"@6 S#qe딇(/P( Dy~TOϻ<4:-+F`0||;Xl-"uw$Цi󼕝mKʩorz"mϺ$F:~E'ҐvD\y?Rr8_He@ e~O,T.(ފR*cY^m|cVR[8 JҡSm!ΆԨb)RHG{?MpqrmN>߶Y)\p,d#xۆWY*,l6]v0h15M˙MS8+EdI='LBJIH7_9{Caз*Lq,dt >+~ّeʏ?xԕ4bBAŚjﵫ!'\Ը$WNvKO}ӽmSşذqsOy?\[,d@'73'j%kOe`1.g2"e =YIzS2|zŐƄa\U,dP;jhhhaxǶ?КZ՚.q SE+XrbOu%\GتX(H,N^~]JyEZQKceTQ]VGYqnah;y$cQahT&QPZ*iZ8UQQM.qo/T\7X"u?Mttl2Xq(IoW{R^ ux*SYJ! 4S.Jy~ BROS[V|žKNɛP(L6V^|cR7i7nZW1Fd@ Ara{詑|(T*dN]Ko?s=@ |_EvF]׍kR)eBJc" MUUbY6`~V޴dJKß&~'d3i WWWWWW
Current Directory: /opt/support/venv/lib/python3.13/site-packages/rads
Viewing File: /opt/support/venv/lib/python3.13/site-packages/rads/_users.py
"""Functions for fetching basic info on user accounts""" from pathlib import Path import pwd import grp import re import os import tarfile from typing import Literal, overload import yaml from ._yaml import DumbYamlLoader from . import SYS_USERS, STAFF_GROUPS, OUR_RESELLERS class CpuserError(Exception): """Raised when there's something wrong collecting cPanel user info""" __module__ = 'rads' def get_login() -> str: """Obtain which user ran this script Returns: username """ try: blame = os.getlogin() except OSError: blame = pwd.getpwuid(os.geteuid()).pw_name return blame get_login.__module__ = 'rads' def is_cwpuser(user: str) -> bool: """Checks if the user is a valid CWP user. Args: user (str): CWP username to check Returns: bool: whether or not the CWP user exists and is valid """ try: homedir = pwd.getpwnam(user).pw_dir except KeyError: return False return all( ( os.path.isdir(homedir), os.path.isfile(os.path.join( '/usr/local/cwpsrv/conf.d/users/', f'{user}.conf' )) ) ) is_cwpuser.__module__ = 'rads' def is_cpuser(user: str) -> bool: """Checks if a user is a valid cPanel user. Warning: This only checks if the user exists and will also be true for restricted cPanel users. Use ``cpuser_safe`` instead if you need to check for those Args: user: cPanel username to check Returns: Whether the cPanel user exists """ try: homedir = pwd.getpwnam(user).pw_dir except KeyError: return False return all( ( os.path.isdir(homedir), os.path.isfile(os.path.join('/var/cpanel/users', user)), os.path.isdir(os.path.join('/var/cpanel/userdata', user)), ) ) is_cpuser.__module__ = 'rads' @overload def all_cpusers(owners: Literal[False] = False) -> list[str]: ... @overload def all_cpusers(owners: Literal[True] = True) -> dict[str, str]: ... def all_cpusers(owners: bool = False) -> dict[str, str] | list[str]: """Returns cPanel users from /etc/trueuserowners Args: owners: whether to return users as a dict with owners as the values Raises: CpuserError: if /etc/trueuserowners is invalid Returns: either a list of all users, or a dict of users (keys) to owners (vals) """ with open('/etc/trueuserowners', encoding='utf-8') as userowners: userdict = yaml.load(userowners, DumbYamlLoader) if not isinstance(userdict, dict): raise CpuserError('/etc/trueuserowners is invalid') if owners: return userdict return list(userdict.keys()) all_cpusers.__module__ = 'rads' def main_cpusers() -> list: """Get a all non-child, non-system, "main" cPanel users Raises: CpuserError: if /etc/trueuserowners is invalid""" return [ user for user, owner in all_cpusers(owners=True).items() if owner in OUR_RESELLERS or owner == user ] main_cpusers.__module__ = 'rads' def get_owner(user: str) -> str: """Get a user's owner (even if the account has reseller ownership of itself) Warning: the owner may be root, which is not a cPanel user Hint: If looking this up for multiple users, use ``get_cpusers(owners=True)`` instead Args: user: cPanel username to find the owner for Raises: CpuserError: if /etc/trueuserowners is invalid or the requested user is not defined in there """ try: return all_cpusers(owners=True)[user] except KeyError as exc: raise CpuserError(f'{user} is not in /etc/trueuserowners') from exc get_owner.__module__ = 'rads' def is_child(user: str) -> bool: """Check if a cPanel user is not self-owned and not owned by a system user Args: user: cPanel username to check Raises: CpuserError: if /etc/trueuserowners is invalid or the requested user is not defined in there """ owner = get_owner(user) return owner not in OUR_RESELLERS and owner != user is_child.__module__ = 'rads' def get_children(owner: str) -> list[str]: """Get a list of child accounts for a reseller Args: owner: cPanel username to lookup Returns: all child accounts of a reseller, excluding itself Raises: CpuserError: if /etc/trueuserowners is invalid """ return [ usr for usr, own in all_cpusers(owners=True).items() if own == owner and usr != own ] get_children.__module__ = 'rads' def cpuser_safe(user: str) -> bool: """Checks whether the user is safe for support to operate on - The user exists and is a valid cPanel user - The user is not a reserved account - The user is not in a staff group Args: user: cPanel or CWP username to check """ # SYS_USERS includes SECURE_USER if user in SYS_USERS or user in OUR_RESELLERS: return False if not is_cpuser(user) and not is_cwpuser(user): return False for group in [x.gr_name for x in grp.getgrall() if user in x.gr_mem]: if group in STAFF_GROUPS: return False return True cpuser_safe.__module__ = 'rads' def cpuser_suspended(user: str) -> bool: """Check if a user is currently suspended Warning: This does not check for pending suspensions Args: user: cPanel username to check """ return os.path.exists(os.path.join('/var/cpanel/suspended', user)) cpuser_suspended.__module__ = 'rads' def get_homedir(user: str): """Get home directory path for a cPanel user Args: user: cPanel username to check Raises: CpuserError: if the user does not exist or the home directory path found does not match the expected pattern """ try: homedir = pwd.getpwnam(user).pw_dir except KeyError as exc: raise CpuserError(f'{user}: no such user') from exc if re.match(r'/home[0-9]*/\w+', homedir) is None: # Even though we fetched the homedir successfully from /etc/passwd, # treat this as an error due to unexpected output. If the result was # '/' for example, some calling programs might misbehave or even # rm -rf / depending on what it's being used for raise CpuserError(f'{user!r} does not match expected pattern') return homedir get_homedir.__module__ = 'rads' def get_primary_domain(user: str) -> str: """Get primary domain from cpanel userdata Args: user: cPanel username to check Raises: CpuserError: if cpanel userdata cannot be read or main_domain is missing """ userdata_path = os.path.join('/var/cpanel/userdata', user, 'main') try: with open(userdata_path, encoding='utf-8') as userdata_filehandle: return yaml.safe_load(userdata_filehandle)['main_domain'] except (yaml.YAMLError, KeyError, OSError) as exc: raise CpuserError(exc) from exc get_primary_domain.__module__ = 'rads' def whoowns(domain: str) -> str: """ Get the cPanel username that owns a domain Args: domain: Domain name to look up Returns: The name of a cPanel user that owns the domain name, or None on failure """ try: with open('/etc/userdomains', encoding='utf-8') as file: match = next(x for x in file if x.startswith(f'{domain}: ')) return match.rstrip().split(': ')[1] except (OSError, FileNotFoundError, StopIteration): return None whoowns.__module__ = 'rads' def get_plan(user: str) -> str | None: """ Retrieves the hosting plan name for a given cPanel user. This function reads the user's configuration file from /var/cpanel/users and extracts the value assigned to the PLAN variable, if present. Parameters: user (str): The cPanel username. Returns: Optional[str]: The plan name if found, otherwise None. """ path = Path(f"/var/cpanel/users/{user}") if not path.exists(): return None for line in path.read_text(encoding="utf-8").splitlines(): if line.startswith("PLAN="): return line.split("=", 1)[1].strip() return None get_plan.__module__ = 'rads' class UserData: """Object representing the data parsed from userdata Args: user: cPanel username to read cPanel userdata for. Required if pkgacct is not set. data_dir: override this to read /var/cpanel/userdata from some other directory, such as from a restored backup. Ignored if pkgacct is set all_subs: if True, list all subdomains, even those which have were created so an addon domain can be parked on them pkgacct: Don't set this manually. See UserData.from_pkgacct instead. tar: Don't set this manually. See UserData.from_pkgacct instead. Raises: CpuserError: if cPanel userdata is invalid Attributes: user (str): username primary (UserDomain): UserDomain object for the main domain addons (list): UserDomain objects for addon domains parked (list): UserDomain objects for parked domains subs (list): UserDomain objects for subdomains Hint: Use vars() to view this ``UserData`` object as a dict """ user: str primary: 'UserDomain' addons: list['UserDomain'] parked: list['UserDomain'] subs: list['UserDomain'] __module__ = 'rads' def __init__( self, user: str | None = None, data_dir: str = '/var/cpanel/userdata', all_subs: bool = False, pkgacct: str | None = None, tar: tarfile.TarFile | None = None, ): """Initializes a UserData object given a cPanel username""" self.pkgacct = pkgacct if user is None and pkgacct is None: raise TypeError("either user or pkgacct must be set") if user is not None and pkgacct is not None: raise TypeError("user cannot be set if pkgacct is set") if pkgacct is not None and tar is None: raise TypeError( "tar must be set if pkgacct is set; " "use the UserData.from_pkgacct alias instead" ) if pkgacct: filename = Path(pkgacct).name file_re = re.compile(r'(?:cpmove|pkgacct)-(.*).tar.gz$') if match := file_re.match(filename): self.user = match.group(1) else: raise CpuserError( f"{filename} does not follow the expected cpmove/pkgacct " "filename pattern." ) else: self.user = user main_data = self._read_userdata( user=self.user, data_dir=data_dir, pkgacct=pkgacct, domfile='main', required={ 'main_domain': str, 'addon_domains': dict, 'parked_domains': list, 'sub_domains': list, }, tar=tar, ) dom_data = self._read_userdata( user=self.user, domfile=main_data['main_domain'], required={'documentroot': str}, data_dir=data_dir, pkgacct=pkgacct, tar=tar, ) # populate primary domain self.primary = UserDomain( domain=main_data['main_domain'], has_ssl=dom_data['has_ssl'], docroot=dom_data['documentroot'], ) # populate addon domains self.addons = [] addon_subs = set() for addon, addon_file in main_data['addon_domains'].items(): addon_subs.add(addon_file) addon_data = self._read_userdata( user=self.user, domfile=addon_file, required={'documentroot': str}, data_dir=data_dir, pkgacct=pkgacct, tar=tar, ) self.addons.append( UserDomain( subdom=addon_file, domain=addon, has_ssl=addon_data['has_ssl'], docroot=addon_data['documentroot'], ) ) # populate parked domains self.parked = [] for parked in main_data['parked_domains']: self.parked.append( UserDomain( domain=parked, has_ssl=False, docroot=self.primary.docroot ) ) # populate subdomains self.subs = [] for sub in main_data['sub_domains']: if all_subs or sub not in addon_subs: sub_data = self._read_userdata( user=self.user, domfile=sub, required={'documentroot': str}, data_dir=data_dir, pkgacct=pkgacct, tar=tar, ) self.subs.append( UserDomain( domain=sub, has_ssl=sub_data['has_ssl'], docroot=sub_data['documentroot'], ) ) @staticmethod def from_pkgacct(path: str) -> 'UserData': """Alternate constructor to read userdata from a pkgacct/cpmove file""" try: with tarfile.open(path, 'r:gz') as tar: return UserData(pkgacct=path, tar=tar) except FileNotFoundError as exc: raise CpuserError(exc) from exc @classmethod def _read_userdata( cls, user: str, data_dir: str, pkgacct: dict | None, domfile: str, required: dict, tar: tarfile.TarFile | None, ): if pkgacct: return cls._read_from_pkgacct(pkgacct, domfile, required, tar) return cls._read_userdata_file(user, domfile, required, data_dir) @staticmethod def _tar_extract(tar: tarfile.TarFile, path: str): # docs say non-file members return None and missing files raise KeyError # This makes it return None in both error cases try: return tar.extractfile(path) except KeyError: return None @classmethod def _read_from_pkgacct( cls, tar_path: str, domfile: str, required: dict, tar: tarfile.TarFile ) -> dict: prefix = Path(tar_path).name[:-7] path = f"{prefix}/userdata/{domfile}" contents = cls._tar_extract(tar, path).read() has_ssl = cls._tar_extract(tar, f"{path}_SSL") is not None if not contents: raise CpuserError( f"{path} was not a file in the contents of {tar_path}" ) try: data = yaml.load(str(contents, 'utf-8'), Loader=yaml.SafeLoader) if not isinstance(data, dict): raise ValueError except ValueError as exc: raise CpuserError( f'{path} inside {tar_path} could not be parsed' ) from exc for key, req_type in required.items(): if key not in data: raise CpuserError(f'{path} is missing {key!r}') if not isinstance(data[key], req_type): raise CpuserError(f'{path} contains invalid data for {key!r}') data['has_ssl'] = has_ssl return data def __repr__(self): if self.pkgacct: return f'UserData(pkgacct={self.pkgacct!r})' return f'UserData({self.user!r})' @property def __dict__(self): return { 'user': self.user, 'primary': vars(self.primary), 'addons': [vars(x) for x in self.addons], 'parked': [vars(x) for x in self.parked], 'subs': [vars(x) for x in self.subs], } @property def all_roots(self) -> list[str]: """All site document roots (list)""" all_dirs = {self.primary.docroot} all_dirs.update([x.docroot for x in self.subs]) all_dirs.update([x.docroot for x in self.addons]) return list(all_dirs) @property def merged_roots(self) -> list[str]: """Merged, top-level document roots for a user (list)""" merged = [] for test_path in sorted(self.all_roots): head, tail = os.path.split(test_path) while head and tail: if head in merged: break head, tail = os.path.split(head) else: if test_path not in merged: merged.append(test_path) return merged @staticmethod def _read_userdata_file( user: str, domfile: str, required: dict, data_dir: str ) -> dict: """Internal helper function for UserData to strictly parse YAML files""" path = os.path.join(data_dir, user, domfile) try: with open(path, encoding='utf-8') as handle: data = yaml.load(handle, Loader=yaml.SafeLoader) if not isinstance(data, dict): raise ValueError except OSError as exc: raise CpuserError(f'{path} could not be opened') from exc except ValueError as exc: raise CpuserError(f'{path} could not be parsed') from exc for key, req_type in required.items(): if key not in data: raise CpuserError(f'{path} is missing {key!r}') if not isinstance(data[key], req_type): raise CpuserError(f'{path} contains invalid data for {key!r}') data['has_ssl'] = os.path.isfile(f'{path}_SSL') return data class UserDomain: """Object representing a cPanel domain in ``rads.UserData()`` Attributes: domain (str): domain name has_ssl (bool): True/False if the domain has ssl docroot (str): document root on the disk subdom (str|None): if this is an addon domain, this is the subdomain it's parked on which is also its config's filename Hint: vars() can be run on this object to convert it into a dict """ __module__ = 'rads' def __init__( self, domain: str, has_ssl: bool, docroot: str, subdom: str | None = None, ): self.domain = domain self.has_ssl = has_ssl self.docroot = docroot self.subdom = subdom def __repr__(self): if self.subdom: return ( f"UserDomain(domain={self.domain!r}, has_ssl={self.has_ssl!r}, " f"docroot={self.docroot!r}, subdom={self.subdom!r})" ) return ( f"UserDomain(domain={self.domain!r}, has_ssl={self.has_ssl!r}, " f"docroot={self.docroot!r})" ) @property def __dict__(self): myvars = {} for attr in ('domain', 'has_ssl', 'docroot'): myvars[attr] = getattr(self, attr) if self.subdom is not None: myvars['subdom'] = self.subdom return myvars