From caf45cf35a19d3227bcbe12dd1f96470082f9ba0 Mon Sep 17 00:00:00 2001 From: Sudo-Ivan Date: Sun, 29 Dec 2024 21:44:10 -0600 Subject: [PATCH] 0.3.0 --- README.md | 31 +++++ bun.lockb | Bin 622365 -> 622922 bytes package.json | 3 +- src/App.css | 90 ++++++------ src/App.js | 193 +++++++++++++++++++++----- src/App.test.js | 6 +- src/CustomNodes.js | 125 ++++++++++++----- src/Sidebar.js | 2 +- src/components/ImportExportButtons.js | 32 +++++ src/components/SearchBar.js | 16 +++ src/constants.js | 21 +++ src/index.js | 5 +- src/utils/aclValidation.js | 33 +++++ 13 files changed, 438 insertions(+), 119 deletions(-) create mode 100644 src/components/ImportExportButtons.js create mode 100644 src/components/SearchBar.js create mode 100644 src/constants.js create mode 100644 src/utils/aclValidation.js diff --git a/README.md b/README.md index 5933434..11275a6 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,37 @@ Build and visualize Tailscale ACLs with a reactflow diagram. ![Tailscale ACL Builder Showcase](./showcase/showcase.png) +## Features + +- **Visual ACL Building**: Drag-and-drop interface for creating Tailscale ACLs +- **Node Types**: + - Source nodes (users, groups, tags) + - Destination nodes (IP:port) + - Action nodes (accept/deny) + - Tag nodes for tag definitions +- **Real-time Validation**: + - Source format validation (email, group:name, tag:name) + - Destination format validation (IP:port) + - Action validation (accept/deny) + - Tag format validation +- **Import/Export**: + - Import existing Tailscale ACL JSON + - Export flow as JSON + - Copy ACL to clipboard +- **Search & Filter**: + - Search through nodes by value + - Filter visible nodes dynamically +- **Visual Tools**: + - Mini-map for navigation + - Node controls + - Dark mode interface +- **Keyboard Shortcuts**: + - Delete selected nodes + - Reset flow +- **HuJSON Support**: + - Parse and validate HuJSON format + - Maintain comments in ACL configuration + ## Contributing Feel free to open an issue or submit a PR to improve the project. diff --git a/bun.lockb b/bun.lockb index bb1ef9815b756567b0e2b533bdbc6a5f6f8ce4cb..d1d74d29c97fe52abe365c7f5994853c0a1bb20b 100755 GIT binary patch delta 61607 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

