AuthenticationController.m [plain text]
/*
* Copyright 2008 Massachusetts Institute of Technology.
* All Rights Reserved.
*
* Export of this software from the United States of America may
* require a specific license from the United States Government.
* It is the responsibility of any person or organization contemplating
* export to obtain such a license before exporting.
*
* WITHIN THAT CONSTRAINT, permission to use, copy, modify, and
* distribute this software and its documentation for any purpose and
* without fee is hereby granted, provided that the above copyright
* notice appear in all copies and that both that copyright notice and
* this permission notice appear in supporting documentation, and that
* the name of M.I.T. not be used in advertising or publicity pertaining
* to distribution of the software without specific, written prior
* permission. Furthermore if you modify this software you must label
* your software as modified software and not distribute it in such a
* fashion that it might be confused with the original M.I.T. software.
* M.I.T. makes no representations about the suitability of
* this software for any purpose. It is provided "as is" without express
* or implied warranty.
*/
#import "AuthenticationController.h"
#import "IPCClient.h"
#import "KerberosFormatters.h"
#import "BadgedImageView.h"
// to get kim_prompt_type enum
#import <Kerberos/kim_ui_plugin.h>
/*
* glueController KVC mapping is as follows:
* name = client app name
* path = client app bundle path
* title = suggested label for prompt field
* message = desired large text message
* description = longer, detailed, small text message
* username = 'user' part of 'user@REALM.ORG'
* realm = 'REALM.ORG' part of 'user@REALM.ORG'
* realm_history = past realms the user has entered
* prompt_response = auth prompt response
* allow_save_password = whether or not to show the 'save password in keychain' checkbox
* should_save_password = whether or not to save the password in the keychain
* old_password = for change password dialog
* new_password = "
* verify_password = "
*/
// localization keys and tables
#define ACLocalizationTable @"AuthenticationController"
#define ACAppPrincReqKey @"AuthControllerApplicationPrincipalRequest"
#define ACPrincReqKey @"AuthControllerPrincipalRequest"
#define ACAppPasswordReqKey @"AuthControllerApplicationPasswordRequest"
#define ACPasswordReqKey @"AuthControllerPasswordRequest"
#define ACPasswordChangeExpired @"ChangePasswordPasswordExpired"
#define ACPasswordChangeApp @"ChangePasswordApplicationRequest"
#define ACPasswordChangePrinc @"ChangePasswordPrincipalRequest"
@implementation AuthenticationController
@synthesize associatedClient;
@synthesize favoriteIdentities;
@synthesize favoriteOptions;
- (id) init
{
return [self initWithWindowNibName: @"Authentication"];
}
- (void) awakeFromNib
{
[[self window] center];
// We need to float over the loginwindow and SecurityAgent so use its hardcoded level.
[[self window] setLevel:NSModalPanelWindowLevel];
visibleAsSheet = NO;
lifetimeFormatter.displaySeconds = NO;
lifetimeFormatter.displayShortFormat = NO;
[glueController addObserver:self
forKeyPath:identity_string_keypath
options:NSKeyValueObservingOptionNew
context:ACKVOContext];
[glueController addObserver:self
forKeyPath:prompt_response_keypath
options:NSKeyValueObservingOptionNew
context:ACKVOContext];
[glueController addObserver:self
forKeyPath:old_password_keypath
options:NSKeyValueObservingOptionNew
context:ACKVOContext];
[glueController addObserver:self
forKeyPath:new_password_keypath
options:NSKeyValueObservingOptionNew
context:ACKVOContext];
[glueController addObserver:self
forKeyPath:verify_password_keypath
options:NSKeyValueObservingOptionNew
context:ACKVOContext];
}
- (void) dealloc
{
[glueController removeObserver:self forKeyPath:identity_string_keypath];
[glueController removeObserver:self forKeyPath:prompt_response_keypath];
[super dealloc];
}
- (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
BOOL valid = NO;
if ([(NSString *) context isEqualToString:ACKVOContext]) {
if ([keyPath isEqualToString:identity_string_keypath]) {
valid = [KIMUtilities validateIdentity:[glueController valueForKeyPath:identity_string_keypath]];
[glueController setValue:[NSNumber numberWithBool:valid]
forKeyPath:enable_identity_ok_keypath];
}
else if ([keyPath isEqualToString:prompt_response_keypath]) {
valid = ([[glueController valueForKeyPath:prompt_response_keypath] length] > 0);
[glueController setValue:[NSNumber numberWithBool:valid]
forKeyPath:enable_prompt_ok_keypath];
}
else if ([keyPath isEqualToString:old_password_keypath] ||
[keyPath isEqualToString:new_password_keypath] ||
[keyPath isEqualToString:verify_password_keypath]) {
NSString *oldString = [glueController valueForKeyPath:old_password_keypath];
NSString *newString = [glueController valueForKeyPath:new_password_keypath];
NSString *verifyString = [glueController valueForKeyPath:verify_password_keypath];
valid = ([oldString length] > 0 && [newString length] > 0 &&
[verifyString length] > 0 && [newString isEqualToString:verifyString]);
[glueController setValue:[NSNumber numberWithBool:valid]
forKeyPath:change_password_ok_keypath];
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
else {
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
}
- (IBAction) showWindow: (id) sender
{
[super showWindow:sender];
}
- (void) showWithParent: (NSWindow *) parentWindow
{
// attach as sheet if given a parentWindow
if (parentWindow && !visibleAsSheet) {
[NSApp beginSheet:[self window]
modalForWindow:parentWindow
modalDelegate:self
didEndSelector:@selector(authSheetDidEnd:returnCode:contextInfo:)
contextInfo:NULL];
}
// else, display as normal
else {
[self showWindow:nil];
}
}
- (void) windowWillBeginSheet: (NSNotification *) notification
{
visibleAsSheet = YES;
}
- (void) windowDidEndSheet: (NSNotification *) notification
{
visibleAsSheet = NO;
}
- (void) setContent: (NSMutableDictionary *) newContent
{
[self window]; // wake up the nib connections
[glueController setContent:newContent];
}
- (void) swapView: (NSView *) aView
{
NSWindow *theWindow = [self window];
NSRect windowFrame;
NSRect viewFrame;
[[containerView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];
windowFrame = [theWindow frame];
viewFrame = [theWindow frameRectForContentRect:[aView frame]];
windowFrame.origin.y -= viewFrame.size.height - windowFrame.size.height;
windowFrame.size.width = viewFrame.size.width;
windowFrame.size.height = viewFrame.size.height;
[theWindow setFrame:windowFrame display:YES animate:YES];
[containerView addSubview:aView];
}
- (void) showSpinny
{
[enterSpinny startAnimation: nil];
[passwordSpinny startAnimation: nil];
[samSpinny startAnimation: nil];
[changePasswordSpinny startAnimation: nil];
[glueController setValue:[NSNumber numberWithBool:NO]
forKeyPath:accepting_input_keypath];
}
- (void) hideSpinny
{
[enterSpinny stopAnimation: nil];
[passwordSpinny stopAnimation: nil];
[samSpinny stopAnimation: nil];
[changePasswordSpinny stopAnimation: nil];
[glueController setValue:[NSNumber numberWithBool:YES]
forKeyPath:accepting_input_keypath];
}
- (void) clearSensitiveInputs
{
[glueController setValue:@""
forKeyPath:prompt_response_keypath];
}
- (void) clearAllInputs
{
[glueController setValue:@""
forKeyPath:old_password_keypath];
[glueController setValue:@""
forKeyPath:new_password_keypath];
[glueController setValue:@""
forKeyPath:verify_password_keypath];
[self clearSensitiveInputs];
}
- (void) showEnterIdentity: (NSWindow *) parentWindow
{
kim_error err = KIM_NO_ERROR;
NSWindow *theWindow = [self window];
NSString *key = (associatedClient.name) ? ACAppPrincReqKey : ACPrincReqKey;
NSString *message = [NSString stringWithFormat:
NSLocalizedStringFromTable(key, ACLocalizationTable, NULL),
associatedClient.name];
self.favoriteIdentities = [NSMutableArray array];
self.favoriteOptions = [NSMutableDictionary dictionary];
// get array of favorite identity strings and associated options
if (!err) {
kim_preferences preferences = NULL;
kim_options kimOptions = NULL;
kim_count i;
kim_count count = 0;
err = kim_preferences_create(&preferences);
if (!err) {
err = kim_preferences_get_number_of_favorite_identities(preferences,
&count);
}
for (i = 0; !err && i < count; i++) {
kim_identity kimIdentity = NULL;
kim_string display_string = NULL;
NSString *identityString = nil;
err = kim_preferences_get_favorite_identity_at_index(preferences,
i,
&kimIdentity,
&kimOptions);
if (!err) {
err = kim_identity_get_display_string(kimIdentity, &display_string);
}
if (!err && display_string) {
identityString = [NSString stringWithUTF8String:display_string];
[favoriteIdentities addObject:identityString];
}
if (!err) {
[favoriteOptions setObject:[KIMUtilities dictionaryForKimOptions:kimOptions]
forKey:identityString];
}
kim_options_free(&kimOptions);
kim_string_free (&display_string);
kim_identity_free (&kimIdentity);
}
kim_preferences_free(&preferences);
}
[glueController setValue:favoriteIdentities forKeyPath:favorite_strings_keypath];
// wake up the nib connections and adjust window size
[self window];
// set up controls with info from associatedClient
[enterBadge setBadgePath:associatedClient.path];
[glueController setValue:message
forKeyPath:message_keypath];
[self hideSpinny];
[self clearAllInputs];
[self swapView:identityView];
[theWindow makeFirstResponder:identityField];
[self showWithParent: parentWindow];
}
- (void) showAuthPrompt: (NSWindow *) parentWindow
{
uint32_t type = [[glueController valueForKeyPath:@"content.prompt_type"] unsignedIntegerValue];
[self hideSpinny];
[self clearSensitiveInputs];
switch (type) {
case kim_prompt_type_password :
[self showEnterPassword: parentWindow]; break;
case kim_prompt_type_preauth :
default :
[self showSAM: parentWindow]; break;
}
}
- (void) showEnterPassword: (NSWindow *) parentWindow
{
CGFloat shrinkBy;
NSRect frame;
NSString *key = nil;
NSString *message = nil;
NSWindow *theWindow = [self window];
if ([associatedClient.name isEqualToString:[[NSBundle mainBundle] bundlePath]]) {
key = ACPasswordReqKey;
message = [NSString stringWithFormat:
NSLocalizedStringFromTable(key, ACLocalizationTable, NULL),
[glueController valueForKeyPath:identity_string_keypath]];
} else {
key = ACAppPasswordReqKey;
message = [NSString stringWithFormat:
NSLocalizedStringFromTable(key, ACLocalizationTable, NULL),
associatedClient.name,
[glueController valueForKeyPath:identity_string_keypath]];
}
[glueController setValue:message
forKeyPath:message_keypath];
// set badge
[passwordBadge setBadgePath:associatedClient.path];
// adjust for checkbox visibility
if (![[glueController valueForKeyPath:allow_save_password_keypath] boolValue] &&
![rememberPasswordInKeychainCheckBox isHidden]) {
[rememberPasswordInKeychainCheckBox setHidden:YES];
frame = [passwordView frame];
shrinkBy = ([passwordField frame].origin.y -
[rememberPasswordInKeychainCheckBox frame].origin.y);
frame.size.height -= shrinkBy;
[passwordView setFrame:frame];
}
[self swapView:passwordView];
[theWindow makeFirstResponder:passwordField];
[self showWithParent:parentWindow];
}
- (void) showSAM: (NSWindow *) parentWindow
{
// set badge
[samBadge setBadgePath:associatedClient.path];
[glueController setValue:[NSNumber numberWithBool:NO]
forKeyPath:allow_save_password_keypath];
[self swapView:samView];
[[self window] makeFirstResponder:samPromptField];
[self showWithParent:parentWindow];
}
- (void) showChangePassword: (NSWindow *) parentWindow
{
NSString *key = ([glueController valueForKeyPath:password_expired_keypath]) ? ACAppPrincReqKey : ACPrincReqKey;
NSString *message = [NSString stringWithFormat:
NSLocalizedStringFromTable(key, ACLocalizationTable, NULL),
associatedClient.name];
NSWindow *theWindow = [self window];
BOOL expired = [[glueController valueForKeyPath:password_expired_keypath] boolValue];
BOOL calledBySelf = [associatedClient.path isEqualToString:[[NSBundle mainBundle] bundlePath]];
if (calledBySelf) {
key = ACPasswordChangePrinc;
message = [NSString stringWithFormat:
NSLocalizedStringFromTable(key, ACLocalizationTable, NULL),
[glueController valueForKeyPath:identity_string_keypath]];
// Please change the Kerberos password for \" } else if (!expired) {
key = ACPasswordChangeApp;
message = [NSString stringWithFormat:
NSLocalizedStringFromTable(key, ACLocalizationTable, NULL),
associatedClient.name,
[glueController valueForKeyPath:identity_string_keypath]];
// } else {
key = ACPasswordChangeExpired;
message = [NSString stringWithFormat:
NSLocalizedStringFromTable(key, ACLocalizationTable, NULL),
[glueController valueForKeyPath:identity_string_keypath]];
// Your password has expired, would you like to change it?
}
[glueController setValue:message forKeyPath:message_keypath];
// set badge
[changePasswordBadge setBadgePath:associatedClient.path];
[self hideSpinny];
if (![[self window] isVisible]) {
[self clearAllInputs];
}
[self swapView:changePasswordView];
[self showWithParent:parentWindow];
[theWindow makeFirstResponder:oldPasswordField];
}
- (void) showError: (NSWindow *) parentWindow
{
// wake up the nib connections and adjust window size
[self window];
// set badge
[errorBadge setBadgePath:associatedClient.path];
[self hideSpinny];
[self swapView:errorView];
[self showWithParent:parentWindow];
}
- (IBAction) checkboxDidChange: (id) sender
{
if ([[ticketOptionsController valueForKeyPath:uses_default_options_keypath] boolValue]) {
// merge defaults onto current options
NSMutableDictionary *currentOptions = [ticketOptionsController content];
NSDictionary *defaultOptions = [KIMUtilities dictionaryForKimOptions:NULL];
[currentOptions addEntriesFromDictionary:defaultOptions];
// update the sliders, since their values aren't bound
[validLifetimeSlider setDoubleValue:[[ticketOptionsController valueForKeyPath:valid_lifetime_keypath] doubleValue]];
[renewableLifetimeSlider setDoubleValue:[[ticketOptionsController valueForKeyPath:renewal_lifetime_keypath] doubleValue]];
}
}
- (IBAction) sliderDidChange: (id) sender
{
NSInteger increment = 0;
NSInteger newValue = 0;
NSString *keyPath = nil;
if ([sender isEqual:validLifetimeSlider]) {
increment = VALID_LIFETIME_INCREMENT;
keyPath = valid_lifetime_keypath;
}
else if ([sender isEqual:renewableLifetimeSlider]) {
increment = RENEWABLE_LIFETIME_INCREMENT;
keyPath = renewal_lifetime_keypath;
}
if (increment > 0) {
newValue = ([sender integerValue] / increment) * increment;
[ticketOptionsController setValue:[NSNumber numberWithInteger:newValue]
forKeyPath:keyPath];
}
}
- (IBAction) showTicketOptions: (id) sender
{
NSDictionary *options = nil;
NSString *expandedString = nil;
// if this is a favorite, try to load its default options
[identityField validateEditing];
expandedString = [KIMUtilities expandedIdentity:[identityField stringValue]];
// edit the favorite options for this favorite identity
if (expandedString) {
options = [favoriteOptions objectForKey:expandedString];
}
// else, it's not a favorite identity. use default options
if (!options) {
options = [KIMUtilities dictionaryForKimOptions:KIM_OPTIONS_DEFAULT];
}
[ticketOptionsController setContent:[[options mutableCopy] autorelease]];
[ticketOptionsController setValue:[NSNumber numberWithInteger:[KIMUtilities minValidLifetime]]
forKeyPath:min_valid_keypath];
[ticketOptionsController setValue:[NSNumber numberWithInteger:[KIMUtilities maxValidLifetime]]
forKeyPath:max_valid_keypath];
[ticketOptionsController setValue:[NSNumber numberWithInteger:[KIMUtilities minRenewableLifetime]]
forKeyPath:min_renewable_keypath];
[ticketOptionsController setValue:[NSNumber numberWithInteger:[KIMUtilities maxRenewableLifetime]]
forKeyPath:max_renewable_keypath];
[validLifetimeSlider setIntegerValue:
[[ticketOptionsController valueForKeyPath:valid_lifetime_keypath] integerValue]];
[renewableLifetimeSlider setIntegerValue:
[[ticketOptionsController valueForKeyPath:renewal_lifetime_keypath] integerValue]];
[self sliderDidChange:validLifetimeSlider];
[self sliderDidChange:renewableLifetimeSlider];
[NSApp beginSheet:ticketOptionsSheet
modalForWindow:[self window]
modalDelegate:self
didEndSelector:@selector(ticketOptionsSheetDidEnd:returnCode:contextInfo:)
contextInfo:NULL];
}
- (IBAction) cancelTicketOptions: (id) sender
{
[NSApp endSheet:ticketOptionsSheet returnCode:NSUserCancelledError];
}
- (IBAction) saveTicketOptions: (id) sender
{
[NSApp endSheet:ticketOptionsSheet];
}
- (IBAction) cancelAuthSheet: (id) sender
{
[NSApp endSheet:[self window]];
}
- (void) authSheetDidEnd: (NSWindow *) sheet
returnCode: (int) returnCode
contextInfo: (void *) contextInfo
{
[sheet orderOut:nil];
}
- (void) ticketOptionsSheetDidEnd: (NSWindow *) sheet
returnCode: (int) returnCode
contextInfo: (void *) contextInfo
{
if (returnCode == NSUserCancelledError) {
// discard new options
[ticketOptionsController setContent:nil];
} else {
kim_error err = KIM_NO_ERROR;
kim_preferences prefs = NULL;
kim_identity identity = NULL;
kim_options options = NULL;
NSString *expandedString = [KIMUtilities expandedIdentity:[identityField stringValue]];;
// replace options if favorite exists
// add to favorites if not already in list
if (!expandedString) {
err = KIM_BAD_PRINCIPAL_STRING_ERR;
}
if (!err) {
[favoriteOptions setObject:[ticketOptionsController content]
forKey:expandedString];
}
if (!err) {
err = kim_preferences_create(&prefs);
}
if (!err) {
err = kim_identity_create_from_string(&identity, [[identityField stringValue] UTF8String]);
}
if (!err) {
options = [KIMUtilities kimOptionsForDictionary:[ticketOptionsController content]];
}
if (!identity) { err = KIM_BAD_PRINCIPAL_STRING_ERR; }
if (!err) {
err = kim_preferences_remove_favorite_identity(prefs, identity);
}
if (!err) {
err = kim_preferences_add_favorite_identity(prefs, identity, options);
}
if (!err) {
err = kim_preferences_synchronize(prefs);
}
kim_preferences_free(&prefs);
kim_options_free(&options);
kim_identity_free(&identity);
}
[ticketOptionsSheet orderOut:nil];
}
- (IBAction) changePasswordGearAction: (id) sender
{
NSString *expandedString = [KIMUtilities expandedIdentity:[identityField stringValue]];
NSDictionary *options = [favoriteOptions objectForKey:expandedString];
if (!options) {
options = [glueController valueForKeyPath:options_keypath];
}
[self showSpinny];
// the principal must already be valid to get this far
[associatedClient didEnterIdentity:expandedString options:options wantsChangePassword:YES];
}
- (IBAction) cancel: (id) sender
{
[NSApp endSheet:[self window]];
[associatedClient didCancel];
}
- (IBAction) enterIdentity: (id) sender
{
NSString *expandedString = [KIMUtilities expandedIdentity:[identityField stringValue]];
NSDictionary *options = [favoriteOptions objectForKey:expandedString];
if (!options) {
options = [glueController valueForKeyPath:options_keypath];
}
[self showSpinny];
// the principal must already be valid to get this far
[associatedClient didEnterIdentity:expandedString options:options wantsChangePassword:NO];
}
- (IBAction) answerAuthPrompt: (id) sender
{
NSString *responseString = [glueController valueForKeyPath:prompt_response_keypath];
NSNumber *saveResponse = [glueController valueForKeyPath:should_save_password_keypath];
if (!saveResponse) {
saveResponse = [NSNumber numberWithBool:NO];
}
[self showSpinny];
[associatedClient didPromptForAuth:responseString
saveResponse:saveResponse];
}
- (IBAction) changePassword: (id) sender
{
NSString *oldString = [glueController valueForKeyPath:old_password_keypath];
NSString *newString = [glueController valueForKeyPath:new_password_keypath];
NSString *verifyString = [glueController valueForKeyPath:verify_password_keypath];
[self showSpinny];
[associatedClient didChangePassword:oldString
newPassword:newString
verifyPassword:verifyString];
[NSApp endSheet:[self window]];
}
- (IBAction) showedError: (id) sender
{
[associatedClient didHandleError];
}
@end