mirror of
https://github.com/HeidiSQL/HeidiSQL.git
synced 2025-08-06 18:24:26 +08:00
1758 lines
60 KiB
ObjectPascal
1758 lines
60 KiB
ObjectPascal
unit usermanager;
|
|
|
|
|
|
interface
|
|
|
|
uses
|
|
Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, ComCtrls, StdCtrls,
|
|
CheckLst, ExtCtrls, ToolWin, ClipBrd,
|
|
mysql_connection, helpers, VirtualTrees;
|
|
|
|
{$I const.inc}
|
|
|
|
|
|
type
|
|
TPrivilege = class(TObject)
|
|
private
|
|
FDBOType: TListNodeType;
|
|
// Internal Flags
|
|
FDeleted: Boolean;
|
|
FAdded: Boolean;
|
|
FModified: Boolean;
|
|
function GetDBOKey: String;
|
|
function GetDBOPrettyKey: String;
|
|
function GetPrettyPrivNames: TStringList;
|
|
public
|
|
DBONames: TStringList;
|
|
PrivNames: TStringList;
|
|
SelectedPrivNames: TStringList;
|
|
constructor Create(Fields: TMySQLQuery; FieldDefs: TMySQLQuery=nil; AvoidFieldDefs: TMySQLQuery=nil; CropFieldDefs: TMySQLQuery=nil; SimulateDbField: Boolean = False);
|
|
procedure Merge(Fields: TMySQLQuery);
|
|
property DBOType: TListNodeType read FDBOType;
|
|
property DBOKey: String read GetDBOKey;
|
|
property DBOPrettyKey: String read GetDBOPrettyKey;
|
|
property PrettyPrivNames: TStringList read GetPrettyPrivNames;
|
|
property Added: Boolean read FAdded write FAdded;
|
|
property Modified: Boolean read FModified write FModified;
|
|
property Deleted: Boolean read FDeleted write FDeleted;
|
|
end;
|
|
|
|
TUser = class; // Forward declaration
|
|
|
|
TPrivileges = class(TObject)
|
|
private
|
|
FPrivilegeItems: Array of TPrivilege;
|
|
FOwner: TUser;
|
|
function GetPrivilege(Index: Integer): TPrivilege;
|
|
function GetCount: Integer;
|
|
public
|
|
constructor Create(AOwner: TUser);
|
|
function AddPrivilege(Fields: TMySQLQuery; FieldDefs: TMySQLQuery=nil; AvoidFieldDefs: TMySQLQuery=nil; CropFieldDefs: TMySQLQuery=nil; SimulateDbField: Boolean = False): TPrivilege;
|
|
property Items[Index: Integer]: TPrivilege read GetPrivilege; default;
|
|
property Count: Integer read GetCount;
|
|
procedure DeletePrivilege(Index: Integer);
|
|
function FindPrivilege(Fields: TMySQLQuery; SimulateDbField: Boolean): TPrivilege;
|
|
end;
|
|
|
|
TUser = class(TObject)
|
|
private
|
|
FOldName: String;
|
|
FNewName: String;
|
|
FNameChanged: Boolean;
|
|
FPassword: String;
|
|
FOldPasswordHashed: String;
|
|
FOldHost: String;
|
|
FNewHost: String;
|
|
FOldMaxQuestions: Cardinal;
|
|
FOldMaxUpdates: Cardinal;
|
|
FOldMaxConnections: Cardinal;
|
|
FOldMaxUserConnections: Cardinal;
|
|
FMaxQuestions: Cardinal;
|
|
FMaxUpdates: Cardinal;
|
|
FMaxConnections: Cardinal;
|
|
FMaxUserConnections: Cardinal;
|
|
// Internal Flags
|
|
FDeleted: Boolean;
|
|
FDisabled: Boolean;
|
|
FAdded: Boolean;
|
|
FPasswordModified: Boolean;
|
|
FOtherModified: Boolean;
|
|
FPrivileges: TPrivileges;
|
|
function GetModified: Boolean;
|
|
function GetName: String;
|
|
procedure SetName(str: String);
|
|
function GetHost: String;
|
|
procedure SetHost(str: String);
|
|
procedure SetPassword(str: String);
|
|
public
|
|
constructor Create(Fields: TMySQLQuery=nil); overload;
|
|
constructor Create(Name: String; Host: String); overload;
|
|
property Name: String read GetName write SetName;
|
|
property Host: String read GetHost write SetHost;
|
|
property Password: String read FPassword write SetPassword;
|
|
property OldPasswordHashed: String read FOldPasswordHashed write FOldPasswordHashed;
|
|
property Privileges: TPrivileges read FPrivileges;
|
|
property MaxQuestions: Cardinal read FMaxQuestions write FMaxQuestions;
|
|
property MaxUpdates: Cardinal read FMaxUpdates write FMaxUpdates;
|
|
property MaxConnections: Cardinal read FMaxConnections write FMaxConnections;
|
|
property MaxUserConnections: Cardinal read FMaxUserConnections write FMaxUserConnections;
|
|
property Deleted: Boolean read FDeleted write FDeleted;
|
|
property Disabled: Boolean read FDisabled write FDisabled;
|
|
property Added: Boolean read FAdded;
|
|
property Modified: Boolean read GetModified write FOtherModified;
|
|
property PasswordModified: Boolean read FPasswordModified write FPasswordModified;
|
|
end;
|
|
|
|
TUsers = class(TObject)
|
|
constructor Create;
|
|
private
|
|
FUserItems: Array of TUser;
|
|
function GetUser(Index: Integer): TUser;
|
|
function GetCount: Integer;
|
|
public
|
|
// The default property, can be access via Object[nr]
|
|
property Items[Index: Integer]: TUser read GetUser; default;
|
|
property Count: Integer read GetCount;
|
|
procedure AddUser(User: TUser);
|
|
procedure DeleteUser(Index: Integer);
|
|
function FindUser(Name: String; Host: String): TUser;
|
|
end;
|
|
|
|
TUserManagerForm = class(TForm)
|
|
pnlTop: TPanel;
|
|
pnlRight: TPanel;
|
|
PageControlUser: TPageControl;
|
|
tabSettings: TTabSheet;
|
|
lblFromHost: TLabel;
|
|
lblPassword: TLabel;
|
|
lblUsername: TLabel;
|
|
lblWarning: TLabel;
|
|
lblHostHints: TLabel;
|
|
editPassword: TButtonedEdit;
|
|
editFromHost: TEdit;
|
|
editUsername: TEdit;
|
|
chkDisabled: TCheckBox;
|
|
tabLimitations: TTabSheet;
|
|
lblMaxQuestions: TLabel;
|
|
lblMaxUpdates: TLabel;
|
|
lblMaxConnections: TLabel;
|
|
lblMaxUserConnections: TLabel;
|
|
lblLimitHint: TLabel;
|
|
editMaxUserConnections: TEdit;
|
|
editMaxConnections: TEdit;
|
|
editMaxUpdates: TEdit;
|
|
editMaxQuestions: TEdit;
|
|
udMaxQuestions: TUpDown;
|
|
udMaxUpdates: TUpDown;
|
|
udMaxConnections: TUpDown;
|
|
udMaxUserConnections: TUpDown;
|
|
tabUserInfo: TTabSheet;
|
|
lblFullName: TLabel;
|
|
lblDescription: TLabel;
|
|
lblEmail: TLabel;
|
|
lblContactInfo: TLabel;
|
|
editFullName: TEdit;
|
|
editDescription: TEdit;
|
|
editEmail: TEdit;
|
|
memoContactInfo: TMemo;
|
|
tabHints: TTabSheet;
|
|
lblHints: TLabel;
|
|
pnlLeft: TPanel;
|
|
listUsers: TVirtualStringTree;
|
|
tlbUsers: TToolBar;
|
|
btnAddUser: TToolButton;
|
|
btnDeleteUser: TToolButton;
|
|
tlbObjects: TToolBar;
|
|
btnAddObject: TToolButton;
|
|
btnDeleteObject: TToolButton;
|
|
treeObjects: TVirtualStringTree;
|
|
Splitter1: TSplitter;
|
|
btnOK: TButton;
|
|
btnCancel: TButton;
|
|
procedure btnAddObjectClick(Sender: TObject);
|
|
procedure FormDestroy(Sender: TObject);
|
|
procedure btnAddUserClick(Sender: TObject);
|
|
procedure btnDeleteObjectClick(Sender: TObject);
|
|
procedure btnDeleteUserClick(Sender: TObject);
|
|
procedure btnOKClick(Sender: TObject);
|
|
procedure chkDisabledClick(Sender: TObject);
|
|
procedure editUsernameHostExit(Sender: TObject);
|
|
procedure editLimitations(Sender: TObject);
|
|
procedure editPasswordExit(Sender: TObject);
|
|
procedure FormClose(Sender: TObject; var Action: TCloseAction);
|
|
procedure FormCreate(Sender: TObject);
|
|
procedure FormShow(Sender: TObject);
|
|
procedure listUsersGetText(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
|
|
var CellText: string);
|
|
procedure listUsersGetImageIndex(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex;
|
|
var Ghosted: Boolean; var ImageIndex: Integer);
|
|
procedure listUsersBeforePaint(Sender: TBaseVirtualTree;
|
|
TargetCanvas: TCanvas);
|
|
procedure treeObjectsInitNode(Sender: TBaseVirtualTree; ParentNode,
|
|
Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
|
|
procedure treeObjectsBeforePaint(Sender: TBaseVirtualTree;
|
|
TargetCanvas: TCanvas);
|
|
procedure listUsersFocusChanged(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex);
|
|
procedure treeObjectsGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
|
|
Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
|
|
procedure treeObjectsGetImageIndex(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex;
|
|
var Ghosted: Boolean; var ImageIndex: Integer);
|
|
procedure treeObjectsInitChildren(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; var ChildCount: Cardinal);
|
|
procedure treeObjectsChecked(Sender: TBaseVirtualTree; Node: PVirtualNode);
|
|
procedure treeObjectsExpanded(Sender: TBaseVirtualTree; Node: PVirtualNode);
|
|
procedure treeObjectsFocusChanged(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex);
|
|
procedure editPasswordRightButtonClick(Sender: TObject);
|
|
private
|
|
{ Private declarations }
|
|
Users: TUsers;
|
|
procedure RefreshGUI;
|
|
public
|
|
function TestUserAdmin: Boolean;
|
|
{ Public declarations }
|
|
end;
|
|
|
|
procedure GetPrivilegeRowKey(Fields: TMySQLQuery; SimulateDbField: Boolean; out DBOType: TListNodeType; out DBONames: TStringList);
|
|
|
|
implementation
|
|
|
|
|
|
uses
|
|
main, selectdbobject;
|
|
|
|
|
|
var
|
|
db: String;
|
|
// Results from SELECT * FROM user/db/...
|
|
dsUser, dsDb, dsTables, dsColumns,
|
|
// Results from SHOW FIELDS FROM user/db/...
|
|
dsTablesFields, dsColumnsFields : TMySQLQuery;
|
|
|
|
|
|
{$R *.DFM}
|
|
|
|
|
|
|
|
{**
|
|
FormCreate: Restore GUI setup
|
|
}
|
|
procedure TUserManagerForm.FormCreate(Sender: TObject);
|
|
begin
|
|
InheritFont(Font);
|
|
Width := GetRegValue(REGNAME_USERMNGR_WINWIDTH, Width);
|
|
Height := GetRegValue(REGNAME_USERMNGR_WINHEIGHT, Height);
|
|
pnlLeft.Width := GetRegValue(REGNAME_USERMNGR_LISTWIDTH, pnlLeft.Width);
|
|
db := Mainform.Mask(DBNAME_MYSQL);
|
|
SetWindowSizeGrip( Self.Handle, True );
|
|
FixVT(listUsers);
|
|
FixVT(treeObjects);
|
|
end;
|
|
|
|
|
|
{**
|
|
FormDestroy: Save GUI setup
|
|
}
|
|
procedure TUserManagerForm.FormDestroy(Sender: TObject);
|
|
begin
|
|
OpenRegistry;
|
|
MainReg.WriteInteger( REGNAME_USERMNGR_WINWIDTH, Width );
|
|
MainReg.WriteInteger( REGNAME_USERMNGR_WINHEIGHT, Height );
|
|
MainReg.WriteInteger( REGNAME_USERMNGR_LISTWIDTH, pnlLeft.Width );
|
|
end;
|
|
|
|
|
|
{**
|
|
Fail if no access to privilege database.
|
|
}
|
|
function TUserManagerForm.TestUserAdmin: Boolean;
|
|
var
|
|
test_result: String;
|
|
begin
|
|
// Test if we can access the privileges database and tables by
|
|
// A. Using the mysql-DB
|
|
try
|
|
MainForm.ActiveConnection.Database := DBNAME_MYSQL;
|
|
except
|
|
MessageDlg('You have no access to the privileges database.', mtError, [mbOK], 0);
|
|
Result := false;
|
|
Exit;
|
|
end;
|
|
// B. retrieving a count of all users.
|
|
test_result := MainForm.ActiveConnection.GetVar('SELECT COUNT(*) FROM '+db+'.'+Mainform.Mask(PRIVTABLE_USERS));
|
|
if test_result = '' then begin
|
|
MessageDlg('You have no access to the privileges tables.', mtError, [mbOK], 0);
|
|
Result := false;
|
|
Exit;
|
|
end;
|
|
Result := true;
|
|
end;
|
|
|
|
|
|
{**
|
|
FormShow: Load users and privileges into memory
|
|
}
|
|
procedure TUserManagerForm.FormShow(Sender: TObject);
|
|
var
|
|
snr: String;
|
|
begin
|
|
// Set hints text
|
|
snr := MainForm.ActiveConnection.GetVar('SHOW VARIABLES LIKE ' + esc('skip_name_resolve'));
|
|
if snr = '' then snr := 'Unknown';
|
|
lblHostHints.Caption := StringReplace(lblHostHints.Caption, '$SNR', snr, []);
|
|
|
|
// Load users into memory
|
|
try
|
|
Users := TUsers.Create;
|
|
except
|
|
// At least one priv table is missing or non-accessible when we get an exception.
|
|
// Proceeding would result in follow up errors, so cancel the whole dialog in that case.
|
|
on E:EDatabaseError do begin
|
|
MessageDlg(E.Message+CRLF+CRLF+'The user manager cannot proceed with this error.', mtError, [mbOK], 0);
|
|
PostMessage(Handle, WM_CLOSE, 0, 0);
|
|
Exit;
|
|
end;
|
|
end;
|
|
// Enable limitations editors only if relevant columns exist
|
|
lblMaxQuestions.Enabled := dsUser.ColExists('max_questions');
|
|
editMaxQuestions.Enabled := lblMaxQuestions.Enabled;
|
|
udMaxQuestions.Enabled := lblMaxQuestions.Enabled;
|
|
|
|
lblMaxUpdates.Enabled := dsUser.ColExists('max_updates');
|
|
editMaxQuestions.Enabled := lblMaxUpdates.Enabled;
|
|
udMaxUpdates.Enabled := lblMaxUpdates.Enabled;
|
|
|
|
lblMaxConnections.Enabled := dsUser.ColExists('max_connections');
|
|
editMaxConnections.Enabled := lblMaxConnections.Enabled;
|
|
udMaxConnections.Enabled := lblMaxConnections.Enabled;
|
|
|
|
lblMaxUserConnections.Enabled := dsUser.ColExists('max_user_connections');
|
|
editMaxUserConnections.Enabled := lblMaxUserConnections.Enabled;
|
|
udMaxUserConnections.Enabled := lblMaxUserConnections.Enabled;
|
|
|
|
RefreshGUI;
|
|
end;
|
|
|
|
|
|
{**
|
|
FormClose: Free memory from privileges
|
|
}
|
|
procedure TUserManagerForm.FormClose(Sender: TObject; var Action: TCloseAction);
|
|
begin
|
|
Users.Free;
|
|
end;
|
|
|
|
|
|
{**
|
|
Load all users into GUI and select one in the pulldown
|
|
}
|
|
procedure TUserManagerForm.RefreshGUI;
|
|
begin
|
|
listUsers.Clear;
|
|
listUsers.Repaint;
|
|
listUsersFocusChanged(listUsers, listUsers.FocusedNode, listUsers.FocusedColumn);
|
|
treeObjects.Repaint;
|
|
treeObjectsFocusChanged(treeObjects, treeObjects.FocusedNode, treeObjects.FocusedColumn);
|
|
end;
|
|
|
|
|
|
procedure TUserManagerForm.listUsersBeforePaint(Sender: TBaseVirtualTree;
|
|
TargetCanvas: TCanvas);
|
|
begin
|
|
(Sender as TVirtualStringTree).RootNodeCount := Users.Count;
|
|
end;
|
|
|
|
|
|
procedure TUserManagerForm.listUsersGetImageIndex(
|
|
Sender: TBaseVirtualTree; Node: PVirtualNode; Kind: TVTImageKind;
|
|
Column: TColumnIndex; var Ghosted: Boolean; var ImageIndex: Integer);
|
|
var
|
|
u: TUser;
|
|
begin
|
|
if not (Kind in [ikNormal, ikSelected]) then Exit;
|
|
u := Users[Node.Index];
|
|
// Detect modified status of the user object itself
|
|
if u.Name = '' then begin
|
|
if u.Deleted then ImageIndex := 86
|
|
else if u.Added then ImageIndex := 85
|
|
else if u.Modified then ImageIndex := 84
|
|
else ImageIndex := 11;
|
|
end else begin
|
|
if u.Deleted then ImageIndex := 83
|
|
else if u.Added then ImageIndex := 21
|
|
else if u.Modified then ImageIndex := 12
|
|
else ImageIndex := 43;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TUserManagerForm.listUsersGetText(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
|
|
var CellText: string);
|
|
var
|
|
u: TUser;
|
|
begin
|
|
// Compose displayed username
|
|
u := Users[Node.Index];
|
|
CellText := u.Name;
|
|
if u.Name = '' then
|
|
CellText := 'Everybody';
|
|
if u.Host <> '%' then
|
|
CellText := CellText + '@' + u.Host;
|
|
end;
|
|
|
|
|
|
{**
|
|
Create new user
|
|
}
|
|
procedure TUserManagerForm.btnAddUserClick(Sender: TObject);
|
|
const
|
|
name: String = 'New User';
|
|
var
|
|
u: TUser;
|
|
begin
|
|
PageControlUser.ActivePage := tabSettings;
|
|
// Avoid duplicates.
|
|
u := Users.FindUser(name, '%');
|
|
if u <> nil then begin
|
|
MessageDlg('User/host combination "'+name+'@%" already exists.'+CRLF+CRLF+'Please chose a different username.', mtError, [mbOK], 0);
|
|
Exit;
|
|
end;
|
|
Users.AddUser(TUser.Create(name, '%'));
|
|
RefreshGUI;
|
|
// Select newly added item.
|
|
SelectNode(listUsers, listUsers.RootNodeCount - 1);
|
|
// Focus the user name entry box.
|
|
editUserName.SetFocus;
|
|
end;
|
|
|
|
|
|
{**
|
|
Delete user
|
|
}
|
|
procedure TUserManagerForm.btnDeleteUserClick(Sender: TObject);
|
|
begin
|
|
if MessageDlg('Delete user "'+listUsers.Text[listUsers.FocusedNode, listUsers.FocusedColumn]+'"?', mtConfirmation, [mbYes, mbCancel], 0 ) <> mrYes then
|
|
Exit;
|
|
Users.DeleteUser(listUsers.FocusedNode.Index);
|
|
RefreshGUI;
|
|
end;
|
|
|
|
|
|
{**
|
|
Disable a user
|
|
}
|
|
procedure TUserManagerForm.chkDisabledClick(Sender: TObject);
|
|
var
|
|
disabled: Boolean;
|
|
u: TUser;
|
|
begin
|
|
disabled := TCheckbox(Sender).Checked;
|
|
u := Users[listUsers.FocusedNode.Index];
|
|
u.Disabled := disabled;
|
|
// Reset password from "!" to empty string. Avoids again disabling it
|
|
// by just leaving and saving the exclamation mark
|
|
if u.Password = '!' then u.Password := '';
|
|
u.Modified := True;
|
|
listUsers.Invalidate;
|
|
end;
|
|
|
|
|
|
procedure TUserManagerForm.btnAddObjectClick(Sender: TObject);
|
|
var
|
|
NewObj: TStringList;
|
|
ds, FieldDefs: TMySQLQuery;
|
|
NewPriv: TPrivilege;
|
|
u: TUser;
|
|
i: Integer;
|
|
begin
|
|
NewObj := SelectDBO;
|
|
if NewObj <> nil then begin
|
|
u := Users[listUsers.FocusedNode.Index];
|
|
for i := 0 to u.Privileges.Count - 1 do begin
|
|
NewObj.Delimiter := u.Privileges[i].DBONames.Delimiter;
|
|
if (u.Privileges[i].DBOKey = NewObj.DelimitedText) and (not u.Privileges[i].Deleted) then begin
|
|
MessageDlg(NewObj.DelimitedText+' already exists.', mtError, [mbOK], 0);
|
|
Exit;
|
|
end;
|
|
end;
|
|
ds := nil;
|
|
FieldDefs := nil;
|
|
case NewObj.Count of
|
|
1: ds := dsDb;
|
|
2: begin ds := dsTables; FieldDefs := dsTablesFields; end;
|
|
3: begin ds := dsColumns; FieldDefs := dsColumnsFields; end;
|
|
else
|
|
Exception.Create('Added privilege object has an invalid number of segments ('+IntToStr(NewObj.Count)+')');
|
|
end;
|
|
NewPriv := u.Privileges.AddPrivilege(ds, FieldDefs);
|
|
NewPriv.Added := True;
|
|
NewPriv.DBONames := NewObj;
|
|
u.Modified := True;
|
|
listUsers.Invalidate;
|
|
treeObjects.ReinitChildren(treeObjects.RootNode, false);
|
|
treeObjects.Repaint;
|
|
SelectNode(treeObjects, treeObjects.GetLastChild(treeObjects.RootNode));
|
|
treeObjects.Expanded[treeObjects.FocusedNode] := True;
|
|
end;
|
|
end;
|
|
|
|
|
|
{**
|
|
Revoke access to an object
|
|
}
|
|
procedure TUserManagerForm.btnDeleteObjectClick(Sender: TObject);
|
|
var
|
|
Node: PVirtualNode;
|
|
FocusIndex: Cardinal;
|
|
begin
|
|
case treeObjects.GetNodeLevel(treeObjects.FocusedNode) of
|
|
0: Node := treeObjects.FocusedNode;
|
|
1: Node := treeObjects.FocusedNode.Parent;
|
|
else Raise Exception.Create(SUnhandledTreeLevel);
|
|
end;
|
|
FocusIndex := Node.Index;
|
|
Users[listUsers.FocusedNode.Index].Privileges.DeletePrivilege(Node.Index);
|
|
treeObjects.ReInitNode(Node, True);
|
|
SelectNode(treeObjects, FocusIndex-1);
|
|
treeObjects.Invalidate;
|
|
end;
|
|
|
|
|
|
{**
|
|
Username edited
|
|
}
|
|
procedure TUserManagerForm.editUsernameHostExit(Sender: TObject);
|
|
var
|
|
u, f: TUser;
|
|
begin
|
|
if TEdit(Sender).Modified then begin
|
|
// In case the user field contains '%', it's sort of a visual hoax.
|
|
// The TUser.Name and mysql database contents of a match-all user is ''.
|
|
// Allow entering '%' too, changing it to '' automatically.
|
|
if editUsername.Text = '%' then editUsername.Text := '';
|
|
u := Users[listUsers.FocusedNode.Index];
|
|
// Check if user/host combination already exists
|
|
f := Users.FindUser(editUsername.Text, editFromHost.Text);
|
|
if (f = nil) or (f = u) then begin
|
|
u.Name := editUsername.Text;
|
|
u.Host := editFromHost.Text;
|
|
end else
|
|
MessageDlg('User/host combination "'+editUsername.Text+'@'+editFromHost.Text+'" already exists.'+CRLF+CRLF+'Please chose a different username and/or hostname.', mtError, [mbOK], 0);
|
|
// Blank user field means:
|
|
// - this user is used for authentication failures
|
|
// - privileges for this user applies to everyone
|
|
// In effect, User='' means the same as User='%', except
|
|
// that the latter syntax is not allowed (in the database).
|
|
if u.Name = '' then editUserName.Text := '%'
|
|
else editUsername.Text := u.Name;
|
|
editFromHost.Text := u.Host;
|
|
editUsername.Modified := False;
|
|
listUsers.Invalidate;
|
|
end;
|
|
end;
|
|
|
|
|
|
{**
|
|
Password field is unfocused: Apply change pw to user object
|
|
}
|
|
procedure TUserManagerForm.editPasswordExit(Sender: TObject);
|
|
var
|
|
u: TUser;
|
|
t: TNotifyEvent;
|
|
begin
|
|
if not editPassword.Modified then
|
|
Exit;
|
|
u := Users[listUsers.FocusedNode.Index];
|
|
u.Password := editPassword.Text;
|
|
if u.PasswordModified then begin
|
|
u.Disabled := False;
|
|
t := chkDisabled.OnClick;
|
|
chkDisabled.OnClick := nil;
|
|
chkDisabled.Checked := False;
|
|
chkDisabled.OnClick := t;
|
|
editPassword.TextHint := '';
|
|
end;
|
|
// Show security warning for empty password if user is not disabled
|
|
lblWarning.Visible := (not u.Disabled) and (
|
|
(u.PasswordModified and (u.Password = '')) or
|
|
((not u.PasswordModified) and (u.OldPasswordHashed = ''))
|
|
);
|
|
lblWarning.Caption := 'This user has a blank password.';
|
|
listUsers.Invalidate;
|
|
end;
|
|
|
|
|
|
procedure TUserManagerForm.editPasswordRightButtonClick(Sender: TObject);
|
|
begin
|
|
// Auto generate a random password, display it for a second and copy it to clipboard
|
|
Screen.Cursor := crHourglass;
|
|
editPassword.SetFocus;
|
|
editPassword.Text := GeneratePassword(8);
|
|
editPassword.Modified := True;
|
|
editPassword.PasswordChar := #0;
|
|
editPassword.Repaint;
|
|
Sleep(1000);
|
|
editPassword.PasswordChar := '*';
|
|
Clipboard.AsText := editPassword.Text;
|
|
Screen.Cursor := crDefault;
|
|
end;
|
|
|
|
|
|
procedure TUserManagerForm.editLimitations(Sender: TObject);
|
|
var
|
|
u: TUser;
|
|
isModified: Boolean;
|
|
begin
|
|
// OnChange is called during form construction, avoid that.
|
|
if not Assigned(listUsers.FocusedNode) then Exit;
|
|
u := Users[listUsers.FocusedNode.Index];
|
|
isModified :=
|
|
Cardinal(u.MaxQuestions) +
|
|
Cardinal(u.MaxUpdates) +
|
|
Cardinal(u.MaxConnections) +
|
|
Cardinal(u.MaxUserConnections) <>
|
|
Cardinal(udMaxQuestions.Position) +
|
|
Cardinal(udMaxUpdates.Position) +
|
|
Cardinal(udMaxConnections.Position) +
|
|
Cardinal(udMaxUserConnections.Position);
|
|
u.MaxQuestions := udMaxQuestions.Position;
|
|
u.MaxUpdates := udMaxUpdates.Position;
|
|
u.MaxConnections := udMaxConnections.Position;
|
|
u.MaxUserConnections := udMaxUserConnections.Position;
|
|
if isModified then begin
|
|
u.Modified := True;
|
|
listUsers.Invalidate;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TUserManagerForm.treeObjectsBeforePaint(Sender: TBaseVirtualTree; TargetCanvas: TCanvas);
|
|
var
|
|
VT: TVirtualStringTree;
|
|
begin
|
|
VT := Sender as TVirtualStringTree;
|
|
if not Assigned(listUsers.FocusedNode) then
|
|
VT.RootNodeCount := 0
|
|
else
|
|
VT.RootNodeCount := Users[listUsers.FocusedNode.Index].Privileges.Count;
|
|
VT.ReinitNode(nil, True);
|
|
end;
|
|
|
|
|
|
procedure TUserManagerForm.treeObjectsChecked(Sender: TBaseVirtualTree; Node: PVirtualNode);
|
|
var
|
|
u: TUser;
|
|
p: TPrivilege;
|
|
i: Integer;
|
|
begin
|
|
case Sender.GetNodeLevel(Node) of
|
|
0: Sender.Expanded[Node] := True;
|
|
1: begin
|
|
u := Users[listUsers.FocusedNode.Index];
|
|
p := u.Privileges[Node.Parent.Index];
|
|
case Node.CheckState of
|
|
csCheckedNormal: p.SelectedPrivNames.Add(p.PrivNames[Node.Index]);
|
|
csUncheckedNormal: begin
|
|
i := p.SelectedPrivNames.IndexOf(p.PrivNames[Node.Index]);
|
|
p.SelectedPrivNames.Delete(i);
|
|
end;
|
|
end;
|
|
p.Modified := True;
|
|
u.Modified := True;
|
|
listUsers.Invalidate;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TUserManagerForm.treeObjectsExpanded(Sender: TBaseVirtualTree; Node: PVirtualNode);
|
|
var
|
|
n: PVirtualNode;
|
|
begin
|
|
// Collapse all uninvolved tree nodes, keeping the tree usable
|
|
n := Sender.GetFirstChild(Node.Parent);
|
|
while Assigned(n) do begin
|
|
Sender.Expanded[n] := n = Node;
|
|
n := Sender.GetNextSibling(n);
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TUserManagerForm.treeObjectsFocusChanged(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex);
|
|
var
|
|
EnableDelete: Boolean;
|
|
i: Integer;
|
|
priv: TPrivilege;
|
|
begin
|
|
EnableDelete := False;
|
|
if Assigned(Sender.FocusedNode) then begin
|
|
case Sender.GetNodeLevel(Node) of
|
|
0: i := Node.Index;
|
|
1: i := Node.Parent.Index;
|
|
else Raise Exception.Create(SUnhandledTreeLevel);
|
|
end;
|
|
priv := Users[listUsers.FocusedNode.Index].Privileges[i];
|
|
EnableDelete := (priv.DBOType <> lntNone) and
|
|
(priv.DBOKey <> '%') and (not priv.Deleted);
|
|
end;
|
|
if Assigned(listUsers.FocusedNode) then
|
|
EnableDelete := EnableDelete and (not Users[listUsers.FocusedNode.Index].Deleted);
|
|
btnDeleteObject.Enabled := EnableDelete;
|
|
end;
|
|
|
|
|
|
procedure TUserManagerForm.treeObjectsGetImageIndex(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Kind: TVTImageKind; Column: TColumnIndex;
|
|
var Ghosted: Boolean; var ImageIndex: Integer);
|
|
var
|
|
u: TUser;
|
|
begin
|
|
if not (Kind in [ikNormal, ikSelected]) then Exit;
|
|
case Sender.GetNodeLevel(Node) of
|
|
0: begin
|
|
u := Users[listUsers.FocusedNode.Index];
|
|
case u.Privileges[Node.Index].DBOType of
|
|
lntNone: ImageIndex := ICONINDEX_SERVER;
|
|
lntDb: ImageIndex := ICONINDEX_DB;
|
|
lntTable: ImageIndex := ICONINDEX_TABLE;
|
|
lntView: ImageIndex := ICONINDEX_VIEW;
|
|
lntColumn: ImageIndex := ICONINDEX_FIELD;
|
|
end;
|
|
if u.Privileges[Node.Index].Deleted then
|
|
ImageIndex := 46;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TUserManagerForm.treeObjectsGetText(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex; TextType: TVSTTextType;
|
|
var CellText: string);
|
|
var
|
|
u: TUser;
|
|
begin
|
|
case Sender.GetNodeLevel(Node) of
|
|
0: begin
|
|
u := Users[listUsers.FocusedNode.Index];
|
|
CellText := u.Privileges[Node.Index].DBOPrettyKey;
|
|
if u.Privileges[Node.Index].Deleted then
|
|
CellText := CellText + ' (deleted)';
|
|
end;
|
|
1: CellText := Users[listUsers.FocusedNode.Index].Privileges[Node.Parent.Index].PrivNames[Node.Index];
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TUserManagerForm.treeObjectsInitChildren(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; var ChildCount: Cardinal);
|
|
begin
|
|
// Only first level nodes have child nodes
|
|
case Sender.GetNodeLevel(Node) of
|
|
0: ChildCount := Users[listUsers.FocusedNode.Index].Privileges[Node.Index].PrivNames.Count;
|
|
1: ChildCount := 0;
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure TUserManagerForm.treeObjectsInitNode(Sender: TBaseVirtualTree;
|
|
ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
|
|
var
|
|
p: TPrivilege;
|
|
begin
|
|
Node.CheckType := ctTriStateCheckBox;
|
|
case Sender.GetNodeLevel(Node) of
|
|
0: begin
|
|
// Display plus/minus button
|
|
Include(InitialStates, ivsHasChildren);
|
|
p := Users[listUsers.FocusedNode.Index].Privileges[Node.Index];
|
|
// AutoOptions.toAutoTristateTracking does a good job but it does not auto-check parent nodes when initializing
|
|
if p.SelectedPrivNames.Count = 0 then
|
|
Node.CheckState := csUncheckedNormal
|
|
else if p.SelectedPrivNames.Count < p.PrivNames.Count then
|
|
Node.CheckState := csMixedNormal
|
|
else
|
|
Node.CheckState := csCheckedNormal;
|
|
end;
|
|
1: begin
|
|
p := Users[listUsers.FocusedNode.Index].Privileges[Node.Parent.Index];
|
|
case p.SelectedPrivNames.IndexOf(p.PrivNames[Node.Index]) of
|
|
-1: Node.CheckState := csUncheckedNormal;
|
|
else Node.CheckState := csCheckedNormal;
|
|
end;
|
|
end;
|
|
else Raise Exception.Create(SUnhandledTreeLevel);
|
|
end;
|
|
if p.Deleted then
|
|
Include(Node.States, vsDisabled);
|
|
end;
|
|
|
|
procedure TUserManagerForm.listUsersFocusChanged(Sender: TBaseVirtualTree;
|
|
Node: PVirtualNode; Column: TColumnIndex);
|
|
var
|
|
u: TUser;
|
|
Enable: Boolean;
|
|
t: TNotifyEvent;
|
|
begin
|
|
lblWarning.Visible := False;
|
|
Enable := False;
|
|
if Assigned(Sender.FocusedNode) then begin
|
|
u := Users[Sender.FocusedNode.Index];
|
|
Enable := not u.Deleted;
|
|
|
|
t := editUsername.OnChange;
|
|
editUsername.OnChange := nil;
|
|
editUsername.Text := u.Name;
|
|
if u.Name = '' then editUsername.Text := '%';
|
|
editUsername.OnChange := t;
|
|
|
|
editPassword.Text := '';
|
|
editPassword.TextHint := u.OldPasswordHashed;
|
|
editPasswordExit(Sender);
|
|
|
|
t := editFromHost.OnChange;
|
|
editFromHost.OnChange := nil;
|
|
editFromHost.Text := u.Host;
|
|
editFromHost.OnChange := t;
|
|
|
|
t := chkDisabled.OnClick;
|
|
chkDisabled.OnClick := nil;
|
|
chkDisabled.Checked := u.Disabled;
|
|
chkDisabled.OnClick := t;
|
|
|
|
// Activate "deleted" warning
|
|
if u.Deleted then begin
|
|
lblWarning.Visible := True;
|
|
lblWarning.Caption := 'User is marked for deletion.';
|
|
end;
|
|
|
|
// Limitations
|
|
t := editMaxQuestions.OnChange;
|
|
editMaxQuestions.OnChange := nil;
|
|
editMaxUpdates.OnChange := nil;
|
|
editMaxConnections.OnChange := nil;
|
|
editMaxUserConnections.OnChange := nil;
|
|
udMaxQuestions.Position := u.MaxQuestions;
|
|
udMaxUpdates.Position := u.MaxUpdates;
|
|
udMaxConnections.Position := u.MaxConnections;
|
|
udMaxUserConnections.Position := u.MaxUserConnections;
|
|
editMaxQuestions.OnChange := t;
|
|
editMaxUpdates.OnChange := t;
|
|
editMaxConnections.OnChange := t;
|
|
editMaxUserConnections.OnChange := t;
|
|
end;
|
|
|
|
// Update top buttons
|
|
btnDeleteUser.Enabled := Enable;
|
|
// Update control in Settings tab
|
|
lblUsername.Enabled := Enable;
|
|
editUsername.Enabled := Enable;
|
|
lblPassword.Enabled := Enable;
|
|
editPassword.Enabled := Enable;
|
|
editPassword.Modified := False;
|
|
lblFromHost.Enabled := Enable;
|
|
editFromHost.Enabled := Enable;
|
|
lblHostHints.Enabled := Enable;
|
|
chkDisabled.Enabled := Enable;
|
|
// Update controls in Limitations tab
|
|
lblMaxQuestions.Enabled := Enable;
|
|
editMaxQuestions.Enabled := Enable;
|
|
udMaxQuestions.Enabled := Enable;
|
|
lblMaxUpdates.Enabled := Enable;
|
|
editMaxUpdates.Enabled := Enable;
|
|
udMaxUpdates.Enabled := Enable;
|
|
lblMaxConnections.Enabled := Enable;
|
|
editMaxConnections.Enabled := Enable;
|
|
udMaxConnections.Enabled := Enable;
|
|
lblMaxUserConnections.Enabled := Enable;
|
|
editMaxUserConnections.Enabled := Enable;
|
|
udMaxUserConnections.Enabled := Enable;
|
|
// Update controls for privileges
|
|
btnAddObject.Enabled := Enable;
|
|
treeObjects.Clear;
|
|
treeObjects.Invalidate;
|
|
end;
|
|
|
|
|
|
{**
|
|
OK clicked: Apply all changes
|
|
}
|
|
procedure TUserManagerForm.btnOKClick(Sender: TObject);
|
|
var
|
|
i, j, k: Integer;
|
|
c: Char;
|
|
u: TUser;
|
|
p: TPrivilege;
|
|
sql, TableName, SetFieldName: String;
|
|
TableSet: TMySQLQuery;
|
|
AcctWhere, AcctValues, PrivWhere: String;
|
|
AcctUpdates, PrivValues, PrivUpdates: TStringList;
|
|
procedure LogSQL(sql: String);
|
|
begin
|
|
Mainform.LogSQL(sql);
|
|
end;
|
|
function Mask(sql: String): String;
|
|
begin
|
|
Result := Mainform.Mask(sql);
|
|
end;
|
|
function Delim(list: TStringList; spacing: Boolean = True): String;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
Result := '';
|
|
for i := 0 to list.Count - 1 do begin
|
|
if i > 0 then begin
|
|
Result := Result + list.Delimiter;
|
|
if spacing then Result := Result + ' ';
|
|
end;
|
|
Result := Result + list[i];
|
|
end;
|
|
end;
|
|
function AccountSqlClause(name: String; host: String; canon: Boolean = False): String;
|
|
begin
|
|
Result := ' WHERE ' + mask('User') + '=' + esc(name) + ' AND (';
|
|
Result := Result + mask('Host') + '=' + esc(host);
|
|
if canon and (host = '%') then begin
|
|
// Canonicalization: both '%' and '' means any host.
|
|
Result := Result + ' OR ' + mask('Host') + '=' + esc('');
|
|
end;
|
|
Result := Result + ')';
|
|
end;
|
|
begin
|
|
LogSQL('Removing accounts marked for deletion...');
|
|
for i := 0 to Users.Count - 1 do begin
|
|
u := Users[i];
|
|
// Only process deleted users in this loop.
|
|
if not u.Deleted then Continue;
|
|
// No need to delete users that do not exist yet.
|
|
if u.Added then Continue;
|
|
// Go.
|
|
LogSQL('Deleting account ' + u.FOldName + '@' + u.FOldHost + '.');
|
|
AcctWhere := AccountSqlClause(u.FOldName, u.FOldHost, True);
|
|
sql := 'DELETE FROM ' + db + '.';
|
|
try
|
|
MainForm.ActiveConnection.Query(sql + mask(PRIVTABLE_COLUMNS) + AcctWhere);
|
|
MainForm.ActiveConnection.Query(sql + mask(PRIVTABLE_TABLES) + AcctWhere);
|
|
MainForm.ActiveConnection.Query(sql + mask(PRIVTABLE_DB) + AcctWhere);
|
|
MainForm.ActiveConnection.Query(sql + mask(PRIVTABLE_USERS) + AcctWhere);
|
|
except
|
|
on E:EDatabaseError do begin
|
|
MessageDlg(E.Message, mtError, [mbOK], 0);
|
|
Exit;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
AcctUpdates := TStringList.Create;
|
|
AcctUpdates.Delimiter := ',';
|
|
LogSQL('Propagating key changes for renamed and redesignated accounts...');
|
|
for i := 0 to Users.Count - 1 do begin
|
|
u := Users[i];
|
|
// Process accounts with name or host changes.
|
|
if (u.FOldName = u.Name) and (u.FOldHost = u.Host) then Continue;
|
|
// Note: must not skip added orphan users; they might have orphaned privileges that needs a key update.
|
|
if (u.FOldName = 'New User') then Continue;
|
|
AcctUpdates.Clear;
|
|
// Apply user name update.
|
|
if u.FOldName <> u.Name then
|
|
AcctUpdates.Add(mask('User') + '=' + esc(u.Name));
|
|
// Apply host criteria update.
|
|
if u.FOldHost <> u.Host then
|
|
AcctUpdates.Add(mask('Host') + '=' + esc(u.Host));
|
|
// Skip accounts with other kinds of changes, they will be processed later.
|
|
if AcctUpdates.Count = 0 then Continue;
|
|
// Go.
|
|
LogSQL('Renaming/redesignating account ' + u.FOldName + '@' + u.FOldHost + '.');
|
|
AcctWhere := AccountSqlClause(u.FOldName, u.FOldHost);
|
|
AcctValues := ' SET ' + Delim(AcctUpdates);
|
|
sql := 'UPDATE ' + db + '.';
|
|
try
|
|
// Todo: Allow concurrency by skipping this account and removing from Users array if changing key in mysql.user fails.
|
|
MainForm.ActiveConnection.Query(sql + mask(PRIVTABLE_USERS) + AcctValues + AcctWhere);
|
|
MainForm.ActiveConnection.Query(sql + mask(PRIVTABLE_DB) + AcctValues + AcctWhere);
|
|
MainForm.ActiveConnection.Query(sql + mask(PRIVTABLE_TABLES) + AcctValues + AcctWhere);
|
|
MainForm.ActiveConnection.Query(sql + mask(PRIVTABLE_COLUMNS) + AcctValues + AcctWhere);
|
|
except
|
|
on E:EDatabaseError do begin
|
|
MessageDlg(E.Message, mtError, [mbOK], 0);
|
|
Exit;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
LogSQL('Applying changes to authentication details and limitations...');
|
|
for i := 0 to Users.Count - 1 do begin
|
|
u := Users[i];
|
|
// Process accounts with changes in this loop.
|
|
if not u.Modified then Continue;
|
|
// No need to modify users that do not exist yet.
|
|
if u.Added then Continue;
|
|
// Decide what needs to be updated.
|
|
AcctUpdates.Clear;
|
|
// Apply password update.
|
|
if u.Disabled then begin
|
|
if u.OldPasswordHashed <> '!' then
|
|
AcctUpdates.Add(mask('Password') + '=' + esc('!'));
|
|
end else if u.PasswordModified then
|
|
AcctUpdates.Add(mask('Password') + '= PASSWORD(' + esc(u.Password) + ')');
|
|
// Apply limitation updates.
|
|
if dsUser.ColExists('max_questions') then
|
|
if u.FOldMaxQuestions <> u.MaxQuestions then
|
|
AcctUpdates.Add(mask('max_questions') + '=' + IntToStr(u.MaxQuestions));
|
|
if dsUser.ColExists('max_updates') then
|
|
if u.FOldMaxUpdates <> u.MaxUpdates then
|
|
AcctUpdates.Add(mask('max_updates') + '=' + IntToStr(u.MaxUpdates));
|
|
if dsUser.ColExists('max_connections') then
|
|
if u.FOldMaxConnections <> u.MaxConnections then
|
|
AcctUpdates.Add(mask('max_connections') + '=' + IntToStr(u.MaxConnections));
|
|
if dsUser.ColExists('max_user_connections') then
|
|
if u.FOldMaxUserConnections <> u.MaxUserConnections then
|
|
AcctUpdates.Add(mask('max_user_connections') + '=' + IntToStr(u.MaxUserConnections));
|
|
// Skip accounts with fx only username / host changes, they've already been processed.
|
|
if AcctUpdates.Count = 0 then Continue;
|
|
// Go.
|
|
LogSQL('Updating account ' + u.Name + '@' + u.Host + '.');
|
|
AcctWhere := AccountSqlClause(u.Name, u.Host);
|
|
AcctValues := ' SET ' + Delim(AcctUpdates);
|
|
sql := 'UPDATE ' + db + '.';
|
|
try
|
|
MainForm.ActiveConnection.Query(sql + mask(PRIVTABLE_USERS) + AcctValues + AcctWhere);
|
|
except
|
|
on E:EDatabaseError do begin
|
|
MessageDlg(E.Message, mtError, [mbOK], 0);
|
|
Exit;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
LogSQL('Creating new accounts...');
|
|
for i := 0 to Users.Count - 1 do begin
|
|
u := Users[i];
|
|
// Process only added accounts in this loop.
|
|
if not u.Added then Continue;
|
|
// Go.
|
|
LogSQL('Creating account ' + u.Name + '@' + u.Host + '.');
|
|
AcctUpdates.Clear;
|
|
// Apply user name and host designation.
|
|
AcctUpdates.Add(mask('Host') + '=' + esc(u.Host));
|
|
AcctUpdates.Add(mask('User') + '=' + esc(u.Name));
|
|
// Apply password.
|
|
if u.Disabled then
|
|
AcctUpdates.Add(mask('Password') + '=' + esc('!'))
|
|
else
|
|
AcctUpdates.Add(mask('Password') + '=PASSWORD(' + esc(u.Password) + ')');
|
|
// Apply limits.
|
|
if dsUser.ColExists('max_questions') then
|
|
AcctUpdates.Add(mask('max_questions') + '=' + IntToStr(u.MaxQuestions));
|
|
if dsUser.ColExists('max_updates') then
|
|
AcctUpdates.Add(mask('max_updates') + '=' + IntToStr(u.MaxUpdates));
|
|
if dsUser.ColExists('max_connections') then
|
|
AcctUpdates.Add(mask('max_connections') + '=' + IntToStr(u.MaxConnections));
|
|
if dsUser.ColExists('max_user_connections') then
|
|
AcctUpdates.Add(mask('max_user_connections') + '=' + IntToStr(u.MaxUserConnections));
|
|
// Special case: work around missing default values (bug) in MySQL.
|
|
if dsUser.ColExists('ssl_cipher') then
|
|
AcctUpdates.Add(mask('ssl_cipher') + '=' + esc(''));
|
|
if dsUser.ColExists('x509_issuer') then
|
|
AcctUpdates.Add(mask('x509_issuer') + '=' + esc(''));
|
|
if dsUser.ColExists('x509_subject') then
|
|
AcctUpdates.Add(mask('x509_subject') + '=' + esc(''));
|
|
sql := 'INSERT INTO ' + db + '.' + mask(PRIVTABLE_USERS);
|
|
sql := sql + ' SET ' + Delim(AcctUpdates);
|
|
try
|
|
// Todo: Allow concurrency by skipping this account and removing from Users array if inserting key in mysql.user fails.
|
|
MainForm.ActiveConnection.Query(sql);
|
|
except
|
|
on E:EDatabaseError do begin
|
|
MessageDlg(E.Message, mtError, [mbOK], 0);
|
|
Exit;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
LogSQL('Applying privilege changes...');
|
|
PrivUpdates := TStringList.Create;
|
|
PrivUpdates.Delimiter := ',';
|
|
PrivValues := TStringList.Create;
|
|
PrivValues.Delimiter := ',';
|
|
for i := 0 to Users.Count - 1 do begin
|
|
u := Users[i];
|
|
AcctWhere := AccountSqlClause(u.Name, u.Host, True);
|
|
PrivUpdates.Clear;
|
|
// Traverse the privilege definitions.
|
|
for j := 0 to u.Privileges.Count - 1 do begin
|
|
p := u.Privileges[j];
|
|
// Apply only a collection of privilege values if it has modifications.
|
|
if (not p.Modified) and (not p.Deleted) then Continue;
|
|
// Go.
|
|
LogSQL('Applying privilege to account ' + u.Name + '@' + u.Host + ' for ' + p.DBOPrettyKey + '.');
|
|
case p.DBOType of
|
|
lntNone: begin
|
|
TableSet := dsUser;
|
|
TableName := mask(PRIVTABLE_USERS);
|
|
SetFieldName := '';
|
|
end;
|
|
lntDb: begin
|
|
TableSet := dsDb;
|
|
TableName := mask(PRIVTABLE_DB);
|
|
SetFieldName := '';
|
|
end;
|
|
lntTable: begin
|
|
TableSet := dsTables;
|
|
TableName := mask(PRIVTABLE_TABLES);
|
|
SetFieldName := 'table_priv';
|
|
end;
|
|
lntColumn: begin
|
|
TableSet := dsColumns;
|
|
TableName := mask(PRIVTABLE_COLUMNS);
|
|
SetFieldName := 'column_priv';
|
|
end;
|
|
else begin
|
|
raise Exception.Create('Processed privilege has an undefined db object type: ' + IntToStr(Integer(p.DBOType)));
|
|
end;
|
|
end;
|
|
// Deduce a key for this privilege definition, appropriate for DELETE.
|
|
PrivWhere := '';
|
|
for k := 0 to p.DBONames.Count - 1 do begin
|
|
case k of
|
|
0: PrivWhere := PrivWhere + ' AND ' + mask('Db') + '=' + esc(p.DBONames[k]);
|
|
1: PrivWhere := PrivWhere + ' AND ' + mask('Table_name') + '=' + esc(p.DBONames[k]);
|
|
2: PrivWhere := PrivWhere + ' AND ' + mask('Column_name') + '=' + esc(p.DBONames[k]);
|
|
end;
|
|
end;
|
|
// Special case: remove redundant privileges in mysql.user.
|
|
if (p.DBOType = lntDb) and (p.DBOKey = '%') then begin
|
|
PrivUpdates.Clear;
|
|
for k := 0 to p.PrivNames.Count - 1 do begin
|
|
if dsUser.ColExists(p.PrivNames[k] + '_priv') then
|
|
PrivUpdates.Add(mask(p.PrivNames[k] + '_priv') + '=' + esc('N'));
|
|
end;
|
|
sql := 'UPDATE ' + db + '.' + mask(PRIVTABLE_USERS);
|
|
sql := sql + ' SET ' + Delim(PrivUpdates);
|
|
try
|
|
MainForm.ActiveConnection.Query(sql + AcctWhere);
|
|
except
|
|
on E:EDatabaseError do begin
|
|
MessageDlg(E.Message, mtError, [mbOK], 0);
|
|
Exit;
|
|
end;
|
|
end;
|
|
end;
|
|
// Remove old privilege definition.
|
|
if (p.DBOType <> lntNone) then begin
|
|
sql := 'DELETE FROM ' + db + '.' + TableName;
|
|
try
|
|
MainForm.ActiveConnection.Query(sql + AcctWhere + PrivWhere);
|
|
except
|
|
on E:EDatabaseError do begin
|
|
MessageDlg(E.Message, mtError, [mbOK], 0);
|
|
Exit;
|
|
end;
|
|
end;
|
|
if p.Deleted then
|
|
Continue;
|
|
end else begin
|
|
// Special case: avoid removing old definition when dealing with
|
|
// server-level privileges, since they're entangled with
|
|
// authentication details over in mysql.user.
|
|
// instead, we have to set them manually to 'N'.
|
|
PrivUpdates.Clear;
|
|
for k := 0 to p.PrivNames.Count - 1 do
|
|
if TableSet.ColExists(p.PrivNames[k] + '_priv') then
|
|
PrivUpdates.Add(mask(p.PrivNames[k] + '_priv') + '=' + esc('N'));
|
|
sql := 'UPDATE ' + db + '.' + TableName;
|
|
sql := sql + ' SET ' + Delim(PrivUpdates);
|
|
sql := sql + AcctWhere;
|
|
end;
|
|
// Deduce a new key for this privilege definition, one appropriate for INSERT/UPDATE.
|
|
PrivUpdates.Clear;
|
|
PrivUpdates.Add(mask('Host') + '=' + esc(u.Host));
|
|
PrivUpdates.Add(mask('User') + '=' + esc(u.Name));
|
|
for k := 0 to p.DBONames.Count - 1 do begin
|
|
case k of
|
|
0: PrivUpdates.Add(mask('Db') + '=' + esc(p.DBONames[k]));
|
|
1: PrivUpdates.Add(mask('Table_name') + '=' + esc(p.DBONames[k]));
|
|
2: PrivUpdates.Add(mask('Column_name') + '=' + esc(p.DBONames[k]));
|
|
end;
|
|
end;
|
|
PrivWhere := Delim(PrivUpdates);
|
|
// Assemble list of key columns for new privilege definition.
|
|
PrivUpdates.Clear;
|
|
PrivValues.Clear;
|
|
// Assemble values of new privilege definition.
|
|
for k := 0 to p.PrivNames.Count - 1 do begin
|
|
if p.SelectedPrivNames.IndexOf(p.PrivNames[k]) > -1 then c := 'Y' else c := 'N';
|
|
if TableSet.ColExists(p.PrivNames[k] + '_priv') then begin
|
|
// There's an ENUM field matching the privilege name.
|
|
PrivUpdates.Add(mask(p.PrivNames[k] + '_priv') + '=' + esc(c));
|
|
end else
|
|
// It must be part of a SET field, then.
|
|
if c = 'Y' then PrivValues.Add(p.PrivNames[k]);
|
|
end;
|
|
// Insert new privilege definition.
|
|
sql := 'INSERT IGNORE INTO ' + db + '.' + TableName;
|
|
if SetFieldName <> '' then
|
|
PrivUpdates.Add(mask(SetFieldName) + '=' + esc(Delim(PrivValues, False)));
|
|
sql := sql + ' SET ' + PrivWhere + ', ' + Delim(PrivUpdates);
|
|
// Special case: UPDATE instead of INSERT for server-level privileges (see further above).
|
|
if (p.DBOType = lntNone) then begin
|
|
// Server barfs if we do not set missing defaults, sigh.
|
|
PrivValues.Clear;
|
|
if dsUser.ColExists('ssl_cipher') then
|
|
PrivValues.Add(mask('ssl_cipher') + '=' + esc(''));
|
|
if dsUser.ColExists('x509_issuer') then
|
|
PrivValues.Add(mask('x509_issuer') + '=' + esc(''));
|
|
if dsUser.ColExists('x509_subject') then
|
|
PrivValues.Add(mask('x509_subject') + '=' + esc(''));
|
|
if PrivValues.Count > 0 then
|
|
sql := sql + ', ' + Delim(PrivValues);
|
|
end;
|
|
try
|
|
MainForm.ActiveConnection.Query(sql);
|
|
if MainForm.ActiveConnection.RowsAffected = 0 then
|
|
MainForm.ActiveConnection.Query('UPDATE ' + db + '.' + TableName + ' SET ' +Delim(PrivUpdates) + AcctWhere);
|
|
except
|
|
on E:EDatabaseError do begin
|
|
MessageDlg(E.Message, mtError, [mbOK], 0);
|
|
Exit;
|
|
end;
|
|
end;
|
|
// Special case: update redundant column privileges in mysql.tables_priv.
|
|
if (p.DBOType = lntColumn) and dsTables.ColExists('column_priv') then begin
|
|
// We need to deduce a completely new key because column_priv in mysql.tables_priv does not have a column field next to it, sigh.
|
|
PrivUpdates.Clear;
|
|
PrivUpdates.Add(mask('Host') + '=' + esc(u.Host));
|
|
PrivUpdates.Add(mask('User') + '=' + esc(u.Name));
|
|
for k := 0 to p.DBONames.Count - 1 do begin
|
|
case k of
|
|
0: PrivUpdates.Add(mask('Db') + '=' + esc(p.DBONames[k]));
|
|
1: PrivUpdates.Add(mask('Table_name') + '=' + esc(p.DBONames[k]));
|
|
// (2: Special case, do nothing.)
|
|
end;
|
|
end;
|
|
PrivWhere := Delim(PrivUpdates);
|
|
sql := 'INSERT INTO ' + db + '.' + PRIVTABLE_TABLES;
|
|
sql := sql + ' SET ' + PrivWhere;
|
|
sql := sql + ', ' + mask('column_priv') + '=' + esc(Delim(PrivValues, False));
|
|
try
|
|
MainForm.ActiveConnection.Query(sql);
|
|
except
|
|
on E:EDatabaseError do begin
|
|
MessageDlg(E.Message, mtError, [mbOK], 0);
|
|
Exit;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
try
|
|
MainForm.ActiveConnection.Query('FLUSH PRIVILEGES');
|
|
ModalResult := mrOK;
|
|
except
|
|
on E:EDatabaseError do begin
|
|
MessageDlg(E.Message, mtError, [mbOK], 0);
|
|
Exit;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
|
|
{ *** TUsers *** }
|
|
|
|
constructor TUsers.Create;
|
|
var
|
|
u: TUser;
|
|
i: Integer;
|
|
user, host: String;
|
|
begin
|
|
dsUser := MainForm.ActiveConnection.GetResults('SELECT * FROM '+db+'.'+Mainform.Mask(PRIVTABLE_USERS) + ' ORDER BY '
|
|
+ Mainform.Mask('User')+', '
|
|
+ Mainform.Mask('Host'));
|
|
dsDb := MainForm.ActiveConnection.GetResults('SELECT * FROM '+db+'.'+Mainform.Mask(PRIVTABLE_DB)
|
|
// Ignore db entries that contain magic pointers to the mysql.host table.
|
|
+ ' WHERE Db <> '#39#39
|
|
+ ' ORDER BY '
|
|
+ Mainform.Mask('User')+', '
|
|
+ Mainform.Mask('Host')+', '
|
|
+ Mainform.Mask('Db'));
|
|
dsTables := MainForm.ActiveConnection.GetResults('SELECT * FROM '+db+'.'+Mainform.Mask(PRIVTABLE_TABLES) + ' ORDER BY '
|
|
+ Mainform.Mask('User')+', '
|
|
+ Mainform.Mask('Host')+', '
|
|
+ Mainform.Mask('Db')+', '
|
|
+ Mainform.Mask('Table_name'));
|
|
dsColumns := MainForm.ActiveConnection.GetResults('SELECT * FROM '+db+'.'+Mainform.Mask(PRIVTABLE_COLUMNS) + ' ORDER BY '
|
|
+ Mainform.Mask('User')+', '
|
|
+ Mainform.Mask('Host')+', '
|
|
+ Mainform.Mask('Db')+', '
|
|
+ Mainform.Mask('Table_name')+', '
|
|
+ Mainform.Mask('Column_name'));
|
|
dsTablesFields := MainForm.ActiveConnection.GetResults('SHOW FIELDS FROM '+db+'.tables_priv LIKE ''%\_priv''');
|
|
dsColumnsFields := MainForm.ActiveConnection.GetResults('SHOW FIELDS FROM '+db+'.columns_priv LIKE ''%\_priv''');
|
|
for i:=0 to dsUser.RecordCount-1 do begin
|
|
// Avoid using dsUser.Next and dsUser.Eof here because TUser.Create
|
|
// also iterates through the global dsUser result and moves the cursor
|
|
dsUser.RecNo := i;
|
|
u := TUser.Create(dsUser);
|
|
AddUser(u);
|
|
end;
|
|
// Find orphaned privileges in mysql.db.
|
|
for i:=0 to dsDb.RecordCount-1 do begin
|
|
dsDb.RecNo := i;
|
|
user := dsDb.Col('User');
|
|
host := dsDb.Col('Host');
|
|
if FindUser(user, host) = nil then AddUser(TUser.Create(user, host));
|
|
end;
|
|
// Find orphaned privileges in mysql.tables_priv.
|
|
for i:=0 to dsTables.RecordCount-1 do begin
|
|
dsTables.RecNo := i;
|
|
user := dsTables.Col('User');
|
|
host := dsTables.Col('Host');
|
|
if FindUser(user, host) = nil then AddUser(TUser.Create(user, host));
|
|
end;
|
|
// Find orphaned privileges in mysql.columns_priv.
|
|
for i:=0 to dsColumns.RecordCount-1 do begin
|
|
dsColumns.RecNo := i;
|
|
user := dsColumns.Col('User');
|
|
host := dsColumns.Col('Host');
|
|
if FindUser(user, host) = nil then AddUser(TUser.Create(user, host));
|
|
end;
|
|
end;
|
|
|
|
procedure TUsers.AddUser(User: TUser);
|
|
begin
|
|
SetLength(FUserItems, Length(FUserItems)+1);
|
|
FUserItems[Length(FUserItems)-1] := User;
|
|
end;
|
|
|
|
function TUsers.GetUser(Index: Integer): TUser;
|
|
begin
|
|
Result := FUserItems[Index];
|
|
end;
|
|
|
|
function TUsers.GetCount: Integer;
|
|
begin
|
|
Result := Length(FUserItems);
|
|
end;
|
|
|
|
procedure TUsers.DeleteUser(Index: Integer);
|
|
var
|
|
i: Integer;
|
|
begin
|
|
if FUserItems[Index].Added then begin
|
|
// This user was created only in memory, not yet on the server.
|
|
// Erase this item out of UserItems, not only display the deleted icon
|
|
for i := Index+1 to Length(FUserItems) - 1 do
|
|
FUserItems[i-1] := FUserItems[i];
|
|
SetLength(FUserItems, Length(FUserItems)-1);
|
|
end else
|
|
FUserItems[Index].Deleted := True
|
|
end;
|
|
|
|
function TUsers.FindUser(Name: String; Host: String): TUser;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
Result := nil;
|
|
// Host='' is canonicalized to Host='%' on load, so search for that instead.
|
|
if Host = '' then Host := '%';
|
|
for i := 0 to Length(FUserItems) - 1 do begin
|
|
if (FUserItems[i].Name = Name) and (FUserItems[i].Host = Host) then begin
|
|
Result := FUserItems[i];
|
|
Exit;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
|
|
{ *** TUser *** }
|
|
|
|
constructor TUser.Create(Name: String; Host: String);
|
|
begin
|
|
FAdded := True;
|
|
FDeleted := False;
|
|
FOtherModified := False;
|
|
FPasswordModified := False;
|
|
FDisabled := True;
|
|
FOldPasswordHashed := '';
|
|
FNameChanged := False;
|
|
FOldName := Name;
|
|
FOldHost := Host;
|
|
if FOldHost = '' then begin
|
|
// Get rid of duplicate entries.
|
|
// Todo: handle collisions.
|
|
FNewHost := '%';
|
|
FOtherModified := True;
|
|
end;
|
|
FPassword := '';
|
|
FPrivileges := TPrivileges.Create(Self);
|
|
end;
|
|
|
|
constructor TUser.Create(Fields: TMySQLQuery);
|
|
begin
|
|
// Loading an existing user
|
|
FAdded := False;
|
|
FDeleted := False;
|
|
FOtherModified := False;
|
|
FPasswordModified := False;
|
|
FDisabled := False;
|
|
FNameChanged := False;
|
|
FOldName := Fields.Col('User', True);
|
|
FOldHost := Fields.Col('Host', True);
|
|
if FOldHost = '' then begin
|
|
// Get rid of duplicate entries.
|
|
// Todo: handle collisions.
|
|
FNewHost := '%';
|
|
FOtherModified := True;
|
|
end;
|
|
FPassword := '';
|
|
FOldPasswordHashed := Fields.Col('Password', True);
|
|
FOldMaxQuestions := 0;
|
|
if Fields.ColExists('max_questions') then
|
|
FOldMaxQuestions := MakeInt(Fields.Col('max_questions'));
|
|
FMaxQuestions := FOldMaxQuestions;
|
|
FOldMaxUpdates := 0;
|
|
if Fields.ColExists('max_updates') then
|
|
FOldMaxUpdates := MakeInt(Fields.Col('max_updates'));
|
|
FMaxUpdates := FOldMaxUpdates;
|
|
FOldMaxConnections := 0;
|
|
if Fields.ColExists('max_connections') then
|
|
FOldMaxConnections := MakeInt(Fields.Col('max_connections'));
|
|
FMaxConnections := FOldMaxConnections;
|
|
FOldMaxUserConnections := 0;
|
|
if Fields.ColExists('max_user_connections') then
|
|
FOldMaxUserConnections := MakeInt(Fields.Col('max_user_connections'));
|
|
FMaxUserConnections := FOldMaxUserConnections;
|
|
|
|
FPrivileges := TPrivileges.Create(Self);
|
|
end;
|
|
|
|
function TUser.GetModified: Boolean;
|
|
begin
|
|
Result := FPasswordModified or FOtherModified;
|
|
end;
|
|
|
|
function TUser.GetName: String;
|
|
begin
|
|
Result := FOldName;
|
|
if FNameChanged then Result := FNewName;
|
|
end;
|
|
|
|
procedure TUser.SetName(str: String);
|
|
begin
|
|
FOtherModified := FOtherModified or (Name <> str);
|
|
FNameChanged := True;
|
|
FNewName := str;
|
|
end;
|
|
|
|
function TUser.GetHost: String;
|
|
begin
|
|
Result := FOldHost;
|
|
if FNewHost <> '' then
|
|
Result := FNewHost;
|
|
end;
|
|
|
|
procedure TUser.SetHost(str: String);
|
|
begin
|
|
if str = '' then
|
|
str := '%';
|
|
FOtherModified := FOtherModified or (Host <> str);
|
|
FNewHost := str;
|
|
end;
|
|
|
|
procedure TUser.SetPassword(str: String);
|
|
begin
|
|
// If password was edited but hasn't changed (fx by a copy+paste action)
|
|
if FOldPasswordHashed = str then begin
|
|
FPasswordModified := false;
|
|
FPassword := '';
|
|
end else begin
|
|
FPasswordModified := true;
|
|
FPassword := str;
|
|
end;
|
|
end;
|
|
|
|
|
|
{ *** TPrivileges *** }
|
|
constructor TPrivileges.Create(AOwner: TUser);
|
|
var
|
|
p: TPrivilege;
|
|
host, user: String;
|
|
// Extract privilege objects from results of user, db, tables_priv + columns_priv
|
|
procedure LoadPrivs(ds: TMySQLQuery; FieldDefs: TMySQLQuery=nil; AvoidFieldDefs: TMySQLQuery=nil; CropDbFieldDefs: TMySQLQuery=nil);
|
|
var
|
|
hasUserField : Boolean;
|
|
simulateDb : Boolean;
|
|
begin
|
|
ds.First;
|
|
// The priv table 'host' does not have a user field, use '%'.
|
|
hasUserField := ds.ColExists('User');
|
|
user := '%';
|
|
// When cropping the priv table 'user' to load only db privileges, use '%' for db.
|
|
simulateDb := cropDbFieldDefs <> nil;
|
|
while not ds.Eof do begin
|
|
if hasUserField then user := ds.Col('User');
|
|
host := ds.Col('Host');
|
|
// Canonicalize: Host='' and Host='%' means the same.
|
|
if host = '' then host := '%';
|
|
if (host = FOwner.FOldHost) and (user = FOwner.FOldName) then begin
|
|
// Find existing privilege, or create + add new one.
|
|
p := FindPrivilege(ds, simulateDb);
|
|
if (p = nil) then begin
|
|
p := AddPrivilege(ds, FieldDefs, AvoidFieldDefs, CropDbFieldDefs, simulateDb);
|
|
end;
|
|
// Merge grants from row into the privilege object.
|
|
p.Merge(ds)
|
|
end;
|
|
ds.Next;
|
|
end;
|
|
end;
|
|
begin
|
|
FOwner := AOwner;
|
|
if AOwner.Added then begin
|
|
// Create blanket "server privileges" and "all objects" items.
|
|
AddPrivilege(dsUser, nil, dsDb);
|
|
AddPrivilege(dsUser, nil, nil, dsDb, True);
|
|
end;
|
|
// Load server privileges from 'user' rows, avoiding db privileges.
|
|
LoadPrivs(dsUser, nil, dsDb);
|
|
// Load db privileges from 'user' rows, avoiding server privileges.
|
|
LoadPrivs(dsUser, nil, nil, dsDb);
|
|
// Load the rest of the privileges.
|
|
LoadPrivs(dsDb);
|
|
LoadPrivs(dsTables, dsTablesFields);
|
|
LoadPrivs(dsColumns, dsColumnsFields);
|
|
end;
|
|
|
|
function TPrivileges.GetPrivilege(Index: Integer): TPrivilege;
|
|
begin
|
|
Result := FPrivilegeItems[Index];
|
|
end;
|
|
|
|
function TPrivileges.GetCount: Integer;
|
|
begin
|
|
Result := Length(FPrivilegeItems);
|
|
end;
|
|
|
|
function TPrivileges.FindPrivilege(Fields: TMySQLQuery; SimulateDbField: Boolean): TPrivilege;
|
|
var
|
|
i : Integer;
|
|
DBOType: TListNodeType;
|
|
DBONames: TStringList;
|
|
begin
|
|
Result := nil;
|
|
GetPrivilegeRowKey(Fields, SimulateDbField, DBOType, DBONames);
|
|
for i := 0 to Length(FPrivilegeItems) - 1 do begin
|
|
if
|
|
(FPrivilegeItems[i].DBOType = DBOType) and
|
|
(DBONames.DelimitedText = FPrivilegeItems[i].DBONames.DelimitedText)
|
|
then begin
|
|
Result := FPrivilegeItems[i];
|
|
Exit;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
function TPrivileges.AddPrivilege(Fields: TMySQLQuery; FieldDefs: TMySQLQuery=nil; AvoidFieldDefs: TMySQLQuery=nil; CropFieldDefs: TMySQLQuery=nil; SimulateDbField: Boolean = False): TPrivilege;
|
|
begin
|
|
Result := TPrivilege.Create(Fields, FieldDefs, AvoidFieldDefs, CropFieldDefs, SimulateDbField);
|
|
SetLength(FPrivilegeItems, Length(FPrivilegeItems)+1);
|
|
FPrivilegeItems[Length(FPrivilegeItems)-1] := Result;
|
|
// Minimum default privs for a new user should be read only for everything, or?
|
|
if FOwner.Added and (Result.FDBOType = lntDb) and (Result.DBOKey = '%') then begin
|
|
Result.SelectedPrivNames.Add('Select');
|
|
Result.Modified := True;
|
|
end;
|
|
end;
|
|
|
|
procedure TPrivileges.DeletePrivilege(Index: Integer);
|
|
var
|
|
i: Integer;
|
|
begin
|
|
if FPrivilegeItems[Index].Added then begin
|
|
// This user was created only in memory, not yet on the server.
|
|
// Erase this item out of UserItems, not only display the deleted icon
|
|
for i := Index+1 to Length(FPrivilegeItems) - 1 do
|
|
FPrivilegeItems[i-1] := FPrivilegeItems[i];
|
|
SetLength(FPrivilegeItems, Length(FPrivilegeItems)-1);
|
|
end else begin
|
|
FPrivilegeItems[Index].Deleted := True;
|
|
FOwner.Modified := True;
|
|
end;
|
|
end;
|
|
|
|
|
|
{ *** TPrivilege *** }
|
|
|
|
constructor TPrivilege.Create(Fields: TMySQLQuery; FieldDefs: TMySQLQuery=nil; AvoidFieldDefs: TMySQLQuery=nil; CropFieldDefs: TMySQLQuery=nil; SimulateDbField: Boolean = False);
|
|
var
|
|
i: Integer;
|
|
tables_col_ignore: Boolean;
|
|
// Find possible values for the given SET column
|
|
function GetSETValues(FieldName: String): TStringList;
|
|
begin
|
|
FieldDefs.First;
|
|
Result := TStringList.Create;
|
|
while not FieldDefs.Eof do begin
|
|
if FieldDefs.Col('Field') = FieldName + '_priv' then begin
|
|
Result.QuoteChar := '''';
|
|
Result.DelimitedText := getEnumValues(FieldDefs.Col('Type'));
|
|
end;
|
|
FieldDefs.Next;
|
|
end;
|
|
end;
|
|
begin
|
|
// Defaults
|
|
FDeleted := False;
|
|
FAdded := False;
|
|
FModified := False;
|
|
DBONames := TStringList.Create;
|
|
PrivNames := TStringList.Create;
|
|
SelectedPrivNames := TStringList.Create;
|
|
// Find out what xxxx_priv privilege columns this server/version has.
|
|
PrivNames.Text := Fields.ColumnNames.Text;
|
|
for i := PrivNames.Count - 1 downto 0 do begin
|
|
if Length(PrivNames[i]) > 5 then begin
|
|
if Copy(PrivNames[i], Length(PrivNames[i]) - 4, 5) = '_priv' then begin
|
|
// Avoid duplicated db privileges in user table.
|
|
if AvoidFieldDefs = nil then Continue;
|
|
if AvoidFieldDefs.ColumnNames.IndexOf(PrivNames[i]) = -1 then Continue;
|
|
end;
|
|
end;
|
|
PrivNames.Delete(i);
|
|
end;
|
|
if CropFieldDefs <> nil then begin
|
|
for i := PrivNames.Count - 1 downto 0 do begin
|
|
if CropFieldDefs.ColumnNames.IndexOf(PrivNames[i]) = -1 then PrivNames.Delete(i);
|
|
end;
|
|
end;
|
|
// Find out what SET columns in tables_priv this server/version has.
|
|
// Only load tables_priv.Table_priv
|
|
i := PrivNames.IndexOf('Table_priv');
|
|
if i > -1 then begin
|
|
PrivNames.Delete(i);
|
|
PrivNames.AddStrings(GetSETValues('Table'));
|
|
end;
|
|
tables_col_ignore := i > -1;
|
|
// Find out what SET columns in columns_priv this server/version has.
|
|
i := PrivNames.IndexOf('Column_priv');
|
|
if i > -1 then begin
|
|
PrivNames.Delete(i);
|
|
if not tables_col_ignore then
|
|
PrivNames.AddStrings(GetSETValues('Column'));
|
|
end;
|
|
// Snip "_priv" from ENUM field names.
|
|
for i := PrivNames.Count - 1 downto 0 do begin
|
|
if Length(PrivNames[i]) > 5 then begin
|
|
if Copy(PrivNames[i], Length(PrivNames[i]) - 4, 5) = '_priv' then begin
|
|
PrivNames[i] := Copy(PrivNames[i], 0, Length(PrivNames[i]) - 5);
|
|
end;
|
|
end;
|
|
end;
|
|
// Get unique identifier for this privilege row.
|
|
GetPrivilegeRowKey(Fields, SimulateDbField, FDBOType, DBONames);
|
|
end;
|
|
|
|
procedure TPrivilege.Merge(Fields: TMySQLQuery);
|
|
var
|
|
i: Integer;
|
|
tmp: TStringList;
|
|
begin
|
|
// Apply ENUM privileges, skipping any SET privileges.
|
|
for i := PrivNames.Count - 1 downto 0 do begin
|
|
if Fields.ColumnNames.IndexOf(PrivNames[i] + '_priv') > -1 then begin
|
|
if UpperCase(Fields.Col(PrivNames[i] + '_priv')) = 'Y' then begin
|
|
SelectedPrivNames.Add(PrivNames[i]);
|
|
end;
|
|
end;
|
|
end;
|
|
// Parse SET field in column "tables_priv"
|
|
if Fields.ColExists('Table_priv') then begin
|
|
tmp := TStringList.Create;
|
|
tmp.CommaText := Fields.Col('Table_priv');
|
|
SelectedPrivNames.AddStrings(tmp);
|
|
end else begin
|
|
// Parse SET field in column "columns_priv"
|
|
if Fields.ColExists('Column_priv') then begin
|
|
tmp := TStringList.Create;
|
|
tmp.CommaText := Fields.Col('Column_priv');
|
|
SelectedPrivNames.AddStrings(tmp);
|
|
end;
|
|
end;
|
|
FreeAndNil(tmp);
|
|
end;
|
|
|
|
|
|
function TPrivilege.GetDBOPrettyKey: String;
|
|
begin
|
|
Result := '';
|
|
case FDBOType of
|
|
lntNone: Result := Result + 'Server privileges';
|
|
lntDb: Result := Result + 'Database: ';
|
|
lntTable: Result := Result + 'Table: ';
|
|
lntColumn: Result := Result + 'Column: ';
|
|
end;
|
|
Result := Result + GetDBOKey;
|
|
// Special case "db=%"
|
|
if (FDBOType = lntDb) and (DBOKey = '%') then
|
|
Result := 'All databases';
|
|
end;
|
|
|
|
|
|
function TPrivilege.GetDBOKey: String;
|
|
begin
|
|
DBONames.Delimiter := '.';
|
|
Result := DBONames.DelimitedText;
|
|
end;
|
|
|
|
function TPrivilege.GetPrettyPrivNames: TStringList;
|
|
var
|
|
i: Integer;
|
|
p: String;
|
|
begin
|
|
Result := TStringList.Create;
|
|
for i := 0 to PrivNames.Count - 1 do begin
|
|
// Fetch original name
|
|
p := PrivNames[i];
|
|
p := StringReplace(p, '_', ' ', [rfReplaceAll]);
|
|
p := Trim(p);
|
|
Result.Add(p);
|
|
end;
|
|
end;
|
|
|
|
|
|
procedure GetPrivilegeRowKey(Fields: TMySQLQuery; SimulateDbField: Boolean; out DBOType: TListNodeType; out DBONames: TStringList);
|
|
var
|
|
RowsExist: Boolean;
|
|
begin
|
|
DBOType := lntNone;
|
|
DBONames := TStringList.Create;
|
|
DBONames.Delimiter := '.';
|
|
if SimulateDbField then begin
|
|
DBOType := lntDb;
|
|
DBONames.Add('%');
|
|
end;
|
|
RowsExist := Fields.RecordCount > 0;
|
|
if Fields.ColExists('Db') then begin
|
|
DBOType := lntDb;
|
|
if RowsExist then
|
|
DBONames.Add(Fields.Col('Db'));
|
|
end;
|
|
if Fields.ColExists('Table_name') then begin
|
|
DBOType := lntTable;
|
|
if RowsExist then
|
|
DBONames.Add(Fields.Col('Table_name'));
|
|
end;
|
|
if Fields.ColExists('Column_name') then begin
|
|
DBOType := lntColumn;
|
|
if RowsExist then
|
|
DBONames.Add(Fields.Col('Column_name'));
|
|
end;
|
|
end;
|
|
|
|
end.
|