yB4{&WSJ>^}!hEu%=!w*}( z_0m}%xSSKO!6%IWLK|%!MOt(JE}AgAW=u?|q_YpfFv%DCsH@P&uC5ShJI3BOa4h8G zPQ$yQca5ysX`sNnbV`+EDk{B0Fi`4NuR*;u%v( zXj&v{4osJy*rS58N|qhyJt1`vi3;*zfe_l4I`k^8Dl~P|KKL-c z06>kW_d%%YK+YZe%TrjsYVGBRQdDh%P*^;lxB}tjk)NL4kc`E$$X^h{MQX0XwDm=b zZd!uK$CnPka#ZgWZ+ZuDMqUup;hdKZVLvnmg8ICMw+`VZOyb8eu9ClAB&asy?&`90SFgqT?^%=(KD2in?U1iZH`B(6vvv{F7kLhHmm zUy-INF$QFj*QBvPiNl0AE?&!q;1Iu`mK!fkR#Xst;|LrdCVS@yjqCc_s`wC?iz`Y7 zeNlCG2l>mRbW1V+GGVPPgE@5&D9?|VCU)^6uCZBc-5-EcpnepmfZm{?QJ5O;$*i}G zFONY1tya@VLILx9*Ese*-v`p;)GgEz$wD1c&I*1sOEtjPUdrO?ojfXj9pVdF7?$eM z=O-tiYyEnB0y^4nU2RdLPk1rB*L3{pw^ak5&}zOkNgGRMdh^F{gAnldF<94rZk@t> zs&mjGr(_Mt)zeahRXDz{Q6 z;Eh+spT(op@Up4fJhR`mp7~Q&3GxIH9*yvBK=4W*o-p801BHc^;y_uN5fAO+Co{*N zpQL8?&rrl0S;F-*RDG@d4_A<_bfd>r-Y0pxMG%W=V(J9 z$b(G*KYz2{Ut!%pOP@WyvvHN*YFrh#`$29C2f}>KMgKBxYzPE$7jkyJKjy`gL|vVM G!G8c$M%hpR delta 60331 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 z z{M7*#jK>#XHX(np;;0J9fpOdkPM&*w0VK_90$l(eXGDvJI~L&i_?2ibxZ8+!R2H{P3F13yBg-@cVLZ?IE=iWD9#9M5ciC@5s}DER z=D~MNGPwo-|J{qnccL|fdn+{lV>RDF$?C@d-k%7#A?Y=O#{Xf%lo6f&lFG&qmzbjQ z!b%pV3Ge}O<6Lk{BC~NmWC?KGR%3Ib4&KLvnn4=DOmTQ08EOvqD$)2}OD5X_upuiR zzeqV-!u=rzFy|J&?vhbj0bgSfa>4Hp(OSd(nGE#1&`=V$(K%m;cn=~o!FAGj-v=il zzW0(EUes>ck8_jS1JPcA`>xC>9F??d2UHPF6Rkb8Q2^&4t7xynohZ+sTl_@if0P}9 zy9n4{w2pA6!Og=a8b{+1v9pWT85&;}SQN~6ZL%<3fSsac5-koI|52NVlmDns!>+)a z=9Fp4ELt~cABe`6bkeRn@S$i~MT>{_k!aaO>jCX=&PF)0i`EnFz0gX4bBLCw7d+m# zK;#s$x0;m;L!%FZmP#7lXQ%o?n;=?lF;9RdFGH~eiq;R>IrJ8fJkU7k`U97tl>+C7 z#$l2Od?$7VL>mC@n)k&z1tr0O5O0WKA<+gwyD6IYrJ+gCeiSVPcFZ^#xFuRK(FQ~N z30f)ei_ln@A;9|>v!%f$ZEjN>3J+iYR0dpHG9Cu^AEK2JZ8)?a5V;(rC#jW?cf+7B0T4BQQ1NUfqINV`U&RT3=)TC8YQ zL>mjO9yDGu_y$vEJPzQaExbBZ6Ky=)Kf~>&$TyhMZ~|}}ZeB8?M4JdVU;M~RMh(#> z!QBULUNU&OVY9vl3=)l(8>TlI=ms~BTB4=G-3D%6T55aWt~mvsPjI?Nfq7kG%3!3kFf5 z@#4pfX9F(Lnm}WXb>J3qdkNfB?B>9IN3>?p808J%dzK0Z-v!I`<^o$NuwPq?sNmiK z)Cbr^G@J(<0C@04vCMcraF_y)SHx}s+}?j0_P#iFA+((`i}NM2EX*Qc7qkZ8PSA}1 zEL{xGZV}@olQ*H|KnY^N>_b*y3BZ4$ZUpWDjRjl^96%tiI^H+QE`xg)w5H%*Vz(S_ zzM`K;Z_!q;{&pnD>kS7K!&d?hXgvCf;VQT@i`HMX)zGqtmMGd=(6UN;1E8_B)&P8i zT}v!4jkg~l3x0OW^VKk!u1K7>0&;!YQhgS?2Txh=0tf;%CWlE^bf+XbzRM4lzuZfJa4 zR)6qp(e}WdT{K;^z0m$Z*Cm4IKw|;-0r#NsmaZUk4SmOBz9E{YTnlKtN-dJe zAHm&L?B0aN{yYfehBgSiMC=a1{esvn6T8FEUKDM)Xlc+&^Q%J=94jR95xDC>O9rnJ z?I_&!B=TDl`D17e#cqvg$DsXM6^k(^<%$Z1g?t4TR&=F1Dv8$!F-Q23wR0e zzRiA**j8NNo;O2`Y zc%k_i8l&6<&WZMk*!>9Yf@sIZ?iRGqvC31tkUgO0Nd)rB^qB zFIJ}{;h*8&rv?_neJp=Xjn}L*67(zFfuemS+HcTkw*q`twBN;!+BwnwfV(2HS_wW6 zjcxWPPzD;0uf^^z+(R(=uI9+P1d$E!7r+;xt^t1|hJVA&=O=lIxgy#5&gwsk<_B#W zG|uX`MDvHncaCya=N%xt7Mt2z7!Avxx8qCxIJy5MK@Mmx(S8;!6SS((K7`#bqB)^e zfX3rjXl$R%(D+uv-Ei}T-tZbfmB&{`?uGjg(fFPCeSSfp_@{{3ATEXBK4^C(LH?fJ zDrh|Z7CYX#tQNa_qUD5EQS9zR<5?5{Z7j6?a9hzqEFee7B+>l&Ms4^lx$(hQ93Oz& z219BbUhPD)i^d=G8;ZyW5$+I;KkPS5v`nJ$)d~5a9fBR#N#V8dcm4PR*)+H_ix#9> z6hS8xKoH+6%*#Vo2?~ZbS+s1T6@02A<;^T7AAJL zVOL7D3SvjCv}oaC$N8&_X!w~#BRy(mMT>yO(c}IFVmT2jiOAWkyl9o7aolp|3Kgvi z{2Zdx!bFRNpD#z|ct486;w57_#M}pz(E) zJR&7>4QPBWl#@zT(Q3k78+M#c_`ZFR@q3VbAuN9=v$|+5xcSyup6K59@z>J2Vfqu1 zFXw0E+W43Ojc0#NXe>+}xH-~!_Pa!T3GT|U<564U)`b=+S{>2qslCNfBkc}28#JkgS2$Z3MVg1}+d9`0}D4DTR@uR^;Djc0#H(K^8Wy=a|8>j>?d z#O*9vCusg?N}l~)ps`gu!)=2Dvpv2yiD4Ire6Ln{a5vH7MB|y>U9_&E@vM#)tsC4t zb9rL+5Uo4hycqN3>M2@0+5K81{r)7mYvXK)YV}VDod_ z_Z1DxdL`_+YxaRO2*SSF;9S-NpPqQ5UroOge2VkA#wC^Bn^@z z65;LzjRPcE%m={Dm+W&e3>Ix5+#kcuP97rKAh`LiT>dQUP|=d$=0Z68X_#oqaN{j1 z*)8m-;UW%JV@sk;Lm=@dLD^p;C1@zzZ=hOiwNauCgPUby3yl_SxM-~M7|}++&FZtR z{Ot;mWh6d0fT^X3HVSSmqPr|C@K_N?!_D6tWr4?uHU@5H%!J2_Hj|eJ97}7f9;NK{ zwH0b_DSPqK+zOoubO71|uL4mpdl_5}Tpd7jSu!hUX}hNle`}LlihO}9Uq`nA;9~*r z0(=kNJYW&P?L@vOuQSjEhy%I;-GJ^uJizz&)j-mf)JLW55w_;=)>j?N*n^}0#{ACR z#QVSl&h-e=0PZGo_wYkt2e1>^4eSB;a2Vi=OSzd?4;XG!p=IsGY+SpTsM?gZ_YB~^;342J zkOmx8$I4>wcN?4^03QMyfQ`T?U^L(cQUJc9nLB>#fepYd^w@2Hn|9o!`vq7HeGTw7 z@DAXQoNa&|mRZlUS<9i8m9x9ut)R6Ac%RGrTHeO;ww1T3 zTwrwpwSkv_xF98$*iUM14Dsi`xyOZ1b z<7nKGK6!%HIWe38)Ly1L^|}fQCRLpfS({XbLm~ zS^zBpeEl%iwAz9BdzU&e$4?awwHGM82ySk=y$LJR>g(d^X09H zB%%QqP#fSL+OW)OSE#+do3B^oD;N2G#;iEQzCgR50_MQ~1~3>H1oQ&>0Q}wB765M|{a_qAf%E&+~yKz|?+7@#_a z*}rjT#|X^<d5IBdjo(C=f7Xkj_UKyY)z&*3i0dA9>1o&qA z!vNo{%Iz<1cU1&JfwDkJpcGJ)FMZYE@B{c}*mJ;nU>X{a@Ahp3jD$N8;5U4JQ{M{& z0R;fA;nz}-1g#IrsLJ(aC<&? z0-XJT0l+}uHcsZBfIor1fd>G$)C-}kg@HxzF9zNOmH^!CS_Vu2xTD1#tn$cQ7y^s{Za_Z)d5(O z_;?B*M@a!n0o;e-zDrr498ewzQ*jaYD9xo7N7(cAb3n-iWCkAK?6QFQbF6;@KLY$| z+3x^um>dN@295!r0Q_OyEr_xW;6epgB)HnJ6W9+N01g6&fWyEMRkV^l%FS&NZiDch ztGsRJZ8~qudCNT$<776V19O160KXkf2TujMz|Fg`<-iJHC9oQ3jIbuaT)5`}^8vo| zbqK)c82bTWc!hvqU^w#NlTNP#T2jZf+Y*xiY&*D(|0xP$f&ljoiUOqp zZVhAwenqA40r!Ci09W&y16-}o4&((20L1~W%yT8a3=j^~0=T%o8}ar6`v9(a9{@fA zQuupQqhN3ewLA^vM=gT@eqrMGBYyYcFEV#WSUj)-H3$H6*_Y3(@OczIf5O$>&w-Ob z547R}fNP!#*bJ=z5D2^ttOf2O&2ixI4e>Dnm;}5Aqyhs0E&~q(Isxr~IG{C9A1DsI z0K5qB*Oq?(K2;|oaSP3#x1J1efj1wJA1DCi1VT|4u2udC+y(vuxRA<4Qm)CK0r;w7 zep}#Iuyw#HPrShx4|Zx7ob^PXWdP6M;#kWRMR$4y`gC z;LG>lLxwd_lcLCgi!EHwr~+^eqbg7h;G#thwX>Q%%*{23MnEi34B(e%K7n)%xPY=< z0{oC2zwmwx90Q6XOBb@J4b%Y!BVJFS7r@_J>JIR?n%V&!fLDOxKzkJJccjbTT;gv# z;Q>dtr4AD0V}*QVus*;|$n7XMAH2JX`n`n)T??!O{z6zypcYUEhylt2VL%0-6WS{l zXan$3tXkl$C@de3;?qzkkwzTyYXtB~sJE@?VLqp{1K0z61bhvneJBtgE#f=te*+2w zqmUSPN4Uwc1K0_0=d3!yqJV8^iS59C;Aa&69&jJn3wVIbNbfrE6YxIr;3Fh|a7%A6 z@B@TT0d7XU0@Mc@0DO$2G0+5v1^BqeO8^(wxtM+%;G%vXAOYwP3<8pYHb4Zh2u+>` z>;N(W$KdA!6iKL%yE{H60Aqpd5c$LepL~c0_`m`mP?!Zw19re>H}Da#1I@{21NbPw zHefrD1LK@q{!4+qu;*4kxAt2D+tK9Qz2}ZQci4{s*g$t1JLdc?H|~;O0?q>;p@|Lv zCxJf@aWpa<0GvSSkFy5AZh$*y+|WLU7U6Gw@b^Cs15SWH9J3Bs3#&4Ch|(A82|jGUhgJCiuBcTN@K8*sOf#%UKbPpaaka@OE!gxSIhl@k=H*JGcpQ2e<)ngXb#1jjJ9&TY!6jJ<$U5!2N(k zAPGnS`U2g6t^l|3ngRT=`(J=xf!~1Nf$IQw@5-T@tbiZj=E5)^ljqhPx8lwN+9m>UQLmcxGti5dNjF7gfIc34S(+g;zFWv&3+<>F5OUO~%z16+k0 zVQwSvAezw-5COkSVfh-k3~<|O4{BeF8d~Z-U^SqC`M?5zyHnha$_{Yr$pu6LRe_oS ze|xJunA=D>0B#+<4{&E_1%E4TH5}a584pYXxO2munwNp0@N+jN9}oy|$K?UQ?Ul&@ zw@W-#;<`3fEySNj0E}sodND%uupTL8@L<6%?EBad<}R{&Es%;bzjZx6}kS% zC1Ef7_Gc&}^Y>eZ0sOhfZ&8cy!NgT)-vQqP0|0IWaNU0mDmFvF_5B<`ZTM>e)d2q1 zSVi8i1;F7!;8uW(`CPl_T0NVU3+W93E{Zn+6o7?qBaG|X?E$W34+kc~&Gl+Ntvv;9 z{-S0)&;#JA9ari6k(VFv6>PYi{4>|E`J<~`Mji~XZ?2)q+W}Y5v{!-efv?fTmw|5p zzGsw6!dcK-T=C_vD?H9zNGH6l$={YFdn(jU+)2P7AQ9*f@aGC&1=;{Dfd#;Pz#FFnG93v&Thj`j3ho8;1i19*PQZr^8FK-4 zD0nz90{9EH{tfsUxDI4yjR7vG9zl)0U@h6to00e%z#M=HGhG&_6|m4p^A@Z(!uvb0 ze#9Ts?S=%fVrndqW<>^>fVqeS0|VX!`51C`gi-&L8#Fk&4CX><3D3uM>@y=W&*Y*q zj%^3jh#~e8>VqNn0>OD81mRwAVM>iNgClNzYt=ec4h@eCtzZdNw}#k*BI?2*7zPa% zIr}szuzooV!XrW}!=N)lvLWRBJ9VeOt~PaNNX5{I(26Q*s69A10l_GH%EHem)rH-PF8twZKtK0!>E4lB)A?RH}v=4m+)ZdY8WT>JWVi>M!_$ zhaqwxBCnmE_~C*ZvvfobM`YwbO@)m>>MLN77Y3)w{kr!++v{gwfEGZ#E!)-T5%!>h zyyp#oK|+n!KDxN5@l2+G1d#uiR<&t_y<)-Zu*e0A3O!B^{CwMr+@ZuedqUVHX^g*EG^Y>ns-#Q-1b0q zZ=^j$8>9-2LYxVR!xnKCX=C(l!hmhNCNRGNaLjzaW=f7zr|d}OAcl2LDE-t;WTU;N9HUWCT}6$CZ@FqS z+FrIM7uIOwkMYg16Wz(WEUUcO34-dy z8uts*KnkH1BQ3ungw4_M^jC?C>T1J{kSc7{2X@uTjqzyoL=bxO9XC2bM?$QfTG1HQ>Rt-U z))}Fg&{HNnc>m46^5@|xBcr~L3LlFmdROh;1nQ&Cl5#6I#WL#HSbM%gm5gt_n5$AQ z-9PqTsY=;6FQ67Ih+z^``DxXuWI2TjFy}9px~|v0^mhlNXvT00QNiPo$r`n6Ji6)w z)gs9eq{fZ27r>cN^h-PX3lp}w>B96>SDhGVkHt6{+Zew3s^$$uY^mBLJ0gNx2bhiI zOssM6dW!`gTD37%Lc^<}ksnR@$MHycv5Fgt6jrN|CN0SVpE*2KzX|q2m8YVbZ1RZN z-A3$PymT~DgTisGz??XKh>D9#tJu_Ov_a+2idFG1HoGUFJloWn38+Xhm2ILuDEBJN zW=tq?-tin`CO1(zwQ{Q3M4WK}YU69@ky821>GNpP^cDSDIPY6EbZmHN6@0r=mnI>( zJ}P!H>{4LIrY~OZmDrQg6=}S4LfO==gj^Y8JamsLwk$)oXzQt^++8nKfQ9F z6~?*~s^e~x^Y()2#a3ce@I1!QEvyQqqMdKR03$SIg;CoKTb+DvM{EFK9~?VYOdV)punCY_fA+f9kv|j zOTnCO4}86J(2~wS6-1l})Tjb(fYqHTI16_v*Hl#OBy{CR4HCDEn_J}I|OPS_5 zXD&Iha?5w|FqbRF+|uUI2?+W~<^QzmDTMI$3q60XwCX?8UeS83v|0|nVTLVK$~d2?|!P!y>pud9&kZEZfxP-st0Pq-vFU zhqZq;nyPN7c`9@r_(IKES#nf0463n)x(d)SrG6Wx%IWqd*4tsKOD0Di^}22^Wc@Qt zy{F@1d<%9Qr|owCXj^pQW+@|fI0|T~r~X20t(D3%2j|g#H7mPu^%*>c+ksm8AEHKQUw{*t!!MHj`;F@^nOC;u}5<#lR8}>@Lfm-+mq&T%~ z1a5M0DLGkLefowyzx6_8HFFdazg1av*$BQ{Sq)HV7e94pB_iZfE#_h#2~o3F!&gQH zOu^j}${VS6zX>5$ja&fgq;Aed-Mwjg(=Z|jX=~L$*ruoKQg;;MG*Kao%`3T?xH_e9 z6_!Ac(iZ(QikV(;8oo&6M*VMlCeNIo>07f8$h_^@+=L-71TP9%9`&1*Tnv^6;YupD9P z7z~5!!hp9GUya|JKkp}Zym?~iKGrKTMB1s1V;n)jeG!dg`)2nm*M6HkvWbzd(JLd= z%@nk;^xiDk@k>+=|2Hppu2?Wd^2c4tTk6Cjj5X;eV|)b3$m8J1=MS5QBXFbYw-&9o zM=e_nI;uVdSx?tgrxxRD)_0Og{)va@?|S9!ZyvVBy_%{5imPg(t%C!j&H3s~J{6iP zrN@YY9AsO*wDM(;3H8wT2J^(yUF!j50j0&7>UGkD+evtoyl@u7Mh230+B(N2?1L;nXh}Jx z_o(nSI1MpQ`m5v1jd3z}IpUR3mzJW4VpUlUoQ#SsQen$bo>jHY8<#AXqpmmBXJs>T zHE!IuB7}p>ug%aYSLYV|P0EAuYPNfx!f3ODFydE|rSmga3E4jFmKdQ3A!;MiHw)<{ zoL5GHjFNK-Lb;5uX_#${D#<{x8(peNEzIiY^{eDF{QqgqbN0!qF*kb8U^D5*PjHzc zpHt=pTWeMN#454!a4ME;)aq5ZESOoh1{AnNoTZ z>6I{A(>NK8W|YXP%Uh2o>aDza+Df_Pa*@Y|*??D!(T@Djm>7+*#jjM2*I<}GrQ0zS zrEATcWQeM?HMnq^R}|k?@}B9BUu$FnczOhxDASXC*lz5aB5N^+-i5}2`C@F*r3YN+ zavKBFn8M8F;#p!&JJ#=NsvWQ4aw+-wBc@D9CDqBb_OWvP&tLF%n3-mX^>U{cKmN$| z7p@uMHTxYWM156z9jJ|Zb)CJevt2Xu z^z&D*Lk->lFDIvc?xvR#FMilbPABBBQ-$5K=T&Fc+4DFL!YT)>%%lolX>NYQh}c^6 zjSbdSe;MDk@a+Rj|IJmocTp@&g_J`HGONz-+Uuo9Z=-U|gxXEj90|gMwC`fZmRqvT zD(t<7g)OYYk7GJH-b#&m&t4`yfs$$?jVh_4M{)o6^?P>z;99NC`LEEdM*X`j)rVTO zGWe>%<7M>##=-qyz)MEWgPBUT%5(_piCzPrWU$r}yB;Zq)wfx{0& zu=0ec<$HC;ZW;{wM&*2SN59CD4-F0@1VcS#dzm*1-P$&1lT};g$-UM7gAI$`;rq}g z#sYO76}rP-7}AoRkc?91u~o!6OiML(hrOo+DCOj@DtZ3s)ph+?^7VP=VN<;x%(fMo-J#Ls@HydFAT90`^~ao zo=}AkpcNk-gECqlDWz)rh9kW>jIQ#@5zXOSy+=bhxK#FID2~)Vec07L_^?SHjbnC? zPvbt)0VuZ20gv>9IW0h9j{iKnnA)Qzavn;t(!>6d`4g)9WSjJw86|kwsAff;(nM<8 zkw?4A+Z)mi(lY;OfhXxcEhdFFN+Pq6Sy5voc&AdcvCQr<%d6^`T{U(-u7;AwvJ<#u z>^X*W@_%%bk*UmblJt9@r1!iz2LGn;ux;3xPne{6S{Nh8i2u;sr-o>eP;O;$UTCoZ zS4Gs1sXeWX($6wJ9wuqVVehC(Cyc_Pz7Lb}P11Bf*1HhZfY0pXusr-=pd+txer_*> z8;FvhV^Z#;Hg0kRd1icWpQL#gwacEgZ?`_Md#;|eSJA9D?5gl-EYM&Bqa_$?@3V&B zYtoyi?UjN*!0iRUg_fAS|DEec>RrP`iftBN7mWpnAZ7m&<$c29L+q2pAkPbD>`OJ= zt$y^C{TgWfS(MPAyejuOdtbQS=j_K>q~_=CTZHqd;0yK^a<8E#U9fM}ig{{YgyhxM ze2pnY?nO*eIWJ+Nc~n|=31!`>4qdVzmk`f}%l5Wb9z{m*xh)+9Rxw-f^n;ezkw98MmE*s#-}$HTCgtXst)@ zJcB*Gf46^*+&ne^w72uK{$^M6{0s_*XGTWKXcQ4f$j3hK$JIjUP>)7THEkg-3LSB>;@pnHuQ zQFYbN(UcW}9X6Ydownxwjvm@hkH_D!M8gdqlHyA5K)YkCIqrfySsjj+M(S9Lo1kfJ zJ>NPVg*1F4@yi0E^erFk^+cF0VVrOOxB*wKvpOnC=K0m~td6bpAkF-0P&UUnt*!bs znUPgfNgCv-73dh`lp7fvmgy~1)v-b-WHC>0VaHp32-#m0*{|~aUKEYYiJ(z&hux}q zJlGLMkNXUaN5{n6s<;9hyY>rV)Unk6Xu4db9Xu=J`Z&s~S!s1#uMowYXw<1axs3ulK^^G|^ z(2o03{>K@=PpI!II;!F8QPBt(OAb3C9M{q5IS!y9inMXq)mN3!G7>oRfFo4RYy@>z zdE63SIDnFsP_rr{lV{Xem8JD6R(l3a(JmygwqOsJ)xXNN|@y$o|WA{N`Rr`a`X(IiEoX$FlGMm{elIp!uFfXrN+Z9 zvav0ru6ixZFXrW2ovMW~`i$nrEr*evvyNk#cd9Imsj{$UZWwI&v+!3lQ{LQ$=^0=3 zxppmIkAJS?Xr*oRGh>RdxKlaWP)I#L0Hc3li? z%sf3`$6&;no>(=v5mJxxU^J&%xgC|YsiTQDSG8;EI4<7&s!lV^@Sj%n+->IQW850` zXaS?uir0yPkMEEAYhx;4dlU0KQnR)+Qft-{=l!#)(6W_xqKoxpYR!}68P?AVQ?uI| z^}&ROi{SrjA`{x-`i7;N58C01>QP18V^%Xpb)dZ}?`xisuDabGV+6*$DK+k(1MR%r zcvn^o_wiQdXrB}l-FU?>lkXWL(70nV=E%Hi#jChx8bkj5r&Z!&2V;nE8d6oe-~xf^ zJAKm1+X?-j;F;gaoZh`g&put+s+Z#&PtDI9YF&+S70~VBJie}*aixgqjy95MvPwk^ zHk_p0Q9|4q#bees#$})y8ILmyXB0OnzlnFeX1XKPn?2BL=4t)3`X=^#tiG@Ga%{yY z{=Z)wUHfs#DbdGK9pfI?VZZbXAkMTvwXQD$J{jaFz;6=Q2RZmnVp;lAJknyh)x0F!?ZM)U zB%CiYH9Ye0PUU|-H0E9OTZ6A7oH`F*tF&(Lh6^_yE(aQqERNNTrwoHUuMBon)yyw4 zyw}S;6c?`aTRuGHa}D>7@xwUftyYVNJ7UxC>HJ5ahmEb$ST1B;Q)~9S4csF045-hUXH_ABCejkNBVOiWa z7w>(R*QKOqKd*Wj@B7P3_`_8y%9u8Eeh>nC`c_5 z$|yuKXiAydc9Q3TC0e;}B{u!lGaHM7M|5DyFgjvPW3#l$V0KUacty4NNJuJnhmeQD^ zGt2fx0+$jqEM?d`mnj+&p;_Odnefs^Z#>(#(E_ilBIy`y@Iard*~8Q97tkLY}D?=h@^Uv zx{JJt)fMt)xCYD}iz ztlSNG6pot=k0o!sT`suE>*}<)&DI)hPR97gpNmK;g=0#>nt_#RAjCz`I zq`F@Z0Boa(X3rMIqb&JIQFpxa^m$7u(JvoOQAG<@O(Q|}eQ#kJ&TM4t^2lB#XePEA zo;QptTA3DUk_^jpoPk?JGHiGN?xe5Pf3>os0dQXw)efX1lYi(%yW1I4KnI4S@ODB6 z4DmJ3IgY1x{#@p*(_9B)lN$X?2lpM~a~oal0wSq(v3dUQBo(}b9hI~h^=&lu3ele^ zJBN#-6S%{pro4~WN4nTYe{9tU-B|uSeWsf`Yc&_&dRUs-P@zchOa!b7X8?10(N9I6 zH1}frtL)kNUa{n7``|oamHB;aP%$fd(i>*?+ttyiFFObj;_%XzucpJI@*vq?^L5BdRO;fb#@2HGG&A%`}2KVH404Q zk-o;CEskO~cf!cQV;Cz6#*V@2>{K^~w!GhQfv$fLNXt|SK0c16Ti5i=aTc{B(kII# z)>`5 setEdges((eds) => addEdge(params, eds)), - [setEdges], - ); + const validateNode = useCallback((node) => { + const errors = []; + + switch (node.type) { + case NODE_TYPES.SOURCE: + if (!node.data.value.match(/^([a-zA-Z0-9_.-]+@[a-zA-Z0-9_.-]+|group:[a-zA-Z0-9_-]+|tag:[a-zA-Z0-9_-]+)$/)) { + errors.push(`Invalid source format: ${node.data.value}`); + } + break; + case NODE_TYPES.ACTION: + if (!VALID_ACTIONS.includes(node.data.value.toLowerCase())) { + errors.push(`Invalid action: ${node.data.value}`); + } + break; + case NODE_TYPES.DESTINATION: + if (!node.data.value.match(/^[0-9.:/*]+$/)) { + errors.push(`Invalid destination format: ${node.data.value}`); + } + break; + case NODE_TYPES.TAG: + if (!node.data.value.match(/^tag:[a-zA-Z0-9_-]+$/)) { + errors.push(`Invalid tag format: ${node.data.value}`); + } + break; + default: + errors.push(`Unknown node type: ${node.type}`); + break; + } + return errors; + }, []); const onNodeDataChange = useCallback( (nodeId, newData) => { setNodes((nds) => nds.map((node) => { if (node.id === nodeId) { - return { ...node, data: { ...node.data, ...newData } }; + const updatedNode = { ...node, data: { ...node.data, ...newData } }; + const errors = validateNode(updatedNode); + if (errors.length > 0) { + setValidationErrors(errors); + } else { + setValidationErrors([]); + } + return updatedNode; } return node; - }), + }) ); }, - [setNodes], + [setNodes, validateNode] ); - const addNode = (type) => { - const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect(); - const position = { - x: Math.random() * (reactFlowBounds.width - 200) + 100, - y: Math.random() * (reactFlowBounds.height - 100) + 50, - }; + const onConnect = useCallback( + (params) => setEdges((eds) => addEdge(params, eds)), + [setEdges], + ); - const newNode = { - id: String(nodes.length + 1), - type: type, - data: { - label: `New ${type}`, - onChange: (newData) => - onNodeDataChange(String(nodes.length + 1), newData), - }, - position: position, - }; - setNodes((nds) => nds.concat(newNode)); - }; + const addNode = useCallback( + (type) => { + const reactFlowBounds = reactFlowWrapper.current.getBoundingClientRect(); + const position = { + x: Math.random() * (reactFlowBounds.width - 200) + 100, + y: Math.random() * (reactFlowBounds.height - 100) + 50, + }; + + const id = `${type}-${Date.now()}`; + const newNode = { + id, + type, + data: { + label: `New ${type}`, + value: "", + onChange: (newData) => onNodeDataChange(id, newData), + }, + position, + }; + setNodes((nds) => nds.concat(newNode)); + }, + [onNodeDataChange, setNodes] + ); const onNodesDelete = useCallback( (deleted) => { @@ -94,10 +141,10 @@ function App() { [nodes, setNodes, onNodesDelete], ); - const resetFlow = () => { + const resetFlow = useCallback(() => { setNodes(initialNodes); setEdges(initialEdges); - }; + }, [setNodes, setEdges]); const handleACLUpdate = useCallback( (parsedAcl) => { @@ -189,17 +236,85 @@ function App() { [setNodes, setEdges, onNodeDataChange], ); + const handleImport = useCallback(async (content) => { + try { + const parsedContent = JSON.parse(content); + const errors = validateACLSyntax(parsedContent); + + if (errors.length > 0) { + setValidationErrors(errors); + return; + } + + const nodeErrors = nodes.flatMap(validateNode); + if (nodeErrors.length > 0) { + setValidationErrors(nodeErrors); + return; + } + + handleACLUpdate(parsedContent); + setValidationErrors([]); + } catch (err) { + setValidationErrors(['Invalid JSON format']); + } + }, [handleACLUpdate, nodes, validateNode]); + + const exportToJson = useCallback(() => { + const flowData = { + nodes: nodes.map(({ id, type, position, data }) => ({ + id, + type, + position, + data: { value: data.value }, + })), + edges, + }; + const dataStr = JSON.stringify(flowData, null, 2); + const dataUri = `data:application/json;charset=utf-8,${encodeURIComponent(dataStr)}`; + const downloadAnchor = document.createElement("a"); + downloadAnchor.setAttribute("href", dataUri); + downloadAnchor.setAttribute("download", "flow-export.json"); + downloadAnchor.click(); + }, [nodes, edges]); + + const handleSearch = useCallback((query) => { + setNodes((nds) => + nds.map((node) => ({ + ...node, + hidden: query + ? !node.data.value.toLowerCase().includes(query.toLowerCase()) + : false, + })) + ); + }, [setNodes]); + return (

- - - + {Object.values(NODE_TYPES).map((type) => ( + + ))}
+ + + + + + {validationErrors.length > 0 && ( +
+ {validationErrors.map((error, index) => ( +
{error}
+ ))} +
+ )} +
- +
); } diff --git a/src/App.test.js b/src/App.test.js index 9382b9a..cac93f7 100644 --- a/src/App.test.js +++ b/src/App.test.js @@ -1,8 +1,8 @@ import { render, screen } from "@testing-library/react"; import App from "./App"; -test("renders learn react link", () => { +test("renders flow controls", () => { render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); + const addSourceButton = screen.getByText(/Add source/i); + expect(addSourceButton).toBeInTheDocument(); }); diff --git a/src/CustomNodes.js b/src/CustomNodes.js index 879cfa3..acf80d3 100644 --- a/src/CustomNodes.js +++ b/src/CustomNodes.js @@ -21,58 +21,121 @@ const inputStyle = { boxSizing: "border-box", }; -const NodeContent = ({ type, data, onChange }) => { +const NodeContent = ({ type, data, onChange, placeholder }) => { const [localData, setLocalData] = useState(data); + const [error, setError] = useState(null); const handleChange = useCallback( (e) => { - const newData = { ...localData, [e.target.name]: e.target.value }; + const value = e.target.value; + const newData = { ...localData, [e.target.name]: value }; + + // Validate input based on node type + const nodeConfig = NodeTypes[type]; + const isValid = nodeConfig.validate ? nodeConfig.validate(value) : true; + setError(isValid ? null : 'Invalid format'); + setLocalData(newData); onChange(newData); }, - [localData, onChange], + [localData, onChange, type], ); return (
{type}
+ {error &&
{error}
}
); }; -export const SourceNode = ({ data, isConnectable }) => ( -
- - -
-); +const NodeTypes = { + sourceNode: { + placeholder: "Enter user, group:name, or tag:name", + width: "220px", + handleConfig: [{ type: "source", position: "right" }], + validate: (value) => { + // Validate user email, group:name, or tag:name format + return value.match(/^([a-zA-Z0-9_.-]+@[a-zA-Z0-9_.-]+|group:[a-zA-Z0-9_-]+|tag:[a-zA-Z0-9_-]+)$/); + } + }, + destinationNode: { + placeholder: "Enter IP:port", + width: "120px", + handleConfig: [{ type: "target", position: "left" }], + validate: (value) => { + // Validate IP:port format + return value.match(/^[0-9.:/*]+$/); + } + }, + actionNode: { + placeholder: "Enter action (accept/deny)", + width: "220px", + handleConfig: [ + { type: "target", position: "left" }, + { type: "source", position: "right" }, + ], + validate: (value) => { + return ["accept", "deny"].includes(value.toLowerCase()); + } + }, + tagNode: { + placeholder: "Enter tag name", + width: "220px", + handleConfig: [ + { type: "target", position: "left" }, + { type: "source", position: "right" }, + ], + validate: (value) => { + return value.match(/^tag:[a-zA-Z0-9_-]+$/); + } + } +}; -export const DestinationNode = ({ data, isConnectable }) => ( -
- - -
-); +const BaseNode = ({ type, data, isConnectable, style = {} }) => { + const nodeConfig = NodeTypes[type]; + + return ( +
+ {nodeConfig.handleConfig.map(({ type, position }) => ( + + ))} + +
+ ); +}; -export const ActionNode = ({ data, isConnectable }) => ( -
- - - -
+export const SourceNode = (props) => ; +export const DestinationNode = (props) => ( + ); +export const ActionNode = (props) => ; +export const TagNode = (props) => ; diff --git a/src/Sidebar.js b/src/Sidebar.js index 81c5bf1..a1ba170 100644 --- a/src/Sidebar.js +++ b/src/Sidebar.js @@ -172,7 +172,7 @@ const Sidebar = ({ nodes, edges, onACLUpdate }) => { return (
-

Tailscale ACL JSON

+

Tailscale ACL HuJSON

diff --git a/src/components/ImportExportButtons.js b/src/components/ImportExportButtons.js new file mode 100644 index 0000000..524f56a --- /dev/null +++ b/src/components/ImportExportButtons.js @@ -0,0 +1,32 @@ +import React from 'react'; + +const ImportExportButtons = ({ onImport, onExport }) => { + const handleImport = async () => { + try { + const [fileHandle] = await window.showOpenFilePicker({ + types: [ + { + description: 'ACL JSON Files', + accept: { + 'application/json': ['.json', '.hujson'] + } + } + ] + }); + const file = await fileHandle.getFile(); + const content = await file.text(); + onImport(content); + } catch (err) { + console.error('Error importing file:', err); + } + }; + + return ( +
+ + +
+ ); +}; + +export default ImportExportButtons; \ No newline at end of file diff --git a/src/components/SearchBar.js b/src/components/SearchBar.js new file mode 100644 index 0000000..6aca3ed --- /dev/null +++ b/src/components/SearchBar.js @@ -0,0 +1,16 @@ +import React from 'react'; + +const SearchBar = ({ onSearch }) => { + return ( +
+ onSearch(e.target.value)} + className="search-input" + /> +
+ ); +}; + +export default SearchBar; \ No newline at end of file diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..41151c3 --- /dev/null +++ b/src/constants.js @@ -0,0 +1,21 @@ +export const NODE_TYPES = { + SOURCE: "sourceNode", + DESTINATION: "destinationNode", + ACTION: "actionNode", + TAG: "tagNode", +}; + +export const NODE_LABELS = { + [NODE_TYPES.SOURCE]: "Source", + [NODE_TYPES.DESTINATION]: "Destination", + [NODE_TYPES.ACTION]: "Action", + [NODE_TYPES.TAG]: "Tag", +}; + +export const TAG_PREFIXES = { + USER: "", + GROUP: "group:", + TAG: "tag:", +}; + +export const VALID_ACTIONS = ["accept", "deny"]; diff --git a/src/index.js b/src/index.js index 7e8f5d0..9cdc24f 100644 --- a/src/index.js +++ b/src/index.js @@ -3,13 +3,10 @@ import ReactDOM from "react-dom/client"; import "./index.css"; import "./App.css"; import App from "./App"; -import reportWebVitals from "./reportWebVitals"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( - , + ); - -reportWebVitals(); diff --git a/src/utils/aclValidation.js b/src/utils/aclValidation.js new file mode 100644 index 0000000..97cb473 --- /dev/null +++ b/src/utils/aclValidation.js @@ -0,0 +1,33 @@ +const validateACLSyntax = (parsedAcl) => { + const errors = []; + + // Validate users and groups syntax + if (parsedAcl.acls) { + parsedAcl.acls.forEach((rule, index) => { + // Validate source format (user/group) + if (!rule.src.every(src => + src.match(/^([a-zA-Z0-9_.-]+@[a-zA-Z0-9_.-]+|group:[a-zA-Z0-9_-]+)$/) + )) { + errors.push(`Invalid source format in rule ${index}`); + } + + // Validate action + if (!['accept', 'deny'].includes(rule.action)) { + errors.push(`Invalid action in rule ${index}: must be 'accept' or 'deny'`); + } + + // Validate destination format + if (rule.dst) { + rule.dst.forEach(dst => { + if (!dst.match(/^[0-9.:/*]+$/)) { + errors.push(`Invalid destination format in rule ${index}`); + } + }); + } + }); + } + + return errors; +}; + +export { validateACLSyntax }; \ No newline at end of file