Talk:FireDAC.Phys.MSAcc.TFDMSAccessService

From RAD Studio API Documentation
Jump to: navigation, search

About create a secure database:

When creating database necessarily use CREATE_DBV4 instead CREATE_DB, because CREATE_DB created database in newest 2007 format, which not supported secure.

Helper unit for work with secure database (create, compact), this draft version:

unit ComTpsDB.MSAccService;

interface

uses FireDAC.Phys.MSAcc, FireDAC.Stan.Def;

{$DEFINE FireDAC_Monitor}

type
  TFDMSAccessServiceHelper=class helper for TFDMSAccessService
  private
    function GetAction: TFDMSAccessAction;
  public
    property Action: TFDMSAccessAction read GetAction;
  end;

  TFDMSAccessServiceNew=class(TFDMSAccessService)
  private
    FNeedSystemDB:boolean;
    FSystemDB:string;
    FUserName:string;
    FUserPass:string;
  protected
    procedure InternalExecute; override;
  public
    property NeedSystemDB:boolean read FNeedSystemDB write FNeedSystemDB;
    property SystemDB:string read FSystemDB write FSystemDB;
    property UserName:string read FUserName write FUserName;
    property UserPass:string read FUserPass write FUserPass;
  end;

implementation

uses FireDAC.Stan.Util, FireDAC.Phys.ODBCCli, System.SysUtils, FireDAC.Stan.Error,
     FireDAC.Stan.Consts, System.Win.ComObj, Winapi.Windows, System.Variants,
     FireDAC.Phys.ODBCBase, System.IOUtils, FireDAC.Comp.Client;

{ TFDMSAccessServiceHelper }

function TFDMSAccessServiceHelper.GetAction: TFDMSAccessAction;
begin
  result:=Self.FAction;
end;

{ TFDMSAccessServiceNew }

