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/saltstack/salt/lib/python3.10/site-packages/salt/utils
Viewing File: /opt/saltstack/salt/lib/python3.10/site-packages/salt/utils/vault.py
""" :maintainer: SaltStack :maturity: new :platform: all Utilities supporting modules for Hashicorp Vault. Configuration instructions are documented in the execution module docs. """ import base64 import logging import os import string import tempfile import time import requests import salt.crypt import salt.exceptions import salt.utils.json import salt.utils.versions log = logging.getLogger(__name__) # Load the __salt__ dunder if not already loaded (when called from utils-module) __salt__ = None def __virtual__(): try: global __salt__ # pylint: disable=global-statement if not __salt__: __salt__ = salt.loader.minion_mods(__opts__) logging.getLogger("requests").setLevel(logging.WARNING) return True except Exception as e: # pylint: disable=broad-except log.error("Could not load __salt__: %s", e, exc_info=True) return False return True def _get_token_and_url_from_master(): """ Get a token with correct policies for the minion, and the url to the Vault service """ minion_id = __grains__["id"] pki_dir = __opts__["pki_dir"] # Allow minion override salt-master settings/defaults try: uses = __opts__.get("vault", {}).get("auth", {}).get("uses", None) ttl = __opts__.get("vault", {}).get("auth", {}).get("ttl", None) except (TypeError, AttributeError): # If uses or ttl are not defined, just use defaults uses = None ttl = None # When rendering pillars, the module executes on the master, but the token # should be issued for the minion, so that the correct policies are applied if __opts__.get("__role", "minion") == "minion": private_key = f"{pki_dir}/minion.pem" log.debug("Running on minion, signing token request with key %s", private_key) signature = base64.b64encode(salt.crypt.sign_message(private_key, minion_id)) result = __salt__["publish.runner"]( "vault.generate_token", arg=[minion_id, signature, False, ttl, uses] ) else: private_key = f"{pki_dir}/master.pem" log.debug( "Running on master, signing token request for %s with key %s", minion_id, private_key, ) signature = base64.b64encode(salt.crypt.sign_message(private_key, minion_id)) result = __salt__["saltutil.runner"]( "vault.generate_token", minion_id=minion_id, signature=signature, impersonated_by_master=True, ttl=ttl, uses=uses, ) if not result: log.error( "Failed to get token from master! No result returned - " "is the peer publish configuration correct?" ) raise salt.exceptions.CommandExecutionError(result) if not isinstance(result, dict): log.error("Failed to get token from master! Response is not a dict: %s", result) raise salt.exceptions.CommandExecutionError(result) if "error" in result: log.error( "Failed to get token from master! An error was returned: %s", result["error"], ) raise salt.exceptions.CommandExecutionError(result) if "session" in result.get("token_backend", "session"): # This is the only way that this key can be placed onto __context__ # Thus is tells the minion that the master is configured for token_backend: session log.debug("Using session storage for vault credentials") __context__["vault_secret_path_metadata"] = {} return { "url": result["url"], "token": result["token"], "verify": result.get("verify", None), "namespace": result.get("namespace"), "uses": result.get("uses", 1), "lease_duration": result["lease_duration"], "issued": result["issued"], } def get_vault_connection(): """ Get the connection details for calling Vault, from local configuration if it exists, or from the master otherwise """ def _use_local_config(): log.debug("Using Vault connection details from local config") # Vault Enterprise requires a namespace namespace = __opts__["vault"].get("namespace") try: if __opts__["vault"]["auth"]["method"] == "approle": verify = __opts__["vault"].get("verify", None) if _selftoken_expired(): log.debug("Vault token expired. Recreating one") # Requesting a short ttl token url = "{}/v1/auth/approle/login".format(__opts__["vault"]["url"]) payload = {"role_id": __opts__["vault"]["auth"]["role_id"]} if "secret_id" in __opts__["vault"]["auth"]: payload["secret_id"] = __opts__["vault"]["auth"]["secret_id"] if namespace is not None: headers = {"X-Vault-Namespace": namespace} response = requests.post( url, headers=headers, json=payload, verify=verify, timeout=120, ) else: response = requests.post( url, json=payload, verify=verify, timeout=120 ) if response.status_code != 200: errmsg = "An error occurred while getting a token from approle" raise salt.exceptions.CommandExecutionError(errmsg) __opts__["vault"]["auth"]["token"] = response.json()["auth"][ "client_token" ] if __opts__["vault"]["auth"]["method"] == "wrapped_token": verify = __opts__["vault"].get("verify", None) if _wrapped_token_valid(): url = "{}/v1/sys/wrapping/unwrap".format(__opts__["vault"]["url"]) headers = {"X-Vault-Token": __opts__["vault"]["auth"]["token"]} if namespace is not None: headers["X-Vault-Namespace"] = namespace response = requests.post( url, headers=headers, verify=verify, timeout=120 ) if response.status_code != 200: errmsg = "An error occured while unwrapping vault token" raise salt.exceptions.CommandExecutionError(errmsg) __opts__["vault"]["auth"]["token"] = response.json()["auth"][ "client_token" ] return { "url": __opts__["vault"]["url"], "namespace": namespace, "token": __opts__["vault"]["auth"]["token"], "verify": __opts__["vault"].get("verify", None), "issued": int(round(time.time())), "ttl": 3600, } except KeyError as err: errmsg = 'Minion has "vault" config section, but could not find key "{}" within'.format( err ) raise salt.exceptions.CommandExecutionError(errmsg) config = __opts__["vault"].get("config_location") if config: if config not in ["local", "master"]: log.error("config_location must be either local or master") return False if config == "local": return _use_local_config() elif config == "master": return _get_token_and_url_from_master() if "vault" in __opts__ and __opts__.get("__role", "minion") == "master": if "id" in __grains__: log.debug("Contacting master for Vault connection details") return _get_token_and_url_from_master() else: return _use_local_config() elif any( ( __opts__.get("local", None), __opts__.get("file_client", None) == "local", __opts__.get("master_type", None) == "disable", ) ): return _use_local_config() else: log.debug("Contacting master for Vault connection details") return _get_token_and_url_from_master() def del_cache(): """ Delete cache """ log.debug("Deleting session cache") if "vault_token" in __context__: del __context__["vault_token"] log.debug("Deleting cache file") cache_file = os.path.join(__opts__["cachedir"], "salt_vault_token") if os.path.exists(cache_file): os.remove(cache_file) else: log.debug("Attempted to delete vault cache file, but it does not exist.") def write_cache(connection): """ Write the vault token to cache """ # If uses is 1 and unlimited_use_token is not true, then this is a single use token and should not be cached # In that case, we still want to cache the vault metadata lookup information for paths, so continue on if ( connection.get("uses", None) == 1 and "unlimited_use_token" not in connection and "vault_secret_path_metadata" not in connection ): log.debug("Not caching vault single use token") __context__["vault_token"] = connection return True elif ( "vault_secret_path_metadata" in __context__ and "vault_secret_path_metadata" not in connection ): # If session storage is being used, and info passed is not the already saved metadata log.debug("Storing token only for this session") __context__["vault_token"] = connection return True elif "vault_secret_path_metadata" in __context__: # Must have been passed metadata. This is already handled by _get_secret_path_metadata # and does not need to be resaved return True temp_fp, temp_file = tempfile.mkstemp(dir=__opts__["cachedir"]) cache_file = os.path.join(__opts__["cachedir"], "salt_vault_token") try: log.debug("Writing vault cache file") # Detect if token was issued without use limit if connection.get("uses") == 0: connection["unlimited_use_token"] = True else: connection["unlimited_use_token"] = False with salt.utils.files.fpopen(temp_file, "w", mode=0o600) as fp_: fp_.write(salt.utils.json.dumps(connection)) os.close(temp_fp) # Atomic operation to pervent race condition with concurrent calls. os.rename(temp_file, cache_file) return True except OSError: log.error( "Failed to cache vault information", exc_info_on_loglevel=logging.DEBUG ) return False def _read_cache_file(): """ Return contents of cache file """ try: cache_file = os.path.join(__opts__["cachedir"], "salt_vault_token") with salt.utils.files.fopen(cache_file, "r") as contents: return salt.utils.json.load(contents) except FileNotFoundError: return {} def get_cache(): """ Return connection information from vault cache file """ def _gen_new_connection(): log.debug("Refreshing token") connection = get_vault_connection() write_status = write_cache(connection) return connection connection = _read_cache_file() # If no cache, or only metadata info is saved in cache, generate a new token if not connection or "url" not in connection: return _gen_new_connection() # Drop 10 seconds from ttl to be safe if "lease_duration" in connection: ttl = connection["lease_duration"] else: ttl = connection["ttl"] ttl10 = connection["issued"] + ttl - 10 cur_time = int(round(time.time())) # Determine if ttl still valid if ttl10 < cur_time: log.debug("Cached token has expired %s < %s: DELETING", ttl10, cur_time) del_cache() return _gen_new_connection() else: log.debug("Token has not expired %s > %s", ttl10, cur_time) return connection def make_request( method, resource, token=None, vault_url=None, namespace=None, get_token_url=False, retry=False, **args, ): """ Make a request to Vault """ if "vault_token" in __context__: connection = __context__["vault_token"] else: connection = get_cache() token = connection["token"] if not token else token vault_url = connection["url"] if not vault_url else vault_url namespace = namespace or connection.get("namespace") if "verify" not in args: try: args["verify"] = __opts__.get("vault").get("verify", None) except (TypeError, AttributeError): # Don't worry about setting verify if it doesn't exist pass if "timeout" not in args: args["timeout"] = 120 url = f"{vault_url}/{resource}" headers = {"X-Vault-Token": str(token), "Content-Type": "application/json"} if namespace is not None: headers["X-Vault-Namespace"] = namespace response = requests.request( # pylint: disable=missing-timeout method, url, headers=headers, **args ) if not response.ok and response.json().get("errors", None) == ["permission denied"]: log.info("Permission denied from vault") del_cache() if not retry: log.debug("Retrying with new credentials") response = make_request( method, resource, token=None, vault_url=vault_url, get_token_url=get_token_url, retry=True, **args, ) else: log.error("Unable to connect to vault server: %s", response.text) return response elif not response.ok: log.error("Error from vault: %s", response.text) return response # Decrement vault uses, only on secret URL lookups and multi use tokens if ( "uses" in connection and not connection.get("unlimited_use_token") and not resource.startswith("v1/sys") ): log.debug("Decrementing Vault uses on limited token for url: %s", resource) connection["uses"] -= 1 if connection["uses"] <= 0: log.debug("Cached token has no more uses left.") if "vault_token" not in __context__: del_cache() else: log.debug("Deleting token from memory") del __context__["vault_token"] else: log.debug("Token has %s uses left", connection["uses"]) write_cache(connection) if get_token_url: return response, token, vault_url else: return response def _selftoken_expired(): """ Validate the current token exists and is still valid """ try: verify = __opts__["vault"].get("verify", None) # Vault Enterprise requires a namespace namespace = __opts__["vault"].get("namespace") url = "{}/v1/auth/token/lookup-self".format(__opts__["vault"]["url"]) if "token" not in __opts__["vault"]["auth"]: return True headers = {"X-Vault-Token": __opts__["vault"]["auth"]["token"]} if namespace is not None: headers["X-Vault-Namespace"] = namespace response = requests.get(url, headers=headers, verify=verify, timeout=120) if response.status_code != 200: return True return False except Exception as e: # pylint: disable=broad-except raise salt.exceptions.CommandExecutionError( f"Error while looking up self token : {e}" ) def _wrapped_token_valid(): """ Validate the wrapped token exists and is still valid """ try: verify = __opts__["vault"].get("verify", None) # Vault Enterprise requires a namespace namespace = __opts__["vault"].get("namespace") url = "{}/v1/sys/wrapping/lookup".format(__opts__["vault"]["url"]) if "token" not in __opts__["vault"]["auth"]: return False headers = {"X-Vault-Token": __opts__["vault"]["auth"]["token"]} if namespace is not None: headers["X-Vault-Namespace"] = namespace response = requests.post(url, headers=headers, verify=verify, timeout=120) if response.status_code != 200: return False return True except Exception as e: # pylint: disable=broad-except raise salt.exceptions.CommandExecutionError( f"Error while looking up wrapped token : {e}" ) def is_v2(path): """ Determines if a given secret path is kv version 1 or 2 CLI Example: .. code-block:: bash salt '*' vault.is_v2 "secret/my/secret" """ ret = {"v2": False, "data": path, "metadata": path, "delete": path, "type": None} path_metadata = _get_secret_path_metadata(path) if not path_metadata: # metadata lookup failed. Simply return not v2 return ret ret["type"] = path_metadata.get("type", "kv") if ( ret["type"] == "kv" and path_metadata["options"] is not None and path_metadata.get("options", {}).get("version", "1") in ["2"] ): ret["v2"] = True ret["data"] = _v2_the_path(path, path_metadata.get("path", path)) ret["metadata"] = _v2_the_path( path, path_metadata.get("path", path), "metadata" ) ret["destroy"] = _v2_the_path(path, path_metadata.get("path", path), "destroy") return ret def _v2_the_path(path, pfilter, ptype="data"): """ Given a path, a filter, and a path type, properly inject 'data' or 'metadata' into the path CLI Example: .. code-block:: python _v2_the_path('dev/secrets/fu/bar', 'dev/secrets', 'data') => 'dev/secrets/data/fu/bar' """ possible_types = ["data", "metadata", "destroy"] assert ptype in possible_types msg = ( "Path {} already contains {} in the right place - saltstack duct tape?".format( path, ptype ) ) path = path.rstrip("/").lstrip("/") pfilter = pfilter.rstrip("/").lstrip("/") together = pfilter + "/" + ptype otype = possible_types[0] if possible_types[0] != ptype else possible_types[1] other = pfilter + "/" + otype if path.startswith(other): path = path.replace(other, together, 1) msg = 'Path is a "{}" type but "{}" type requested - Flipping: {}'.format( otype, ptype, path ) elif not path.startswith(together): msg = "Converting path to v2 {} => {}".format( path, path.replace(pfilter, together, 1) ) path = path.replace(pfilter, together, 1) log.debug(msg) return path def _get_secret_path_metadata(path): """ Given a path, query vault to determine mount point, type, and version CLI Example: .. code-block:: python _get_secret_path_metadata('dev/secrets/fu/bar') """ ckey = "vault_secret_path_metadata" # Attempt to lookup from cache if ckey in __context__: cache_content = __context__[ckey] else: cache_content = _read_cache_file() if ckey not in cache_content: cache_content[ckey] = {} ret = None if path.startswith(tuple(cache_content[ckey].keys())): log.debug("Found cached metadata for %s", path) ret = next(v for k, v in cache_content[ckey].items() if path.startswith(k)) else: log.debug("Fetching metadata for %s", path) try: url = f"v1/sys/internal/ui/mounts/{path}" response = make_request("GET", url) if response.ok: response.raise_for_status() if response.json().get("data", False): log.debug("Got metadata for %s", path) ret = response.json()["data"] # Write metadata to cache file # Check for new cache content from make_request if "url" not in cache_content: if ckey in __context__: cache_content = __context__[ckey] else: cache_content = _read_cache_file() if ckey not in cache_content: cache_content[ckey] = {} cache_content[ckey][path] = ret write_cache(cache_content) else: raise response.json() except Exception as err: # pylint: disable=broad-except log.error("Failed to get secret metadata %s: %s", type(err).__name__, err) return ret def expand_pattern_lists(pattern, **mappings): """ Expands the pattern for any list-valued mappings, such that for any list of length N in the mappings present in the pattern, N copies of the pattern are returned, each with an element of the list substituted. pattern: A pattern to expand, for example ``by-role/{grains[roles]}`` mappings: A dictionary of variables that can be expanded into the pattern. Example: Given the pattern `` by-role/{grains[roles]}`` and the below grains .. code-block:: yaml grains: roles: - web - database This function will expand into two patterns, ``[by-role/web, by-role/database]``. Note that this method does not expand any non-list patterns. """ expanded_patterns = [] f = string.Formatter() # This function uses a string.Formatter to get all the formatting tokens from # the pattern, then recursively replaces tokens whose expanded value is a # list. For a list with N items, it will create N new pattern strings and # then continue with the next token. In practice this is expected to not be # very expensive, since patterns will typically involve a handful of lists at # most. for _, field_name, _, _ in f.parse(pattern): if field_name is None: continue (value, _) = f.get_field(field_name, None, mappings) if isinstance(value, list): token = f"{{{field_name}}}" expanded = [pattern.replace(token, str(elem)) for elem in value] for expanded_item in expanded: result = expand_pattern_lists(expanded_item, **mappings) expanded_patterns += result return expanded_patterns return [pattern]