source: binary-improvements2/WebServer/src/WebAPI/AbsRestApi.cs@ 418

Last change on this file since 418 was 418, checked in by alloc, 21 months ago

Refactored API authorization to support per-HTTP-method permission levels

File size: 8.5 KB
RevLine 
[391]1using System;
[402]2using System.Collections.Generic;
[391]3using System.IO;
4using System.Net;
[402]5using Utf8Json;
[418]6using Webserver.Permissions;
[391]7
8namespace Webserver.WebAPI {
9 public abstract class AbsRestApi : AbsWebAPI {
[394]10 private static readonly UnityEngine.Profiling.CustomSampler jsonDeserializeSampler = UnityEngine.Profiling.CustomSampler.Create ("JSON_Deserialize");
11
[418]12 protected readonly string[] CachedPerMethodModuleNames = new string[(int)ERequestMethod.Count];
13
[410]14 protected AbsRestApi (string _name = null) : this(null, _name) {
15 }
16
17 protected AbsRestApi (Web _parentWeb, string _name = null) : base(_parentWeb, _name) {
18 }
19
[418]20 protected override void RegisterPermissions () {
21 base.RegisterPermissions ();
22
23 for (int i = 0; i < (int)ERequestMethod.Count; i++) {
24 ERequestMethod method = (ERequestMethod)i;
25
26 if (method is not (ERequestMethod.GET or ERequestMethod.PUT or ERequestMethod.POST or ERequestMethod.DELETE)) {
27 continue;
28 }
29
30 CachedPerMethodModuleNames [i] = $"webapi.{Name}:{method.ToStringCached ()}";
31 AdminWebModules.Instance.AddKnownModule (CachedPerMethodModuleNames [i], DefaultMethodPermissionLevel (method));
32 }
33 }
34
[391]35 public sealed override void HandleRequest (RequestContext _context) {
[402]36 IDictionary<string, object> inputJson = null;
37 byte[] jsonInputData = null;
38
[391]39 if (_context.Request.HasEntityBody) {
[402]40 Stream requestInputStream = _context.Request.InputStream;
41
42 jsonInputData = new byte[_context.Request.ContentLength64];
43 requestInputStream.Read (jsonInputData, 0, (int)_context.Request.ContentLength64);
44
45 try {
46 jsonDeserializeSampler.Begin ();
47 inputJson = JsonSerializer.Deserialize<IDictionary<string, object>> (jsonInputData);
48
49 // Log.Out ("JSON body:");
50 // foreach ((string key, object value) in inputJson) {
51 // Log.Out ($" - {key} = {value} ({value.GetType ()})");
52 // }
53
54 jsonDeserializeSampler.End ();
55 } catch (Exception e) {
56 jsonDeserializeSampler.End ();
[391]57
[402]58 SendErrorResult (_context, HttpStatusCode.BadRequest, null, "INVALID_BODY", e);
59 return;
[391]60 }
61 }
62
63 try {
[418]64 switch (_context.Method) {
65 case ERequestMethod.GET:
[402]66 if (inputJson != null) {
67 SendErrorResult (_context, HttpStatusCode.BadRequest, jsonInputData, "GET_WITH_BODY");
[391]68 return;
69 }
70
71 HandleRestGet (_context);
72 return;
[418]73 case ERequestMethod.POST:
[391]74 if (!string.IsNullOrEmpty (_context.RequestPath)) {
[402]75 SendErrorResult (_context, HttpStatusCode.BadRequest, jsonInputData, "POST_WITH_ID");
[391]76 return;
77 }
78
[402]79 if (inputJson == null) {
80 SendErrorResult (_context, HttpStatusCode.BadRequest, null, "POST_WITHOUT_BODY");
[391]81 return;
82 }
83
[402]84 HandleRestPost (_context, inputJson, jsonInputData);
[391]85 return;
[418]86 case ERequestMethod.PUT:
[391]87 if (string.IsNullOrEmpty (_context.RequestPath)) {
[402]88 SendErrorResult (_context, HttpStatusCode.BadRequest, jsonInputData, "PUT_WITHOUT_ID");
[391]89 return;
90 }
91
[402]92 if (inputJson == null) {
93 SendErrorResult (_context, HttpStatusCode.BadRequest, null, "PUT_WITHOUT_BODY");
[391]94 return;
95 }
96
[402]97 HandleRestPut (_context, inputJson, jsonInputData);
[391]98 return;
[418]99 case ERequestMethod.DELETE:
[391]100 if (string.IsNullOrEmpty (_context.RequestPath)) {
[402]101 SendErrorResult (_context, HttpStatusCode.BadRequest, jsonInputData, "DELETE_WITHOUT_ID");
[391]102 return;
103 }
104
[402]105 if (inputJson != null) {
106 SendErrorResult (_context, HttpStatusCode.BadRequest, null, "DELETE_WITH_BODY");
[391]107 return;
108 }
109
110 HandleRestDelete (_context);
111 return;
112 default:
[402]113 SendErrorResult (_context, HttpStatusCode.BadRequest, null, "INVALID_METHOD");
[391]114 return;
115 }
116 } catch (Exception e) {
[402]117 SendErrorResult (_context, HttpStatusCode.InternalServerError, jsonInputData, "ERROR_PROCESSING", e);
[391]118 }
119 }
120
[404]121 protected virtual void HandleRestGet (RequestContext _context) {
122 SendErrorResult (_context, HttpStatusCode.MethodNotAllowed, null, "Unsupported");
123 }
124
125 protected virtual void HandleRestPost (RequestContext _context, IDictionary<string, object> _jsonInput, byte[] _jsonInputData) {
126 SendErrorResult (_context, HttpStatusCode.MethodNotAllowed, _jsonInputData, "Unsupported");
127 }
128
129 protected virtual void HandleRestPut (RequestContext _context, IDictionary<string, object> _jsonInput, byte[] _jsonInputData) {
130 SendErrorResult (_context, HttpStatusCode.MethodNotAllowed, _jsonInputData, "Unsupported");
131 }
132
133 protected virtual void HandleRestDelete (RequestContext _context) {
134 SendErrorResult (_context, HttpStatusCode.MethodNotAllowed, null, "Unsupported");
135 }
136
[418]137 public override bool Authorized (RequestContext _context) {
138 return ActiveMethodPermissionLevel (_context.Method) >= _context.PermissionLevel;
139 }
[404]140
[418]141 /// <summary>
142 /// Define default permission levels per HTTP method
143 /// </summary>
144 /// <param name="_method">HTTP method to return the default value for</param>
145 /// <returns>Default permission level for the given HTTP method. A value of int.MinValue means no per-method default, use per-API default</returns>
146 public virtual int DefaultMethodPermissionLevel (ERequestMethod _method) => int.MinValue;
147
148 public virtual int ActiveMethodPermissionLevel (ERequestMethod _method) {
149 string methodApiModuleName = CachedPerMethodModuleNames [(int)_method];
150
151 if (methodApiModuleName == null) {
152 return 0;
153 }
154
155 AdminWebModules.WebModule? overrideModule = AdminWebModules.Instance.GetModule (methodApiModuleName, false);
156 if (overrideModule.HasValue) {
157 return overrideModule.Value.PermissionLevel;
158 }
159
160 overrideModule = AdminWebModules.Instance.GetModule (CachedApiModuleName, false);
161 if (overrideModule.HasValue) {
162 return overrideModule.Value.PermissionLevel;
163 }
164
165 int defaultMethodPermissionLevel = DefaultMethodPermissionLevel (_method);
166 // ReSharper disable once ConvertIfStatementToReturnStatement
167 if (defaultMethodPermissionLevel != int.MinValue) {
168 return defaultMethodPermissionLevel;
169 }
170
171 return DefaultPermissionLevel ();
172 }
173
[404]174#region Helpers
175
[402]176 protected static readonly byte[] JsonEmptyData;
177
[418]178 static AbsRestApi () {
179 JsonWriter writer = new JsonWriter ();
180 writer.WriteBeginArray ();
181 writer.WriteEndArray ();
182 JsonEmptyData = writer.ToUtf8ByteArray ();
183 }
184
[404]185 protected static void PrepareEnvelopedResult (out JsonWriter _writer) {
[402]186 WebUtils.PrepareEnvelopedResult (out _writer);
187 }
[391]188
[404]189 protected static void SendEnvelopedResult (RequestContext _context, ref JsonWriter _writer, HttpStatusCode _statusCode = HttpStatusCode.OK,
[402]190 byte[] _jsonInputData = null, string _errorCode = null, Exception _exception = null) {
191
192 WebUtils.SendEnvelopedResult (_context, ref _writer, _statusCode, _jsonInputData, _errorCode, _exception);
[391]193 }
194
[404]195 protected static void SendErrorResult (RequestContext _context, HttpStatusCode _statusCode, byte[] _jsonInputData = null, string _errorCode = null, Exception _exception = null) {
196 PrepareEnvelopedResult (out JsonWriter writer);
197 writer.WriteRaw (JsonEmptyData);
198 SendEnvelopedResult (_context, ref writer, _statusCode, _jsonInputData, _errorCode, _exception);
199 }
200
201 protected static bool TryGetJsonField (IDictionary<string, object> _jsonObject, string _fieldName, out int _value) {
[391]202 _value = default;
203
[402]204 if (!_jsonObject.TryGetValue (_fieldName, out object fieldNode)) {
[391]205 return false;
206 }
207
[402]208 if (fieldNode is not double value) {
[391]209 return false;
210 }
211
212 try {
[402]213 _value = (int)value;
[391]214 return true;
215 } catch (Exception) {
216 return false;
217 }
218 }
219
[404]220 protected static bool TryGetJsonField (IDictionary<string, object> _jsonObject, string _fieldName, out double _value) {
[391]221 _value = default;
222
[402]223 if (!_jsonObject.TryGetValue (_fieldName, out object fieldNode)) {
[391]224 return false;
225 }
226
[402]227 if (fieldNode is not double value) {
[391]228 return false;
229 }
230
231 try {
[402]232 _value = value;
[391]233 return true;
234 } catch (Exception) {
235 return false;
236 }
237 }
238
[404]239 protected static bool TryGetJsonField (IDictionary<string, object> _jsonObject, string _fieldName, out string _value) {
[391]240 _value = default;
241
[402]242 if (!_jsonObject.TryGetValue (_fieldName, out object fieldNode)) {
[391]243 return false;
244 }
245
[402]246 if (fieldNode is not string value) {
[391]247 return false;
248 }
249
250 try {
[402]251 _value = value;
[391]252 return true;
253 } catch (Exception) {
254 return false;
255 }
256 }
[404]257
[391]258
[404]259#endregion
[391]260 }
261}
Note: See TracBrowser for help on using the repository browser.