procedure TFDMSAccessServiceNew.InternalExecute;
var
  DoInherited:boolean;
  sStr, sDrv, sDb: String;
  lLastDrv, lSort:boolean;
  aFDConnection:TFDConnection;

  function NormFileName(AName: String): String;
  begin
    Result := FDExpandStr(AName);
    if (Pos(' ', Result) > 0) and (Result[1] <> '"') then
      Result := '"' + Result + '"';
  end;

  procedure DeleteDatabase(const AName: String);
  var
    sFile: String;
  begin
    sFile := FDExpandStr(AName);
    System.SysUtils.DeleteFile(sFile);
    System.SysUtils.DeleteFile(ChangeFileExt(sFile, '.ldb'));
    System.SysUtils.DeleteFile(ChangeFileExt(sFile, '.laccdb'));
  end;

  {$IFDEF MSWINDOWS}
  function RenameDatabase(const AOldName, ANewName: String): Boolean;
  var
    sOldFile, sNewFile, sLdbFile: String;
  begin
    sOldFile := FDExpandStr(AOldName);
    sNewFile := FDExpandStr(ANewName);
    Result := RenameFile(sOldFile, sNewFile);
    if Result then begin
      sLdbFile := ChangeFileExt(sOldFile, '.ldb');
      if FileExists(sLdbFile) then
        RenameFile(sLdbFile, ChangeFileExt(sNewFile, '.ldb'));
      sLdbFile := ChangeFileExt(sOldFile, '.laccdb');
      if FileExists(sLdbFile) then
        RenameFile(sLdbFile, ChangeFileExt(sNewFile, '.laccdb'));
    end;
  end;

  function GenTempFileName(const AName: String): String;
  var
    sPath: String;
  begin
    sPath := ExtractFilePath(FDExpandStr(AName));
    SetLength(Result, MAX_PATH);
    GetTempFileName(PChar(sPath), 'FD', 0, PChar(Result));
    Result := PChar(Result);
  end;

  procedure ProcessOLEException(AExc: EOleSysError);
  var
    sMsg: String;
    iCode: Integer;
    oExc: EFDDBEngineException;
    oErr: TFDDBError;
  begin
    sMsg := AExc.Message;
    iCode := er_FD_AccUnknownOleError;
    if AExc is EOLEException then begin
      if Pos('Class not registered', sMsg) > 0 then
        iCode := er_FD_AccClassNotRegistered
      else if Pos('Unrecognized database format', sMsg) > 0 then
        iCode := er_FD_AccUnrecognizedDbFormat
      else if Pos('Not a valid password', sMsg) > 0 then
        iCode := er_FD_AccNotValidPassword;
    end
    else
      if Pos('Class not registered', sMsg) > 0 then
        iCode := er_FD_AccSysClassNotRegistered;

    oExc := FDDBEngineExceptionCreate(EMSAccessNativeException, iCode, []);
    oErr := TFDDBError.Create;
    oErr.Message := sMsg;
    oExc.Append(oErr);
    FDException(Self, oExc{$IFDEF FireDAC_Monitor}, False{$ENDIF});
  end;

  procedure CompactRepair;
  // https://msdn.microsoft.com/uk-ua/bb237197(v=office.12).aspx
  var
    vJro: Variant;
    sSource, sDest, sProvider, sDestDatabase: String;
    iEngine: Integer;
    lRename: Boolean;
  begin
    if not FileExists(Database) then
      FDException(Self, FDDBEngineExceptionCreate(EMSAccessNativeException,
        er_FD_AccDbNotExists, [Database]) {$IFDEF FireDAC_Monitor}, False{$ENDIF});
    try
      vJro := CreateOLEObject('JRO.JetEngine');
      try
        lRename := (DestDatabase = '') or (DestDatabase = Database);
        if lRename then
          sDestDatabase := GenTempFileName(Database)
        else
          sDestDatabase := DestDatabase;
        DeleteDatabase(sDestDatabase);
        sSource := 'Provider=%s;Data Source=%s;Jet OLEDB:Engine Type=%d';
        sDest := sSource;
        if Password <> '' then begin
          sSource := sSource + ';Jet OLEDB:Database Password=' + Password;
          if not ResetPassword then
            sDest := sSource;
        end;
        if lSort then
          sDest := sDest + ';LocaleIdentifier=' + SortOrder;
        if Encrypted then
          sDest := sDest + ';Jet OLEDB:Encrypt Database=True';
        iEngine := 5;
        case DBVersion of
        avDefault:
          begin
            if sDrv = 'Microsoft Access Driver (*.mdb, *.accdb)' then
              sProvider := 'Microsoft.ACE.OLEDB.12.0'
            else
              sProvider := 'Microsoft.Jet.OLEDB.4.0';
          end;
        avAccess2:
          begin
            sProvider := 'Microsoft.Jet.OLEDB.4.0';
            iEngine := 3;
          end;
        avAccess95,
        avAccess97:
          begin
            sProvider := 'Microsoft.Jet.OLEDB.4.0';
            iEngine := 4;
          end;
        avAccess2000,
        avAccess2003:
          begin
            sProvider := 'Microsoft.Jet.OLEDB.4.0';
            iEngine := 5;
          end;
        avAccess2007:
          begin
            sProvider := 'Microsoft.ACE.OLEDB.12.0';
            iEngine := 5;
          end;
        end;
        //
        if FSystemDB<>'' then sSource := sSource + ';Jet OLEDB:System Database=' + FSystemDB;
        if FUserName<>'' then sSource := sSource + ';User Id=' + FUserName;
        if FUserPass<>'' then sSource := sSource + ';Password=' + FUserPass;
        //
        vJro.CompactDatabase(Format(sSource, [sProvider, FDExpandStr(Database),
          iEngine]), Format(sDest, [sProvider, FDExpandStr(sDestDatabase), iEngine]));
        if lRename then begin
          DeleteDatabase(Database);
          if RenameDatabase(sDestDatabase, Database) then
            DeleteDatabase(sDestDatabase);
        end;
      finally
        vJro := Unassigned;
      end;
    except
      on E: EOleSysError do
        ProcessOLEException(EOleSysError(E))
    end;
  end;
  {$ENDIF}

  function GeneratePID:string;
  var
    g1:TGUID;
  begin
    CreateGUID(g1);
    result:=g1.ToString;
    result:=result.Replace('{','');
    result:=result.Replace('}','');
    result:=result.Replace('-','');
    if result.Length>20 then result:=result.Substring(0,20);
  end;

