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: /usr/share/ruby/vendor_ruby/puppet/provider/user
Viewing File: /usr/share/ruby/vendor_ruby/puppet/provider/user/directoryservice.rb
require 'puppet' require 'facter/util/plist' require 'base64' Puppet::Type.type(:user).provide :directoryservice do desc "User management on OS X." ## ## ## Provider Settings ## ## ## # Provider command declarations commands :uuidgen => '/usr/bin/uuidgen' commands :dsimport => '/usr/bin/dsimport' commands :dscl => '/usr/bin/dscl' commands :plutil => '/usr/bin/plutil' commands :dscacheutil => '/usr/bin/dscacheutil' # Provider confines and defaults confine :operatingsystem => :darwin defaultfor :operatingsystem => :darwin # Need this to create getter/setter methods automagically # This command creates methods that return @property_hash[:value] mk_resource_methods # JJM: OS X can manage passwords. has_feature :manages_passwords # 10.8 Passwords use a PBKDF2 salt value has_features :manages_password_salt #provider can set the user's shell has_feature :manages_shell ## ## ## Class Methods ## ## ## # This method exists to map the dscl values to the correct Puppet # properties. This stays relatively consistent, but who knows what # Apple will do next year... def self.ds_to_ns_attribute_map { 'RecordName' => :name, 'PrimaryGroupID' => :gid, 'NFSHomeDirectory' => :home, 'UserShell' => :shell, 'UniqueID' => :uid, 'RealName' => :comment, 'Password' => :password, 'GeneratedUID' => :guid, 'IPAddress' => :ip_address, 'ENetAddress' => :en_address, 'GroupMembership' => :members, } end def self.ns_to_ds_attribute_map @ns_to_ds_attribute_map ||= ds_to_ns_attribute_map.invert end # Prefetching is necessary to use @property_hash inside any setter methods. # self.prefetch uses self.instances to gather an array of user instances # on the system, and then populates the @property_hash instance variable # with attribute data for the specific instance in question (i.e. it # gathers the 'is' values of the resource into the @property_hash instance # variable so you don't have to read from the system every time you need # to gather the 'is' values for a resource. The downside here is that # populating this instance variable for every resource on the system # takes time and front-loads your Puppet run. def self.prefetch(resources) instances.each do |prov| if resource = resources[prov.name] resource.provider = prov end end end # This method assembles an array of provider instances containing # information about every instance of the user type on the system (i.e. # every user and its attributes). The `puppet resource` command relies # on self.instances to gather an array of user instances in order to # display its output. def self.instances get_all_users.collect do |user| self.new(generate_attribute_hash(user)) end end # Return an array of hashes containing information about every user on # the system. def self.get_all_users Plist.parse_xml(dscl '-plist', '.', 'readall', '/Users') end # This method accepts an individual user plist, passed as a hash, and # strips the dsAttrTypeStandard: prefix that dscl adds for each key. # An attribute hash is assembled and returned from the properties # supported by the user type. def self.generate_attribute_hash(input_hash) attribute_hash = {} input_hash.keys.each do |key| ds_attribute = key.sub("dsAttrTypeStandard:", "") next unless ds_to_ns_attribute_map.keys.include?(ds_attribute) ds_value = input_hash[key] case ds_to_ns_attribute_map[ds_attribute] when :gid, :uid # OS X stores objects like uid/gid as strings. # Try casting to an integer for these cases to be # consistent with the other providers and the group type # validation begin ds_value = Integer(ds_value[0]) rescue ArgumentError ds_value = ds_value[0] end else ds_value = ds_value[0] end attribute_hash[ds_to_ns_attribute_map[ds_attribute]] = ds_value end attribute_hash[:ensure] = :present attribute_hash[:provider] = :directoryservice attribute_hash[:shadowhashdata] = get_attribute_from_dscl('Users', attribute_hash[:name], 'ShadowHashData') ############## # Get Groups # ############## groups_array = [] get_list_of_groups.each do |group| if group["dsAttrTypeStandard:GroupMembership"] and group["dsAttrTypeStandard:GroupMembership"].include?(attribute_hash[:name]) groups_array << group["dsAttrTypeStandard:RecordName"][0] end if group["dsAttrTypeStandard:GroupMembers"] and group["dsAttrTypeStandard:GroupMembers"].include?(attribute_hash[:guid]) groups_array << group["dsAttrTypeStandard:RecordName"][0] end end attribute_hash[:groups] = groups_array.uniq.sort.join(',') ################################ # Get Password/Salt/Iterations # ################################ if (Puppet::Util::Package.versioncmp(get_os_version, '10.7') == -1) attribute_hash[:password] = get_sha1(attribute_hash[:guid]) else if attribute_hash[:shadowhashdata].empty? attribute_hash[:password] = '*' else embedded_binary_plist = get_embedded_binary_plist(attribute_hash[:shadowhashdata]) if embedded_binary_plist['SALTED-SHA512'] attribute_hash[:password] = get_salted_sha512(embedded_binary_plist) else attribute_hash[:password] = get_salted_sha512_pbkdf2('entropy', embedded_binary_plist) attribute_hash[:salt] = get_salted_sha512_pbkdf2('salt', embedded_binary_plist) attribute_hash[:iterations] = get_salted_sha512_pbkdf2('iterations', embedded_binary_plist) end end end attribute_hash end def self.get_os_version @os_version ||= Facter.value(:macosx_productversion_major) end # Use dscl to retrieve an array of hashes containing attributes about all # of the local groups on the machine. def self.get_list_of_groups @groups ||= Plist.parse_xml(dscl '-plist', '.', 'readall', '/Groups') end # Perform a dscl lookup at the path specified for the specific keyname # value. The value returned is the first item within the array returned # from dscl def self.get_attribute_from_dscl(path, username, keyname) Plist.parse_xml(dscl '-plist', '.', 'read', "/#{path}/#{username}", keyname) end # The plist embedded in the ShadowHashData key is a binary plist. The # facter/util/plist library doesn't read binary plists, so we need to # extract the binary plist, convert it to XML, and return it. def self.get_embedded_binary_plist(shadow_hash_data) embedded_binary_plist = Array(shadow_hash_data['dsAttrTypeNative:ShadowHashData'][0].delete(' ')).pack('H*') convert_binary_to_xml(embedded_binary_plist) end # This method will accept a hash that has been returned from Plist::parse_xml # and convert it to a binary plist (string value). def self.convert_xml_to_binary(plist_data) Puppet.debug('Converting XML plist to binary') Puppet.debug('Executing: \'plutil -convert binary1 -o - -\'') IO.popen('plutil -convert binary1 -o - -', 'r+') do |io| io.write Plist::Emit.dump(plist_data) io.close_write @converted_plist = io.read end @converted_plist end # This method will accept a binary plist (as a string) and convert it to a # hash via Plist::parse_xml. def self.convert_binary_to_xml(plist_data) Puppet.debug('Converting binary plist to XML') Puppet.debug('Executing: \'plutil -convert xml1 -o - -\'') IO.popen('plutil -convert xml1 -o - -', 'r+') do |io| io.write plist_data io.close_write @converted_plist = io.read end Puppet.debug('Converting XML values to a hash.') Plist::parse_xml(@converted_plist) end # The salted-SHA512 password hash in 10.7 is stored in the 'SALTED-SHA512' # key as binary data. That data is extracted and converted to a hex string. def self.get_salted_sha512(embedded_binary_plist) embedded_binary_plist['SALTED-SHA512'].string.unpack("H*")[0] end # This method reads the passed embedded_binary_plist hash and returns values # according to which field is passed. Arguments passed are the hash # containing the value read from the 'ShadowHashData' key in the User's # plist, and the field to be read (one of 'entropy', 'salt', or 'iterations') def self.get_salted_sha512_pbkdf2(field, embedded_binary_plist) case field when 'salt', 'entropy' embedded_binary_plist['SALTED-SHA512-PBKDF2'][field].string.unpack('H*').first when 'iterations' Integer(embedded_binary_plist['SALTED-SHA512-PBKDF2'][field]) else raise Puppet::Error, 'Puppet has tried to read an incorrect value from the ' + "SALTED-SHA512-PBKDF2 hash. Acceptable fields are 'salt', " + "'entropy', or 'iterations'." end end # In versions 10.5 and 10.6 of OS X, the password hash is stored in a file # in the /var/db/shadow/hash directory that matches the GUID of the user. def self.get_sha1(guid) password_hash = nil password_hash_file = "#{password_hash_dir}/#{guid}" if Puppet::FileSystem.exist?(password_hash_file) and File.file?(password_hash_file) raise Puppet::Error, "Could not read password hash file at #{password_hash_file}" if not File.readable?(password_hash_file) f = File.new(password_hash_file) password_hash = f.read f.close end password_hash end ## ## ## Ensurable Methods ## ## ## def exists? begin dscl '.', 'read', "/Users/#{@resource.name}" rescue Puppet::ExecutionFailure => e Puppet.debug("User was not found, dscl returned: #{e.inspect}") return false end true end # This method is called if ensure => present is passed and the exists? # method returns false. Dscl will directly set most values, but the # setter methods will be used for any exceptions. def create create_new_user(@resource.name) # Retrieve the user's GUID @guid = self.class.get_attribute_from_dscl('Users', @resource.name, 'GeneratedUID')['dsAttrTypeStandard:GeneratedUID'][0] # Get an array of valid User type properties valid_properties = Puppet::Type.type('User').validproperties # Iterate through valid User type properties valid_properties.each do |attribute| next if attribute == :ensure value = @resource.should(attribute) # Value defaults if value.nil? value = case attribute when :gid '20' when :uid next_system_id when :comment @resource.name when :shell '/bin/bash' when :home "/Users/#{@resource.name}" else nil end end # Ensure group names are converted to integers. value = Puppet::Util.gid(value) if attribute == :gid ## Set values ## # For the :password and :groups properties, call the setter methods # to enforce those values. For everything else, use dscl with the # ns_to_ds_attribute_map to set the appropriate values. if value != "" and not value.nil? case attribute when :password self.password = value when :iterations self.iterations = value when :salt self.salt = value when :groups value.split(',').each do |group| merge_attribute_with_dscl('Groups', group, 'GroupMembership', @resource.name) merge_attribute_with_dscl('Groups', group, 'GroupMembers', @guid) end else merge_attribute_with_dscl('Users', @resource.name, self.class.ns_to_ds_attribute_map[attribute], value) end end end end # This method is called when ensure => absent has been set. # Deleting a user is handled by dscl def delete dscl '.', '-delete', "/Users/#{@resource.name}" end ## ## ## Getter/Setter Methods ## ## ## # In the setter method we're only going to take action on groups for which # the user is not currently a member. def groups=(value) guid = self.class.get_attribute_from_dscl('Users', @resource.name, 'GeneratedUID')['dsAttrTypeStandard:GeneratedUID'][0] groups_to_add = value.split(',') - groups.split(',') groups_to_add.each do |group| merge_attribute_with_dscl('Groups', group, 'GroupMembership', @resource.name) merge_attribute_with_dscl('Groups', group, 'GroupMembers', guid) end end # If you thought GETTING a password was bad, try SETTING it. This method # makes me want to cry. A thousand tears... # # I've been unsuccessful in tracking down a way to set the password for # a user using dscl that DOESN'T require passing it as plaintext. We were # also unable to get dsimport to work like this. Due to these downfalls, # the sanest method requires opening the user's plist, dropping in the # password hash, and serializing it back to disk. The problems with THIS # method revolve around dscl. Any time you directly modify a user's plist, # you need to flush the cache that dscl maintains. def password=(value) if (Puppet::Util::Package.versioncmp(self.class.get_os_version, '10.7') == -1) write_sha1_hash(value) else if self.class.get_os_version == '10.7' if value.length != 136 raise Puppet::Error, "OS X 10.7 requires a Salted SHA512 hash password of 136 characters. Please check your password and try again." end else if value.length != 256 raise Puppet::Error, "OS X versions > 10.7 require a Salted SHA512 PBKDF2 password hash of 256 characters. Please check your password and try again." end assert_full_pbkdf2_password end # Methods around setting the password on OS X are the ONLY methods that # cannot use dscl (because the only way to set it via dscl is by passing # a plaintext password - which is bad). Because of this, we have to change # the user's plist directly. DSCL has its own caching mechanism, which # means that every time we call dscl in this provider we're not directly # changing values on disk (instead, those calls are cached and written # to disk according to Apple's prioritization algorithms). When Puppet # needs to set the password property on OS X > 10.6, the provider has to # tell dscl to write its cache to disk before modifying the user's # plist. The 'dscacheutil -flushcache' command does this. Another issue # is how fast Puppet makes calls to dscl and how long it takes dscl to # enter those calls into its cache. We have to sleep for 2 seconds before # flushing the dscl cache to allow all dscl calls to get INTO the cache # first. This could be made faster (and avoid a sleep call) by finding # a way to enter calls into the dscl cache faster. A sleep time of 1 # second would intermittantly require a second Puppet run to set # properties, so 2 seconds seems to be the minimum working value. sleep 2 flush_dscl_cache write_password_to_users_plist(value) # Since we just modified the user's plist, we need to flush the ds cache # again so dscl can pick up on the changes we made. flush_dscl_cache end end # The iterations and salt properties, like the password property, can only # be modified by directly changing the user's plist. Because of this fact, # we have to treat the ds cache just like you would in the password= # method. def iterations=(value) if (Puppet::Util::Package.versioncmp(self.class.get_os_version, '10.7') > 0) assert_full_pbkdf2_password sleep 2 flush_dscl_cache users_plist = get_users_plist(@resource.name) shadow_hash_data = get_shadow_hash_data(users_plist) set_salted_pbkdf2(users_plist, shadow_hash_data, 'iterations', value) flush_dscl_cache end end # The iterations and salt properties, like the password property, can only # be modified by directly changing the user's plist. Because of this fact, # we have to treat the ds cache just like you would in the password= # method. def salt=(value) if (Puppet::Util::Package.versioncmp(self.class.get_os_version, '10.7') > 0) assert_full_pbkdf2_password sleep 2 flush_dscl_cache users_plist = get_users_plist(@resource.name) shadow_hash_data = get_shadow_hash_data(users_plist) set_salted_pbkdf2(users_plist, shadow_hash_data, 'salt', value) flush_dscl_cache end end ##### # Dynamically create setter methods for dscl properties ##### # # Setter methods are only called when a resource currently has a value for # that property and it needs changed (true here since all of these values # have a default that is set in the create method). We don't want to merge # in additional values if an incorrect value is set, we want to CHANGE it. # When using the -change argument in dscl, the old value needs to be passed # first (followed by the new value). Because of this, we get the current # value from the @property_hash variable and then use the value passed as # the new value. Because we're prefetching instances of the provider, it's # possible that the value determined at the start of the run may be stale # (i.e. someone changed the value by hand during a Puppet run) - if that's # the case we rescue the error from dscl and alert the user. # # In the event that the user doesn't HAVE a value for the attribute, the # provider should use the -merge option with dscl to add the attribute value # for the user record ['home', 'uid', 'gid', 'comment', 'shell'].each do |setter_method| define_method("#{setter_method}=") do |value| if @property_hash[setter_method.intern] begin dscl '.', '-change', "/Users/#{resource.name}", self.class.ns_to_ds_attribute_map[setter_method.intern], @property_hash[setter_method.intern], value rescue Puppet::ExecutionFailure => e raise Puppet::Error, "Cannot set the #{setter_method} value of '#{value}' for user " + "#{@resource.name} due to the following error: #{e.inspect}", e.backtrace end else begin dscl '.', '-merge', "/Users/#{resource.name}", self.class.ns_to_ds_attribute_map[setter_method.intern], value rescue Puppet::ExecutionFailure => e raise Puppet::Error, "Cannot set the #{setter_method} value of '#{value}' for user " + "#{@resource.name} due to the following error: #{e.inspect}", e.backtrace end end end end ## ## ## Helper Methods ## ## ## def assert_full_pbkdf2_password missing = [:password, :salt, :iterations].select { |parameter| @resource[parameter].nil? } if !missing.empty? raise Puppet::Error, "OS X versions > 10\.7 use PBKDF2 password hashes, which requires all three of salt, iterations, and password hash. This resource is missing: #{missing.join(', ')}." end end def users_plist_dir '/var/db/dslocal/nodes/Default/users' end def self.password_hash_dir '/var/db/shadow/hash' end # This method will merge in a given value using dscl def merge_attribute_with_dscl(path, username, keyname, value) begin dscl '.', '-merge', "/#{path}/#{username}", keyname, value rescue Puppet::ExecutionFailure => detail raise Puppet::Error, "Could not set the dscl #{keyname} key with value: #{value} - #{detail.inspect}", detail.backtrace end end # Create the new user with dscl def create_new_user(username) dscl '.', '-create', "/Users/#{username}" end # Get the next available uid on the system by getting a list of user ids, # sorting them, grabbing the last one, and adding a 1. Scientific stuff here. def next_system_id(min_id=20) dscl_output = dscl '.', '-list', '/Users', 'uid' # We're ok with throwing away negative uids here. Also, remove nil values. user_ids = dscl_output.split.compact.collect { |l| l.to_i if l.match(/^\d+$/) } ids = user_ids.compact!.sort! { |a,b| a.to_f <=> b.to_f } # We're just looking for an unused id in our sorted array. ids.each_index do |i| next_id = ids[i] + 1 return next_id if ids[i+1] != next_id and next_id >= min_id end end # This method is only called on version 10.7 or greater. On 10.7 machines, # passwords are set using a salted-SHA512 hash, and on 10.8 machines, # passwords are set using PBKDF2. It's possible to have users on 10.8 # who have upgraded from 10.7 and thus have a salted-SHA512 password hash. # If we encounter this, do what 10.8 does - remove that key and give them # a 10.8-style PBKDF2 password. def write_password_to_users_plist(value) users_plist = get_users_plist(@resource.name) shadow_hash_data = get_shadow_hash_data(users_plist) if self.class.get_os_version == '10.7' set_salted_sha512(users_plist, shadow_hash_data, value) else # It's possible that a user could exist on the system and NOT have # a ShadowHashData key (especially if the system was upgraded from 10.6). # In this case, a conditional check is needed to determine if the # shadow_hash_data variable is a Hash (it would be false if the key # didn't exist for this user on the system). If the shadow_hash_data # variable IS a Hash and contains the 'SALTED-SHA512' key (indicating an # older 10.7-style password hash), it will be deleted and a newer # 10.8-style (PBKDF2) password hash will be generated. if (shadow_hash_data.class == Hash) && (shadow_hash_data.has_key?('SALTED-SHA512')) shadow_hash_data.delete('SALTED-SHA512') end set_salted_pbkdf2(users_plist, shadow_hash_data, 'entropy', value) end end def flush_dscl_cache dscacheutil '-flushcache' end def get_users_plist(username) # This method will retrieve the data stored in a user's plist and # return it as a native Ruby hash. Plist::parse_xml(plutil('-convert', 'xml1', '-o', '/dev/stdout', "#{users_plist_dir}/#{username}.plist")) end # This method will return the binary plist that's embedded in the # ShadowHashData key of a user's plist, or false if it doesn't exist. def get_shadow_hash_data(users_plist) if users_plist['ShadowHashData'] password_hash_plist = users_plist['ShadowHashData'][0].string self.class.convert_binary_to_xml(password_hash_plist) else false end end # This method will embed the binary plist data comprising the user's # password hash (and Salt/Iterations value if the OS is 10.8 or greater) # into the ShadowHashData key of the user's plist. def set_shadow_hash_data(users_plist, binary_plist) if users_plist.has_key?('ShadowHashData') users_plist['ShadowHashData'][0].string = binary_plist else users_plist['ShadowHashData'] = [new_stringio_object(binary_plist)] end write_users_plist_to_disk(users_plist) end # This method returns a new StringIO object. Why does it exist? # Well, StringIO objects have their own 'serial number', so when # writing rspec tests it's difficult to compare StringIO objects # due to this serial number. If this action is wrapped in its own # method, it can be mocked for easier testing. def new_stringio_object(value = '') StringIO.new(value) end # This method accepts an argument of a hex password hash, and base64 # decodes it into a format that OS X 10.7 and 10.8 will store # in the user's plist. def base64_decode_string(value) Base64.decode64([[value].pack("H*")].pack("m").strip) end # Puppet requires a salted-sha512 password hash for 10.7 users to be passed # in Hex, but the embedded plist stores that value as a Base64 encoded # string. This method converts the string and calls the # set_shadow_hash_data method to serialize and write the plist to disk. def set_salted_sha512(users_plist, shadow_hash_data, value) unless shadow_hash_data shadow_hash_data = Hash.new shadow_hash_data['SALTED-SHA512'] = new_stringio_object end shadow_hash_data['SALTED-SHA512'].string = base64_decode_string(value) binary_plist = self.class.convert_xml_to_binary(shadow_hash_data) set_shadow_hash_data(users_plist, binary_plist) end # This method accepts a passed value and one of three fields: 'salt', # 'entropy', or 'iterations'. These fields correspond with the fields # utilized in a PBKDF2 password hashing system # (see http://en.wikipedia.org/wiki/PBKDF2 ) where 'entropy' is the # password hash, 'salt' is the password hash salt value, and 'iterations' # is an integer recommended to be > 10,000. The remaining arguments are # the user's plist itself, and the shadow_hash_data hash containing the # existing PBKDF2 values. def set_salted_pbkdf2(users_plist, shadow_hash_data, field, value) shadow_hash_data = Hash.new unless shadow_hash_data shadow_hash_data['SALTED-SHA512-PBKDF2'] = Hash.new unless shadow_hash_data['SALTED-SHA512-PBKDF2'] case field when 'salt', 'entropy' shadow_hash_data['SALTED-SHA512-PBKDF2'][field] = new_stringio_object unless shadow_hash_data['SALTED-SHA512-PBKDF2'][field] shadow_hash_data['SALTED-SHA512-PBKDF2'][field].string = base64_decode_string(value) when 'iterations' shadow_hash_data['SALTED-SHA512-PBKDF2'][field] = Integer(value) else raise Puppet::Error "Puppet has tried to set an incorrect field for the 'SALTED-SHA512-PBKDF2' hash. Acceptable fields are 'salt', 'entropy', or 'iterations'." end # on 10.8, this field *must* contain 8 stars, or authentication will # fail. users_plist['passwd'] = ('*' * 8) # Convert shadow_hash_data to a binary plist, and call the # set_shadow_hash_data method to serialize and write the data # back to the user's plist. binary_plist = self.class.convert_xml_to_binary(shadow_hash_data) set_shadow_hash_data(users_plist, binary_plist) end # This method will accept a plist in XML format, save it to disk, convert # the plist to a binary format, and flush the dscl cache. def write_users_plist_to_disk(users_plist) Plist::Emit.save_plist(users_plist, "#{users_plist_dir}/#{@resource.name}.plist") plutil'-convert', 'binary1', "#{users_plist_dir}/#{@resource.name}.plist" end # This is a simple wrapper method for writing values to a file. def write_to_file(filename, value) begin File.open(filename, 'w') { |f| f.write(value)} rescue Errno::EACCES => detail raise Puppet::Error, "Could not write to file #{filename}: #{detail}", detail.backtrace end end def write_sha1_hash(value) users_guid = self.class.get_attribute_from_dscl('Users', @resource.name, 'GeneratedUID')['dsAttrTypeStandard:GeneratedUID'][0] password_hash_file = "#{self.class.password_hash_dir}/#{users_guid}" write_to_file(password_hash_file, value) # NBK: For shadow hashes, the user AuthenticationAuthority must contain a value of # ";ShadowHash;". The LKDC in 10.5 makes this more interesting though as it # will dynamically generate ;Kerberosv5;;username@LKDC:SHA1 attributes if # missing. Thus we make sure we only set ;ShadowHash; if it is missing, and # we can do this with the merge command. This allows people to continue to # use other custom AuthenticationAuthority attributes without stomping on them. # # There is a potential problem here in that we're only doing this when setting # the password, and the attribute could get modified at other times while the # hash doesn't change and so this doesn't get called at all... but # without switching all the other attributes to merge instead of create I can't # see a simple enough solution for this that doesn't modify the user record # every single time. This should be a rather rare edge case. (famous last words) merge_attribute_with_dscl('Users', @resource.name, 'AuthenticationAuthority', ';ShadowHash;') end end