Simplest way to create tracing and authorization layers in Backend API. Special attribute to arrange API for authorization layers.
1. Trace layer.
Each important Backend API (working with money for example) or for debugging purposes need tracing input parameters and output result. In this page I want to describe my way.
Firstly, need to understand you can not use Request to tracing, because any API usually perform API from another controllers, in this case HttpContext.Request will be nothing. Second point that Net Core use PipeReader and special reader BodyReader. Nobody understanding how to use it with Kestrel, even developers of this reader. This two restriction getting us simplest way to receive finally result quickly, safe and without trouble.
So, in controller's methods I use this pattern.
In the beginning of each method I perform the same line:
TraceRequest(Reflection.MethodBase.GetCurrentMethod.Name, Model)
And in the normal end of each methods I use line:
TraceResult(_WithResult, _RequestTraceID, RetResult)
Goal of this function is tracing intercontrollers calls (when Request is nothing). This is simple functions based on Reflection.
1: Imports System.Reflection
2: Imports System.Runtime.CompilerServices
3: Imports BackendAPI.Model
4: Imports BackendAPI.Services
5: Imports Microsoft.AspNetCore.Mvc
6: Imports Microsoft.EntityFrameworkCore
7: Imports BackendAPI.Helper
8: Imports Renci.SshNet.Sftp
9:
10: Namespace WebApi.Controllers
11: Partial Friend Module Tracer
12: Private Property _DB As ApplicationDbContext
13: Private Property _Trace As Boolean = False
14: Private Property _WithResult As Boolean = False
15: Private Property _UserService As IUserService
16: Private Property _RequestTraceID As FieldInfo
17: ''' <summary>
18: ''' Require definition 5 property in controller - _DB As ApplicationDbContext,_Trace As Boolean,_WithResult As Boolean,_UserService,_RequestTraceID
19: ''' Can be call in start of each API
20: ''' </summary>
21: <Extension>
22: Public Sub TraceRequest(Controller As ControllerBase, MethodName As String, Model As Object)
23: Dim Y As Type = Controller.GetType
24: For Each OneProp As FieldInfo In Y.GetRuntimeFields
25: If OneProp.Name = "_DbForTrace" Then
26: _DB = OneProp.GetValue(Controller)
27: ElseIf OneProp.Name = "_Trace" Then
28: _Trace = OneProp.GetValue(Controller)
29: ElseIf OneProp.Name = "_WithResult" Then
30: _WithResult = OneProp.GetValue(Controller)
31: ElseIf OneProp.Name = "_UserService" Then
32: _UserService = OneProp.GetValue(Controller)
33: ElseIf OneProp.Name = "_RequestTraceID" Then
34: _RequestTraceID = OneProp
35: End If
36: Next
37: If _DB IsNot Nothing And _UserService IsNot Nothing Then
38: If _Trace Then
39: Dim TraceID As String = ""
40: If Controller.HttpContext IsNot Nothing Then
41: TraceID = Controller.Request.HttpContext.TraceIdentifier
42: Else
43: TraceID = Guid.NewGuid.ToString
44: End If
45: _RequestTraceID.SetValue(Controller, TraceID)
46: Dim UserName As String = ""
47: If Controller.HttpContext IsNot Nothing Then
48: If Controller.HttpContext.Request.Headers("JwtUserName").Count > 0 Then
49: Dim CurUsers As ApplicationUser = _UserService.GetCurrentUser(Controller.HttpContext.Request.Headers("JwtUserName")(0))
50: UserName = CurUsers.UserName
51: End If
52: End If
53: Dim ApiName As String = ""
54: If Controller.Request IsNot Nothing Then
55: ApiName = Controller.Request.HttpContext.Request.Path.Value
56: Else
57: ApiName = $"/{Y.Name}/{MethodName}"
58: End If
59: Dim RequestPrm As String = Newtonsoft.Json.JsonConvert.SerializeObject(Model)
60: _DB = _DB.ReOpen
61: Sql.ExecNonQuery(_DB, $"INSERT INTO `BackendApiTraceLog`(`TraceID`,`CrDate`,`UserName`,`ApiName`,`Request`) VALUES ('{TraceID}',Now(),{If(String.IsNullOrEmpty(UserName), "null", $"'{UserName}'")},'{ApiName}','{RequestPrm.Replace("'", "^")}');")
62: _DB.Database.GetDbConnection.Close()
63: End If
64: End If
65: End Sub
66: End Module
67: End Namespace
68:
1: Imports Microsoft.EntityFrameworkCore
2: Imports BackendAPI.Helper
3: Imports Microsoft.AspNetCore.Http
4: Imports Renci.SshNet.Sftp
5: Imports System.Runtime.CompilerServices
6: Imports BackendAPI.Model
7:
8: Namespace WebApi.Controllers
9: Partial Friend Module Tracer
10:
11: ''' <summary>
12: ''' Additional result tracer for directly call API from another API
13: ''' </summary>
14: <Extension>
15: Sub TraceResult(_DB As ApplicationDbContext, SaveToDb As Boolean, TraceID As String, Result As Object)
16: If SaveToDb Then
17: Dim Guid As Guid
18: Dim ResultStr As String = Newtonsoft.Json.JsonConvert.SerializeObject(Result)
19: _DB = _DB.ReOpen
20: Sql.ExecNonQuery(_DB, $"Update `cryptochestmax`.`BackendApiTraceLog` SET Response='{ResultStr.Replace("'", "^")}' where `TraceID`='{TraceID}';")
21: _DB.Database.GetDbConnection.Close()
22: _DB.Database.GetDbConnection.Dispose()
23: End If
24: End Sub
25: End Module
26: End Namespace
As you can see, tracing can be turn OFF directly from site config.
And because I use the same line in all methods in any controllers - each controller need to definition a 5 values.
21: Private ReadOnly _UserService As IUserService
22: Private _DbForTrace As ApplicationDbContext
24: Private ReadOnly _Trace As Boolean
25: Private ReadOnly _WithResult As Boolean
29: Friend _RequestTraceID As String
30:
Because each controller usually has one normal exit with OK result and many error exit (code 500) with various errors I usually trace abnormal exit by attributes. For this purposes I adding to each methods the same attribute.
1: <TraceResponse(SaveStatusList:="500")>
If methods can not perform internally and can use only externally (with HttpContext) - in this case we can omit parameter of this attribute. This is code of Attribute.
1: Imports System.Reflection
2: Imports BackendAPI.Model
3: Imports BackendAPI.WebApi.Controllers
4: Imports Microsoft.AspNetCore.Mvc
5: Imports Microsoft.AspNetCore.Mvc.Filters
6: Imports Microsoft.Extensions.Logging
7: Imports Microsoft.EntityFrameworkCore
8: Imports BackendAPI.Helper
9: Imports BackendAPI.Services
10: Imports System.Buffers
11: Imports System.IO.Pipelines
12: Imports System.Text
13: Imports Renci.SshNet.Sftp
14: Imports Newtonsoft.Json
15: Imports Microsoft.AspNetCore.Http
16: Imports Microsoft.AspNetCore.Authentication
17: Imports System.IO
18:
19: <AttributeUsage(AttributeTargets.Method)>
20: Public Class TraceResponseAttribute
21: Inherits ActionFilterAttribute
22: ''' <summary>
23: ''' "500,200" - default all code by WebRequest is saved to DB (but not return code by directly call API, for directly call use TraceResult)
24: ''' </summary>
25: Public Property SaveStatusList As String
26: Private Property _DB As ApplicationDbContext
27: Private Property _Trace As Boolean = False
28: Private Property _WithResult As Boolean = False
29: Private Property _UserService As IUserService
30: Private Property _RequestTraceID As String
31: Private Property _StatusList As List(Of Integer)
32: Private Property _RetStatusCode As Integer = 0
33:
34: Public Overrides Sub OnActionExecuted(context As ActionExecutedContext)
35: Dim Y As Type = context.Controller.GetType
36: For Each OneProp As FieldInfo In Y.GetRuntimeFields
37: If OneProp.Name = "_DB" Then
38: _DB = OneProp.GetValue(context.Controller)
39: ElseIf OneProp.Name = "_Trace" Then
40: _Trace = OneProp.GetValue(context.Controller)
41: ElseIf OneProp.Name = "_WithResult" Then
42: _WithResult = OneProp.GetValue(context.Controller)
43: ElseIf OneProp.Name = "_UserService" Then
44: _UserService = OneProp.GetValue(context.Controller)
45: ElseIf OneProp.Name = "_RequestTraceID" Then
46: _RequestTraceID = OneProp.GetValue(context.Controller)
47: End If
48: Next
49: If _DB IsNot Nothing And _UserService IsNot Nothing Then
50: If _Trace And _WithResult Then
51: If context.Result IsNot Nothing Then
52: If context.Result.GetType.Name = "OkObjectResult" Then
53: _RetStatusCode = CType(context.Result, OkObjectResult).StatusCode
54: ElseIf context.Result.GetType.Name = "JsonResult" Then
55: ElseIf context.Result.GetType.Name = "ObjectResult" Then
56: If CType(context.Result, ObjectResult)?.StatusCode IsNot Nothing Then
57: _RetStatusCode = CType(context.Result, ObjectResult)?.StatusCode
58: End If
59: End If
60: Else
61: _RetStatusCode = 0
62: End If
63: _StatusList = New List(Of Integer)
64: If Not String.IsNullOrEmpty(SaveStatusList) Then
65: Dim Arr1 = SaveStatusList.Split(",")
66: For i As Integer = 0 To Arr1.Count - 1
67: If Not String.IsNullOrEmpty(Arr1(i)) Then
68: _StatusList.Add(CInt(Arr1(i)))
69: End If
70: Next
71: Else
72: GoTo SaveToDB
73: End If
74: Dim CodePresent As Integer = _StatusList.Where(Function(X) X = _RetStatusCode).Count
75: If CodePresent > 0 Then
76: SaveToDB:
77: Dim RequestID As String = CType(context.HttpContext, Microsoft.AspNetCore.Http.DefaultHttpContext).TraceIdentifier
78: If String.IsNullOrEmpty(RequestID) Then
79: RequestID = _RequestTraceID
80: End If
81: Dim Result As String = ""
82: If context.Result IsNot Nothing Then
83: If context.Result.GetType.Name = "OkObjectResult" Then
84: Result = JsonConvert.SerializeObject(CType(context.Result, OkObjectResult).Value)
85: ElseIf context.Result.GetType.Name = "JsonResult" Then
86: Result = JsonConvert.SerializeObject(CType(context.Result, JsonResult).Value)
87: ElseIf context.Result.GetType.Name = "ObjectResult" Then
88: Result = JsonConvert.SerializeObject(CType(context.Result, ObjectResult).Value)
89: End If
90: End If
91: _DB = _DB.ReOpen
92: Sql.ExecNonQuery(_DB, $"Update `BackendApiTraceLog` SET Response='{Result.Replace("'", "^")}', ResponseStatus='{_RetStatusCode}' where `TraceID`='{RequestID}';")
93: _DB.Database.GetDbConnection.Close()
94: End If
95: End If
96: End If
97: MyBase.OnActionExecuted(context)
98: End Sub
99: End Class
100:
What main point in this tracing? Main point is _DbContext must be different related to main _Db controller context, in other case you will see a lot of error message from hell.
If you can interesting for other my usually attributes look to this page - My TDD Technique for Backend API development with Xunit (Custom Attribute, WebClient GET/POST, JWT auth, Fact, Theory, InlineData, ClassData iterator function, Inject Log, Txt parsers for console output).
2. Authorization layer.
For understanding this description you can clear understand difference between Authentication and Authorization. Authentication usually made as middle like this https://github.com/Alex-1347/BackendAPI/tree/master/Jwt, look to description of my project template BackendAPI (Net Core 5) project template with Custom Attribute, Service and Controller Example, MySQL database with masked password in config, SwaggerUI, EF Core and extension to EF Core.
Authorization is another operation what define in BackandAPI what exactly operation in Backend authenticated can doing and what exactly operation user can not doing. For example user "Lux@Lux.com" can delete VirtualMachine 1 and and can not delete VirtualMachine 2 - this is Authorization layer.
I use many way to create authorization, for example hardest way with DB and set up right dynamically, middle way for difficulties is role based way (this way like dumb Microsoft developers), but in this page I want to describe more flexible and simple way.
In the beginning of each method I add the same code line.
50: If CurUsers.IsAdmin Or Check(Reflection.MethodBase.GetCurrentMethod.Name, Model, _DB) Then
... 'Method body69: Else70: Return Unauthorized()71: End If72: End Function
This is absolute minimal and safe way to authorizing. Than we need to create one simple function Check.
This layers contains a bunch of functions for analyzing request and return true (if API is allow - for example Authenticate API) or false (for example for GetAll API - this API allow only for Admin).
Some API deleted from checking authorization, this means this API allow only for Admin.
3. Special attribute to arrange API for authorization layers.
As you can see in previous screens I aso use special attribute to arrange API to security layers.
1: Namespace WebApi.Controllers
2: Public Enum SecurityLevel
3: Anon = 1
4: User = 2
5: AdminOnly = 3
6: Notification = 4
7: End Enum
8: <AttributeUsage(AttributeTargets.Method)>
9: Public Class SecurityAttribute
10: Inherits Attribute
11:
12: Public ReadOnly Value As SecurityLevel
13: Public Sub New(value As SecurityLevel)
14: Me.Value = value
15: End Sub
16:
17: End Class
18:
19: End Namespace
This way allow me ordering API by layers.
For arrange API I use this code.
1: Imports System.Reflection
2: Imports Xunit
3: Imports Xunit.Abstractions
4:
5: Public Class ProjectState
6: Private ReadOnly Log As ITestOutputHelper
7:
8: Public Sub New(_Log As ITestOutputHelper)
9: Log = _Log
10: End Sub
11:
12: <Fact>
13: Sub GetApiState()
14: Dim AllApi As Integer, OkApi As Integer, NfApi As Integer, AdmApi As Integer, UserApi As Integer, NotifApi As Integer
15: Dim API As Assembly = Assembly.LoadFile("G:\Projects\CryptoChestMax\BackendAPI\BackendAPI\bin\Debug\net6.0\BackendAPI.dll")
16: Dim Types = API.GetTypes()
17: Dim Controllers As List(Of Type) = Types.Where(Function(X) X.Name.Contains("Controller")).ToList
18: For i As Integer = 0 To Controllers.Count - 1
19: Log.WriteLine($"{i + 1}. {Controllers(i).Name.Replace("Controller", "")}")
20: Dim Methods As List(Of MethodInfo) = DirectCast(Controllers(i), System.Reflection.TypeInfo).DeclaredMethods.ToList
21: For j As Integer = 0 To Methods.Count - 1
22: Dim Attributes As List(Of CustomAttributeData) = Methods(j).CustomAttributes.ToList
23: Dim StateVal As String
24: Dim SecurityVal As String, SecurityMark As String
25: For k = 0 To Attributes.Count - 1
26: If Attributes(k).AttributeType.Name = "StateAttribute" Then
27: StateVal = Attributes(k).ToString.Replace("[BackendAPI.StateAttribute(", "").Replace(")]", "")
28: AllApi += 1
29: If StateVal.ToLower.Contains("ok.") Then
30: OkApi += 1
31: ElseIf StateVal.ToLower.Contains("api not found") Then
32: NfApi += 1
33: End If
34: ElseIf Attributes(k).AttributeType.Name = "SecurityAttribute" Then
35: '"[BackendAPI.WebApi.Controllers.SecurityAttribute((BackendAPI.WebApi.Controllers.SecurityLevel)1)]"
36: SecurityVal = Attributes(k).ToString.Replace("[BackendAPI.WebApi.Controllers.SecurityAttribute", "").Replace("((BackendAPI.WebApi.Controllers.SecurityLevel)", "").Replace(")]", "")
37: Select Case CInt(SecurityVal)
38: Case 1 : SecurityMark = "(Anon) "
39: Case 2 : SecurityMark = "(User) " : UserApi = UserApi + 1
40: Case 3 : SecurityMark = "(Admin)" : AdmApi = AdmApi + 1
41: Case 4 : SecurityMark = "(Notif)" : NotifApi = NotifApi + 1
42: End Select
43: End If
44: Next
45: Log.WriteLine($" {SecurityMark} {j + 1}. {Methods(j).Name} : {StateVal}")
46: Next
47: Next
48: Log.WriteLine($"Summary: Total {AllApi}, Finished {OkApi}, NotFound {NfApi}. Admin {AdmApi}, User {UserApi}, NotifApi {NotifApi}.")
49: End Sub
50: End Class
|