begin
  sDrv := (DriverLink.DriverIntf as IFDPhysODBCDriver).ODBCDriver;
  DoInherited:=true;
  case Action of
  aaCompactDB:
{$IFDEF MSWINDOWS}
    begin
      DoInherited:=false;
      //
      CompactRepair;
    end;
{$ELSE}
    begin
      DoInherited:=false;
      //
      sDb := NormFileName(Database);
      sStr := 'COMPACT_DB=' + sDb + ' ';
      if DestDatabase = '' then
        sStr := sStr + sDb
      else
        sStr := sStr + NormFileName(DestDatabase);
      if SortOrder <> '' then
        sStr := sStr + ' ' + SortOrder;
      ExecuteBase(ODBC_ADD_DSN, sDrv, sStr);
    end;
{$ENDIF}
  aaRepairDB:
{$IFDEF MSWINDOWS}
  begin
    DoInherited:=false;
    //
    CompactRepair;
  end;
{$ELSE}
    begin
      DoInherited:=false;
      //
      sStr := 'REPAIR_DB=' + NormFileName(Database);
      ExecuteBase(ODBC_ADD_DSN, sDrv, sStr);
    end;
{$ENDIF}
    aaCreateDB:begin
      if FNeedSystemDB then
      begin
        DoInherited:=false;
        // http://www.litwindow.com/Knowhow/HowTo/howto_create_secure_access_dat.html
        // http://stackoverflow.com/questions/4949429/where-can-i-find-the-documentation-for-the-particular-kind-of-sql-used-by-the-je
        // https://msdn.microsoft.com/en-us/library/aa139977(office.10).aspx
        // 1.
        if TFile.Exists(FSystemDB)then TFile.Delete(FSystemDB);
        if TFile.Exists(Database)then TFile.Delete(Database);
        sStr:='CREATE_SYSDB='+FSystemDB+chr(0)+chr(0);
        ExecuteBase(ODBC_ADD_DSN, sDrv, sStr);
        if TFile.Exists(FSystemDB+'.mdb')then
        begin
          TFile.Copy(FSystemDB+'.mdb',FSystemDB,true);
          TFile.Delete(FSystemDB+'.mdb');
        end;
        // 2.
        sStr := 'CREATE_DB'; // use DBVersion=avAccess2003
        case DBVersion of
          avAccess2:    sStr := sStr + 'V2';
          avAccess95,
          avAccess97:   sStr := sStr + 'V3';
          avAccess2000,
          avAccess2003: sStr := sStr + 'V4';
        end;
        sStr:=sStr+'='+Database+chr(0)+chr(0);
        ExecuteBase(ODBC_ADD_DSN, sDrv, sStr);
        // 3.
        aFDConnection:=TFDConnection.Create(nil);
        aFDConnection.DriverName:='MSAcc';
        aFDConnection.Params.DriverID:='MSAcc';
        aFDConnection.Params.Values['Database']:=Database;
        aFDConnection.Params.Values['SystemDB']:=FSystemDB;
        aFDConnection.Params.Values['User_Name']:='admin';
        aFDConnection.Open;
        // 4.
        aFDConnection.ExecSQL('CREATE GROUP my_admin_group '+GeneratePID);
        aFDConnection.ExecSQL('CREATE USER '+FUserName+' "" '+GeneratePID);
        aFDConnection.ExecSQL('ADD USER '+FUserName+' TO my_admin_group');
        aFDConnection.ExecSQL('ADD USER '+FUserName+' TO admins');
        // 5.
        aFDConnection.Close;
        aFDConnection.Free;
        if TFile.Exists(Database)then TFile.Delete(Database);
        // 6.
        sStr := 'CREATE_DB'; // use DBVersion=avAccess2003
        lLastDrv := sDrv = 'Microsoft Access Driver (*.mdb, *.accdb)';
        case DBVersion of
        avDefault:    ;
        avAccess2:    sStr := sStr + 'V2';
        avAccess95,
        avAccess97:   sStr := sStr + 'V3';
        avAccess2000,
        avAccess2003: sStr := sStr + 'V4';
        avAccess2007:
          begin
            sDrv := 'Microsoft Access Driver (*.mdb, *.accdb)';
            lLastDrv := True;
          end;
        end;
        sStr := sStr + '=' + NormFileName(Database);
        lSort := SortOrder <> '';
        if lSort and (Pos('0x', SortOrder) = 0) then begin
          sStr := sStr + ' ' + SortOrder;
  {$IFDEF MSWINDOWS}
          lSort := False;
  {$ENDIF}
        end;
        if Encrypted and not lLastDrv then
          sStr := sStr + ' ENCRYPT';
        sStr:='SYSTEMDB='+FSystemDB+chr(0)+'UID='+FUserName+chr(0)+sStr+chr(0)+chr(0);
        ExecuteBase(ODBC_ADD_DSN, sDrv, sStr);
  {$IFDEF MSWINDOWS}
        if (Password <> '') or lSort or (Encrypted and lLastDrv) then begin
          sDb := DestDatabase;
          DestDatabase := '';
          try
            {$IFDEF MSWINDOWS}
            CompactRepair;
            {$ENDIF}
          finally
            DestDatabase := sDb;
          end;
        end;
  {$ENDIF}
        // 7.
        aFDConnection:=TFDConnection.Create(nil);
        aFDConnection.DriverName:='MSAcc';
        aFDConnection.Params.DriverID:='MSAcc';
        aFDConnection.Params.Values['Database']:=Database;
        aFDConnection.Params.Values['SystemDB']:=FSystemDB;
        aFDConnection.Params.Values['User_Name']:=FUserName;
        aFDConnection.Open;
        // 8.
        aFDConnection.ExecSQL('GRANT ALL PRIVILEGES ON DATABASE TO my_admin_group');
        aFDConnection.ExecSQL('REVOKE ALL PRIVILEGES ON DATABASE FROM admin');
        aFDConnection.ExecSQL('REVOKE ALL PRIVILEGES ON DATABASE FROM admins');
        aFDConnection.ExecSQL('REVOKE ALL PRIVILEGES ON DATABASE FROM users');
        // 9.
        aFDConnection.ExecSQL('ALTER USER '+FUserName+' PASSWORD '+FUserPass+' ""');
        // 10.
        //Your database is now secure. But while you are at it, you could create your own version of a ‘users’ group as well. All new users should be made part of this group.
        //CREATE GROUP my_users a_good_pid
        //GRANT whatever privileges ON whatever objects TO my_users
        //ADD my_admin TO my_users
        aFDConnection.Close;
        aFDConnection.Free;
      end else begin

      end;
    end;
  end;
  if DoInherited then inherited;
end;

end.

Usage:

uses FireDAC.Phys.MSAcc, ComTpsDB.MSAccService;
var
  FDPhysMSAccessDriverLink: TFDPhysMSAccessDriverLink;
  FDMSAccessService: TFDMSAccessServiceNew;
begin
  FDPhysMSAccessDriverLink:=TFDPhysMSAccessDriverLink.Create(nil);
  FDMSAccessService:=TFDMSAccessServiceNew.Create(nil);
  FDMSAccessService.DriverLink:=FDPhysMSAccessDriverLink;
  FDMSAccessService.DBVersion:=avAccess2003;
  FDMSAccessService.NeedSystemDB:=true;
  FDMSAccessService.UserName:='my_admin';
  FDMSAccessService.UserPass:='12345678';
  FDMSAccessService.Database:='sample.mdb';
  FDMSAccessService.SystemDB:='sample.mdw';
  // crerate
  FDMSAccessService.CreateDB;
  // compact
  FDMSAccessService.DestDatabase:='sample2.mdb';
  FDMSAccessService.Compact;
  //
  FDPhysMSAccessDriverLink.Free;
  FDMSAccessService.Free;
end;