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

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

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

File size: 8.5 KB
Line 
1using System;
2using System.Collections.Generic;
3using System.IO;
4using System.Net;
5using Utf8Json;
6using Webserver.Permissions;
7
8namespace Webserver.WebAPI {
9 public abstract class AbsRestApi : AbsWebAPI {
10 private static readonly UnityEngine.Profiling.CustomSampler jsonDeserializeSampler = UnityEngine.Profiling.CustomSampler.Create ("JSON_Deserialize");
11
12 protected readonly string[] CachedPerMethodModuleNames = new string[(int)ERequestMethod.Count];
13
14 protected AbsRestApi (string _name = null) : this(null, _name) {
15 }
16
17 protected AbsRestApi (Web _parentWeb, string _name = null) : base(_parentWeb, _name) {
18 }
19
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
35 public sealed override void HandleRequest (RequestContext _context) {
36 IDictionary<string, object> inputJson = null;
37 byte[] jsonInputData = null;
38
39 if (_context.Request.HasEntityBody) {
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 ();
57
58 SendErrorResult (_context, HttpStatusCode.BadRequest, null, "INVALID_BODY", e);
59 return;
60 }
61 }
62
63 try {
64 switch (_context.Method) {
65 case ERequestMethod.GET:
66 if (inputJson != null) {
67 SendErrorResult (_context, HttpStatusCode.BadRequest, jsonInputData, "GET_WITH_BODY");
68 return;
69 }
70
71 HandleRestGet (_context);
72 return;
73 case ERequestMethod.POST:
74 if (!string.IsNullOrEmpty (_context.RequestPath)) {
75 SendErrorResult (_context, HttpStatusCode.BadRequest, jsonInputData, "POST_WITH_ID");
76 return;
77 }
78
79 if (inputJson == null) {
80 SendErrorResult (_context, HttpStatusCode.BadRequest, null, "POST_WITHOUT_BODY");
81 return;
82 }
83
84 HandleRestPost (_context, inputJson, jsonInputData);
85 return;
86 case ERequestMethod.PUT:
87 if (string.IsNullOrEmpty (_context.RequestPath)) {
88 SendErrorResult (_context, HttpStatusCode.BadRequest, jsonInputData, "PUT_WITHOUT_ID");
89 return;
90 }
91
92 if (inputJson == null) {
93 SendErrorResult (_context, HttpStatusCode.BadRequest, null, "PUT_WITHOUT_BODY");
94 return;
95 }
96
97 HandleRestPut (_context, inputJson, jsonInputData);
98 return;
99 case ERequestMethod.DELETE:
100 if (string.IsNullOrEmpty (_context.RequestPath)) {
101 SendErrorResult (_context, HttpStatusCode.BadRequest, jsonInputData, "DELETE_WITHOUT_ID");
102 return;
103 }
104
105 if (inputJson != null) {
106 SendErrorResult (_context, HttpStatusCode.BadRequest, null, "DELETE_WITH_BODY");
107 return;
108 }
109
110 HandleRestDelete (_context);
111 return;
112 default:
113 SendErrorResult (_context, HttpStatusCode.BadRequest, null, "INVALID_METHOD");
114 return;
115 }
116 } catch (Exception e) {
117 SendErrorResult (_context, HttpStatusCode.InternalServerError, jsonInputData, "ERROR_PROCESSING", e);
118 }
119 }
120
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
137 public override bool Authorized (RequestContext _context) {
138 return ActiveMethodPermissionLevel (_context.Method) >= _context.PermissionLevel;
139 }
140
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
174#region Helpers
175
176 protected static readonly byte[] JsonEmptyData;
177
178 static AbsRestApi () {
179 JsonWriter writer = new JsonWriter ();
180 writer.WriteBeginArray ();
181 writer.WriteEndArray ();
182 JsonEmptyData = writer.ToUtf8ByteArray ();
183 }
184
185 protected static void PrepareEnvelopedResult (out JsonWriter _writer) {
186 WebUtils.PrepareEnvelopedResult (out _writer);
187 }
188
189 protected static void SendEnvelopedResult (RequestContext _context, ref JsonWriter _writer, HttpStatusCode _statusCode = HttpStatusCode.OK,
190 byte[] _jsonInputData = null, string _errorCode = null, Exception _exception = null) {
191
192 WebUtils.SendEnvelopedResult (_context, ref _writer, _statusCode, _jsonInputData, _errorCode, _exception);
193 }
194
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) {
202 _value = default;
203
204 if (!_jsonObject.TryGetValue (_fieldName, out object fieldNode)) {
205 return false;
206 }
207
208 if (fieldNode is not double value) {
209 return false;
210 }
211
212 try {
213 _value = (int)value;
214 return true;
215 } catch (Exception) {
216 return false;
217 }
218 }
219
220 protected static bool TryGetJsonField (IDictionary<string, object> _jsonObject, string _fieldName, out double _value) {
221 _value = default;
222
223 if (!_jsonObject.TryGetValue (_fieldName, out object fieldNode)) {
224 return false;
225 }
226
227 if (fieldNode is not double value) {
228 return false;
229 }
230
231 try {
232 _value = value;
233 return true;
234 } catch (Exception) {
235 return false;
236 }
237 }
238
239 protected static bool TryGetJsonField (IDictionary<string, object> _jsonObject, string _fieldName, out string _value) {
240 _value = default;
241
242 if (!_jsonObject.TryGetValue (_fieldName, out object fieldNode)) {
243 return false;
244 }
245
246 if (fieldNode is not string value) {
247 return false;
248 }
249
250 try {
251 _value = value;
252 return true;
253 } catch (Exception) {
254 return false;
255 }
256 }
257
258
259#endregion
260 }
261}
Note: See TracBrowser for help on using the repository browser.