Index: binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/Blacklist.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/Blacklist.cs	(revision 434)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/Blacklist.cs	(revision 434)
@@ -0,0 +1,120 @@
+using System;
+using System.Collections.Generic;
+using System.Net;
+using JetBrains.Annotations;
+using Utf8Json;
+using Webserver.Permissions;
+
+namespace Webserver.WebAPI.APIs.Permissions {
+	[UsedImplicitly]
+	public class Blacklist : AbsRestApi {
+		private const string propertyName = "name";
+		private const string propertyUserId = "userId";
+		private const string propertyBannedUntil = "bannedUntil";
+		private const string propertyBanReason = "banReason";
+
+		private static readonly byte[] jsonKeyName = JsonWriter.GetEncodedPropertyNameWithBeginObject (propertyName);
+		private static readonly byte[] jsonKeyUserId = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator (propertyUserId);
+		private static readonly byte[] jsonKeyBannedUntil = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator (propertyBannedUntil);
+		private static readonly byte[] jsonKeyBanReason = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator (propertyBanReason);
+
+		private static AdminBlacklist BlacklistInstance => GameManager.Instance.adminTools.Blacklist;
+
+		protected override void HandleRestGet (RequestContext _context) {
+			string id = _context.RequestPath;
+
+			PrepareEnvelopedResult (out JsonWriter writer);
+
+			if (string.IsNullOrEmpty (id)) {
+				writer.WriteBeginArray ();
+
+				bool first = true;
+				foreach (AdminBlacklist.BannedUser ban in BlacklistInstance.GetBanned ()) {
+					if (!first) {
+						writer.WriteValueSeparator ();
+					}
+
+					first = false;
+
+					writeBan (ref writer, ban);
+				}
+
+				writer.WriteEndArray ();
+
+				SendEnvelopedResult (_context, ref writer);
+				return;
+			}
+
+			writer.WriteRaw (WebUtils.JsonEmptyData);
+			SendEnvelopedResult (_context, ref writer, HttpStatusCode.BadRequest);
+		}
+
+		private void writeBan (ref JsonWriter _writer, AdminBlacklist.BannedUser _ban) {
+			_writer.WriteRaw (jsonKeyName);
+			_writer.WriteString (_ban.Name ?? "");
+			_writer.WriteRaw (jsonKeyUserId);
+			JsonCommons.WritePlatformUserIdentifier (ref _writer, _ban.UserIdentifier);
+			_writer.WriteRaw (jsonKeyBannedUntil);
+			JsonCommons.WriteDateTime (ref _writer, _ban.BannedUntil);
+			_writer.WriteRaw (jsonKeyBanReason);
+			_writer.WriteString (_ban.BanReason);
+			_writer.WriteEndObject ();
+		}
+
+		protected override void HandleRestPost (RequestContext _context, IDictionary<string, object> _jsonInput, byte[] _jsonInputData) {
+			if (!TryParseId (_context, _jsonInputData, out PlatformUserIdentifierAbs userId)) {
+				return;
+			}
+
+			if (!JsonCommons.TryReadDateTime (_jsonInput, propertyBannedUntil, out DateTime bannedUntil)) {
+				SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_OR_INVALID_BANNED_UNTIL");
+				return;
+			}
+
+			JsonCommons.TryGetJsonField (_jsonInput, propertyBanReason, out string banReason);
+
+			JsonCommons.TryGetJsonField (_jsonInput, propertyName, out string name);
+
+			BlacklistInstance.AddBan (name, userId, bannedUntil, banReason);
+
+			SendEmptyResponse (_context, HttpStatusCode.Created);
+		}
+
+		protected override void HandleRestDelete (RequestContext _context) {
+			if (!TryParseId (_context, null, out PlatformUserIdentifierAbs userId)) {
+				return;
+			}
+
+			bool removed = BlacklistInstance.RemoveBan (userId);
+
+			SendEmptyResponse (_context, removed ? HttpStatusCode.NoContent : HttpStatusCode.NotFound);
+		}
+
+		private bool TryParseId (RequestContext _context, byte[] _jsonInputData, out PlatformUserIdentifierAbs _userId) {
+			string id = _context.RequestPath;
+			_userId = default;
+
+			if (string.IsNullOrEmpty (id)) {
+				SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_USER");
+				return false;
+			}
+
+			bool validId = PlatformUserIdentifierAbs.TryFromCombinedString (id, out _userId);
+			if (!validId) {
+				SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "INVALID_USER");
+			}
+
+			return validId;
+		}
+
+		protected override bool AllowPostWithId => true;
+
+		public override int[] DefaultMethodPermissionLevels () => new[] {
+			AdminWebModules.MethodLevelNotSupported,
+			AdminWebModules.MethodLevelInheritGlobal,
+			AdminWebModules.MethodLevelInheritGlobal,
+			AdminWebModules.MethodLevelNotSupported,
+			AdminWebModules.MethodLevelInheritGlobal
+		};
+	}
+}
Index: binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/CommandPermissions.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/CommandPermissions.cs	(revision 434)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/CommandPermissions.cs	(revision 434)
@@ -0,0 +1,104 @@
+using System.Collections.Generic;
+using System.Net;
+using JetBrains.Annotations;
+using Utf8Json;
+using Webserver.Permissions;
+
+namespace Webserver.WebAPI.APIs.Permissions {
+	[UsedImplicitly]
+	public class CommandPermissions : AbsRestApi {
+		private const string propertyCommand = "command";
+		private const string propertyPermissionLevel = "permissionLevel";
+
+		private static readonly byte[] jsonKeyCommand = JsonWriter.GetEncodedPropertyNameWithBeginObject (propertyCommand);
+		private static readonly byte[] jsonKeyPermissionLevel = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator (propertyPermissionLevel);
+
+		private static AdminCommands CommandsInstance => GameManager.Instance.adminTools.Commands;
+
+		protected override void HandleRestGet (RequestContext _context) {
+			string id = _context.RequestPath;
+
+			PrepareEnvelopedResult (out JsonWriter writer);
+			
+			if (string.IsNullOrEmpty (id)) {
+				
+				writer.WriteBeginArray ();
+			
+				bool first = true;
+				foreach ((_, AdminCommands.CommandPermission commandPermission) in CommandsInstance.GetCommands ()) {
+					if (!first) {
+						writer.WriteValueSeparator ();
+					}
+
+					first = false;
+
+					writeCommandJson (ref writer, commandPermission);
+				}
+
+				writer.WriteEndArray ();
+				
+				SendEnvelopedResult (_context, ref writer);
+				return;
+			}
+
+			writer.WriteRaw (WebUtils.JsonEmptyData);
+			SendEnvelopedResult (_context, ref writer, HttpStatusCode.BadRequest);
+		}
+
+		private void writeCommandJson (ref JsonWriter _writer, AdminCommands.CommandPermission _commandPermission) {
+			_writer.WriteRaw (jsonKeyCommand);
+			_writer.WriteString (_commandPermission.Command);
+			_writer.WriteRaw (jsonKeyPermissionLevel);
+			_writer.WriteInt32 (_commandPermission.PermissionLevel);
+			_writer.WriteEndObject ();
+		}
+
+		protected override void HandleRestPost (RequestContext _context, IDictionary<string, object> _jsonInput, byte[] _jsonInputData) {
+			string id = _context.RequestPath;
+
+			if (string.IsNullOrEmpty (id)) {
+				SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_COMMAND");
+				return;
+			}
+
+			IConsoleCommand cmd = SdtdConsole.Instance.GetCommand (id);
+			if (cmd == null) {
+				SendEmptyResponse (_context, HttpStatusCode.NotFound, _jsonInputData, "INVALID_COMMAND");
+				return;
+			}
+
+			if (!JsonCommons.TryGetJsonField (_jsonInput, propertyPermissionLevel, out int permissionLevel)) {
+				SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_OR_INVALID_PERMISSION_LEVEL");
+				return;
+			}
+
+			CommandsInstance.AddCommand (cmd.GetCommands ()[0], permissionLevel);
+
+			SendEmptyResponse (_context, HttpStatusCode.Created);
+		}
+
+		protected override void HandleRestDelete (RequestContext _context) {
+			string id = _context.RequestPath;
+
+			IConsoleCommand cmd = SdtdConsole.Instance.GetCommand (id);
+			if (cmd == null || !CommandsInstance.IsPermissionDefined (cmd.GetCommands ())) {
+				SendEmptyResponse (_context, HttpStatusCode.NotFound);
+				return;
+			}
+
+			bool removed = CommandsInstance.RemoveCommand (cmd.GetCommands ());
+
+			SendEmptyResponse (_context, removed ? HttpStatusCode.NoContent : HttpStatusCode.NotFound);
+		}
+
+		protected override bool AllowPostWithId => true;
+
+		public override int[] DefaultMethodPermissionLevels () => new[] {
+			AdminWebModules.MethodLevelNotSupported,
+			AdminWebModules.MethodLevelInheritGlobal,
+			AdminWebModules.MethodLevelInheritGlobal,
+			AdminWebModules.MethodLevelNotSupported,
+			AdminWebModules.MethodLevelInheritGlobal
+		};
+	}
+}
Index: binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/PermissionsApiHelpers.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/PermissionsApiHelpers.cs	(revision 434)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/PermissionsApiHelpers.cs	(revision 434)
@@ -0,0 +1,42 @@
+using System;
+using System.Net;
+
+namespace Webserver.WebAPI.APIs.Permissions {
+	public static class PermissionsApiHelpers {
+		public static bool TryParseId (RequestContext _context, byte[] _jsonInputData, out PlatformUserIdentifierAbs _userId,
+			out string _groupId) {
+			string id = _context.RequestPath;
+			_userId = default;
+			_groupId = default;
+
+			if (string.IsNullOrEmpty (id)) {
+				WebUtils.SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_USER_OR_GROUP");
+				return false;
+			}
+
+			const string kindUserPrefix = "user/";
+			if (id.StartsWith (kindUserPrefix, StringComparison.Ordinal)) {
+				bool validId = PlatformUserIdentifierAbs.TryFromCombinedString (id.Substring (kindUserPrefix.Length), out _userId);
+				if (!validId) {
+					WebUtils.SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "INVALID_USER");
+				}
+
+				return validId;
+			}
+
+			const string kindGroupPrefix = "group/";
+			if (id.StartsWith (kindGroupPrefix, StringComparison.Ordinal)) {
+				_groupId = id.Substring (kindGroupPrefix.Length);
+				bool validId = _groupId.Length > 0;
+				if (!validId) {
+					WebUtils.SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "INVALID_GROUP");
+				}
+
+				return validId;
+			}
+
+			WebUtils.SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "INVALID_KIND");
+			return false;
+		}
+	}
+}
Index: binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/RegisterUser.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/RegisterUser.cs	(revision 433)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/RegisterUser.cs	(revision 434)
@@ -9,5 +9,5 @@
 using Webserver.UrlHandlers;
 
