// // SPUserManager.m // sequel-pro // // Created by Mark Townsend on Jan 1, 2009. // Copyright (c) 2009 Mark Townsend. All rights reserved. // // Permission is hereby granted, free of charge, to any person // obtaining a copy of this software and associated documentation // files (the "Software"), to deal in the Software without // restriction, including without limitation the rights to use, // copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the // Software is furnished to do so, subject to the following // conditions: // // The above copyright notice and this permission notice shall be // included in all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR // OTHER DEALINGS IN THE SOFTWARE. // // More info at #import "SPUserManager.h" #import "SPUserMO.h" #import "ImageAndTextCell.h" #import "SPGrowlController.h" #import "SPConnectionController.h" #import "SPServerSupport.h" #import "SPAlertSheets.h" #import "SPSplitView.h" #import "SPDatabaseDocument.h" #import #import static NSString * const SPTableViewNameColumnID = @"NameColumn"; @interface SPUserManager () - (void)_initializeTree:(NSArray *)items; - (void)_initializeUsers; - (void)_selectParentFromSelection; - (NSArray *)_fetchUserWithUserName:(NSString *)username; - (NSManagedObject *)_createNewSPUser; - (void)_grantPrivileges:(NSArray *)thePrivileges onDatabase:(NSString *)aDatabase forUser:(NSString *)aUser host:(NSString *)aHost; - (void)_revokePrivileges:(NSArray *)thePrivileges onDatabase:(NSString *)aDatabase forUser:(NSString *)aUser host:(NSString *)aHost; - (BOOL)_checkAndDisplayMySqlError; - (void)_clearData; - (void)_initializeChild:(NSManagedObject *)child withItem:(NSDictionary *)item; - (void)_initializeSchemaPrivsForChild:(NSManagedObject *)child fromData:(NSArray *)dataForUser; - (void)_initializeSchemaPrivs; - (NSArray *)_fetchPrivsWithUser:(NSString *)username schema:(NSString *)selectedSchema host:(NSString *)host; - (void)_setSchemaPrivValues:(NSArray *)objects enabled:(BOOL)enabled; - (void)_initializeAvailablePrivs; - (void)_renameUserFrom:(NSString *)originalUser host:(NSString *)originalHost to:(NSString *)newUser host:(NSString *)newHost; - (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void*)context; @end @implementation SPUserManager @synthesize connection; @synthesize databaseDocument; @synthesize privsSupportedByServer; @synthesize managedObjectContext; @synthesize managedObjectModel; @synthesize persistentStoreCoordinator; @synthesize schemas; @synthesize grantedSchemaPrivs; @synthesize availablePrivs; @synthesize treeSortDescriptors; @synthesize serverSupport; #pragma mark - #pragma mark Initialisation - (id)init { if ((self = [super initWithWindowNibName:@"UserManagerView"])) { // When reading privileges from the database, they are converted automatically to a // lowercase key used in the user privileges stores, from which a GRANT syntax // is derived automatically. While most keys can be automatically converted without // any difficulty, some keys differ slightly in mysql column storage to GRANT syntax; // this dictionary provides mappings for those values to ensure consistency. privColumnToGrantMap = [[NSDictionary alloc] initWithObjectsAndKeys: @"Grant_option_priv", @"Grant_priv", @"Show_databases_priv", @"Show_db_priv", @"Create_temporary_tables_priv", @"Create_tmp_table_priv", @"Replication_slave_priv", @"Repl_slave_priv", @"Replication_client_priv", @"Repl_client_priv", nil]; schemas = [[NSMutableArray alloc] init]; availablePrivs = [[NSMutableArray alloc] init]; grantedSchemaPrivs = [[NSMutableArray alloc] init]; isSaving = NO; } return self; } /** * UI specific items to set up when the window loads. This is different than awakeFromNib * as it's only called once. */ - (void)windowDidLoad { [tabView selectTabViewItemAtIndex:0]; [splitView setMinSize:120.f ofSubviewAtIndex:0]; [splitView setMinSize:620.f ofSubviewAtIndex:1]; NSTableColumn *tableColumn = [outlineView tableColumnWithIdentifier:SPTableViewNameColumnID]; ImageAndTextCell *imageAndTextCell = [[[ImageAndTextCell alloc] init] autorelease]; [imageAndTextCell setEditable:NO]; [tableColumn setDataCell:imageAndTextCell]; // Set schema table double-click actions [grantedTableView setDoubleAction:@selector(doubleClickSchemaPriv:)]; [availableTableView setDoubleAction:@selector(doubleClickSchemaPriv:)]; [self _initializeSchemaPrivs]; [self _initializeUsers]; [self _initializeAvailablePrivs]; treeSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"displayName" ascending:YES]; [self setTreeSortDescriptors:[NSArray arrayWithObject:treeSortDescriptor]]; [super windowDidLoad]; } /** * This method reads in the users from the mysql.user table of the current * connection. Then uses this information to initialize the NSOutlineView. */ - (void)_initializeUsers { isInitializing = YES; // Don't want to do some of the notifications if initializing NSMutableString *privKey; NSArray *privRow; NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init]; NSMutableArray *usersResultArray = [NSMutableArray array]; // Select users from the mysql.user table SPMySQLResult *result = [[self connection] queryString:@"SELECT * FROM mysql.user ORDER BY user"]; [result setReturnDataAsStrings:YES]; [usersResultArray addObjectsFromArray:[result getAllRows]]; [self _initializeTree:usersResultArray]; // Set up the array of privs supported by this server. [[self privsSupportedByServer] removeAllObjects]; result = nil; // Attempt to obtain user privileges if supported if ([serverSupport supportsShowPrivileges]) { result = [[self connection] queryString:@"SHOW PRIVILEGES"]; [result setReturnDataAsStrings:YES]; } if (result && [result numberOfRows]) { while ((privRow = [result getRowAsArray])) { privKey = [NSMutableString stringWithString:[[privRow objectAtIndex:0] lowercaseString]]; // Skip the special "Usage" key if ([privKey isEqualToString:@"usage"]) continue; [privKey replaceOccurrencesOfString:@" " withString:@"_" options:NSLiteralSearch range:NSMakeRange(0, [privKey length])]; [privKey appendString:@"_priv"]; [[self privsSupportedByServer] setValue:@YES forKey:privKey]; } } // If that fails, base privilege support on the mysql.users columns else { result = [[self connection] queryString:@"SHOW COLUMNS FROM mysql.user"]; [result setReturnDataAsStrings:YES]; while ((privRow = [result getRowAsArray])) { privKey = [NSMutableString stringWithString:[privRow objectAtIndex:0]]; if (![privKey hasSuffix:@"_priv"]) continue; if ([privColumnToGrantMap objectForKey:privKey]) privKey = [privColumnToGrantMap objectForKey:privKey]; [[self privsSupportedByServer] setValue:@YES forKey:[privKey lowercaseString]]; } } [pool release]; isInitializing = NO; } /** * Initialize the outline view tree. The NSOutlineView gets it's data from a NSTreeController which gets * it's data from the SPUser Entity objects in the current managedObjectContext. */ - (void)_initializeTree:(NSArray *)items { // Retrieve all the user data in order to be able to initialise the schema privs for each child, // copying into a dictionary keyed by user, each with all the host rows. NSMutableDictionary *schemaPrivilegeData = [NSMutableDictionary dictionary]; SPMySQLResult *queryResults = [[self connection] queryString:@"SELECT * FROM mysql.db"]; [queryResults setReturnDataAsStrings:YES]; for (NSDictionary *privRow in queryResults) { if (![schemaPrivilegeData objectForKey:[privRow objectForKey:@"User"]]) { [schemaPrivilegeData setObject:[NSMutableArray array] forKey:[privRow objectForKey:@"User"]]; } [[schemaPrivilegeData objectForKey:[privRow objectForKey:@"User"]] addObject:privRow]; // If "all database" values were found, add them to the schemas list if not already present NSString *schemaName = [privRow objectForKey:@"Db"]; if ([schemaName isEqualToString:@""] || [schemaName isEqualToString:@"%"]) { if (![schemas containsObject:schemaName]) { [schemas addObject:schemaName]; [schemasTableView noteNumberOfRowsChanged]; } } } // Go through each item that contains a dictionary of key-value pairs // for each user currently in the database. for (NSUInteger i = 0; i < [items count]; i++) { NSString *username = [[items objectAtIndex:i] objectForKey:@"User"]; NSArray *parentResults = [[self _fetchUserWithUserName:username] retain]; NSDictionary *item = [items objectAtIndex:i]; NSManagedObject *parent; NSManagedObject *child; // Check to make sure if we already have added the parent if (parentResults != nil && [parentResults count] > 0) { // Add Children parent = [parentResults objectAtIndex:0]; child = [self _createNewSPUser]; } else { // Add Parent parent = [self _createNewSPUser]; child = [self _createNewSPUser]; // We only care about setting the user and password keys on the parent, together with their // original values for comparison purposes [parent setPrimitiveValue:username forKey:@"user"]; [parent setPrimitiveValue:username forKey:@"originaluser"]; [parent setPrimitiveValue:[item objectForKey:@"Password"] forKey:@"password"]; [parent setPrimitiveValue:[item objectForKey:@"Password"] forKey:@"originalpassword"]; } // Setup the NSManagedObject with values from the dictionary [self _initializeChild:child withItem:item]; NSMutableSet *children = [parent mutableSetValueForKey:@"children"]; [children addObject:child]; [self _initializeSchemaPrivsForChild:child fromData:[schemaPrivilegeData objectForKey:username]]; // Save the initialized objects so that any new changes will be tracked. NSError *error = nil; [[self managedObjectContext] save:&error]; if (error != nil) { [[NSApplication sharedApplication] presentError:error]; } [parentResults release]; } // Reload data of the outline view with the changes. [outlineView reloadData]; [treeController rearrangeObjects]; } /** * Initialize the available user privileges. */ - (void)_initializeAvailablePrivs { // Initialize available privileges NSManagedObjectContext *moc = [self managedObjectContext]; NSEntityDescription *privEntityDescription = [NSEntityDescription entityForName:@"Privileges" inManagedObjectContext:moc]; NSArray *props = [privEntityDescription attributeKeys]; [availablePrivs removeAllObjects]; for (NSString *prop in props) { if ([prop hasSuffix:@"_priv"] && [[[self privsSupportedByServer] objectForKey:prop] boolValue]) { NSString *displayName = [[prop stringByReplacingOccurrencesOfString:@"_priv" withString:@""] replaceUnderscoreWithSpace]; [availablePrivs addObject:[NSDictionary dictionaryWithObjectsAndKeys:displayName, @"displayName", prop, @"name", nil]]; } } [availableController rearrangeObjects]; } /** * Initialize the available schema privileges. */ - (void)_initializeSchemaPrivs { // Initialize Databases [schemas removeAllObjects]; [schemas addObjectsFromArray:[databaseDocument allDatabaseNames]]; [schemasTableView reloadData]; } /** * Set NSManagedObject with values from the passed in dictionary. */ - (void)_initializeChild:(NSManagedObject *)child withItem:(NSDictionary *)item { for (NSString *key in item) { // In order to keep the priviledges a little more dynamic, just // go through the keys that have the _priv suffix. If a priviledge is // currently not supported in the model, then an exception is thrown. // We catch that exception and print to the console for future enhancement. NS_DURING if ([key hasSuffix:@"_priv"]) { BOOL value = [[item objectForKey:key] boolValue]; // Special case keys if ([privColumnToGrantMap objectForKey:key]) { key = [privColumnToGrantMap objectForKey:key]; } [child setValue:[NSNumber numberWithBool:value] forKey:key]; } else if ([key hasPrefix:@"max"]) // Resource Management restrictions { NSNumber *value = [NSNumber numberWithInteger:[[item objectForKey:key] integerValue]]; [child setValue:value forKey:key]; } else if (![key isEqualToString:@"User"] && ![key isEqualToString:@"Password"]) { NSString *value = [item objectForKey:key]; [child setValue:value forKey:key]; } NS_HANDLER NS_ENDHANDLER } } /** * Initialize the schema privileges for the supplied child object. * * Assumes that the child has already been initialized with values from the * global user table. */ - (void)_initializeSchemaPrivsForChild:(NSManagedObject *)child fromData:(NSArray *)dataForUser { NSMutableSet *privs = [child mutableSetValueForKey:@"schema_privileges"]; // Set an originalhost key on the child to allow the tracking of edits [child setPrimitiveValue:[child valueForKey:@"host"] forKey:@"originalhost"]; for (NSDictionary *rowDict in dataForUser) { // Verify that the host matches, or skip this entry if (![[rowDict objectForKey:@"Host"] isEqualToString:[child valueForKey:@"host"]]) { continue; } NSManagedObject *dbPriv = [NSEntityDescription insertNewObjectForEntityForName:@"Privileges" inManagedObjectContext:[self managedObjectContext]]; for (NSString *key in rowDict) { if ([key hasSuffix:@"_priv"]) { BOOL boolValue = [[rowDict objectForKey:key] boolValue]; // Special case keys if ([privColumnToGrantMap objectForKey:key]) { key = [privColumnToGrantMap objectForKey:key]; } [dbPriv setValue:[NSNumber numberWithBool:boolValue] forKey:key]; } else if ([key isEqualToString:@"Db"]) { NSString *db = [[rowDict objectForKey:key] stringByReplacingOccurrencesOfString:@"\\_" withString:@"_"]; [dbPriv setValue:db forKey:key]; } else if (![key isEqualToString:@"Host"] && ![key isEqualToString:@"User"]) { [dbPriv setValue:[rowDict objectForKey:key] forKey:key]; } } [privs addObject:dbPriv]; } } /** * Creates, retains, and returns the managed object model for the application * by merging all of the models found in the application bundle. */ - (NSManagedObjectModel *)managedObjectModel { if (managedObjectModel != nil) return managedObjectModel; managedObjectModel = [[NSManagedObjectModel mergedModelFromBundles:nil] retain]; return managedObjectModel; } /** * Returns the persistent store coordinator for the application. This * implementation will create and return a coordinator, having added the * store for the application to it. (The folder for the store is created, * if necessary.) */ - (NSPersistentStoreCoordinator *)persistentStoreCoordinator { if (persistentStoreCoordinator != nil) return persistentStoreCoordinator; NSError *error; persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]]; if (![persistentStoreCoordinator addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:&error]) { [[NSApplication sharedApplication] presentError:error]; } return persistentStoreCoordinator; } /** * Returns the managed object context for the application (which is already * bound to the persistent store coordinator for the application.) */ - (NSManagedObjectContext *)managedObjectContext { if (managedObjectContext != nil) return managedObjectContext; NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator]; if (coordinator != nil) { managedObjectContext = [[NSManagedObjectContext alloc] init]; [managedObjectContext setPersistentStoreCoordinator:coordinator]; } [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(contextDidSave:) name:NSManagedObjectContextDidSaveNotification object:nil]; return managedObjectContext; } - (void)beginSheetModalForWindow:(NSWindow *)docWindow completionHandler:(void (^)())callback { //copy block from stack to heap, otherwise it wouldn't live long enough to be invoked later. void *heapCallback = callback? Block_copy(callback) : NULL; [NSApp beginSheet:[self window] modalForWindow:docWindow modalDelegate:self didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:) contextInfo:heapCallback]; } - (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void*)context { //[NSApp endSheet...] does not close the window [[self window] orderOut:self]; //notify delegate if(context) { void (^callback)() = context; //directly invoking callback would risk that we are dealloc'd while still in this run loop iteration. dispatch_async(dispatch_get_main_queue(), callback); Block_release(callback); } } #pragma mark - #pragma mark General IBAction methods /** * Closes the user manager and reverts any changes made. */ - (IBAction)doCancel:(id)sender { // Discard any pending changes [treeController discardEditing]; // Change the first responder to end editing in any field [[self window] makeFirstResponder:self]; [[self managedObjectContext] rollback]; // Close sheet [NSApp endSheet:[self window] returnCode:0]; } /** * Closes the user manager and applies any changes made. */ - (IBAction)doApply:(id)sender { // If editing can't be committed, cancel the apply if (![treeController commitEditing]) { return; } errorsString = [[NSMutableString alloc] init]; // Change the first responder to end editing in any field [[self window] makeFirstResponder:self]; isSaving = YES; NSError *error = nil; [[self managedObjectContext] save:&error]; isSaving = NO; if (error) [errorsString appendString:[error localizedDescription]]; [[self connection] queryString:@"FLUSH PRIVILEGES"]; // Display any errors if ([errorsString length]) { [errorsTextView setString:errorsString]; [NSApp beginSheet:errorsSheet modalForWindow:[NSApp keyWindow] modalDelegate:nil didEndSelector:NULL contextInfo:nil]; [errorsString release]; return; } [errorsString release]; // Otherwise, close the sheet [NSApp endSheet:[self window] returnCode:0]; } /** * Enables all privileges. */ - (IBAction)checkAllPrivileges:(id)sender { id selectedUser = [[treeController selectedObjects] objectAtIndex:0]; // Iterate through the supported privs, setting the value of each to YES for (NSString *key in [self privsSupportedByServer]) { if (![key hasSuffix:@"_priv"]) continue; // Perform the change in a try/catch check to avoid exceptions for unhandled privs NS_DURING [selectedUser setValue:@YES forKey:key]; NS_HANDLER NS_ENDHANDLER } } /** * Disables all privileges. */ - (IBAction)uncheckAllPrivileges:(id)sender { id selectedUser = [[treeController selectedObjects] objectAtIndex:0]; // Iterate through the supported privs, setting the value of each to NO for (NSString *key in [self privsSupportedByServer]) { if (![key hasSuffix:@"_priv"]) continue; // Perform the change in a try/catch check to avoid exceptions for unhandled privs NS_DURING [selectedUser setValue:@NO forKey:key]; NS_HANDLER NS_ENDHANDLER } } /** * Adds a new user to the current database. */ - (IBAction)addUser:(id)sender { // Adds a new SPUser objects to the managedObjectContext and sets default values if ([[treeController selectedObjects] count] > 0) { if ([[[treeController selectedObjects] objectAtIndex:0] parent] != nil) { [self _selectParentFromSelection]; } } NSManagedObject *newItem = [self _createNewSPUser]; NSManagedObject *newChild = [self _createNewSPUser]; [newChild setValue:@"localhost" forKey:@"host"]; [newItem addChildrenObject:newChild]; [treeController addObject:newItem]; [outlineView expandItem:[outlineView itemAtRow:[outlineView selectedRow]]]; [[self window] makeFirstResponder:userNameTextField]; } /** * Removes the currently selected user from the current database. */ - (IBAction)removeUser:(id)sender { NSString *username = [[[treeController selectedObjects] objectAtIndex:0] valueForKey:@"originaluser"]; NSArray *children = [[[treeController selectedObjects] objectAtIndex:0] valueForKey:@"children"]; // On all the children - host entries - set the username to be deleted, // for later query contruction. for (NSManagedObject *child in children) { [child setPrimitiveValue:username forKey:@"user"]; } // Unset the host on the user, so that only the host entries are dropped [[[treeController selectedObjects] objectAtIndex:0] setPrimitiveValue:nil forKey:@"host"]; [treeController remove:sender]; } /** * Adds a new host to the currently selected user. */ - (IBAction)addHost:(id)sender { if ([[treeController selectedObjects] count] > 0) { if ([[[treeController selectedObjects] objectAtIndex:0] parent] != nil) { [self _selectParentFromSelection]; } } [treeController addChild:sender]; // The newly added item will be selected as it is added, but only after the next iteration of the // run loop - edit it after a tiny delay. [self performSelector:@selector(editNewHost) withObject:nil afterDelay:0.1]; } /** * Perform a deferred edit of the currently selected row. */ - (void)editNewHost { [outlineView editColumn:0 row:[outlineView selectedRow] withEvent:nil select:YES]; } /** * Removes the currently selected host from it's parent user. */ - (IBAction)removeHost:(id)sender { // Set the username on the child so that it's accessabile when building // the drop sql command NSManagedObject *child = [[treeController selectedObjects] objectAtIndex:0]; NSManagedObject *parent = [child valueForKey:@"parent"]; [child setPrimitiveValue:[[child valueForKey:@"parent"] valueForKey:@"user"] forKey:@"user"]; [treeController remove:sender]; if ([[parent valueForKey:@"children"] count] == 0) { SPBeginAlertSheet(NSLocalizedString(@"Unable to remove host", @"error removing host message"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [self window], self, nil, nil, NSLocalizedString(@"This user doesn't seem to have any associated hosts and will be removed unless a host is added.", @"error removing host informative message")); } } /** * Adds a new schema privilege. */ - (IBAction)addSchemaPriv:(id)sender { NSArray *selectedObjects = [availableController selectedObjects]; [grantedController addObjects:selectedObjects]; [grantedTableView noteNumberOfRowsChanged]; [availableController removeObjects:selectedObjects]; [availableTableView noteNumberOfRowsChanged]; [schemasTableView setNeedsDisplay:YES]; [self _setSchemaPrivValues:selectedObjects enabled:YES]; } /** * Removes a schema privilege. */ - (IBAction)removeSchemaPriv:(id)sender { NSArray *selectedObjects = [grantedController selectedObjects]; [availableController addObjects:selectedObjects]; [availableTableView noteNumberOfRowsChanged]; [grantedController removeObjects:selectedObjects]; [grantedTableView noteNumberOfRowsChanged]; [schemasTableView setNeedsDisplay:YES]; [self _setSchemaPrivValues:selectedObjects enabled:NO]; } /** * Move double-clicked rows across to the other table, using the * appropriate methods. */ - (IBAction)doubleClickSchemaPriv:(id)sender { // Ignore double-clicked header cells if ([sender clickedRow] == -1) return; if (sender == availableTableView) { [self addSchemaPriv:sender]; } else { [self removeSchemaPriv:sender]; } } /** * Refreshes the current list of users. */ - (IBAction)refresh:(id)sender { if ([[self managedObjectContext] hasChanges]) { NSAlert *alert = [NSAlert alertWithMessageText:NSLocalizedString(@"Unsaved changes", @"unsaved changes message") defaultButton:NSLocalizedString(@"Continue", @"continue button") alternateButton:NSLocalizedString(@"Cancel", @"cancel button") otherButton:nil informativeTextWithFormat:NSLocalizedString(@"Changes have been made, which will be lost if this window is closed. Are you sure you want to continue", @"unsaved changes informative message")]; [alert setAlertStyle:NSWarningAlertStyle]; // Cancel if ([alert runModal] == NSAlertAlternateReturn) return; } [[self managedObjectContext] reset]; [grantedSchemaPrivs removeAllObjects]; [grantedTableView reloadData]; [self _initializeAvailablePrivs]; [outlineView reloadData]; [treeController rearrangeObjects]; // Get all the stores on the current MOC and remove them. NSArray *stores = [[[self managedObjectContext] persistentStoreCoordinator] persistentStores]; for (NSPersistentStore* store in stores) { [[[self managedObjectContext] persistentStoreCoordinator] removePersistentStore:store error:nil]; } // Add a new store [[[self managedObjectContext] persistentStoreCoordinator] addPersistentStoreWithType:NSInMemoryStoreType configuration:nil URL:nil options:nil error:nil]; // Reinitialize the tree with values from the database. [self _initializeUsers]; // After the reset, ensure all original password and user values are up-to-date. NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"SPUser" inManagedObjectContext:[self managedObjectContext]]; NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease]; [request setEntity:entityDescription]; NSArray *userArray = [[self managedObjectContext] executeFetchRequest:request error:nil]; for (NSManagedObject *user in userArray) { if (![user parent]) { [user setPrimitiveValue:[user valueForKey:@"user"] forKey:@"originaluser"]; [user setPrimitiveValue:[user valueForKey:@"password"] forKey:@"originalpassword"]; } } } - (void)_setSchemaPrivValues:(NSArray *)objects enabled:(BOOL)enabled { // The passed in objects should be an array of NSDictionaries with a key // of "name". NSManagedObject *selectedHost = [[treeController selectedObjects] objectAtIndex:0]; NSString *selectedDb = [schemas objectAtIndex:[schemasTableView selectedRow]]; NSArray *selectedPrivs = [self _fetchPrivsWithUser:[selectedHost valueForKeyPath:@"parent.user"] schema:[selectedDb stringByReplacingOccurrencesOfString:@"_" withString:@"\\_"] host:[selectedHost valueForKey:@"host"]]; BOOL isNew = NO; NSManagedObject *priv = nil; if ([selectedPrivs count] > 0){ priv = [selectedPrivs objectAtIndex:0]; } else { priv = [NSEntityDescription insertNewObjectForEntityForName:@"Privileges" inManagedObjectContext:[self managedObjectContext]]; [priv setValue:selectedDb forKey:@"db"]; isNew = YES; } // Now setup all the items that are selected to their enabled value for (NSDictionary *obj in objects) { [priv setValue:[NSNumber numberWithBool:enabled] forKey:[obj valueForKey:@"name"]]; } if (isNew) { // Set up relationship NSMutableSet *privs = [selectedHost mutableSetValueForKey:@"schema_privileges"]; [privs addObject:priv]; } } - (void)_clearData { [managedObjectContext reset]; [managedObjectContext release], managedObjectContext = nil; } /** * Menu item validation. */ - (BOOL)validateMenuItem:(NSMenuItem *)menuItem { // Only allow removing hosts of a host node is selected. if ([menuItem action] == @selector(removeHost:)) { return (([[treeController selectedObjects] count] > 0) && [[[treeController selectedObjects] objectAtIndex:0] parent] != nil); } else if ([menuItem action] == @selector(addHost:)) { return ([[treeController selectedObjects] count] > 0); } return YES; } - (void)_selectParentFromSelection { if ([[treeController selectedObjects] count] > 0) { NSTreeNode *firstSelectedNode = [[treeController selectedNodes] objectAtIndex:0]; NSTreeNode *parentNode = [firstSelectedNode parentNode]; if (parentNode) { NSIndexPath *parentIndex = [parentNode indexPath]; [treeController setSelectionIndexPath:parentIndex]; } else { NSArray *selectedIndexPaths = [treeController selectionIndexPaths]; [treeController removeSelectionIndexPaths:selectedIndexPaths]; } } } - (void)_selectFirstChildOfParentNode { if ([[treeController selectedObjects] count] > 0) { [outlineView expandItem:[outlineView itemAtRow:[outlineView selectedRow]]]; id selectedObject = [[treeController selectedObjects] objectAtIndex:0]; NSTreeNode *firstSelectedNode = [[treeController selectedNodes] objectAtIndex:0]; id parent = [selectedObject parent]; // If this is already a parent, then parentNode should be null. // If a child is already selected, then we want to not change the selection if (!parent) { NSIndexPath *childIndex = [[[firstSelectedNode childNodes] objectAtIndex:0] indexPath]; [treeController setSelectionIndexPath:childIndex]; } } } /** * Closes the supplied sheet, before closing the master window. */ - (IBAction)closeErrorsSheet:(id)sender { [NSApp endSheet:[sender window] returnCode:[sender tag]]; [[sender window] orderOut:self]; } #pragma mark - #pragma mark Notifications /** * This notification is called when the managedObjectContext save happens. * This takes the inserted, updated, and deleted arrays and applies them to * the database. */ - (void)contextDidSave:(NSNotification *)notification { NSManagedObjectContext *notificationContext = (NSManagedObjectContext *)[notification object]; // If there are multiple user manager windows open, it's possible to get this // notification from foreign windows. Ignore those notifications. if (notificationContext != [self managedObjectContext]) return; if (!isInitializing) { NSArray *updated = [[notification userInfo] valueForKey:NSUpdatedObjectsKey]; NSArray *inserted = [[notification userInfo] valueForKey:NSInsertedObjectsKey]; NSArray *deleted = [[notification userInfo] valueForKey:NSDeletedObjectsKey]; if ([inserted count] > 0) { [self insertUsers:inserted]; } if ([updated count] > 0) { [self updateUsers:updated]; } if ([deleted count] > 0) { [self deleteUsers:deleted]; } } } - (void)contextDidChange:(NSNotification *)notification { if (!isInitializing) [outlineView reloadData]; } /** * Updates the supplied array of users. */ - (BOOL)updateUsers:(NSArray *)updatedUsers { for (NSManagedObject *user in updatedUsers) { if ([[[user entity] name] isEqualToString:@"Privileges"]) { [self grantDbPrivilegesWithPrivilege:user]; } // If the parent user has changed, either the username or password have been edited. else if (![user parent]) { NSArray *hosts = [user valueForKey:@"children"]; // If the user has been changed, update the username on all hosts. // Don't check for errors, as some hosts may be new. if (![[user valueForKey:@"user"] isEqualToString:[user valueForKey:@"originaluser"]]) { for (NSManagedObject *child in hosts) { [self _renameUserFrom:[user valueForKey:@"originaluser"] host:[child valueForKey:@"originalhost"] ? [child valueForKey:@"originalhost"] : [child host] to:[user valueForKey:@"user"] host:[child host]]; } } // If the password has been changed, use the same password on all hosts if (![[user valueForKey:@"password"] isEqualToString:[user valueForKey:@"originalpassword"]]) { for (NSManagedObject *child in hosts) { NSString *changePasswordStatement = [NSString stringWithFormat: @"SET PASSWORD FOR %@@%@ = PASSWORD(%@)", [[user valueForKey:@"user"] tickQuotedString], [[child host] tickQuotedString], ([user valueForKey:@"password"]) ? [[user valueForKey:@"password"] tickQuotedString] : @"''"]; [[self connection] queryString:changePasswordStatement]; [self _checkAndDisplayMySqlError]; } } } else { // If the hostname has changed, remane the detail before editing details if (![[user valueForKey:@"host"] isEqualToString:[user valueForKey:@"originalhost"]]) { [self _renameUserFrom:[[user parent] valueForKey:@"originaluser"] host:[user valueForKey:@"originalhost"] to:[[user parent] valueForKey:@"user"] host:[user valueForKey:@"host"]]; } if ([serverSupport supportsUserMaxVars]) [self updateResourcesForUser:user]; [self grantPrivilegesToUser:user]; } } return YES; } - (BOOL)deleteUsers:(NSArray *)deletedUsers { NSMutableString *droppedUsers = [NSMutableString string]; for (NSManagedObject *user in deletedUsers) { if (![[[user entity] name] isEqualToString:@"Privileges"] && ([user valueForKey:@"host"] != nil)) { [droppedUsers appendFormat:@"%@@%@, ", [[user valueForKey:@"user"] tickQuotedString], [[user valueForKey:@"host"] tickQuotedString]]; } } if ([droppedUsers length] > 2) { droppedUsers = [[droppedUsers substringToIndex:([droppedUsers length] - 2)] mutableCopy]; // Before MySQL 5.0.2 DROP USER just removed users with no privileges, so revoke // all their privileges first. Also, REVOKE ALL PRIVILEGES was added in MySQL 4.1.2, so use the // old multiple query approach (damn, I wish there were only one MySQL version!). if (![serverSupport supportsFullDropUser]) { [connection queryString:[NSString stringWithFormat:@"REVOKE ALL PRIVILEGES ON *.* FROM %@", droppedUsers]]; [connection queryString:[NSString stringWithFormat:@"REVOKE GRANT OPTION ON *.* FROM %@", droppedUsers]]; } // DROP USER was added in MySQL 4.1.1 if ([serverSupport supportsDropUser]) { [[self connection] queryString:[NSString stringWithFormat:@"DROP USER %@", droppedUsers]]; } // Otherwise manually remove the user rows from the mysql.user table else { NSArray *users = [droppedUsers componentsSeparatedByString:@", "]; for (NSString *user in users) { NSArray *userDetails = [user componentsSeparatedByString:@"@"]; [connection queryString:[NSString stringWithFormat:@"DELETE FROM mysql.user WHERE User = %@ and Host = %@", [userDetails objectAtIndex:0], [userDetails objectAtIndex:1]]]; } } [droppedUsers release]; } return YES; } /** * Inserts (creates) the supplied users in the database. */ - (BOOL)insertUsers:(NSArray *)insertedUsers { for (NSManagedObject *user in insertedUsers) { if ([[[user entity] name] isEqualToString:@"Privileges"]) continue; NSString *createStatement = nil; // Note that if the database does not support the use of the CREATE USER statment, then // we must resort to using GRANT. Doing so means we must specify the privileges and the database // for which these apply, so make them as restrictive as possible, but then revoke them to get the // same affect as CREATE USER. That is, a new user with no privleges. NSString *host = [[user valueForKey:@"host"] tickQuotedString]; if ([user parent] && [[user parent] valueForKey:@"user"] && [[user parent] valueForKey:@"password"]) { NSString *username = [[[user parent] valueForKey:@"user"] tickQuotedString]; NSString *password = [[[user parent] valueForKey:@"password"] tickQuotedString]; createStatement = ([serverSupport supportsCreateUser]) ? [NSString stringWithFormat:@"CREATE USER %@@%@ IDENTIFIED BY %@%@", username, host, [[user parent] valueForKey:@"originaluser"]?@"PASSWORD ":@"", password] : [NSString stringWithFormat:@"GRANT SELECT ON mysql.* TO %@@%@ IDENTIFIED BY %@%@", username, host, [[user parent] valueForKey:@"originaluser"]?@"PASSWORD ":@"", password]; } else if ([user parent] && [[user parent] valueForKey:@"user"]) { NSString *username = [[[user parent] valueForKey:@"user"] tickQuotedString]; createStatement = ([serverSupport supportsCreateUser]) ? [NSString stringWithFormat:@"CREATE USER %@@%@", username, host] : [NSString stringWithFormat:@"GRANT SELECT ON mysql.* TO %@@%@", username, host]; } if (createStatement) { // Create user in database [connection queryString:createStatement]; if ([self _checkAndDisplayMySqlError]) { if ([serverSupport supportsUserMaxVars]) [self updateResourcesForUser:user]; // If we created the user with the GRANT statment (MySQL < 5), then revoke the // privileges we gave the new user. if (![serverSupport supportsUserMaxVars]) { [connection queryString:[NSString stringWithFormat:@"REVOKE SELECT ON mysql.* FROM %@@%@", [[[user parent] valueForKey:@"user"] tickQuotedString], host]]; } [self grantPrivilegesToUser:user]; } } } return YES; } /** * Grant or revoke DB privileges for the supplied user. */ - (BOOL)grantDbPrivilegesWithPrivilege:(NSManagedObject *)schemaPriv { NSMutableArray *grantPrivileges = [NSMutableArray array]; NSMutableArray *revokePrivileges = [NSMutableArray array]; NSString *dbName = [schemaPriv valueForKey:@"db"]; dbName = [dbName stringByReplacingOccurrencesOfString:@"_" withString:@"\\_"]; NSString *statement = [NSString stringWithFormat:@"SELECT USER, HOST FROM mysql.db WHERE USER = %@ AND HOST = %@ AND DB = %@", [[schemaPriv valueForKeyPath:@"user.parent.user"] tickQuotedString], [[schemaPriv valueForKeyPath:@"user.host"] tickQuotedString], [dbName tickQuotedString]]; NSArray *matchingUsers = [[self connection] getAllRowsFromQuery:statement]; for (NSString *key in [self privsSupportedByServer]) { if (![key hasSuffix:@"_priv"]) continue; NSString *privilege = [key stringByReplacingOccurrencesOfString:@"_priv" withString:@""]; NS_DURING if ([[schemaPriv valueForKey:key] boolValue] == YES) { [grantPrivileges addObject:[privilege replaceUnderscoreWithSpace]]; } else { if ([matchingUsers count] || [grantPrivileges count] > 0) { [revokePrivileges addObject:[privilege replaceUnderscoreWithSpace]]; } } NS_HANDLER NS_ENDHANDLER } // Grant privileges [self _grantPrivileges:grantPrivileges onDatabase:dbName forUser:[schemaPriv valueForKeyPath:@"user.parent.user"] host:[schemaPriv valueForKeyPath:@"user.host"]]; // Revoke privileges [self _revokePrivileges:revokePrivileges onDatabase:dbName forUser:[schemaPriv valueForKeyPath:@"user.parent.user"] host:[schemaPriv valueForKeyPath:@"user.host"]]; return YES; } /** * Update resource limites for given user */ - (BOOL)updateResourcesForUser:(NSManagedObject *)user { if ([user valueForKey:@"parent"] != nil) { NSString *updateResourcesStatement = [NSString stringWithFormat: @"UPDATE mysql.user SET max_questions = %@, max_updates = %@, max_connections = %@ WHERE User = %@ AND Host = %@", [user valueForKey:@"max_questions"], [user valueForKey:@"max_updates"], [user valueForKey:@"max_connections"], [[[user valueForKey:@"parent"] valueForKey:@"user"] tickQuotedString], [[user valueForKey:@"host"] tickQuotedString]]; [[self connection] queryString:updateResourcesStatement]; [self _checkAndDisplayMySqlError]; } return YES; } /** * Grant or revoke privileges for the supplied user. */ - (BOOL)grantPrivilegesToUser:(NSManagedObject *)user { if ([user valueForKey:@"parent"] != nil) { NSMutableArray *grantPrivileges = [NSMutableArray array]; NSMutableArray *revokePrivileges = [NSMutableArray array]; for (NSString *key in [self privsSupportedByServer]) { if (![key hasSuffix:@"_priv"]) continue; NSString *privilege = [key stringByReplacingOccurrencesOfString:@"_priv" withString:@""]; // Check the value of the priv and assign to grant or revoke query as appropriate; do this // in a try/catch check to avoid exceptions for unhandled privs NS_DURING if ([[user valueForKey:key] boolValue] == YES) { [grantPrivileges addObject:[privilege replaceUnderscoreWithSpace]]; } else { [revokePrivileges addObject:[privilege replaceUnderscoreWithSpace]]; } NS_HANDLER NS_ENDHANDLER } // Grant privileges [self _grantPrivileges:grantPrivileges onDatabase:nil forUser:[[user parent] valueForKey:@"user"] host:[user valueForKey:@"host"]]; // Revoke privileges [self _revokePrivileges:revokePrivileges onDatabase:nil forUser:[[user parent] valueForKey:@"user"] host:[user valueForKey:@"host"]]; } for (NSManagedObject *priv in [user valueForKey:@"schema_privileges"]) { [self grantDbPrivilegesWithPrivilege:priv]; } return YES; } /** * Gets any NSManagedObject (SPUser) from the managedObjectContext that may * already exist with the given username. */ - (NSArray *)_fetchUserWithUserName:(NSString *)username { NSManagedObjectContext *moc = [self managedObjectContext]; NSPredicate *predicate = [NSPredicate predicateWithFormat:@"user == %@ AND parent == nil", username]; NSEntityDescription *entityDescription = [NSEntityDescription entityForName:@"SPUser" inManagedObjectContext:moc]; NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease]; [request setEntity:entityDescription]; [request setPredicate:predicate]; NSError *error = nil; NSArray *array = [moc executeFetchRequest:request error:&error]; if (error != nil) { [[NSApplication sharedApplication] presentError:error]; } return array; } - (NSArray *)_fetchPrivsWithUser:(NSString *)username schema:(NSString *)selectedSchema host:(NSString *)host { NSManagedObjectContext *moc = [self managedObjectContext]; NSPredicate *predicate; NSEntityDescription *privEntity = [NSEntityDescription entityForName:@"Privileges" inManagedObjectContext:moc]; NSFetchRequest *request = [[[NSFetchRequest alloc] init] autorelease]; // Construct the predicate depending on whether a user and schema were supplied; // blank schemas indicate a default priv value (as per %) if ([username length]) { if ([selectedSchema length]) { predicate = [NSPredicate predicateWithFormat:@"(user.parent.user like[cd] %@) AND (user.host like[cd] %@) AND (db like[cd] %@)", username, host, selectedSchema]; } else { predicate = [NSPredicate predicateWithFormat:@"(user.parent.user like[cd] %@) AND (user.host like[cd] %@) AND (db == '')", username, host]; } } else { if ([selectedSchema length]) { predicate = [NSPredicate predicateWithFormat:@"(user.parent.user == '') AND (user.host like[cd] %@) AND (db like[cd] %@)", host, selectedSchema]; } else { predicate = [NSPredicate predicateWithFormat:@"(user.parent.user == '') AND (user.host like[cd] %@) AND (db == '')", host]; } } [request setEntity:privEntity]; [request setPredicate:predicate]; NSError *error = nil; NSArray *array = [moc executeFetchRequest:request error:&error]; if (error != nil) { [[NSApplication sharedApplication] presentError:error]; } return array; } /** * Creates a new NSManagedObject and inserts it into the managedObjectContext. */ - (NSManagedObject *)_createNewSPUser { return [NSEntityDescription insertNewObjectForEntityForName:@"SPUser" inManagedObjectContext:[self managedObjectContext]]; } /** * Grant the supplied privileges to the specified user and host */ - (void)_grantPrivileges:(NSArray *)thePrivileges onDatabase:(NSString *)aDatabase forUser:(NSString *)aUser host:(NSString *)aHost { if (![thePrivileges count]) return; NSString *grantStatement; // Special case when all items are checked, to allow GRANT OPTION to work if ([[self privsSupportedByServer] count] == [thePrivileges count]) { grantStatement = [NSString stringWithFormat:@"GRANT ALL ON %@.* TO %@@%@ WITH GRANT OPTION", aDatabase?[aDatabase backtickQuotedString]:@"*", [aUser tickQuotedString], [aHost tickQuotedString]]; } else { grantStatement = [NSString stringWithFormat:@"GRANT %@ ON %@.* TO %@@%@", [[thePrivileges componentsJoinedByCommas] uppercaseString], aDatabase?[aDatabase backtickQuotedString]:@"*", [aUser tickQuotedString], [aHost tickQuotedString]]; } [[self connection] queryString:grantStatement]; [self _checkAndDisplayMySqlError]; } /** * Revoke the supplied privileges from the specified user and host */ - (void)_revokePrivileges:(NSArray *)thePrivileges onDatabase:(NSString *)aDatabase forUser:(NSString *)aUser host:(NSString *)aHost { if (![thePrivileges count]) return; NSString *revokeStatement; // Special case when all items are checked, to allow GRANT OPTION to work if ([[self privsSupportedByServer] count] == [thePrivileges count]) { revokeStatement = [NSString stringWithFormat:@"REVOKE ALL PRIVILEGES ON %@.* FROM %@@%@", aDatabase?[aDatabase backtickQuotedString]:@"*", [aUser tickQuotedString], [aHost tickQuotedString]]; [[self connection] queryString:revokeStatement]; [self _checkAndDisplayMySqlError]; revokeStatement = [NSString stringWithFormat:@"REVOKE GRANT OPTION ON %@.* FROM %@@%@", aDatabase?[aDatabase backtickQuotedString]:@"*", [aUser tickQuotedString], [aHost tickQuotedString]]; } else { revokeStatement = [NSString stringWithFormat:@"REVOKE %@ ON %@.* FROM %@@%@", [[thePrivileges componentsJoinedByCommas] uppercaseString], aDatabase?[aDatabase backtickQuotedString]:@"*", [aUser tickQuotedString], [aHost tickQuotedString]]; } [[self connection] queryString:revokeStatement]; [self _checkAndDisplayMySqlError]; } /** * Displays an alert panel if there was an error condition on the MySQL connection. */ - (BOOL)_checkAndDisplayMySqlError { if ([[self connection] queryErrored]) { if (isSaving) { [errorsString appendFormat:@"%@\n", [[self connection] lastErrorMessage]]; } else { SPBeginAlertSheet(NSLocalizedString(@"An error occurred", @"mysql error occurred message"), NSLocalizedString(@"OK", @"OK button"), nil, nil, [self window], self, nil, nil, [NSString stringWithFormat:NSLocalizedString(@"An error occurred whilst trying to perform the operation.\n\nMySQL said: %@", @"mysql error occurred informative message"), [[self connection] lastErrorMessage]]); } return NO; } return YES; } #pragma mark - #pragma mark Private API /** * Renames a user account using the supplied parameters. * * @param originalUser The user's original user name * @param originalHost The user's original host * @param newUser The user's new user name * @param newHost The user's new host */ - (void)_renameUserFrom:(NSString *)originalUser host:(NSString *)originalHost to:(NSString *)newUser host:(NSString *)newHost { NSString *renameQuery = nil; if ([serverSupport supportsRenameUser]) { renameQuery = [NSString stringWithFormat:@"RENAME USER %@@%@ TO %@@%@", [originalUser tickQuotedString], [originalHost tickQuotedString], [newUser tickQuotedString], [newHost tickQuotedString]]; } else { // mysql.user is keyed on user and host so there should only ever be one result, // but double check before we do the update. QKQuery *query = [QKQuery selectQueryFromTable:@"user"]; [query setDatabase:SPMySQLDatabase]; [query addField:@"COUNT(1)"]; [query addParameter:@"User" operator:QKEqualityOperator value:originalUser]; [query addParameter:@"Host" operator:QKEqualityOperator value:originalHost]; SPMySQLResult *result = [connection queryString:[query query]]; if ([[[result getRowAsArray] objectAtIndex:0] integerValue] == 1) { QKQuery *updateQuery = [QKQuery queryTable:@"user"]; [updateQuery setQueryType:QKUpdateQuery]; [updateQuery setDatabase:SPMySQLDatabase]; [updateQuery addFieldToUpdate:@"User" toValue:newUser]; [updateQuery addFieldToUpdate:@"Host" toValue:newHost]; [updateQuery addParameter:@"User" operator:QKEqualityOperator value:originalUser]; [updateQuery addParameter:@"Host" operator:QKEqualityOperator value:originalHost]; renameQuery = [updateQuery query]; } } if (renameQuery) { [connection queryString:renameQuery]; } } #pragma mark - - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [managedObjectContext release]; [persistentStoreCoordinator release]; [managedObjectModel release]; [privColumnToGrantMap release]; [connection release]; [privsSupportedByServer release]; [schemas release]; [availablePrivs release]; [grantedSchemaPrivs release]; [treeSortDescriptor release]; [serverSupport release]; [super dealloc]; } @end