{
    This file is part of the Free Pascal run time library.
    Copyright (c) 2019 by the Free Pascal development team

    SQLDB REST HTTP BASIC authentication.

    See the file COPYING.FPC, included in this distribution,
    for details about the copyright.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

 **********************************************************************}
{$IFNDEF FPC_DOTTEDUNITS}
unit sqldbrestauth;
{$ENDIF FPC_DOTTEDUNITS}

{$mode objfpc}{$H+}

interface

{$IFDEF FPC_DOTTEDUNITS}
uses
  System.Classes, System.SysUtils, Data.Sqldb, Data.Db, FpWeb.RestBridge.Consts, FpWeb.RestBridge.Io, FpWeb.Http.Defs;
{$ELSE FPC_DOTTEDUNITS}
uses
  Classes, SysUtils, sqldb, db, sqldbrestconst, sqldbrestio, httpdefs;
{$ENDIF FPC_DOTTEDUNITS}

Type
  TAuthenticateEvent = procedure (Sender : TObject; aRequest : TRequest) of object;

  { TRestAuthenticator }

  TRestAuthenticator = Class(TComponent)
  private
    FAfterAuthenticate: TAuthenticateEvent;
    FBeforeAuthenticate: TAuthenticateEvent;
  Protected
    function DoAuthenticateRequest(io : TRestIO) : Boolean; virtual; abstract;
  Public
    Function AuthenticateRequest(io : TRestIO) : Boolean;
    Function NeedConnection : Boolean; virtual;
  Published
    Property BeforeAuthenticate : TAuthenticateEvent Read FBeforeAuthenticate Write FBeforeAuthenticate;
    Property AfterAuthenticate : TAuthenticateEvent Read FAfterAuthenticate Write FAfterAuthenticate;
  end;

  TBasicAuthenticationEvent = procedure (sender : TObject; Const aUserName,aPassword : UTF8String; Var allow : Boolean; Var UserID : UTF8String) of object;

  { TRestBasicAuthenticator }

  TRestBasicAuthenticator = Class(TRestAuthenticator)
  private
    FAuthConnection: TSQLConnection;
    FAuthenticationRealm: UTF8String;
    FAuthSQL: TStringList;
    FDefaultPassword: UTF8String;
    FDefaultUserID: UTF8String;
    FDefaultUserName: UTF8String;
    FOnBasicAuthentication: TBasicAuthenticationEvent;
    function GetAuthenticationRealm: UTF8String;
    function GetAuthSQL: TStrings;
    function IsNotDefaultRealm: Boolean;
    procedure SetAuthConnection(AValue: TSQLConnection);
    procedure SetAuthSQL(AValue: TStrings);
  Protected
    function HaveAuthSQL: Boolean;
    function AuthenticateUserUsingSQl(IO: TRestIO; const UN, PW: UTF8String; out UID: UTF8String): Boolean; virtual;
  Public
    Constructor Create(AOwner :TComponent); override;
    Destructor Destroy; override;
    class function ExtractUserNamePassword(Req: TRequest; out UN, PW: UTF8String): Boolean;
    class function ExtractUserName(Req: TRequest) : UTF8String;
    Function NeedConnection : Boolean; override;
    function DoAuthenticateRequest(IO : TRestIO) : Boolean; override;
  Published
    Property AuthConnection : TSQLConnection Read FAuthConnection Write SetAuthConnection;
    Property AuthenticateUserSQL : TStrings Read GetAuthSQL Write SetAuthSQL;
    Property DefaultUserName : UTF8String Read FDefaultUserName Write FDefaultUserName;
    Property DefaultPassword : UTF8String Read FDefaultPassword Write FDefaultPassword;
    Property DefaultUserID : UTF8String Read FDefaultUserID Write FDefaultUserID ;
    Property AuthenticationRealm : UTF8String Read GetAuthenticationRealm Write FAuthenticationRealm Stored IsNotDefaultRealm;
    Property OnBasicAuthentication : TBasicAuthenticationEvent Read FOnBasicAuthentication Write FOnBasicAuthentication;
  end;

implementation

{$IFDEF FPC_DOTTEDUNITS}
uses System.StrUtils, System.Hash.Base64;
{$ELSE FPC_DOTTEDUNITS}
uses strutils, base64;
{$ENDIF FPC_DOTTEDUNITS}

{ TRestBasicAuthenticator }

function TRestBasicAuthenticator.GetAuthenticationRealm: UTF8String;
begin
  Result:=FAuthenticationRealm;
  if Result='' then
    Result:=DefaultAuthenticationRealm;
end;

function TRestBasicAuthenticator.GetAuthSQL: TStrings;
begin
  Result:=FAuthSQL;
end;

function TRestBasicAuthenticator.IsNotDefaultRealm: Boolean;
begin
  Result:=(GetAuthenticationRealm<>DefaultAuthenticationRealm);
end;

