delphimvcframework/sources/MVCFramework.Middleware.Authentication.RoleBasedAuthHandler.pas
2018-01-29 17:30:53 +01:00

421 lines
13 KiB
ObjectPascal

// ***************************************************************************
//
// Delphi MVC Framework
//
// Copyright (c) 2010-2018 Daniele Teti and the DMVCFramework Team
//
// Contributor of this file: Janidan - https://github.com/janidan
//
// https://github.com/danieleteti/delphimvcframework
//
// ***************************************************************************
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// *************************************************************************** }
unit MVCFramework.Middleware.Authentication.RoleBasedAuthHandler;
interface
uses
MVCFramework,
MVCFramework.Commons,
MVCFramework.Middleware.Authentication,
System.Generics.Collections,
System.Rtti;
type
MVCRequiresAuthenticationAttribute = class(MVCBaseAttribute);
MVCRoleEval = (reOR, reAND);
MVCRequiresRoleAttribute = class(MVCRequiresAuthenticationAttribute)
public const
DefaultListSeparator = ';';
private
FRole: string;
FRoleEval: MVCRoleEval;
FListSep: Char;
public
constructor Create(const aRole: string); overload;
constructor Create(const aRole: string;
const aRoleEval: MVCRoleEval); overload;
constructor Create(const aRole: string; const aRoleEval: MVCRoleEval;
const aListSep: Char); overload;
function GetRoles: TArray<string>;
property RoleEval: MVCRoleEval read FRoleEval;
end;
IMVCRoleBasedAuthenticationHandler = interface(IMVCAuthenticationHandler)
['{07ABEF93-DBCC-4C55-BD39-BD1F48490A73}']
procedure OnAuthorization(const AContext: TWebContext;
const AUserRoles: TList<string>;
const AControllerQualifiedClassName: string; const AActionName: string;
var AIsAuthorized: Boolean);
end;
TRoleBasedAuthHandler = class(TInterfacedObject, IMVCAuthenticationHandler,
IMVCRoleBasedAuthenticationHandler)
private
FRttiContext: TRttiContext;
function TryGetAttributes<TTypeOfAttribute: TCustomAttribute>
(const aAttributes: TArray<TCustomAttribute>;
out aListOfAttributes: TArray<TTypeOfAttribute>): Boolean;
function CheckUserRoles(const AContext: TWebContext;
const AUserRoles: TList<string>;
const aRoleAttributes: TArray<MVCRequiresRoleAttribute>): Boolean;
function ResolveRole(const AContext: TWebContext;
const aRole: string): string;
function CreateParameterNameList(const aTemplate: string): TList<string>;
public
procedure OnRequest(const ControllerQualifiedClassName: string;
const ActionName: string; var AuthenticationRequired: Boolean);
procedure OnAuthentication(const UserName: string; const Password: string;
UserRoles: TList<string>; var IsValid: Boolean;
const SessionData: TDictionary<string, string>); virtual; abstract;
procedure OnAuthorization(UserRoles: TList<string>;
const ControllerQualifiedClassName: string; const ActionName: string;
var IsAuthorized: Boolean); overload;
procedure OnAuthorization(const AContext: TWebContext;
const AUserRoles: TList<string>;
const AControllerQualifiedClassName: string; const AActionName: string;
var AIsAuthorized: Boolean); overload;
constructor Create;
destructor Destroy; override;
end;
TMVCRoleBasedAuthenticationMiddleware = class
(TMVCCustomAuthenticationMiddleware, IMVCMiddleware)
private
FAuthenticationHandler: IMVCRoleBasedAuthenticationHandler;
procedure DoRoleBasedBeforeControllerAction(const aHandler
: IMVCRoleBasedAuthenticationHandler; const AContext: TWebContext;
const AControllerQualifiedClassName: string; const AActionName: string;
var AHandled: Boolean);
protected
procedure OnBeforeControllerAction(AContext: TWebContext;
const AControllerQualifiedClassName: string; const AActionName: string;
var AHandled: Boolean);
public
constructor Create(const AAuthenticationHandler: IMVCAuthenticationHandler;
const ALoginUrl: string = '/system/users/logged'); override;
end;
implementation
uses
System.SysUtils,
System.RegularExpressions;
{ MVCRequiresRoleAttribute }
constructor MVCRequiresRoleAttribute.Create(const aRole: string);
begin
Self.Create(aRole, MVCRoleEval.reAND, DefaultListSeparator);
end;
constructor MVCRequiresRoleAttribute.Create(const aRole: string;
const aRoleEval: MVCRoleEval);
begin
Self.Create(aRole, aRoleEval, DefaultListSeparator);
end;
constructor MVCRequiresRoleAttribute.Create(const aRole: string;
const aRoleEval: MVCRoleEval; const aListSep: Char);
begin
inherited Create;
FRole := aRole;
FRoleEval := aRoleEval;
FListSep := aListSep;
end;
function MVCRequiresRoleAttribute.GetRoles: TArray<string>;
begin
Result := FRole.Split([FListSep]);
end;
{ TRoleBasedAuthHandler }
function TRoleBasedAuthHandler.CheckUserRoles(const AContext: TWebContext;
const AUserRoles: TList<System.string>;
const aRoleAttributes: TArray<MVCRequiresRoleAttribute>): Boolean;
var
vAttribute: MVCRequiresRoleAttribute;
vSingleRole: string;
begin
// By default we will say that you are good to go.
Result := True;
if (Length(aRoleAttributes) = 0) then
Exit;
// All Attributes MUST match -> AND evaluation
for vAttribute in aRoleAttributes do
// if not AUserRoles.Contains(ResolveRole(AContext, vAttribute.Role)) then
// Exit(False);
begin
if (vAttribute.RoleEval = MVCRoleEval.reAND) then
begin
for vSingleRole in vAttribute.GetRoles do
if not AUserRoles.Contains(ResolveRole(AContext, vSingleRole)) then
Exit(False);
end
else // OR evaluation
begin
// By default we assume we have not found the role.
Result := False;
for vSingleRole in vAttribute.GetRoles do
if AUserRoles.Contains(ResolveRole(AContext, vSingleRole)) then
Result := True;
// If one of the roles does not match we exit the check.
if not Result then
Exit;
end;
end;
end;
constructor TRoleBasedAuthHandler.Create;
begin
inherited Create;
FRttiContext := TRttiContext.Create;
end;
function TRoleBasedAuthHandler.CreateParameterNameList(const aTemplate: string)
: TList<string>;
const
MatchPattern = '\(\$([A-Za-z0-9\_]+)\)'; // Matches ($<name>) placeholders
var
S: string;
Matches: TMatchCollection;
M: TMatch;
I: Integer;
begin
Result := TList<string>.Create;
try
Matches := TRegEx.Matches(aTemplate, MatchPattern,
[roIgnoreCase, roCompiled, roSingleLine]);
for M in Matches do
for I := 0 to M.Groups.Count - 1 do
begin
S := M.Groups[I].Value;
if (Length(S) > 0) and (S[1] <> '(') then
begin
Result.Add(S);
Break;
end;
end;
except
Result.Free;
raise;
end;
end;
destructor TRoleBasedAuthHandler.Destroy;
begin
FRttiContext.Free;
inherited;
end;
procedure TRoleBasedAuthHandler.OnAuthorization(UserRoles: TList<string>;
const ControllerQualifiedClassName, ActionName: string;
var IsAuthorized: Boolean);
begin
Self.OnAuthorization(nil, UserRoles, ControllerQualifiedClassName, ActionName,
IsAuthorized);
end;
procedure TRoleBasedAuthHandler.OnAuthorization(const AContext: TWebContext;
const AUserRoles: TList<string>; const AControllerQualifiedClassName,
AActionName: string; var AIsAuthorized: Boolean);
var
vRttiType: TRttiType;
vAttributes: TArray<MVCRequiresRoleAttribute>;
vRttiMethod: TRttiMethod;
begin
// If there are no restrictions we will allow access to the ressource. (public API)
AIsAuthorized := True;
// Check all Role requirements on the controller level
vRttiType := FRttiContext.FindType(AControllerQualifiedClassName);
if TryGetAttributes<MVCRequiresRoleAttribute>(vRttiType.GetAttributes,
vAttributes) then
if not CheckUserRoles(AContext, AUserRoles, vAttributes) then
begin
AIsAuthorized := False;
Exit;
end;
// At this point the conttoller either has no restrictions or
// we have successfully cleared these.
// Verify all roles on the Action.
vRttiMethod := vRttiType.GetMethod(AActionName);
if TryGetAttributes<MVCRequiresRoleAttribute>(vRttiMethod.GetAttributes,
vAttributes) then
if not CheckUserRoles(AContext, AUserRoles, vAttributes) then
begin
AIsAuthorized := False;
Exit;
end;
end;
procedure TRoleBasedAuthHandler.OnRequest(const ControllerQualifiedClassName,
ActionName: string; var AuthenticationRequired: Boolean);
var
vRttiType: TRttiType;
vAttributes: TArray<MVCRequiresAuthenticationAttribute>;
vRttiMethod: TRttiMethod;
begin
vRttiType := FRttiContext.FindType(ControllerQualifiedClassName);
// Check class and Actions if they have role definitions.
AuthenticationRequired := TryGetAttributes<MVCRequiresAuthenticationAttribute>
(vRttiType.GetAttributes, vAttributes);
if not AuthenticationRequired then
begin
vRttiMethod := vRttiType.GetMethod(ActionName);
AuthenticationRequired :=
TryGetAttributes<MVCRequiresAuthenticationAttribute>
(vRttiMethod.GetAttributes, vAttributes);
end;
end;
function TRoleBasedAuthHandler.ResolveRole(const AContext: TWebContext;
const aRole: string): string;
var
vPlaceholders: TList<string>;
vPlaceholder: string;
begin
Result := aRole;
// In case we don't have a context there is no need to eval for placeholders.
if not Assigned(AContext) then
Exit;
vPlaceholders := CreateParameterNameList(aRole);
try
if (vPlaceholders.Count = 0) then
Exit;
for vPlaceholder in vPlaceholders do
Result := Result.Replace(Format('($%s)', [vPlaceholder]),
AContext.ParamsTable.Items[vPlaceholder], [rfReplaceAll]);
finally
vPlaceholders.Free;
end;
end;
function TRoleBasedAuthHandler.TryGetAttributes<TTypeOfAttribute>
(const aAttributes: TArray<TCustomAttribute>;
out aListOfAttributes: TArray<TTypeOfAttribute>): Boolean;
var
vAttribute: TCustomAttribute;
vResultList: TList<TTypeOfAttribute>;
begin
SetLength(aListOfAttributes, 0);
if (Length(aAttributes) = 0) then
Exit(False);
vResultList := TList<TTypeOfAttribute>.Create;
try
for vAttribute in aAttributes do
if (vAttribute.InheritsFrom(TTypeOfAttribute)) then
vResultList.Add(vAttribute as TTypeOfAttribute);
aListOfAttributes := vResultList.ToArray;
Result := (Length(aListOfAttributes) > 0);
finally
vResultList.Free;
end;
end;
{ TMVCRoleBasedAuthenticationMiddleware }
constructor TMVCRoleBasedAuthenticationMiddleware.Create
(const AAuthenticationHandler: IMVCAuthenticationHandler;
const ALoginUrl: string);
begin
inherited Create(AAuthenticationHandler, ALoginUrl);
Supports(AAuthenticationHandler, IMVCRoleBasedAuthenticationHandler,
FAuthenticationHandler);
end;
procedure TMVCRoleBasedAuthenticationMiddleware.
DoRoleBasedBeforeControllerAction(const aHandler
: IMVCRoleBasedAuthenticationHandler; const AContext: TWebContext;
const AControllerQualifiedClassName, AActionName: string;
var AHandled: Boolean);
var
IsValid: Boolean;
IsAuthorized: Boolean;
AuthRequired: Boolean;
begin
// This procedure is a basic copy of the inherited OnBeforeControllerAction procedure.
// Extention is by enabling the Authorization based on the context the call is being performed.
aHandler.OnRequest(AControllerQualifiedClassName, AActionName, AuthRequired);
if not AuthRequired then
begin
AHandled := False;
Exit;
end;
AContext.LoggedUser.LoadFromSession(AContext.Session);
IsValid := AContext.LoggedUser.IsValid;
if not IsValid then
begin
AContext.SessionStop(False);
SendResponse(AContext, AHandled);
Exit;
end;
IsAuthorized := False;
// Modification here from:
// FAuthenticationHandler.OnAuthorization(AContext.LoggedUser.Roles, AControllerQualifiedClassName, AActionName, IsAuthorized);
// to:
aHandler.OnAuthorization(AContext, AContext.LoggedUser.Roles,
AControllerQualifiedClassName, AActionName, IsAuthorized);
// Modification end
if IsAuthorized then
AHandled := False
else
begin
if IsValid then
SendResponse(AContext, AHandled, HTTP_STATUS.Forbidden)
else
SendResponse(AContext, AHandled, HTTP_STATUS.Unauthorized);
end;
end;
procedure TMVCRoleBasedAuthenticationMiddleware.OnBeforeControllerAction
(AContext: TWebContext; const AControllerQualifiedClassName,
AActionName: string; var AHandled: Boolean);
begin
if Assigned(FAuthenticationHandler) then
DoRoleBasedBeforeControllerAction(FAuthenticationHandler, AContext,
AControllerQualifiedClassName, AActionName, AHandled)
else
inherited OnBeforeControllerAction(AContext, AControllerQualifiedClassName,
AActionName, AHandled);
end;
end.