unit XData.Security.Attributes;

interface

uses
  SysUtils, StrUtils, Types,
  Sparkle.Security,
  XData.Security.Interfaces;

type
  TEntitySetPermission = (List, Get, Insert, Modify, Delete);
  TEntitySetPermissions = set of TEntitySetPermission;

const
  EntitySetPermissionsNone = [];
  EntitySetPermissionsAll = [Low(TEntitySetPermission)..High(TEntitySetPermission)];
  EntitySetPermissionsRead = [TEntitySetPermission.List, TEntitySetPermission.Get];
  EntitySetPermissionsWrite = [TEntitySetPermission.Insert, TEntitySetPermission.Modify,
    TEntitySetPermission.Delete];

type
  TUserAuthenticatedRequirement = class(TInterfacedObject, IAuthorizationRequirement)
  public
    function Check(Context: IAuthorizationContext): IRequirementResult;
  end;

  TScopesRequirement = class(TInterfacedObject, IAuthorizationRequirement)
  strict private
    const ScopeKey = 'scope';
  strict private
    FScopes: TStringDynArray;
  public
    constructor Create(const ScopeNames: string);
    function Check(Context: IAuthorizationContext): IRequirementResult;
  end;

  TStringClaimsRequirement = class(TInterfacedObject, IAuthorizationRequirement)
  strict private
    FClaimName: string;
    FClaimValues: array of string;
  public
    constructor Create(const ClaimName, ClaimValue: string);
    function Check(Context: IAuthorizationContext): IRequirementResult;
  end;

  TClaimsRequirement = class(TInterfacedObject, IAuthorizationRequirement)
  strict private
    FClaimName: string;
  public
    constructor Create(const ClaimName: string);
    function Check(Context: IAuthorizationContext): IRequirementResult;
  end;

  SecurityAttribute = class(TCustomAttribute)
  public
    function GetRequirement: IAuthorizationRequirement; virtual; abstract;
  end;

  AuthorizeAttribute = class(SecurityAttribute)
  strict private
    FRequirement: IAuthorizationRequirement;
  public
    constructor Create;
    function GetRequirement: IAuthorizationRequirement; override;
  end;

  AuthorizeScopesAttribute = class(SecurityAttribute)
  strict private
    FRequirement: IAuthorizationRequirement;
  public
    constructor Create(const Scopes: string);
    function GetRequirement: IAuthorizationRequirement; override;
  end;

  AuthorizeClaimsAttribute = class(SecurityAttribute)
  strict private
    FRequirement: IAuthorizationRequirement;
  public
    constructor Create(const ClaimName: string); overload;
    constructor Create(const ClaimName, ClaimValue: string); overload;
    function GetRequirement: IAuthorizationRequirement; override;
  end;

  EntitySecurityAttribute = class(TCustomAttribute)
  public
    function GetRequirement: IAuthorizationRequirement; virtual; abstract;
    function GetPermissions: TEntitySetPermissions; virtual; abstract;
  end;

  EntityAuthorizeAttribute = class(EntitySecurityAttribute)
  strict private
    FRequirement: IAuthorizationRequirement;
    FPermissions: TEntitySetPermissions;
  public
    constructor Create(Permissions: TEntitySetPermissions = EntitySetPermissionsAll);
    function GetRequirement: IAuthorizationRequirement; override;
    function GetPermissions: TEntitySetPermissions; override;
  end;

  EntityAuthorizeScopesAttribute = class(EntitySecurityAttribute)
  strict private
    FScopes: string;
    FRequirement: IAuthorizationRequirement;
    FPermissions: TEntitySetPermissions;
  public
    constructor Create(const Scopes: string; Permissions: TEntitySetPermissions = EntitySetPermissionsAll);
    function GetRequirement: IAuthorizationRequirement; override;
    function GetPermissions: TEntitySetPermissions; override;
  end;

  EntityAuthorizeClaimsAttribute = class(EntitySecurityAttribute)
  strict private
    FRequirement: IAuthorizationRequirement;
    FPermissions: TEntitySetPermissions;
  public
    constructor Create(const ClaimName: string;
      Permissions: TEntitySetPermissions = EntitySetPermissionsAll); overload;
    constructor Create(const ClaimName, ClaimValue: string;
      Permissions: TEntitySetPermissions = EntitySetPermissionsAll); overload;
    function GetRequirement: IAuthorizationRequirement; override;
    function GetPermissions: TEntitySetPermissions; override;
  end;

implementation


{ AuthorizeAttribute }

constructor AuthorizeAttribute.Create;
begin
  FRequirement := TUserAuthenticatedRequirement.Create;
end;

function AuthorizeAttribute.GetRequirement: IAuthorizationRequirement;
begin
  Result := FRequirement;
end;

{ TUserAuthenticatedRequirement }

function TUserAuthenticatedRequirement.Check(
  Context: IAuthorizationContext): IRequirementResult;
begin
  if Context.User <> nil then
    Result := TRequirementResult.Success
  else
    Result := TRequirementResult.Failed;
end;

{ AuthorizeScopeAttribute }

constructor AuthorizeScopesAttribute.Create(const Scopes: string);
begin
  inherited Create;
  FRequirement := TScopesRequirement.Create(Scopes);
end;

function AuthorizeScopesAttribute.GetRequirement: IAuthorizationRequirement;
begin
  Result := FRequirement;
