From a32321ada7a8f38a790cb04ef6667fd0306e927a Mon Sep 17 00:00:00 2001
From: Jared Hendrickson <jaredhendrickson13@gmail.com>
Date: Wed, 10 Mar 2021 09:10:18 -0700
Subject: [PATCH 01/10] Restructured APIUserCreateModel to fix #95 bug that
 prevent password from being set

---
 .../etc/inc/api/framework/APIResponse.inc     |  30 +++++
 .../etc/inc/api/models/APIUserCreate.inc      | 111 ++++++++++++++----
 .../__pycache__/__init__.cpython-38.pyc       | Bin 0 -> 8765 bytes
 3 files changed, 121 insertions(+), 20 deletions(-)
 create mode 100644 tests/unit_test_framework/__pycache__/__init__.cpython-38.pyc

diff --git a/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc b/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc
index 441a7b461..8c2836faa 100644
--- a/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc
@@ -1726,6 +1726,36 @@ function get($id, $data=[], $all=false) {
             "return" => $id,
             "message" => "Authentication server name already in use"
         ],
+        5036 => [
+            "status" => "bad request",
+            "code" => 400,
+            "return" => $id,
+            "message" => "Invalid characters in username"
+        ],
+        5037 => [
+            "status" => "bad request",
+            "code" => 400,
+            "return" => $id,
+            "message" => "Username is reserved by the system"
+        ],
+        5038 => [
+            "status" => "bad request",
+            "code" => 400,
+            "return" => $id,
+            "message" => "Username cannot contain more than 32 characters"
+        ],
+        5039 => [
+            "status" => "bad request",
+            "code" => 400,
+            "return" => $id,
+            "message" => "Invalid characters is IPsec PSK"
+        ],
+        5040 => [
+            "status" => "bad request",
+            "code" => 400,
+            "return" => $id,
+            "message" => "User expiration date must be in MM/DD/YYYY format"
+        ],
         //6000-6999 reserved for /routing API calls
         6000 => [
             "status" => "bad request",
diff --git a/pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc b/pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc
index 4a93d5bcf..9c83ee2e5 100644
--- a/pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc
@@ -25,57 +25,128 @@ class APIUserCreate extends APIModel {
     }
 
     public function action() {
+        # Increase the system's next UID by one and add our user to the configuration
+        $this->config["system"]["nextuid"] = strval(intval($this->validated_data["uid"]) + 1);
         $this->config['system']['user'][] = $this->validated_data;
-        $this->config["system"]["nextuid"] = strval(intval($this->validated_data["uid"]) + 1);   // Increase our next UID
-        local_user_set_password($this->validated_data, $this->validated_data["password"]);  // Set our new user's password
-        local_user_set($this->validated_data);
+
+        # Write the user to configuration and set the user on the backend. Return response with created user object.
         $this->write_config();
-        $userindex = index_users();    // Update our user index
+        local_user_set($this->validated_data);
         return APIResponse\get(0, $this->validated_data);
     }
 
-    public function validate_payload() {
-        $this->validated_data["uid"] = $this->config["system"]["nextuid"];    // Save our next UID
+    private function __validate_username() {
+        # Check for our required `username` payload value
         if (isset($this->initial_data['username'])) {
-            // Check that our user already exists
-            if (array_key_exists($this->initial_data['username'], index_users())) {
-                $this->errors[] = APIResponse\get(5002);
+            # Ensure a user with this username does not already exist
+            if (!array_key_exists($this->initial_data['username'], index_users())) {
+                # Ensure the username does not contain invalid characters
+                if (!preg_match("/[^a-zA-Z0-9\.\-_]/", $this->initial_data['username'])) {
+                    # Ensure username is not reserved by the system
+                    if (!$this->is_username_reserved($this->initial_data["username"])) {
+                        # Ensure username is not longer that 32 characters
+                        if (strlen($this->initial_data["username"]) > 32) {
+                            $this->validated_data["name"] = $this->initial_data['username'];
+                        } else {
+                            $this->errors[] = APIResponse\get(5038);
+                        }
+                    } else {
+                        $this->errors[] = APIResponse\get(5037);
+                    }
+                } else {
+                    $this->errors[] = APIResponse\get(5036);
+                }
             } else {
-                $this->validated_data["name"] = trim($this->initial_data['username']);
+                $this->errors[] = APIResponse\get(5002);
             }
         } else {
             $this->errors[] = APIResponse\get(5000);
         }
+    }
 
+    private function __validate_password() {
+        # Check for our required `password` payload value
         if (isset($this->initial_data['password'])) {
-            $this->validated_data["password"] = trim($this->initial_data['password']);
+            # Generate the password hash and add it to our validated data
+            local_user_set_password($this->validated_data, $this->initial_data['password']);
         } else {
             $this->errors[] = APIResponse\get(5003);
         }
+    }
 
+    private function __validate_disabled() {
+        # Check for our optional `disabled` payload value
         if ($this->initial_data["disabled"] === true) {
-            $this->validated_data["disabled"] = "";    // Update our user's disabled value if not false
-        } elseif ($this->initial_data["disabled"] === false) {
-            unset($this->validated_data["disabled"]);    // Unset our disabled value if not requested
+            $this->validated_data["disabled"] = "";
         }
+    }
 
+    private function __validate_descr() {
+        # Check for our optional `descr` payload value
         if (isset($this->initial_data['descr'])) {
-            $this->validated_data["descr"] = trim($this->initial_data['descr']);    // Update our user's full name
+            $this->validated_data["descr"] = $this->initial_data['descr'];
         }
+    }
 
+    private function __validate_expires() {
+        # Check for our optional `expires` payload value
         if (isset($this->initial_data['expires'])) {
-            $this->validated_data["expires"] = trim($this->initial_data['expires']);    // Update our user's expiration date
+            # Try to format the date string, return an error if the format is invalid
+            try {
+                $this->validated_data["expires"] = (new DateTime($this->initial_data['expires']))->format("m/d/Y");
+            } catch (Exception $e) {
+                $this->errors[] = APIResponse\get(5040);
+            }
         }
+    }
 
+    private function __validate_authorizedkeys() {
+        # Check for our optional `authorizedkeys` payload value
         if (isset($this->initial_data['authorizedkeys'])) {
-            $this->validated_data["authorizedkeys"] = trim($this->initial_data['authorizedkeys']);    // Update our user's authorized keys
+            $this->validated_data["authorizedkeys"] = $this->initial_data['authorizedkeys'];
         }
+    }
 
+    private function __validate_ipsecpsk() {
+        # Check for our optional `ipsecpsk` payload value
         if (isset($this->initial_data['ipsecpsk'])) {
-            $this->validated_data["ipsecpsk"] = trim($this->initial_data['ipsecpsk']);    // Update our user's IPsec pre-shared key
+            # Ensure the PSK does not contain invalid characters
+            if (preg_match('/^[[:ascii:]]*$/', $_POST['ipsecpsk'])) {
+                $this->validated_data["ipsecpsk"] = $this->initial_data['ipsecpsk'];
+            } else {
+                $this->errors[] = APIResponse\get(5039);
+            }
         }
+    }
+
+    public function validate_payload() {
+        # Set static object values
+        $this->validated_data["uid"] = $this->config["system"]["nextuid"];
+        $this->validated_data["scope"] = "user";
+        $this->validated_data["priv"] = [];
 
-        $this->validated_data["scope"] = "user";    // Set our new user's system scope
-        $this->validated_data["priv"] = [];    // Default our privs to empty array
+        # Run each validation method
+        $this->__validate_username();
+        $this->__validate_password();
+        $this->__validate_descr();
+        $this->__validate_disabled();
+        $this->__validate_expires();
+        $this->__validate_authorizedkeys();
+        $this->__validate_ipsecpsk();
     }
+
+    public function is_username_reserved($user) {
+        # Open the /etc/passwd file to read all system users
+        $etc_passwd = explode(PHP_EOL, file_get_contents("/etc/passwd"));
+
+        # Loop through each system user and check if the username is reserved
+        foreach ($etc_passwd as $sys_user_ent) {
+            $sys_username = explode(":", $sys_user_ent)[0];
+            if ($sys_username == $user) {
+                return true;
+            }
+        }
+        return false;
+    }
+
 }
\ No newline at end of file
diff --git a/tests/unit_test_framework/__pycache__/__init__.cpython-38.pyc b/tests/unit_test_framework/__pycache__/__init__.cpython-38.pyc
new file mode 100644
index 0000000000000000000000000000000000000000..fddbd945b2018f477866dea9d56eb155fad860de
GIT binary patch
literal 8765
zcmcgxS##W0cE$zJX!OQ?(MrMA+8UA6VofHB;#jswk75s{5y{AgqO_sDAc<x-x_yC`
zOsG4RgsN03RjQh&<i)8X-=eoXrBXF7^O)a&O66hZ$$vqaD(5>F=w@?~?aHLwT{w5e
zUCurCobMd0mP&aAzkm40#V`J4PEmeGg~6YO!UuQ)AA~EM)fGEQnayNbwN;sFwkA{E
z)@7QpGcq-7L#A0fi&U+fjhvlhimTM}wL-1v=D7BXG38^0>pb&P;hAmKE^)T3l#OpN
zr?OOL@zgy(aI0<MK6JlqyFvKK5x(cImzix=ozPuximfX39sKoIUStYpqt)7~r`VdS
zyO|whSJ`G*5k}yujWyW$b;T~=DdH*N8NxG+X9UkEp0Q+o_Bc0q_N8J^aFgedPVzi2
zAf4hxUP3y}hxjnkV|;{<B0bK>_&CxNe1cCRJ;|r|G}2T27(b5mG(W*lBAwx<_-UkP
z_zXXT)N<e97Jug@t@B;v-{ogz{w(t6_<5N>hx`S8QRdI{8@$Xfzhw3W{u6!$BV6R~
z@i{(^noImDzlNGJzYf0Mj0<-j+*|d%a0R@Km2bcxMG&bH>HR2g;`s<q@HI$B*->^C
zhP1|NDreJ4PMgNDPAj1<>oeO)y+N%NL)K*_HM#b+in?5M6(i-7k#-7cT1?XtrQy)d
zFy0y2g<w2V{25&dM>q50pBUPbR<2Xq=Zdd>264ouGD~l?Tl4d8*sa%Ul&jV~*AHhs
z9u?DU*xYn|k-;{~WARv}5`>NjD~?!iIU;Z?l~_kUh>f7S;Wpe@-)Mq0S`dm@Z#4z7
zZBdWScHj!%LA%*<g5Y^m@VMx-!;MPI*{U}kPMzt!br+=74r7D6bvJY|(pC`b?pIzI
z=ZIFSP432{pSW93n~vc3{LmGm-3ni`SP$I#+G|Ce##-NeKEH}7gZbKq>vQ2%H-n}>
z|Io!OUf2|#8_a*~g&Xas^R2bO#U+__THbt!V-4opIN%CpD{BHv#$q?;D;1AwD|4-_
z*z6Uq&;d_@D9mJ6*o3AcP5vWnh(!|v(lXb3X`iU5csGkjipm3IaCpjH<;mSH!%?&3
zXlv?@hLX-%hwZ9#d?;mP8{09s%C%iQo2oFv?2d^Ma@6NbXb&8(z7%U8FRb8vLMLno
z-~1;gDd*!H7%lf0#Mzc`E5!E_)d@FfHNg*@sMBhJcX6@dY`UnI3IykebE58I0wcwG
z=;2T^LEUv*ah?hl9GtWDS{0|Ly&(ArQE?i9JeY*cpiK_&C&&av5Jk;{brZ}~M_4pG
z$T-aP<MT{^aDRD4yhF=;7X%#3%Z*D30TaM7DX>Gbw$C(if!??X66a(K5xG%Ork2Y@
zenNy~=O9DIQMZ8SFhfQL8A4<J_#Dw5tnOz>4%c*0gy`0msO~6kBw~ZsAIywNFk<OQ
zX6%y=4Ef{p!nnKe>B7pwKKUpn3M=JEo+KnyVIyhFQG6*#DM>o~VAhbzd2}Re#s=ji
z8Fe3b$TZO-^^J}U>4?{CDUrmJs5mH-S5fES2}o5b2XtER8hrm8;!pAR+h@6sR*t7P
z%jDob`WNXBp5`2;SRa^XeBU&Q8vKJNxr7O71C#8t0&*`0HQJG<@;w~8i5VILvmDyW
z!He;KndFZ!L2F==!&~|PnB^ADFfhy1!BhF6g8V%!yNx-X4a_sWZyqvh=|qHpdp`m`
z1w8V`7+@k<=o;e;_V#*5-BCHP@vhoIdRrlSTp?>+lQ7Fux|;uC`UY9(&p3fSrIz6t
z2?CWG;GW0O>r~pyK(7#p8PIPhyQ?Iv(OhWOavflTT0Si76!+%cWt=R&QY&>do_l3<
zb-=z40Q=0^P;FS!ku?<%@V6-h9L2a}7)j;%+IaFNFT7&A+O~v+qz^u$Jx+A=4nQYH
zp5(<>+OL>UJ34{R<pegCmf}pkS#|2b4=IGpNT_I+?f};TVnt5qHT^ftinZGFFv@=D
zI>Hs!VmTv*!O&RePUytOGgo+PTXB}KLlY(>yWu(<$Spdy@Kx1yxy!Az$!e}yFT!pB
zzlMb%S-kqR8Mx&sLB3d=lYp`Td?+<(Y_waL!WCm^6eC1NsoQX+2WBc*7XYl8VjDoA
zG?y;02Fw^|c)QUG1leUf?_x<U+E!o>ebn@Qw;HY_>$VFIvF@}GXIjGZ!+5AtS!;@h
z6IL3*I%YDGQal8#T;1#&O7U|H4z@_dWHel`kBeDa@FFc(!>SIdWr7%6cml!^idtkN
z_!~%1P#!>3XY`DK8ccnYH&3t`V5=z>9UGMTL1=eK@%4Qe__s<l1i_JB%p*9n%L+tH
zdNUBMONnTG4sV8pN5W2RX=Smj2{Pbumb(Png+2V|%lDV=%EE#WO;KiI6&()jEgs!)
zf(r4z&!B%suR!|S5@H){j76sh_i^Zrsx()4jee9j@W?w%o9SSYo0_-~lBBb3hLqtZ
zGp_Ec+jO%pa1C;<r?Q?|+OzbutyrVOEhLCqIwVryfO+M1<jIi}l5C8yZahsZtTu&!
z#8fuIutk0p@c0fI16rG+y~}hpI(u+eN4#@rZ=|F8dkf(=bd+5tF5tk_T^!bN+MSl{
z?ufm`hH%5S@TDV*?dgK_pCEuC&o6-`HWj~XQ^_Eb8~=*FKWI|}@4S6e1gS|8kvp_^
zOyoIlDq`lqLB%;jSryV4(r1;&m4!#r3S1tS!e;2yD+q}+eIC4hW0QlY)gSQR_O^9`
z&gz}FomKyh#VTQ}-jCdzzO(oDFeRLIb$MKDmj&0kT2ni$rcqd@o1v({P6lP8mhEIn
zESciJL2r|k_pTZ|LtCzEz!c;uz_P1$v)sT4*$%=iuhau0s2D@4c>muqMj;*JH{d7Y
z2i!cabWJ%DLNgdC2jif2avk$KN2+6_QZmx7STKb3Ao{SYbn|kg0xx2u0xw~tLMOjB
zQmwZutcgNCuXMS$85sxZw^0zN*T%=XdWTF<E7bXTI6^z8XbHWCnh9AW=cZad*>9Ob
zPi<$E%qaEk=vZgOi#{3QF+^0xI=JHH>7~W!^1@dwTum4o(ot3qs$W5_>Uz&m{{k`-
zA~w<`%ai8!)IxNjKR|M^EmC>b3%5Hsgy@#ln7J_%;x8c#^2QCknOujryea;Q-k_@<
zUF*-)Tc<RU)^kKOJf9{F_Rk8-_PiJQi*>S)(dfg}(YL(7dgjzU9;?mG71S>*Ke)fN
zykIS_+*w&&zGX$j{i>CFzgW0s#bDxI-Q7DYcWznb@d2&zkXHJX$O9t3B=QR)D?}a>
z`J4zv5X2KAWG93}q(bBiA|((C14-L)9EEaGTF3z<OLo)FB&*k8V(sDHai!|j9(L1n
z?N{`BB-|e8kyeZi^hi~SJ=Pzlf42`PL$Qpt{0E-kryz=|Yk7o#rdSd7w}{*^Q2JAD
zh~;JOlnOnfku^KP##Gp}6N4IMKStPRok>CWt@D4OK+654(uE-dG$r^6?Z|aJ8R!y#
zQ)m@v1>6<fZ=QdZ!Cjy_+|dr>CAx)t<Q34ZtR3ZJbkF$2x=Nu}KGkoT=Eu--oC2@w
z8q!k(ZKpYIFh4_~*Z%kjy`m05*MT~OUBe8&gpliAzsnSO<+BuYP2Qz2Gwvq>-?*Pw
zm-cO*tU0MwVt8-~gsk{BL&S`Qz~x-sRyVWaT!^R{Siz=X3;zynG8QK9kN54jxJ2di
z0f{~!C*exr`0MT!tGdwyptRORvtd2HHv7|?H*eg00*m+=rinAMQ*8EPpZ4e-vEFV#
zC#-C>+yv36b`j#(b|G#+fa@^{XWKm}D?=FWziIh&h!i{=!auq)c>0F}t#h{}-VWZM
zqqu4``4M4AtF^XF;Y$neUJR^@(fNxOf_AMY5Q+t~31uQNhqc!Bs{~we)5EPfd;lnL
zH>e6vqS?rpou#;~AQQC`O|5LW7L~2Ai2)?cMYgO1vs2((53%0_HhThQRo`&yEgFLs
zCTMfsuVLPzGc*p>(Kv_zTh9@U?F|U-M7b9!kGnv>KHd{e5CV*APP-n8FHy3y0L0ES
zM?~iAY!YM^<dE4q!6-XJG>FdARI3j^wUQohnQ(9xJB!2is%{_zO-xfoh1uCXAL1TW
zzfa@?A|HZ8XK5O++M;eacn@Duz*LJ=YC7j!l%JjLg}bHOVP_oP@cif$MWiVRPCW+(
za4jd8gOK$ZFt8$w)eD$Q@5Roh@#^UGgEWwSXf!edQEqlNb(^H?@(rDAboQg<FlotY
zt5x?JUTDE=q^OAAOdTufm)Q9L0duz!inhBLP2Kf^7UH{B+J){9VgTvt5i^!TCR)TX
zVrkhFbtLvs@2WhYuLeRoSa#9joW3N;J0$hJyl8el`LuwRKzep?B*-<Al4TpRQ5?Kd
zIdmXR82k;0qMC-zir*R<xk2gwe~j!v2~Lq=_R4UP_La>8Wwh>p`+G{!%y(a9ID(E>
zcX^e}dGANiu&>lNP#|5#H3D~E>)lL8=M)<_#j{<*KZlZu(q#?^=ue@PN9kw0fRY(1
zytqv|&j9#h9Y89005A!FWg4k^L@P9?dzc5nM+16sbSWyHe0<}210M=bKEB$BN+{iE
zPzYQWsqwuA;=i&qjMBA65&@+OoV|tBia8=Ti2RI58zdT+eHZWC`*h*1H9Px0^%oR@
zlP%X9_dh``{gM{9*K(ur=+48XdrKc%NlSEd)!+1+&wc9)x@uooEzx}D$=E=X?qlzp
z9KNy)I^5Be_1JpR=vpt@UF*r?7k>B2E$fBo#yVZYsJQA=iU0$&8nhd0#H<}NUr;C#
z<@{fe@|KESNK8X<3yE6F*+e2Gpdo*coTz|Q)*2N^Wx<CJ1jPXlO6o6BcMj??Ajd{g
zQ|bJ@VVZyn>7$x}1qK}WJX3#f8vB94{yiSDjeQyVYYZPfm5_iUM`Z8;3uKO<0X{#5
z8CfzUY(`!B{YXWuiTdXudHCMM>0_i?D*(c0$jVY&id+VsJB~243<VSs2KcHU2FUlr
z0EMMQEXczZB;e7+Jbh?;c5U9NLiYxh<ntn}nLJO)U~*-}T_OuaWbp&ak<~Ak#HW;(
zc#=W_(ilj?m1qz$KM9_^Pq`5yqeN)1I7dMxY4gg-7pkKo5aJVb{WCfuinJ?G2Bau1
z0L>wqf~!A9*81dru}dRINk|Ew-WL(vpes<mOy3Jh%n<*{S1J(ScAfIYO6AM8Q%_o?
znoAI`-fnx`&Jcai)<6@c$d5*Zg2h=P^F-bw^2bEZ6S+o&(5IlUYT^tL39-j1H%Wx{
zBd!v;PUIYsOGIuGp>VIDXsozR<RVC1BzMNEra>tg6p#R3!gh(+35N+%zX{MM-VKPn
z3>nN&9f4N*!VoNhm@4!$*m}fYGl$K*nZrMCY6t!^D$ARAvb`sN&ir})q#V0Elt>2I
zn?wNqmU7b|aBb)dwYLuVBR|C1WB4jv_nzLE+l%N40+u$uPgggcbvLlHX%~Bhdx7&5
jhnD(KLE`G{1w;CKjLqB0`Mytr91!z+AB1};Md$t(L4OW`

literal 0
HcmV?d00001


From f212b0e3a4a99190890b6debf2e7a77dfb5f600f Mon Sep 17 00:00:00 2001
From: Jared Hendrickson <jaredhendrickson13@gmail.com>
Date: Wed, 10 Mar 2021 09:10:39 -0700
Subject: [PATCH 02/10] Removed pycache

---
 .../__pycache__/__init__.cpython-38.pyc          | Bin 8765 -> 0 bytes
 1 file changed, 0 insertions(+), 0 deletions(-)
 delete mode 100644 tests/unit_test_framework/__pycache__/__init__.cpython-38.pyc

diff --git a/tests/unit_test_framework/__pycache__/__init__.cpython-38.pyc b/tests/unit_test_framework/__pycache__/__init__.cpython-38.pyc
deleted file mode 100644
index fddbd945b2018f477866dea9d56eb155fad860de..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 8765
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


From da6665ad432811c39cd73f63efb63490c1f72951 Mon Sep 17 00:00:00 2001
From: Jared Hendrickson <jaredhendrickson13@gmail.com>
Date: Wed, 10 Mar 2021 09:11:20 -0700
Subject: [PATCH 03/10] Fixed issue in APIResponse.inc that prevent the default
 error response from being set

---
 pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc b/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc
index 8c2836faa..e95272168 100644
--- a/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc
@@ -1934,7 +1934,7 @@ function get($id, $data=[], $all=false) {
 
 
     ];
-    $response = $responses[(!in_array($id, $responses)) ? $id : 1];
+    $response = $responses[(!array_key_exists($id, $responses)) ? $id : 1];
     $response["data"] = $data;
     if ($all === true) {
         $response = $responses;

From f1cbd1e56dbb7379eb96e5d2f3a43e4ec9993598 Mon Sep 17 00:00:00 2001
From: Jared Hendrickson <jaredhendrickson13@gmail.com>
Date: Wed, 10 Mar 2021 10:32:02 -0700
Subject: [PATCH 04/10] Fixed issue in APITools.inc that prevented the full
 privilege list from being parsed

---
 pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc | 2 +-
 pfSense-pkg-API/files/etc/inc/api/framework/APITools.inc    | 1 +
 pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc  | 2 +-
 3 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc b/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc
index e95272168..81b5084d2 100644
--- a/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc
@@ -1934,7 +1934,7 @@ function get($id, $data=[], $all=false) {
 
 
     ];
-    $response = $responses[(!array_key_exists($id, $responses)) ? $id : 1];
+    $response = $responses[(array_key_exists($id, $responses)) ? $id : 1];
     $response["data"] = $data;
     if ($all === true) {
         $response = $responses;
diff --git a/pfSense-pkg-API/files/etc/inc/api/framework/APITools.inc b/pfSense-pkg-API/files/etc/inc/api/framework/APITools.inc
index 1eea4a4e9..847cc9174 100644
--- a/pfSense-pkg-API/files/etc/inc/api/framework/APITools.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/framework/APITools.inc
@@ -23,6 +23,7 @@ require_once("util.inc");
 require_once("interfaces.inc");
 require_once("interfaces_fast.inc");
 require_once("priv.defs.inc");
+require_once("priv.inc");
 require_once("service-utils.inc");
 require_once("filter.inc");
 require_once("shaper.inc");
diff --git a/pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc b/pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc
index 9c83ee2e5..e3d8b5a33 100644
--- a/pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc
@@ -45,7 +45,7 @@ class APIUserCreate extends APIModel {
                     # Ensure username is not reserved by the system
                     if (!$this->is_username_reserved($this->initial_data["username"])) {
                         # Ensure username is not longer that 32 characters
-                        if (strlen($this->initial_data["username"]) > 32) {
+                        if (strlen($this->initial_data["username"]) <= 32) {
                             $this->validated_data["name"] = $this->initial_data['username'];
                         } else {
                             $this->errors[] = APIResponse\get(5038);

From 319b3e04a2896aac6c2e1ba3882436bfe7d9740e Mon Sep 17 00:00:00 2001
From: Jared Hendrickson <jaredhendrickson13@gmail.com>
Date: Wed, 10 Mar 2021 11:02:00 -0700
Subject: [PATCH 05/10] Added ability to create user with privileges

---
 .../etc/inc/api/models/APIUserCreate.inc      | 30 ++++++++++++++++---
 1 file changed, 26 insertions(+), 4 deletions(-)

diff --git a/pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc b/pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc
index e3d8b5a33..4bed0df73 100644
--- a/pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc
@@ -74,6 +74,29 @@ class APIUserCreate extends APIModel {
         }
     }
 
+    private function __validate_priv() {
+        global $priv_list;
+        $this->validated_data["priv"] = [];
+
+        # Check for our optional `priv` payload value
+        if ($this->initial_data["priv"]) {
+            # Ensure value is an array
+            if (!is_array($this->initial_data["priv"])) {
+                $this->initial_data["priv"] = array($this->initial_data["priv"]);
+            }
+
+             # Loop through each requested privilege and ensure it exists
+            foreach ($this->initial_data["priv"] as $priv) {
+                if (array_key_exists($priv, $priv_list)) {
+                    $this->validated_data["priv"][] = $priv;
+                } else {
+                    $this->errors[] = APIResponse\get(5006);
+                    break;
+                }
+            }
+        }
+    }
+
     private function __validate_disabled() {
         # Check for our optional `disabled` payload value
         if ($this->initial_data["disabled"] === true) {
@@ -123,11 +146,11 @@ class APIUserCreate extends APIModel {
         # Set static object values
         $this->validated_data["uid"] = $this->config["system"]["nextuid"];
         $this->validated_data["scope"] = "user";
-        $this->validated_data["priv"] = [];
 
         # Run each validation method
         $this->__validate_username();
         $this->__validate_password();
+        $this->__validate_priv();
         $this->__validate_descr();
         $this->__validate_disabled();
         $this->__validate_expires();
@@ -137,10 +160,10 @@ class APIUserCreate extends APIModel {
 
     public function is_username_reserved($user) {
         # Open the /etc/passwd file to read all system users
-        $etc_passwd = explode(PHP_EOL, file_get_contents("/etc/passwd"));
+        $sys_users = explode(PHP_EOL, file_get_contents("/etc/passwd"));
 
         # Loop through each system user and check if the username is reserved
-        foreach ($etc_passwd as $sys_user_ent) {
+        foreach ($sys_users as $sys_user_ent) {
             $sys_username = explode(":", $sys_user_ent)[0];
             if ($sys_username == $user) {
                 return true;
@@ -148,5 +171,4 @@ class APIUserCreate extends APIModel {
         }
         return false;
     }
-
 }
\ No newline at end of file

From 8fa65ebdb0be13d4e5a989d29ab221ab08150e0e Mon Sep 17 00:00:00 2001
From: Jared Hendrickson <jaredhendrickson13@gmail.com>
Date: Wed, 10 Mar 2021 11:42:18 -0700
Subject: [PATCH 06/10] Restructured APIUserUpdateModel to fix #95 bug that
 prevent password from being set

---
 .../etc/inc/api/models/APIUserCreate.inc      |   2 +-
 .../etc/inc/api/models/APIUserUpdate.inc      | 126 ++++++++++++++----
 2 files changed, 102 insertions(+), 26 deletions(-)

diff --git a/pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc b/pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc
index 4bed0df73..a024e6074 100644
--- a/pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc
@@ -126,7 +126,7 @@ class APIUserCreate extends APIModel {
     private function __validate_authorizedkeys() {
         # Check for our optional `authorizedkeys` payload value
         if (isset($this->initial_data['authorizedkeys'])) {
-            $this->validated_data["authorizedkeys"] = $this->initial_data['authorizedkeys'];
+            $this->validated_data["authorizedkeys"] = base64_encode($this->initial_data['authorizedkeys']);
         }
     }
 
diff --git a/pfSense-pkg-API/files/etc/inc/api/models/APIUserUpdate.inc b/pfSense-pkg-API/files/etc/inc/api/models/APIUserUpdate.inc
index 78e905c8f..c749aad6a 100644
--- a/pfSense-pkg-API/files/etc/inc/api/models/APIUserUpdate.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/models/APIUserUpdate.inc
@@ -25,52 +25,128 @@ class APIUserUpdate extends APIModel {
     }
 
     public function action() {
-        local_user_set($this->validated_data);
+        # Update our new user in the config and set the user on the backend
+        $this->config["system"]["user"][$this->id] = $this->validated_data;
         $this->write_config();
+        local_user_set($this->validated_data);
         return APIResponse\get(0, $this->validated_data);
     }
 
-    public function validate_payload() {
+    private function __validate_username() {
+        # Check for our required `username` payload value
         if (isset($this->initial_data['username'])) {
-            $this->validated_data =& getUserEntry($this->initial_data['username']);
-            // Check that our user already exists
-            if (!array_key_exists("uid", $this->validated_data)) {
+            # Loop through each configured user and check if this user exists
+            foreach ($this->config["system"]["user"] as $id=>$user) {
+                if ($this->initial_data["username"] === $user["name"]) {
+                    $this->validated_data = $user;
+                    $this->id = intval($id);
+                }
+            }
+            # Set an error if no user was found
+            if (!isset($this->validated_data["uid"])) {
                 $this->errors[] = APIResponse\get(5001);
             }
         } else {
             $this->errors[] = APIResponse\get(5000);
         }
+    }
+
+    private function __validate_password() {
+        # Check for our optional `password` payload value
         if (isset($this->initial_data['password'])) {
-            $password = $this->initial_data['password'];
-            $password = trim($password);
-            local_user_set_password($this->validated_data, $password);  // Set our new user's password
+            # Generate the password hash and add it to our validated data
+            local_user_set_password($this->validated_data, $this->initial_data['password']);
         }
-        if ($this->initial_data["disabled"] === true) {
-            $this->validated_data["disabled"] = "";    // Update our user's disabled value if not false
-        } elseif ($this->initial_data["disabled"] === false) {
-            unset($this->validated_data["disabled"]);    // Unset our disabled value if not requested
+    }
 
+    private function __validate_priv() {
+        global $priv_list;
+
+        # Check for our optional `priv` payload value
+        if ($this->initial_data["priv"]) {
+            # Revert priv array to default
+            $this->validated_data["priv"] = [];
+
+            # Ensure value is an array
+            if (!is_array($this->initial_data["priv"])) {
+                $this->initial_data["priv"] = array($this->initial_data["priv"]);
+            }
+
+            # Loop through each requested privilege and ensure it exists
+            foreach ($this->initial_data["priv"] as $priv) {
+                if (array_key_exists($priv, $priv_list)) {
+                    $this->validated_data["priv"][] = $priv;
+                } else {
+                    $this->errors[] = APIResponse\get(5006);
+                    break;
+                }
+            }
         }
+    }
+
+    private function __validate_disabled() {
+        # Check for our optional `disabled` payload value
+        if ($this->initial_data["disabled"] === true) {
+            $this->validated_data["disabled"] = "";
+        } elseif($this->initial_data["disabled"] === false) {
+            unset($this->validated_data["disabled"]);
+        }
+    }
+
+    private function __validate_descr() {
+        # Check for our optional `descr` payload value
         if (isset($this->initial_data['descr'])) {
-            $descr = $this->initial_data['descr'];
-            $descr = trim($descr);
-            $this->validated_data["descr"] = $descr;    // Update our user's full name
+            $this->validated_data["descr"] = $this->initial_data['descr'];
         }
+    }
+
+    private function __validate_expires() {
+        # Check for our optional `expires` payload value
         if (isset($this->initial_data['expires'])) {
-            $expires = $this->initial_data['expires'];
-            $expires = trim($expires);
-            $this->validated_data["expires"] = $expires;    // Update our user's expiration date
+            # Attempt to format the expiration date if the value is not empty
+            if (!empty($this->initial_data["expires"])) {
+                # Try to format the date string, return an error if the format is invalid
+                try {
+                    $this->validated_data["expires"] = (new DateTime($this->initial_data['expires']))->format("m/d/Y");
+                } catch (Exception $e) {
+                    $this->errors[] = APIResponse\get(5040);
+                }
+            }
+            # Otherwise, if the value was blank, unset the expiration date
+            else {
+                unset($this->validated_data["expires"]);
+            }
         }
+    }
+
+    private function __validate_authorizedkeys() {
+        # Check for our optional `authorizedkeys` payload value
         if (isset($this->initial_data['authorizedkeys'])) {
-            $authorizedkeys = $this->initial_data['authorizedkeys'];
-            $authorizedkeys = trim($authorizedkeys);
-            $this->validated_data["authorizedkeys"] = $authorizedkeys;    // Update our user's authorized keys
+            $this->validated_data["authorizedkeys"] = base64_encode($this->initial_data['authorizedkeys']);
         }
+    }
+
+    private function __validate_ipsecpsk() {
+        # Check for our optional `ipsecpsk` payload value
         if (isset($this->initial_data['ipsecpsk'])) {
-            $ipsecpsk = $this->initial_data['ipsecpsk'];
-            $ipsecpsk = trim($ipsecpsk);
-            $this->validated_data["ipsecpsk"] = $ipsecpsk;    // Update our user's IPsec pre-shared key
+            # Ensure the PSK does not contain invalid characters
+            if (preg_match('/^[[:ascii:]]*$/', $_POST['ipsecpsk'])) {
+                $this->validated_data["ipsecpsk"] = $this->initial_data['ipsecpsk'];
+            } else {
+                $this->errors[] = APIResponse\get(5039);
+            }
         }
-        $this->validated_data["scope"] = "user";    // Set our new user's system scope
+    }
+
+    public function validate_payload() {
+        # Run each validation method
+        $this->__validate_username();
+        $this->__validate_password();
+        $this->__validate_priv();
+        $this->__validate_descr();
+        $this->__validate_disabled();
+        $this->__validate_expires();
+        $this->__validate_authorizedkeys();
+        $this->__validate_ipsecpsk();
     }
 }
\ No newline at end of file

From bf8e9f752f6d0ff2581211b7b73e18fa544c94b8 Mon Sep 17 00:00:00 2001
From: Jared Hendrickson <jaredhendrickson13@gmail.com>
Date: Wed, 10 Mar 2021 12:15:42 -0700
Subject: [PATCH 07/10] Restructured APIUserPrivilegeCreate and
 APIUserPrivilegeDelete to better match the other user endpoints

---
 .../etc/inc/api/models/APIUserCreate.inc      |  1 +
 .../inc/api/models/APIUserPrivilegeCreate.inc | 70 +++++++++++++------
 .../inc/api/models/APIUserPrivilegeDelete.inc | 68 +++++++++---------
 .../etc/inc/api/models/APIUserUpdate.inc      |  1 +
 4 files changed, 85 insertions(+), 55 deletions(-)

diff --git a/pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc b/pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc
index a024e6074..5a134a236 100644
--- a/pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/models/APIUserCreate.inc
@@ -89,6 +89,7 @@ class APIUserCreate extends APIModel {
             foreach ($this->initial_data["priv"] as $priv) {
                 if (array_key_exists($priv, $priv_list)) {
                     $this->validated_data["priv"][] = $priv;
+                    $this->validated_data["priv"] = array_unique($this->validated_data["priv"]);
                 } else {
                     $this->errors[] = APIResponse\get(5006);
                     break;
diff --git a/pfSense-pkg-API/files/etc/inc/api/models/APIUserPrivilegeCreate.inc b/pfSense-pkg-API/files/etc/inc/api/models/APIUserPrivilegeCreate.inc
index 76ec19af0..0e2eb140d 100644
--- a/pfSense-pkg-API/files/etc/inc/api/models/APIUserPrivilegeCreate.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/models/APIUserPrivilegeCreate.inc
@@ -25,42 +25,68 @@ class APIUserPrivilegeCreate extends APIModel {
     }
 
     public function action() {
-        local_user_set($this->validated_data["user_config"]);    // Set user backend parameters
+        $this->config["system"]["user"][$this->id]["priv"] = $this->validated_data["priv"];
         $this->write_config();
-        return APIResponse\get(0, $this->validated_data["user_config"]["priv"]);
+        local_user_set($this->validated_data);
+        return APIResponse\get(0, $this->validated_data["priv"]);
     }
 
     public function validate_payload() {
-        global $priv_list;
+        $this->__validate_username();
+        $this->__validate_priv();
+    }
+
+    private function __validate_username() {
+        # Check for our required `username` payload value
         if (isset($this->initial_data['username'])) {
-            $this->validated_data["username"] = trim($this->initial_data['username']);
-            $this->validated_data["user_config"] =& getUserEntry($this->validated_data["username"]);
-            if (!array_key_exists("uid", $this->validated_data["user_config"])) {
+            # Loop through each configured user and check if this user exists
+            foreach ($this->config["system"]["user"] as $id=>$user) {
+                if ($this->initial_data["username"] === $user["name"]) {
+                    $this->validated_data = $user;
+                    $this->id = intval($id);
+                }
+            }
+            # Set an error if no user was found
+            if (!isset($this->validated_data["uid"])) {
                 $this->errors[] = APIResponse\get(5001);
             }
         } else {
             $this->errors[] = APIResponse\get(5000);
         }
-        if (isset($this->initial_data['priv'])) {
-            // Ensure our new priv is array, if it is a string create an array containing the string
-            if (is_string($this->initial_data["priv"])) {
+    }
+
+    private function __validate_priv() {
+        global $priv_list;
+        $this->__init_config();
+
+        # Check for our optional `priv` payload value
+        if ($this->initial_data["priv"]) {
+            # Ensure value is an array
+            if (!is_array($this->initial_data["priv"])) {
                 $this->initial_data["priv"] = array($this->initial_data["priv"]);
             }
-            if (is_array($this->initial_data["priv"])) {
-                // Loop through our new priv list and check that the privs are valid
-                foreach ($this->initial_data["priv"] as $np) {
-                    if (!array_key_exists($np, $priv_list)) {
-                        $this->errors[] = APIResponse\get(5006);
-                    }
-                    if (!in_array($np, $this->validated_data["user_config"]["priv"])) {
-                        $this->validated_data["user_config"]["priv"][] = $np;
-                    }
+
+            # Loop through each requested privilege and ensure it exists
+            foreach ($this->initial_data["priv"] as $priv) {
+                if (array_key_exists($priv, $priv_list)) {
+                    $this->validated_data["priv"][] = $priv;
+                    $this->validated_data["priv"] = array_unique($this->validated_data["priv"]);
+                } else {
+                    $this->errors[] = APIResponse\get(5006);
+                    break;
                 }
-            } else {
-                $this->errors[] = APIResponse\get(5005);
             }
-        } else {
-            $this->errors[] = APIResponse\get(5004);
+        }
+    }
+
+    private function __init_config() {
+        # Initialize the priv array if the user does not already have one
+        if (empty($this->validated_data["priv"])) {
+            $this->validated_data["priv"] = [];
+        }
+        # If the user has a priv set, but as a string, convert it to an array
+        elseif (is_string($this->validated_data["priv"])) {
+            $this->validated_data["priv"] = array($this->validated_data["priv"]);
         }
     }
 }
\ No newline at end of file
diff --git a/pfSense-pkg-API/files/etc/inc/api/models/APIUserPrivilegeDelete.inc b/pfSense-pkg-API/files/etc/inc/api/models/APIUserPrivilegeDelete.inc
index ee94b63e6..8d6a4c319 100644
--- a/pfSense-pkg-API/files/etc/inc/api/models/APIUserPrivilegeDelete.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/models/APIUserPrivilegeDelete.inc
@@ -23,50 +23,52 @@ class APIUserPrivilegeDelete extends APIModel {
         $this->privileges = ["page-all", "page-system-usermanager-addprivs"];
         $this->change_note = "Deleted privileges for user via API";
     }
-
     public function action() {
-        $user_config =& getUserEntry($this->validated_data["username"]);
-        $user_id = index_users()[$this->validated_data["username"]];    // Save our user's array index ID
-        local_user_set($user_config);    // Set user backend parameters
-        $this->config["system"]["user"][$user_id] = $user_config;    // Add our new config
-        $this->write_config();    // Write to config
+        $this->config["system"]["user"][$this->id]["priv"] = $this->validated_data["priv"];
+        $this->write_config();
+        local_user_set($this->validated_data);
         return APIResponse\get(0, $this->validated_data["priv"]);
     }
 
     public function validate_payload() {
-        global $priv_list;
+        $this->__validate_username();
+        $this->__validate_priv();
+    }
+
+    private function __validate_username() {
+        # Check for our required `username` payload value
         if (isset($this->initial_data['username'])) {
-            $this->validated_data["username"] = $this->initial_data['username'];
-            $this->validated_data["username"] = trim($this->validated_data["username"]);
+            # Loop through each configured user and check if this user exists
+            foreach ($this->config["system"]["user"] as $id=>$user) {
+                if ($this->initial_data["username"] === $user["name"]) {
+                    $this->validated_data = $user;
+                    $this->id = intval($id);
+                }
+            }
+            # Set an error if no user was found
+            if (!isset($this->validated_data["uid"])) {
+                $this->errors[] = APIResponse\get(5001);
+            }
         } else {
             $this->errors[] = APIResponse\get(5000);
         }
-        if (isset($this->initial_data['priv'])) {
-            $this->validated_data["priv"] = $this->initial_data['priv'];
-        } else {
-            $this->errors[] = APIResponse\get(5004);
-        }
-        // Check if our user already exists, if so exit on non-zero
-        $user_config =& getUserEntry($this->validated_data["username"]);
-        if (!array_key_exists("uid", $user_config)) {
-            $this->errors[] = APIResponse\get(5002);
-        }
-        // Ensure our new priv is array, if it is a string create an array containing the string
-        if (is_string($this->validated_data["priv"])) {
-            $this->validated_data["priv"] = array($this->validated_data["priv"]);
-        }
-        if (is_array($this->validated_data["priv"])) {
-            // Loop through our new priv list and check that the privs are valid
-            foreach ($this->validated_data["priv"] as $dp) {
-                if (!array_key_exists($dp, $priv_list)) {
-                    $this->errors[] = APIResponse\get(5006);
-                }
-                if (in_array($dp, $user_config["priv"])) {
-                    $user_config["priv"] = \array_diff($user_config["priv"], array($dp));
+    }
+
+    private function __validate_priv() {
+        # Check for our optional `priv` payload value
+        if ($this->initial_data["priv"]) {
+            # Ensure value is an array
+            if (!is_array($this->initial_data["priv"])) {
+                $this->initial_data["priv"] = array($this->initial_data["priv"]);
+            }
+
+            # Loop through each of the user's stored privileges and remove it if matched
+            foreach ($this->validated_data["priv"] as $id=>$priv) {
+                # Check if this privilege is one that is being requested to remove
+                if (in_array($priv, $this->initial_data["priv"])) {
+                    unset($this->validated_data["priv"][$id]);
                 }
             }
-        } else {
-            $this->errors[] = APIResponse\get(5005);
         }
     }
 }
\ No newline at end of file
diff --git a/pfSense-pkg-API/files/etc/inc/api/models/APIUserUpdate.inc b/pfSense-pkg-API/files/etc/inc/api/models/APIUserUpdate.inc
index c749aad6a..c12bc2572 100644
--- a/pfSense-pkg-API/files/etc/inc/api/models/APIUserUpdate.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/models/APIUserUpdate.inc
@@ -76,6 +76,7 @@ class APIUserUpdate extends APIModel {
             foreach ($this->initial_data["priv"] as $priv) {
                 if (array_key_exists($priv, $priv_list)) {
                     $this->validated_data["priv"][] = $priv;
+                    $this->validated_data["priv"] = array_unique($this->validated_data["priv"]);
                 } else {
                     $this->errors[] = APIResponse\get(5006);
                     break;

From 5755860b83473e0c1253f3550d96c5b7810511e1 Mon Sep 17 00:00:00 2001
From: Jared Hendrickson <jaredhendrickson13@gmail.com>
Date: Wed, 10 Mar 2021 12:46:40 -0700
Subject: [PATCH 08/10] Restructured APIUserDelete model to fix bug that
 allowed system users to be deleted

---
 .../etc/inc/api/framework/APIResponse.inc     |  2 +-
 .../etc/inc/api/models/APIUserDelete.inc      | 37 ++++++++++++-------
 2 files changed, 25 insertions(+), 14 deletions(-)

diff --git a/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc b/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc
index 81b5084d2..1e7b5f23f 100644
--- a/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/framework/APIResponse.inc
@@ -1598,7 +1598,7 @@ function get($id, $data=[], $all=false) {
             "status" => "bad request",
             "code" => 400,
             "return" => $id,
-            "message" => "User privilege must be type array or string"
+            "message" => "System users cannot be deleted"
         ],
         5006 => [
             "status" => "bad request",
diff --git a/pfSense-pkg-API/files/etc/inc/api/models/APIUserDelete.inc b/pfSense-pkg-API/files/etc/inc/api/models/APIUserDelete.inc
index e38bdb178..e5d9e634f 100644
--- a/pfSense-pkg-API/files/etc/inc/api/models/APIUserDelete.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/models/APIUserDelete.inc
@@ -25,26 +25,37 @@ class APIUserDelete extends APIModel {
     }
 
     public function action() {
-        $index_id = index_users()[$this->validated_data["username"]];    // Save our user's index ID number
-        $del_user = $this->config["system"]["user"][$index_id];
-        local_user_del($this->config["system"]["user"][$index_id]);    // Delete our user on the backend
-        unset($this->config['system']['user'][$index_id]);    // Unset our user from config
-        $this->config['system']['user'] = array_values($this->config['system']['user']);    // Reindex our users
-        $this->write_config();    // Write our new config
-        return APIResponse\get(0, $del_user);
+        # Remove user from backend and remove from config
+        local_user_del($this->config["system"]["user"][$this->id]);
+        unset($this->config["system"]["user"][$this->id]);
+        $this->write_config();
+        return APIResponse\get(0, $this->validated_data);
     }
 
-    public function validate_payload() {
-        if (isset($this->initial_data["username"])) {
-            if (!array_key_exists($this->initial_data["username"], index_users())) {
+    private function __validate_username() {
+        # Check for our required `username` payload value
+        if (isset($this->initial_data['username'])) {
+            # Loop through each configured user and check if this user exists
+            foreach ($this->config["system"]["user"] as $id=>$user) {
+                if ($this->initial_data["username"] === $user["name"]) {
+                    $this->validated_data = $user;
+                    $this->id = intval($id);
+                }
+            }
+            # Set an error if no user was found
+            if (!isset($this->validated_data["uid"])) {
                 $this->errors[] = APIResponse\get(5001);
-            } else {
-                $this->validated_data["username"] = $this->initial_data['username'];
-                $this->validated_data["username"] = trim($this->validated_data["username"]);
+            }
+            # Set an error if this is a system user
+            if ($this->validated_data["scope"] !== "user") {
+                $this->errors[] = APIResponse\get(5005);
             }
         } else {
             $this->errors[] = APIResponse\get(5000);
         }
+    }
 
+    public function validate_payload() {
+        $this->__validate_username();
     }
 }
\ No newline at end of file

From 095d70b2272b3fc9ef1b069e5ff9830b38355738 Mon Sep 17 00:00:00 2001
From: Jared Hendrickson <jaredhendrickson13@gmail.com>
Date: Wed, 10 Mar 2021 19:42:57 -0700
Subject: [PATCH 09/10] Bumped port version to v1.1.7, fixed update path for
 pfSense 2.5 on pfsense-api CLI

---
 .../files/usr/local/share/pfSense-pkg-API/manage.php           | 3 ++-
 tools/templates/Makefile.j2                                    | 2 +-
 2 files changed, 3 insertions(+), 2 deletions(-)

diff --git a/pfSense-pkg-API/files/usr/local/share/pfSense-pkg-API/manage.php b/pfSense-pkg-API/files/usr/local/share/pfSense-pkg-API/manage.php
index 958833bfa..dcf55ad55 100644
--- a/pfSense-pkg-API/files/usr/local/share/pfSense-pkg-API/manage.php
+++ b/pfSense-pkg-API/files/usr/local/share/pfSense-pkg-API/manage.php
@@ -81,8 +81,9 @@ function restore() {
 }
 
 function update() {
+    $pf_version = substr(file_get_contents("/etc/version"), 0, 3);
     echo shell_exec("/usr/sbin/pkg delete -y pfSense-pkg-API");
-    echo shell_exec("/usr/sbin/pkg add https://github.com/jaredhendrickson13/pfsense-api/releases/latest/download/pfSense-2.4-pkg-API.txz");
+    echo shell_exec("/usr/sbin/pkg add https://github.com/jaredhendrickson13/pfsense-api/releases/latest/download/pfSense-".$pf_version."-pkg-API.txz");
     echo shell_exec("/etc/rc.restart_webgui");
 }
 
diff --git a/tools/templates/Makefile.j2 b/tools/templates/Makefile.j2
index 3063bb984..d327d5037 100644
--- a/tools/templates/Makefile.j2
+++ b/tools/templates/Makefile.j2
@@ -2,7 +2,7 @@
 
 PORTNAME=pfSense-pkg-API
 PORTVERSION=1.1
-PORTREVISION=6
+PORTREVISION=7
 CATEGORIES=sysutils
 MASTER_SITES=# empty
 DISTFILES=# empty

From 0f7d53b82cfe8a83239d06004f4e2032c31f3c9b Mon Sep 17 00:00:00 2001
From: Vincent Caron <vincent@zerodeux.net>
Date: Thu, 11 Mar 2021 18:57:26 +0100
Subject: [PATCH 10/10] CARP vhid must be unique _for a given interface_ (or
 more exactly L2 segment)

See https://github.com/jaredhendrickson13/pfsense-api/issues/100
---
 .../etc/inc/api/models/APIFirewallVirtualIPCreate.inc     | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/pfSense-pkg-API/files/etc/inc/api/models/APIFirewallVirtualIPCreate.inc b/pfSense-pkg-API/files/etc/inc/api/models/APIFirewallVirtualIPCreate.inc
index 3e5ea732d..f6f53ec2f 100644
--- a/pfSense-pkg-API/files/etc/inc/api/models/APIFirewallVirtualIPCreate.inc
+++ b/pfSense-pkg-API/files/etc/inc/api/models/APIFirewallVirtualIPCreate.inc
@@ -105,7 +105,7 @@ class APIFirewallVirtualIPCreate extends APIModel {
         if ($this->validated_data["mode"] === "carp") {
             # Check for our optional 'vhid' payload value. Assume default if none was specified.
             if (isset($this->initial_data['vhid'])) {
-                if ($this->__vhid_exists($this->initial_data['vhid'])) {
+                if ($this->__vhid_exists($this->initial_data["interface"], $this->initial_data['vhid'])) {
                     $this->errors[] = APIResponse\get(4027);
                 } elseif (1 > $this->initial_data['vhid'] or $this->initial_data['vhid'] > 255) {
                     $this->errors[] = APIResponse\get(4028);
@@ -153,14 +153,14 @@ class APIFirewallVirtualIPCreate extends APIModel {
         $this->validated_data["type"] = "network";
     }
 
-    private function __vhid_exists($vhid) {
+    private function __vhid_exists($interface, $vhid) {
         # Loop through each virtual IP and ensure it is not using the requested vhid
         foreach ($this->config["virtualip"]["vip"] as $vip) {
-            if (intval($vhid) === intval($vip["vhid"])) {
+            if ($interface === $vip["interface"] && intval($vhid) === intval($vip["vhid"])) {
                 return true;
             }
         }
         return false;
     }
 
-}
\ No newline at end of file
+}