-namespace Webserver.WebAPI.APIs {
+namespace Webserver.WebAPI.APIs.Permissions {
 	[UsedImplicitly]
 	public class RegisterUser : AbsRestApi {
@@ -27,10 +27,10 @@
 
 			if (string.IsNullOrEmpty (token)) {
-				SendErrorResult (_context, HttpStatusCode.BadRequest, null, "NO_TOKEN");
+				SendEmptyResponse (_context, HttpStatusCode.BadRequest, null, "NO_TOKEN");
 				return;
 			}
 
 			if (!UserRegistrationTokens.TryValidate (token, out UserRegistrationTokens.RegistrationData regData)) {
-				SendErrorResult (_context, HttpStatusCode.NotFound, null, "INVALID_OR_EXPIRED_TOKEN");
+				SendEmptyResponse (_context, HttpStatusCode.NotFound, null, "INVALID_OR_EXPIRED_TOKEN");
 				return;
 			}
@@ -50,31 +50,31 @@
 
 		protected override void HandleRestPost (RequestContext _context, IDictionary<string, object> _jsonInput, byte[] _jsonInputData) {
-			if (!TryGetJsonField (_jsonInput, "token", out string token)) {
-				SendErrorResult (_context, HttpStatusCode.BadRequest, _jsonInputData, "MISSING_TOKEN");
+			if (!JsonCommons.TryGetJsonField (_jsonInput, "token", out string token)) {
+				SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "MISSING_TOKEN");
 				return;
 			}
 
-			if (!TryGetJsonField (_jsonInput, "username", out string username)) {
-				SendErrorResult (_context, HttpStatusCode.BadRequest, _jsonInputData, "MISSING_USERNAME");
+			if (!JsonCommons.TryGetJsonField (_jsonInput, "username", out string username)) {
+				SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "MISSING_USERNAME");
 				return;
 			}
 
-			if (!TryGetJsonField (_jsonInput, "password", out string password)) {
-				SendErrorResult (_context, HttpStatusCode.BadRequest, _jsonInputData, "MISSING_PASSWORD");
+			if (!JsonCommons.TryGetJsonField (_jsonInput, "password", out string password)) {
+				SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "MISSING_PASSWORD");
 				return;
 			}
 
 			if (!UserRegistrationTokens.TryValidate (token, out UserRegistrationTokens.RegistrationData regData)) {
-				SendErrorResult (_context, HttpStatusCode.Unauthorized, null, "INVALID_OR_EXPIRED_TOKEN");
+				SendEmptyResponse (_context, HttpStatusCode.Unauthorized, null, "INVALID_OR_EXPIRED_TOKEN");
 				return;
 			}
 
 			if (!userValidationRegex.IsMatch (username)) {
-				SendErrorResult (_context, HttpStatusCode.Unauthorized, _jsonInputData, "INVALID_USERNAME");
+				SendEmptyResponse (_context, HttpStatusCode.Unauthorized, _jsonInputData, "INVALID_USERNAME");
 				return;
 			}
 			
 			if (!passValidationRegex.IsMatch (password)) {
-				SendErrorResult (_context, HttpStatusCode.Unauthorized, _jsonInputData, "INVALID_PASSWORD");
+				SendEmptyResponse (_context, HttpStatusCode.Unauthorized, _jsonInputData, "INVALID_PASSWORD");
 				return;
 			}
@@ -86,5 +86,5 @@
 				    !PlatformUserIdentifierAbs.Equals (existingMapping.CrossPlatformUser, regData.CrossPlatformUserId)) {
 					// Username already in use by another player
-					SendErrorResult (_context, HttpStatusCode.Unauthorized, _jsonInputData, "DUPLICATE_USERNAME");
+					SendEmptyResponse (_context, HttpStatusCode.Unauthorized, _jsonInputData, "DUPLICATE_USERNAME");
 					return;
 				}
Index: binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/UserPermissions.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/UserPermissions.cs	(revision 434)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/UserPermissions.cs	(revision 434)
@@ -0,0 +1,159 @@
+using System.Collections.Generic;
+using System.Net;
+using JetBrains.Annotations;
+using Utf8Json;
+using Webserver.Permissions;
+
+namespace Webserver.WebAPI.APIs.Permissions {
+	[UsedImplicitly]
+	public class UserPermissions : AbsRestApi {
+		private const string propertyName = "name";
+		private const string propertyUserId = "userId";
+		private const string propertyPermissionLevel = "permissionLevel";
+		private const string propertyGroupId = "groupId";
+		private const string propertyPermissionLevelMods = "permissionLevelMods";
+		private const string propertyPermissionLevelNormal = "permissionLevelNormal";
+
+		private static readonly byte[] jsonKeyUsers = JsonWriter.GetEncodedPropertyNameWithBeginObject ("users");
+		private static readonly byte[] jsonKeyGroups = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("groups");
+
+		private static readonly byte[] jsonKeyName = JsonWriter.GetEncodedPropertyNameWithBeginObject (propertyName);
+		private static readonly byte[] jsonKeyUserId = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator (propertyUserId);
+
+		private static readonly byte[] jsonKeyPermissionLevel =
+			JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator (propertyPermissionLevel);
+
+		private static readonly byte[] jsonKeyGroupId = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator (propertyGroupId);
+
+		private static readonly byte[] jsonKeyPermissionLevelMods =
+			JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator (propertyPermissionLevelMods);
+
+		private static readonly byte[] jsonKeyPermissionLevelNormal =
+			JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator (propertyPermissionLevelNormal);
+
+		private static AdminUsers UsersInstance => GameManager.Instance.adminTools.Users;
+
+		protected override void HandleRestGet (RequestContext _context) {
+			string id = _context.RequestPath;
+
+			PrepareEnvelopedResult (out JsonWriter writer);
+
+			if (string.IsNullOrEmpty (id)) {
+				writer.WriteRaw (jsonKeyUsers);
+				writer.WriteBeginArray ();
+
+				bool first = true;
+				foreach ((_, AdminUsers.UserPermission userPermission) in UsersInstance.GetUsers ()) {
+					if (!first) {
+						writer.WriteValueSeparator ();
+					}
+
+					first = false;
+
+					writeUserJson (ref writer, userPermission);
+				}
+
+				writer.WriteEndArray ();
+
+				writer.WriteRaw (jsonKeyGroups);
+				writer.WriteBeginArray ();
+
+				first = true;
+				foreach ((_, AdminUsers.GroupPermission groupPermission) in UsersInstance.GetGroups ()) {
+					if (!first) {
+						writer.WriteValueSeparator ();
+					}
+
+					first = false;
+
+					writeGroupJson (ref writer, groupPermission);
+				}
+
+				writer.WriteEndArray ();
+
+				writer.WriteEndObject ();
+
+				SendEnvelopedResult (_context, ref writer);
+				return;
+			}
+
+			writer.WriteRaw (WebUtils.JsonEmptyData);
+			SendEnvelopedResult (_context, ref writer, HttpStatusCode.BadRequest);
+		}
+
+		private void writeUserJson (ref JsonWriter _writer, AdminUsers.UserPermission _userPermission) {
+			_writer.WriteRaw (jsonKeyName);
+			_writer.WriteString (_userPermission.Name ?? "");
+			_writer.WriteRaw (jsonKeyUserId);
+			JsonCommons.WritePlatformUserIdentifier (ref _writer, _userPermission.UserIdentifier);
+			_writer.WriteRaw (jsonKeyPermissionLevel);
+			_writer.WriteInt32 (_userPermission.PermissionLevel);
+			_writer.WriteEndObject ();
+		}
+
+		private void writeGroupJson (ref JsonWriter _writer, AdminUsers.GroupPermission _groupPermission) {
+			_writer.WriteRaw (jsonKeyName);
+			_writer.WriteString (_groupPermission.Name ?? "");
+			_writer.WriteRaw (jsonKeyGroupId);
+			_writer.WriteString (_groupPermission.SteamIdGroup);
+			_writer.WriteRaw (jsonKeyPermissionLevelMods);
+			_writer.WriteInt32 (_groupPermission.PermissionLevelMods);
+			_writer.WriteRaw (jsonKeyPermissionLevelNormal);
+			_writer.WriteInt32 (_groupPermission.PermissionLevelNormal);
+			_writer.WriteEndObject ();
+		}
+
+		protected override void HandleRestPost (RequestContext _context, IDictionary<string, object> _jsonInput, byte[] _jsonInputData) {
+			if (!PermissionsApiHelpers.TryParseId (_context, _jsonInputData, out PlatformUserIdentifierAbs userId, out string groupId)) {
+				return;
+			}
+
+			if (userId != null) {
+				if (!JsonCommons.TryGetJsonField (_jsonInput, propertyPermissionLevel, out int permissionLevel)) {
+					SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_OR_INVALID_PERMISSION_LEVEL");
+					return;
+				}
+
+				JsonCommons.TryGetJsonField (_jsonInput, propertyName, out string name);
+
+				UsersInstance.AddUser (name, userId, permissionLevel);
+			} else {
+				if (!JsonCommons.TryGetJsonField (_jsonInput, propertyPermissionLevelMods, out int permissionLevelMods)) {
+					SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_OR_INVALID_PERMISSION_LEVEL_MODS");
+					return;
+				}
+
+				if (!JsonCommons.TryGetJsonField (_jsonInput, propertyPermissionLevelNormal, out int permissionLevelNormal)) {
+					SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_OR_INVALID_PERMISSION_LEVEL_NORMAL");
+					return;
+				}
+
+				JsonCommons.TryGetJsonField (_jsonInput, propertyName, out string name);
+
+				UsersInstance.AddGroup (name, groupId, permissionLevelNormal, permissionLevelMods);
+			}
+
+			SendEmptyResponse (_context, HttpStatusCode.Created);
+		}
+
+		protected override void HandleRestDelete (RequestContext _context) {
+			if (!PermissionsApiHelpers.TryParseId (_context, null, out PlatformUserIdentifierAbs userId, out string groupId)) {
+				return;
+			}
+
+			bool removed = userId != null ? UsersInstance.RemoveUser (userId) : UsersInstance.RemoveGroup (groupId);
+
+			SendEmptyResponse (_context, removed ? HttpStatusCode.NoContent : HttpStatusCode.NotFound);
+		}
+
+		protected override bool AllowPostWithId => true;
+
+		public override int[] DefaultMethodPermissionLevels () => new[] {
+			AdminWebModules.MethodLevelNotSupported,
+			AdminWebModules.MethodLevelInheritGlobal,
+			AdminWebModules.MethodLevelInheritGlobal,
+			AdminWebModules.MethodLevelNotSupported,
+			AdminWebModules.MethodLevelInheritGlobal
+		};
+	}
+}
Index: binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/WebApiTokens.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/WebApiTokens.cs	(revision 434)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/WebApiTokens.cs	(revision 434)
@@ -0,0 +1,101 @@
+using System.Collections.Generic;
+using System.Net;
+using JetBrains.Annotations;
+using Utf8Json;
+using Webserver.Permissions;
+
+namespace Webserver.WebAPI.APIs.Permissions {
+	[UsedImplicitly]
+	public class WebApiTokens : AbsRestApi {
+		private const string propertyName = "name";
+		private const string propertySecret = "secret";
+		private const string propertyPermissionLevel = "permissionLevel";
+
+		private static readonly byte[] jsonKeyName = JsonWriter.GetEncodedPropertyNameWithBeginObject (propertyName);
+		private static readonly byte[] jsonKeySecret = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator (propertySecret);
+		private static readonly byte[] jsonKeyPermissionLevel = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator (propertyPermissionLevel);
+
+		private static AdminApiTokens ApiTokensInstance => AdminApiTokens.Instance;
+
+		protected override void HandleRestGet (RequestContext _context) {
+			string id = _context.RequestPath;
+
+			PrepareEnvelopedResult (out JsonWriter writer);
+			
+			if (string.IsNullOrEmpty (id)) {
+				
+				writer.WriteBeginArray ();
+			
+				bool first = true;
+				foreach ((_, AdminApiTokens.ApiToken token) in ApiTokensInstance.GetTokens ()) {
+					if (!first) {
+						writer.WriteValueSeparator ();
+					}
+
+					first = false;
+
+					writeTokenJson (ref writer, token);
+				}
+
+				writer.WriteEndArray ();
+				
+				SendEnvelopedResult (_context, ref writer);
+				return;
+			}
+
+			writer.WriteRaw (WebUtils.JsonEmptyData);
+			SendEnvelopedResult (_context, ref writer, HttpStatusCode.BadRequest);
+		}
+
+		private void writeTokenJson (ref JsonWriter _writer, AdminApiTokens.ApiToken _token) {
+			_writer.WriteRaw (jsonKeyName);
+			_writer.WriteString (_token.Name);
+			_writer.WriteRaw (jsonKeySecret);
+			_writer.WriteString (_token.Secret);
+			_writer.WriteRaw (jsonKeyPermissionLevel);
+			_writer.WriteInt32 (_token.PermissionLevel);
+			_writer.WriteEndObject ();
+		}
+
+		protected override void HandleRestPost (RequestContext _context, IDictionary<string, object> _jsonInput, byte[] _jsonInputData) {
+			string id = _context.RequestPath;
+
+			if (string.IsNullOrEmpty (id)) {
+				SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_NAME");
+				return;
+			}
+
+			if (!JsonCommons.TryGetJsonField (_jsonInput, propertySecret, out string secret)) {
+				SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_OR_INVALID_SECRET");
+				return;
+			}
+
+			if (!JsonCommons.TryGetJsonField (_jsonInput, propertyPermissionLevel, out int permissionLevel)) {
+				SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_OR_INVALID_PERMISSION_LEVEL");
+				return;
+			}
+
+			ApiTokensInstance.AddToken (id, secret, permissionLevel);
+
+			SendEmptyResponse (_context, HttpStatusCode.Created);
+		}
+
+		protected override void HandleRestDelete (RequestContext _context) {
+			string id = _context.RequestPath;
+
+			bool removed = ApiTokensInstance.RemoveToken (id);
+
+			SendEmptyResponse (_context, removed ? HttpStatusCode.NoContent : HttpStatusCode.NotFound);
+		}
+
+		protected override bool AllowPostWithId => true;
+
+		public override int[] DefaultMethodPermissionLevels () => new[] {
+			AdminWebModules.MethodLevelNotSupported,
+			AdminWebModules.MethodLevelInheritGlobal,
+			AdminWebModules.MethodLevelInheritGlobal,
+			AdminWebModules.MethodLevelNotSupported,
+			AdminWebModules.MethodLevelInheritGlobal
+		};
+	}
+}
Index: binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/WebModules.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/WebModules.cs	(revision 434)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/WebModules.cs	(revision 434)
@@ -0,0 +1,142 @@
+using System.Collections.Generic;
+using System.Net;
+using JetBrains.Annotations;
+using Utf8Json;
+using Webserver.Permissions;
+
+namespace Webserver.WebAPI.APIs.Permissions {
+	[UsedImplicitly]
+	public class WebModules : AbsRestApi {
+		private const string propertyModule = "module";
+		private const string propertyPermissionLevelGlobal = "permissionLevelGlobal";
+		private const string propertyPermissionLevelPerMethod = "permissionLevelPerMethod";
+		private const string propertyIsDefault = "isDefault";
+		
+
+		private static readonly byte[] jsonKeyModule = JsonWriter.GetEncodedPropertyNameWithBeginObject (propertyModule);
+		private static readonly byte[] jsonKeyPermissionLevelGlobal = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator (propertyPermissionLevelGlobal);
+		private static readonly byte[] jsonKeyPermissionLevelPerMethod = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator (propertyPermissionLevelPerMethod);
+		private static readonly byte[] jsonKeyIsDefault = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator (propertyIsDefault);
+
+		private static readonly byte[][] jsonMethodNameKeys;
+
+		static WebModules () {
+			jsonMethodNameKeys = new byte[(int)ERequestMethod.Count][];
+			for (int i = 0; i < jsonMethodNameKeys.Length; i++) {
+				ERequestMethod method = (ERequestMethod)i;
+				jsonMethodNameKeys [i] = JsonWriter.GetEncodedPropertyName (method.ToStringCached ());
+			}
+		}
+
+		private static AdminWebModules ModulesInstance => AdminWebModules.Instance;
+
+		protected override void HandleRestGet (RequestContext _context) {
+			string id = _context.RequestPath;
+
+			PrepareEnvelopedResult (out JsonWriter writer);
+			
+			if (string.IsNullOrEmpty (id)) {
+				
+				writer.WriteBeginArray ();
+			
+				bool first = true;
+				foreach (AdminWebModules.WebModule module in ModulesInstance.GetModules ()) {
+					if (!first) {
+						writer.WriteValueSeparator ();
+					}
+
+					first = false;
+
+					writeModuleJson (ref writer, module);
+				}
+
+				writer.WriteEndArray ();
+				
+				SendEnvelopedResult (_context, ref writer);
+				return;
+			}
+
+			writer.WriteRaw (WebUtils.JsonEmptyData);
+			SendEnvelopedResult (_context, ref writer, HttpStatusCode.BadRequest);
+		}
+
+		private void writeModuleJson (ref JsonWriter _writer, AdminWebModules.WebModule _module) {
+			_writer.WriteRaw (jsonKeyModule);
+			_writer.WriteString (_module.Name);
+			_writer.WriteRaw (jsonKeyPermissionLevelGlobal);
+			_writer.WriteInt32 (_module.LevelGlobal);
+			_writer.WriteRaw (jsonKeyPermissionLevelPerMethod);
+			
+			_writer.WriteBeginObject ();
+
+			if (_module.LevelPerMethod != null) {
+				bool first = true;
+				for (int iMethod = 0; iMethod < _module.LevelPerMethod.Length; iMethod++) {
+					int methodLevel = _module.LevelPerMethod [iMethod];
+						
+					if (methodLevel == AdminWebModules.MethodLevelNotSupported) {
+						continue;
+					}
+
+					if (!first) {
+						_writer.WriteValueSeparator ();
+					}
+
+					first = false;
+						
+					_writer.WriteRaw (jsonMethodNameKeys [iMethod]);
+					if (methodLevel == AdminWebModules.MethodLevelInheritGlobal) {
+						_writer.WriteString ("inherit");
+					} else {
+						_writer.WriteInt32 (methodLevel);
+					}
+				}
+			}
+
+			_writer.WriteEndObject ();
+			
+			_writer.WriteRaw (jsonKeyIsDefault);
+			_writer.WriteBoolean (_module.IsDefault);
+			
+			_writer.WriteEndObject ();
+		}
+
+		protected override void HandleRestPost (RequestContext _context, IDictionary<string, object> _jsonInput, byte[] _jsonInputData) {
+			string id = _context.RequestPath;
+
+			// TODO
+			
+			if (string.IsNullOrEmpty (id)) {
+				SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_MODULE");
+				return;
+			}
+
+			if (!JsonCommons.TryGetJsonField (_jsonInput, propertyPermissionLevelGlobal, out int permissionLevel)) {
+				SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_OR_INVALID_PERMISSION_LEVEL");
+				return;
+			}
+
+			// ModulesInstance.AddModule (id, permissionLevel);
+
+			SendEmptyResponse (_context, HttpStatusCode.Created);
+		}
+
+		protected override void HandleRestDelete (RequestContext _context) {
+			string id = _context.RequestPath;
+
+			bool removed = ModulesInstance.RemoveModule (id);
+
+			SendEmptyResponse (_context, removed ? HttpStatusCode.NoContent : HttpStatusCode.NotFound);
+		}
+
+		protected override bool AllowPostWithId => true;
+
+		public override int[] DefaultMethodPermissionLevels () => new[] {
+			AdminWebModules.MethodLevelNotSupported,
+			AdminWebModules.MethodLevelInheritGlobal,
+			AdminWebModules.MethodLevelInheritGlobal,
+			AdminWebModules.MethodLevelNotSupported,
+			AdminWebModules.MethodLevelInheritGlobal
+		};
+	}
+}
Index: binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/WebUsers.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/WebUsers.cs	(revision 434)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/WebUsers.cs	(revision 434)
@@ -0,0 +1,127 @@
+using System.Collections.Generic;
+using System.Net;
+using JetBrains.Annotations;
+using Utf8Json;
+using Webserver.Permissions;
+
+namespace Webserver.WebAPI.APIs.Permissions {
+	[UsedImplicitly]
+	public class WebUsers : AbsRestApi {
+		private const string propertyName = "name";
+		private const string propertyPassword = "password";
+		private const string propertyPlatformUserId = "platformUserId";
+		private const string propertyCrossplatformUserId = "crossplatformUserId";
+
+		private static readonly byte[] jsonKeyName = JsonWriter.GetEncodedPropertyNameWithBeginObject (propertyName);
+		private static readonly byte[] jsonKeyPlatformUserId = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator (propertyPlatformUserId);
+		private static readonly byte[] jsonKeyCrossplatformUserId = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator (propertyCrossplatformUserId);
+
+		private static AdminWebUsers WebUsersInstance => AdminWebUsers.Instance;
+
+		protected override void HandleRestGet (RequestContext _context) {
+			string id = _context.RequestPath;
+
+			PrepareEnvelopedResult (out JsonWriter writer);
+
+			if (string.IsNullOrEmpty (id)) {
+				writer.WriteBeginArray ();
+
+				bool first = true;
+				foreach ((_, AdminWebUsers.WebUser user) in WebUsersInstance.GetUsers ()) {
+					if (!first) {
+						writer.WriteValueSeparator ();
+					}
+
+					first = false;
+
+					writeUserJson (ref writer, user);
+				}
+
+				writer.WriteEndArray ();
+
+				SendEnvelopedResult (_context, ref writer);
+				return;
+			}
+
+			writer.WriteRaw (WebUtils.JsonEmptyData);
+			SendEnvelopedResult (_context, ref writer, HttpStatusCode.BadRequest);
+		}
+
+		private void writeUserJson (ref JsonWriter _writer, AdminWebUsers.WebUser _user) {
+			_writer.WriteRaw (jsonKeyName);
+			_writer.WriteString (_user.Name ?? "");
+			_writer.WriteRaw (jsonKeyPlatformUserId);
+			JsonCommons.WritePlatformUserIdentifier (ref _writer, _user.PlatformUser);
+			_writer.WriteRaw (jsonKeyCrossplatformUserId);
+			JsonCommons.WritePlatformUserIdentifier (ref _writer, _user.CrossPlatformUser);
+			_writer.WriteEndObject ();
+		}
+
+		protected override void HandleRestPost (RequestContext _context, IDictionary<string, object> _jsonInput, byte[] _jsonInputData) {
+			if (!TryParseName (_context, _jsonInputData, out string userName)) {
+				return;
+			}
+
+			if (!JsonCommons.TryGetJsonField (_jsonInput, propertyPassword, out string password)) {
+				SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_OR_INVALID_PASSWORD");
+				return;
+			}
+
+			if (!JsonCommons.TryGetJsonField (_jsonInput, propertyPlatformUserId, out IDictionary<string, object> userIdField)) {
+				SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_PLATFORM_USER_ID");
+				return;
+			}
+
+			if (!JsonCommons.TryReadPlatformUserIdentifier (userIdField, out PlatformUserIdentifierAbs platformUserId)) {
+				SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "INVALID_PLATFORM_USER_ID");
+				return;
+			}
+
+			PlatformUserIdentifierAbs crossplatformUserId = null;
+			
+			if (JsonCommons.TryGetJsonField (_jsonInput, propertyCrossplatformUserId, out userIdField)) {
+				if (!JsonCommons.TryReadPlatformUserIdentifier (userIdField, out crossplatformUserId)) {
+					SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "INVALID_CROSSPLATFORM_USER_ID");
+					return;
+				}
+			}
+
+			WebUsersInstance.AddUser (userName, password, platformUserId, crossplatformUserId);
+
+			SendEmptyResponse (_context, HttpStatusCode.Created);
+		}
+
+		protected override void HandleRestDelete (RequestContext _context) {
+			if (!TryParseName (_context, null, out string userName)) {
+				return;
+			}
+
+			bool removed = WebUsersInstance.RemoveUser (userName);
+
+			SendEmptyResponse (_context, removed ? HttpStatusCode.NoContent : HttpStatusCode.NotFound);
+		}
+
+		private bool TryParseName (RequestContext _context, byte[] _jsonInputData, out string _userName) {
+			string id = _context.RequestPath;
+			_userName = default;
+
+			if (string.IsNullOrEmpty (id)) {
+				SendEmptyResponse (_context, HttpStatusCode.BadRequest, _jsonInputData, "NO_USERNAME");
+				return false;
+			}
+
+			_userName = id;
+			return true;
+		}
+
+		protected override bool AllowPostWithId => true;
+
+		public override int[] DefaultMethodPermissionLevels () => new[] {
+			AdminWebModules.MethodLevelNotSupported,
+			AdminWebModules.MethodLevelInheritGlobal,
+			AdminWebModules.MethodLevelInheritGlobal,
+			AdminWebModules.MethodLevelNotSupported,
+			AdminWebModules.MethodLevelInheritGlobal
+		};
+	}
+}
Index: binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/Whitelist.cs
===================================================================
--- binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/Whitelist.cs	(revision 434)
+++ binary-improvements2/WebServer/src/WebAPI/APIs/Permissions/Whitelist.cs	(revision 434)
@@ -0,0 +1,124 @@
+using System.Collections.Generic;
+using System.Net;
+using JetBrains.Annotations;
+using Utf8Json;
+using Webserver.Permissions;
+
+namespace Webserver.WebAPI.APIs.Permissions {
+	[UsedImplicitly]
+	public class Whitelist : AbsRestApi {
+		private const string propertyName = "name";
+		private const string propertyUserId = "userId";
+		private const string propertyGroupId = "groupId";
+
+		private static readonly byte[] jsonKeyUsers = JsonWriter.GetEncodedPropertyNameWithBeginObject ("users");
+		private static readonly byte[] jsonKeyGroups = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator ("groups");
+
+		private static readonly byte[] jsonKeyName = JsonWriter.GetEncodedPropertyNameWithBeginObject (propertyName);
+		private static readonly byte[] jsonKeyUserId = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator (propertyUserId);
+
+		private static readonly byte[] jsonKeyGroupId = JsonWriter.GetEncodedPropertyNameWithPrefixValueSeparator (propertyGroupId);
+
+		private static AdminWhitelist WhitelistInstance => GameManager.Instance.adminTools.Whitelist;
+
+		protected override void HandleRestGet (RequestContext _context) {
+			string id = _context.RequestPath;
+
+			PrepareEnvelopedResult (out JsonWriter writer);
+
+			if (string.IsNullOrEmpty (id)) {
+				writer.WriteRaw (jsonKeyUsers);
+				writer.WriteBeginArray ();
+
+				bool first = true;
+				foreach ((_, AdminWhitelist.WhitelistUser user) in WhitelistInstance.GetUsers ()) {
+					if (!first) {
+						writer.WriteValueSeparator ();
+					}
+
+					first = false;
+
+					writeUserJson (ref writer, user);
+				}
+
+				writer.WriteEndArray ();
+
+				writer.WriteRaw (jsonKeyGroups);
+				writer.WriteBeginArray ();
+
+				first = true;
+				foreach ((_, AdminWhitelist.WhitelistGroup group) in WhitelistInstance.GetGroups ()) {
+					if (!first) {
+						writer.WriteValueSeparator ();
+					}
+
+					first = false;
+
+					writeGroupJson (ref writer, group);
+				}
+
+				writer.WriteEndArray ();
+
+				writer.WriteEndObject ();
+
+				SendEnvelopedResult (_context, ref writer);
+				return;
+			}
+
+			writer.WriteRaw (WebUtils.JsonEmptyData);
+			SendEnvelopedResult (_context, ref writer, HttpStatusCode.BadRequest);
+		}
+
+		private void writeUserJson (ref JsonWriter _writer, AdminWhitelist.WhitelistUser _userPermission) {
+			_writer.WriteRaw (jsonKeyName);
+			_writer.WriteString (_userPermission.Name ?? "");
+			_writer.WriteRaw (jsonKeyUserId);
+			JsonCommons.WritePlatformUserIdentifier (ref _writer, _userPermission.UserIdentifier);
+			_writer.WriteEndObject ();
+		}
+
+		private void writeGroupJson (ref JsonWriter _writer, AdminWhitelist.WhitelistGroup _groupPermission) {
+			_writer.WriteRaw (jsonKeyName);
+			_writer.WriteString (_groupPermission.Name ?? "");
+			_writer.WriteRaw (jsonKeyGroupId);
+			_writer.WriteString (_groupPermission.SteamIdGroup);
+			_writer.WriteEndObject ();
+		}
+
+		protected override void HandleRestPost (RequestContext _context, IDictionary<string, object> _jsonInput, byte[] _jsonInputData) {
+			if (!PermissionsApiHelpers.TryParseId (_context, _jsonInputData, out PlatformUserIdentifierAbs userId, out string groupId)) {
+				return;
+			}
+
+			JsonCommons.TryGetJsonField (_jsonInput, propertyName, out string name);
+
+			if (userId != null) {
+				WhitelistInstance.AddUser (name, userId);
+			} else {
+				WhitelistInstance.AddGroup (name, groupId);
+			}
+
+			SendEmptyResponse (_context, HttpStatusCode.Created);
+		}
+
+		protected override void HandleRestDelete (RequestContext _context) {
+			if (!PermissionsApiHelpers.TryParseId (_context, null, out PlatformUserIdentifierAbs userId, out string groupId)) {
+				return;
+			}
+
+			bool removed = userId != null ? WhitelistInstance.RemoveUser (userId) : WhitelistInstance.RemoveGroup (groupId);
+
+			SendEmptyResponse (_context, removed ? HttpStatusCode.NoContent : HttpStatusCode.NotFound);
+		}
+
+		protected override bool AllowPostWithId => true;
+
+		public override int[] DefaultMethodPermissionLevels () => new[] {
+			AdminWebModules.MethodLevelNotSupported,
+			AdminWebModules.MethodLevelInheritGlobal,
+			AdminWebModules.MethodLevelInheritGlobal,
+			AdminWebModules.MethodLevelNotSupported,
+			AdminWebModules.MethodLevelInheritGlobal
+		};
+	}
+}