procedure TRestBasicAuthenticator.SetAuthConnection(AValue: TSQLConnection);
begin
  if FAuthConnection=AValue then Exit;
  If Assigned(FAuthConnection) then
    FAuthConnection.RemoveFreeNotification(Self);
  FAuthConnection:=AValue;
  If Assigned(FAuthConnection) then
    FAuthConnection.FreeNotification(Self);
end;

procedure TRestBasicAuthenticator.SetAuthSQL(AValue: TStrings);
begin
  if FAuthSQL=AValue then Exit;
  FAuthSQL.Assign(AValue);
end;

constructor TRestBasicAuthenticator.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  FAuthSQL:=TStringList.Create;
end;

destructor TRestBasicAuthenticator.Destroy;
begin
  FreeAndNil(FAuthSQL);
  inherited Destroy;
end;

function TRestBasicAuthenticator.NeedConnection: Boolean;
begin
  Result:=HaveAuthSQL and (AuthConnection=Nil);
end;

function TRestBasicAuthenticator.HaveAuthSQL: Boolean;

begin
  Result:=(FAuthSQL.Count>0) and (Trim(FAuthSQL.Text)<>'');
end;

function TRestBasicAuthenticator.AuthenticateUserUsingSQl(IO: TRestIO;
  const UN, PW: UTF8String; out UID: UTF8String): Boolean;

Var
  Conn : TSQLConnection;
  Q : TSQLQuery;
  P : TParam;

begin
  Result:=HaveAuthSQL;
  if not Result then
     exit;
  Conn:=Self.AuthConnection;
  if Conn=Nil then
    Conn:=IO.Connection;
  Result:=Conn<>Nil;
  if not Result then
    exit;
  Q:=TSQLQuery.Create(Self);
  try
    Q.Database:=Conn;
    if IO.Transaction<>nil then
      Q.Transaction:=IO.Transaction;
    Q.SQL:=FAuthSQL;
    P:=Q.Params.FindParam('UserName');
    if P<>Nil then
      P.AsString:=UN;
    P:=Q.Params.FindParam('Password');
    if P<>Nil then
      P.AsString:=PW;
    Q.UniDirectional:=True;
    Q.UsePrimaryKeyAsKey:=False;
    Q.Open;
    Result:=Not (Q.EOF and Q.BOF);
    If Result then
      UID:=Q.Fields[0].AsString;
  finally
    Q.Free;
  end;
end;

class function TRestBasicAuthenticator.ExtractUserNamePassword(Req: TRequest;
  out UN, PW: UTF8String): Boolean;

Var
  S,A : String;

begin
  S:=Req.Authorization;
  Result:=(S<>'');
  if not Result then
    begin
    UN:='';
    PW:='';
    end
  else
    begin
    A:=ExtractWord(1,S,[' ']);
    S:=ExtractWord(2,S,[' ']);
    if Not SameText(A,'BASIC') then
      Exit(False);
    S:=DecodeStringBase64(S);
    UN:=ExtractWord(1,S,[':']);
    PW:=ExtractWord(2,S,[':']);
    end;
end;

class function TRestBasicAuthenticator.ExtractUserName(Req: TRequest): UTF8String;

Var
  PW : UTF8String;

begin
  if not ExtractUserNamePassword(Req,Result,PW) then
    Result:='?';
end;

function TRestBasicAuthenticator.DoAuthenticateRequest(IO: TRestIO): Boolean;

Var
  UID,UN,PW : UTF8String;

begin
  Result:=False;
  UID:='';
  if ExtractUserNamePassword(IO.Request,UN,PW) then
    begin
    if (UN<>'') and (PW<>'') then
      If (DefaultUserName<>'') and (DefaultPassword<>'') then
        begin
        Result:=(UN=DefaultUserName) and (PW=DefaultPassword);
        If Result then
          begin
          UID:=DefaultUserID;
          If UID='' then
            UID:=DefaultUserName;
          end;
        end
      else
        UID:=UN;
    if Assigned(FOnBasicAuthentication) then
       FOnBasicAuthentication(Self,UN,PW,Result,UID);
    if not Result then
      Result:=AuthenticateUserUsingSQl(IO,UN,PW,UID);
    end;
  If Result then
    IO.UserID:=UID
  else
    begin
    IO.Response.Code:=IO.RestStatuses.GetStatusCode(rsUnauthorized);
    IO.Response.CodeText:=SUnauthorized;
    IO.Response.WWWAuthenticate:=Format('BASIC Realm: "%s"',[AuthenticationRealm]);
    end;
end;

{ TRestAuthenticator }

function TRestAuthenticator.AuthenticateRequest(io: TRestIO): Boolean;
begin
  If Assigned(FBeforeAuthenticate) then
    FBeforeAuthenticate(self,IO.Request);
  Result:=DoAuthenticateRequest(IO);
  If Assigned(FAfterAuthenticate) then
    FAfterAuthenticate(self,IO.Request);
end;

function TRestAuthenticator.NeedConnection: Boolean;
begin
  Result:=False;
end;


end.