end;

{ TScopeRequirement }

function TScopesRequirement.Check(
  Context: IAuthorizationContext): IRequirementResult;
var
  ScopeClaim: TUserClaim;
  AvailableScope: string;
  TargetScope: string;
begin
  if (Context.User = nil) or (Context.User.Claims = nil) then
    Exit(TRequirementResult.Failed);

  ScopeClaim := Context.User.Claims.Find(ScopeKey);
  if (ScopeClaim = nil) or not ScopeClaim.IsString then
    Exit(TRequirementResult.Failed);

  for AvailableScope in SplitString(ScopeClaim.AsString, ' ') do
    for TargetScope in FScopes do
      if AvailableScope = TargetScope then
        Exit(TRequirementResult.Success);

  Result := TRequirementResult.Failed;
end;

constructor TScopesRequirement.Create(const ScopeNames: string);
var
  I: Integer;
begin
  FScopes := SplitString(ScopeNames, ',');
  for I := 0 to Length(FScopes) - 1 do
    FScopes[I] := Trim(FScopes[I]);
end;

{ EntityAuthorizeAttribute }

constructor EntityAuthorizeAttribute.Create(Permissions: TEntitySetPermissions);
begin
  inherited Create;
  FPermissions := Permissions;
  FRequirement := TUserAuthenticatedRequirement.Create;
end;

function EntityAuthorizeAttribute.GetPermissions: TEntitySetPermissions;
begin
  Result := FPermissions;
end;

function EntityAuthorizeAttribute.GetRequirement: IAuthorizationRequirement;
begin
  Result := FRequirement;
end;

{ EntityAuthorizeScopesAttribute }

constructor EntityAuthorizeScopesAttribute.Create(const Scopes: string;
  Permissions: TEntitySetPermissions);
begin
  inherited Create;
  FScopes := Scopes;
  FRequirement := TScopesRequirement.Create(Scopes);
  FPermissions := Permissions;
end;

function EntityAuthorizeScopesAttribute.GetPermissions: TEntitySetPermissions;
begin
  Result := FPermissions;
end;

function EntityAuthorizeScopesAttribute.GetRequirement: IAuthorizationRequirement;
begin
  Result := FRequirement;
end;

{ AuthorizeClaimsAttribute }

constructor AuthorizeClaimsAttribute.Create(const ClaimName,
  ClaimValue: string);
begin
  inherited Create;
  FRequirement := TStringClaimsRequirement.Create(ClaimName, ClaimValue);
end;

constructor AuthorizeClaimsAttribute.Create(const ClaimName: string);
begin
  inherited Create;
  FRequirement := TClaimsRequirement.Create(ClaimName);
end;

function AuthorizeClaimsAttribute.GetRequirement: IAuthorizationRequirement;
begin
  Result := FRequirement;
end;

{ TClaimsRequirement }

function TStringClaimsRequirement.Check(
  Context: IAuthorizationContext): IRequirementResult;
var
  Claim: TUserClaim;
  ExistingValue, TargetValue: string;
begin
  if (Context.User = nil) or (Context.User.Claims = nil) then
    Exit(TRequirementResult.Failed);

  Claim := Context.User.Claims.Find(FClaimName);
  if (Claim = nil) or not Claim.IsString then
    Exit(TRequirementResult.Failed);

  ExistingValue := Claim.AsString;
  for TargetValue in FClaimValues do
    if ExistingValue = TargetValue then
      Exit(TRequirementResult.Success);

  Result := TRequirementResult.Failed;
end;

constructor TStringClaimsRequirement.Create(const ClaimName, ClaimValue: string);
begin
  inherited Create;
  FClaimName := ClaimName;
  SetLength(FClaimValues, 1);
  FClaimValues[0] := ClaimValue;
end;

{ TClaimsRequirement }

function TClaimsRequirement.Check(
  Context: IAuthorizationContext): IRequirementResult;
var
  Claim: TUserClaim;
begin
  if (Context.User = nil) or (Context.User.Claims = nil) then
    Exit(TRequirementResult.Failed);

  Claim := Context.User.Claims.Find(FClaimName);
  if (Claim <> nil) then
    Result := TRequirementResult.Success
  else
    Result := TRequirementResult.Failed;
end;

constructor TClaimsRequirement.Create(const ClaimName: string);
begin
  inherited Create;
  FClaimName := ClaimName;
end;

{ EntityAuthorizeClaimsAttribute }

constructor EntityAuthorizeClaimsAttribute.Create(const ClaimName: string;
  Permissions: TEntitySetPermissions);
begin
  inherited Create;
  FRequirement := TClaimsRequirement.Create(ClaimName);
  FPermissions := Permissions;
end;

constructor EntityAuthorizeClaimsAttribute.Create(const ClaimName,
  ClaimValue: string; Permissions: TEntitySetPermissions);
begin
  inherited Create;
  FRequirement := TStringClaimsRequirement.Create(ClaimName, ClaimValue);
  FPermissions := Permissions;
end;

function EntityAuthorizeClaimsAttribute.GetPermissions: TEntitySetPermissions;
begin
  Result := FPermissions;
end;

function EntityAuthorizeClaimsAttribute.GetRequirement: IAuthorizationRequirement;
begin
  Result := FRequirement;
end;

end.
