SignIn.swift 33.7 KB
Newer Older
Josh Wisenbaker's avatar
Josh Wisenbaker committed
1 2 3 4 5 6 7 8 9 10
//
//  SignIn.swift
//  NoMADLogin
//
//  Created by Joel Rennich on 9/20/17.
//  Copyright © 2017 Joel Rennich. All rights reserved.
//

import Cocoa
import Security.AuthorizationPlugin
11
import os.log
12
import NoMAD_ADAuth
13
import OpenDirectory
Josh Wisenbaker's avatar
Josh Wisenbaker committed
14

15
class SignIn: NSWindowController, DSQueryable {
Josh Wisenbaker's avatar
Josh Wisenbaker committed
16
    
17
    //MARK: - setup properties
Josh Wisenbaker's avatar
Josh Wisenbaker committed
18 19
    var mech: MechanismRecord?
    var session: NoMADSession?
20 21
    var shortName = ""
    var domainName = ""
22
    var passString = ""
23
    var isDomainManaged = false
24
    var isSSLRequired = false
Josh Wisenbaker's avatar
Josh Wisenbaker committed
25 26
    var backgroundWindow: NSWindow!
    var effectWindow: NSWindow!
27
    var passChanged = false
28 29
    var originalPass: String?
    
Josh Wisenbaker's avatar
Josh Wisenbaker committed
30
    @objc var visible = true
Josh Wisenbaker's avatar
Josh Wisenbaker committed
31 32 33 34 35 36 37
    
    //MARK: - IB outlets
    @IBOutlet weak var username: NSTextField!
    @IBOutlet weak var password: NSSecureTextField!
    @IBOutlet weak var domain: NSPopUpButton!
    @IBOutlet weak var signIn: NSButton!
    @IBOutlet weak var imageView: NSImageView!
Josh Wisenbaker's avatar
Josh Wisenbaker committed
38 39 40 41 42 43
    @IBOutlet weak var loginStack: NSStackView!
    @IBOutlet weak var passwordChangeStack: NSStackView!
    @IBOutlet weak var passwordChangeButton: NSButton!
    @IBOutlet weak var oldPassword: NSSecureTextField!
    @IBOutlet weak var newPassword: NSSecureTextField!
    @IBOutlet weak var newPasswordConfirmation: NSSecureTextField!
44
    @IBOutlet weak var alertText: NSTextField!
Josh Wisenbaker's avatar
Josh Wisenbaker committed
45
    
46 47 48 49 50 51 52
    //MARK: - Shutdown and Restart
    
    @IBOutlet weak var restartButton: NSButton!
    @IBOutlet weak var restartText: NSTextField!
    @IBOutlet weak var shutdownButton: NSButton!
    @IBOutlet weak var shutdownText: NSTextField!
    
53 54 55 56 57 58 59 60 61 62 63 64 65 66
    //MARK: - Migrate Box IB outlets
    var migrate = false
    @IBOutlet weak var migrateBox: NSBox!
    @IBOutlet weak var migrateText: NSTextField!
    @IBOutlet weak var migrateUsers: NSPopUpButton!
    @IBOutlet weak var migratePassword: NSSecureTextField!
    @IBOutlet weak var migrateOK: NSButton!
    @IBOutlet weak var migrateCancel: NSButton!
    @IBOutlet weak var MigrateNo: NSButton!
    @IBOutlet weak var migrateSpinner: NSProgressIndicator!
    @IBOutlet weak var usernameLabel: NSTextField!
    var migrateUserRecord : ODRecord?
    let localCheck = LocalCheckAndMigrate()
    var didUpdateFail = false
67
    
Josh Wisenbaker's avatar
Josh Wisenbaker committed
68 69
    //MARK: - UI Methods
    override func windowDidLoad() {
70
        os_log("Calling super.windowDidLoad", log: uiLog, type: .debug)
Josh Wisenbaker's avatar
Josh Wisenbaker committed
71
        super.windowDidLoad()
Josh Wisenbaker's avatar
Josh Wisenbaker committed
72 73 74 75 76 77
        
        os_log("Configure login window", log: uiLog, type: .debug)
        loginApperance()
        
        os_log("create background windows", log: uiLog, type: .debug)
        createBackgroundWindow()
78
        
Josh Wisenbaker's avatar
Josh Wisenbaker committed
79 80 81 82
        os_log("Become first responder", log: uiLog, type: .debug)
        username.becomeFirstResponder()
        os_log("Finsished loading loginwindow", log: uiLog, type: .debug)
    }
83 84
    
    
Josh Wisenbaker's avatar
Josh Wisenbaker committed
85 86 87 88 89 90 91
    fileprivate func createBackgroundWindow() {
        var image: NSImage?
        // Is a background image path set? If not just use gray.
        if let backgroundImage = getManagedPreference(key: .BackgroundImage) as? String  {
            os_log("BackgroundImage preferences found.", log: uiLog, type: .debug)
            image = NSImage(contentsOf: URL(fileURLWithPath: backgroundImage))
        }
92 93 94 95 96
        
        if let backgroundImageData = getManagedPreference(key: .BackgroundImageData) as? Data {
            os_log("BackgroundImageData found", log: uiLog, type: .debug)
            image = NSImage(data: backgroundImageData)
        }
97
        
Josh Wisenbaker's avatar
Josh Wisenbaker committed
98 99 100 101 102 103 104 105 106 107 108 109 110 111
        for screen in NSScreen.screens {
            let view = NSView()
            view.wantsLayer = true
            view.layer!.contents = image
            
            backgroundWindow = NSWindow(contentRect: screen.frame,
                                        styleMask: .fullSizeContentView,
                                        backing: .buffered,
                                        defer: true)
            
            backgroundWindow.backgroundColor = .gray
            backgroundWindow.contentView = view
            backgroundWindow.makeKeyAndOrderFront(self)
            backgroundWindow.canBecomeVisibleWithoutLogin = true
112
            
Josh Wisenbaker's avatar
Josh Wisenbaker committed
113 114 115 116 117 118 119 120 121 122 123
            let effectView = NSVisualEffectView()
            effectView.wantsLayer = true
            effectView.blendingMode = .behindWindow
            effectView.frame = screen.frame
            
            effectWindow = NSWindow(contentRect: screen.frame,
                                    styleMask: .fullSizeContentView,
                                    backing: .buffered,
                                    defer: true)
            
            effectWindow.contentView = effectView
124 125
            
            if let backgroundImageAlpha = getManagedPreference(key: .BackgroundImageAlpha) as? Int {
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152
                
                switch backgroundImageAlpha {
                case 0 :
                    effectWindow.alphaValue = 0.0
                case 1 :
                    effectWindow.alphaValue = 0.1
                case 2 :
                    effectWindow.alphaValue = 0.2
                case 3 :
                    effectWindow.alphaValue = 0.3
                case 4 :
                    effectWindow.alphaValue = 0.4
                case 5 :
                    effectWindow.alphaValue = 0.5
                case 6 :
                    effectWindow.alphaValue = 0.6
                case 7 :
                    effectWindow.alphaValue = 0.7
                case 8 :
                    effectWindow.alphaValue = 0.8
                case 9 :
                    effectWindow.alphaValue = 0.9
                case 10 :
                    effectWindow.alphaValue = 1.0
                default :
                    effectWindow.alphaValue = 1.0
                }
153 154 155 156
            } else {
                effectWindow.alphaValue = 0.8
            }
            
Josh Wisenbaker's avatar
Josh Wisenbaker committed
157 158 159 160
            effectWindow.orderFrontRegardless()
            effectWindow.canBecomeVisibleWithoutLogin = true
        }
    }
161 162
    
    
Josh Wisenbaker's avatar
Josh Wisenbaker committed
163 164
    func loginTransition() {
        os_log("Transitioning... fade our UI away", log: uiLog, type: .debug)
165
        
Josh Wisenbaker's avatar
Josh Wisenbaker committed
166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216
        NSAnimationContext.runAnimationGroup({ (context) in
            context.duration = 1.0
            context.allowsImplicitAnimation = true
            self.window?.alphaValue = 0.0
            self.backgroundWindow.alphaValue = 0.0
            self.effectWindow.alphaValue = 0.0
        }, completionHandler: {
            os_log("Close all the windows", log: uiLog, type: .debug)
            self.window?.close()
            self.backgroundWindow.close()
            self.effectWindow.close()
            self.visible = false
        })
    }
    
    fileprivate func shakeOff() {
        let origin = NSMakePoint((window?.frame.origin.x)!, (window?.frame.origin.y)!)
        let left = NSMakePoint(origin.x - 10, origin.y)
        let right = NSMakePoint(origin.x + 10, origin.y)
        
        NSAnimationContext.runAnimationGroup({ context in
            context.duration = 0.04
            context.allowsImplicitAnimation = true
            self.window?.setFrameOrigin(left)
        }, completionHandler: {
            NSAnimationContext.runAnimationGroup({ (context) in
                context.duration = 0.04
                context.allowsImplicitAnimation = true
                self.window?.setFrameOrigin(right)
            }, completionHandler: {
                NSAnimationContext.runAnimationGroup({ (context) in
                    context.duration = 0.04
                    context.allowsImplicitAnimation = true
                    self.window?.setFrameOrigin(left)
                }, completionHandler: {
                    NSAnimationContext.runAnimationGroup({ context in
                        context.duration = 0.04
                        context.allowsImplicitAnimation = true
                        self.window?.setFrameOrigin(right)
                    }, completionHandler: {
                        NSAnimationContext.runAnimationGroup({ context in
                            context.duration = 0.04
                            context.allowsImplicitAnimation = true
                            self.window?.setFrameOrigin(origin)
                            self.window?.close()
                        })
                    })
                })
            })
        })
    }
217
    
Josh Wisenbaker's avatar
Josh Wisenbaker committed
218
    fileprivate func loginApperance() {
219
        os_log("Setting window level", log: uiLog, type: .debug)
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245
        
        if let windowLevel = getManagedPreference(key: .SignInWindowLevel) as? String {
            switch windowLevel {
                
            case "normal":
                self.window?.level = .normal
            case "floating":
                self.window?.level = .floating
            case "submenu":
                self.window?.level = .submenu
            case "tornOffMenu":
                self.window?.level = .tornOffMenu
            case "mainMenu":
                self.window?.level = .mainMenu
            case "statusBar":
                self.window?.level = .statusBar
            case "modalPanel":
                self.window?.level = .modalPanel
            case "popUpMenu":
                self.window?.level = .popUpMenu
            case "screenSaver":
                self.window?.level = .screenSaver
            }
        } else {
            self.window?.level = .screenSaver
        }
Josh Wisenbaker's avatar
Josh Wisenbaker committed
246
        self.window?.orderFrontRegardless()
247
        
Josh Wisenbaker's avatar
Josh Wisenbaker committed
248
        // make things look better
249
        os_log("Tweaking appearance", log: uiLog, type: .debug)
Josh Wisenbaker's avatar
Josh Wisenbaker committed
250 251 252 253 254 255 256 257 258
        if getManagedPreference(key: .LoginScreen) as? Bool == true {
            os_log("Present as login screen", log: uiLog, type: .debug)
            self.window?.isOpaque = false
            self.window?.hasShadow = false
            self.window?.backgroundColor = .clear
        } else {
            os_log("Present as login window", log: uiLog, type: .debug)
            self.window?.backgroundColor = NSColor.white
        }
259
        self.window?.titlebarAppearsTransparent = true
260 261 262 263
        if !self.domainName.isEmpty {
            username.placeholderString = "Username"
            self.isDomainManaged = true
        }
264 265 266 267 268
        if let usernamePlaceholder = getManagedPreference(key: .UsernameFieldPlaceholder) as? String {
            os_log("Username Field Placeholder preferences found.", log: uiLog, type: .debug)
            username.placeholderString = usernamePlaceholder
        }
        
Josh Wisenbaker's avatar
Josh Wisenbaker committed
269 270
        self.window?.isMovable = false
        self.window?.canBecomeVisibleWithoutLogin = true
271
        
Josh Wisenbaker's avatar
Josh Wisenbaker committed
272 273 274 275 276 277 278 279
        if let logoPath = getManagedPreference(key: .LoginLogo) as? String {
            os_log("Found logoPath: %{public}@", log: uiLog, type: .debug, logoPath)
            if logoPath == "NONE" {
                imageView.image = nil
            } else {
                imageView.image = NSImage(contentsOf: URL(fileURLWithPath: logoPath))
            }
        }
280 281 282 283 284 285 286
        
        if let logoData = getManagedPreference(key: .LoginLogoData) as? Data {
            os_log("Found LoginLogoData key has a value", log: uiLog, type: .debug)
            if let image = NSImage(data: logoData) as NSImage? {
                imageView.image = image
            }
        }
Joel Rennich's avatar
Joel Rennich committed
287 288 289 290 291
        
        // check for Logo Alpha
        
        if let alpha = getManagedPreference(key: .LoginLogoAlpha) as? Int {
            os_log("Updating logo alpha value", log: uiLog, type: .debug)
292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317
            switch alpha {
            case 0 :
                imageView.alphaValue = 0.0
            case 1 :
                imageView.alphaValue = 0.1
            case 2 :
                imageView.alphaValue = 0.2
            case 3 :
                imageView.alphaValue = 0.3
            case 4 :
                imageView.alphaValue = 0.4
            case 5 :
                imageView.alphaValue = 0.5
            case 6 :
                imageView.alphaValue = 0.6
            case 7 :
                imageView.alphaValue = 0.7
            case 8 :
                imageView.alphaValue = 0.8
            case 9 :
                imageView.alphaValue = 0.9
            case 10 :
                imageView.alphaValue = 1.0
            default :
                imageView.alphaValue = 0.0
            }
Joel Rennich's avatar
Joel Rennich committed
318
        }
319 320 321 322 323 324 325 326 327 328
        
        if getManagedPreference(key: .HideRestart) as? Bool ?? false {
            restartText.isHidden = true
            restartButton.isHidden = true
        }
        
        if getManagedPreference(key: .HideShutdown) as? Bool ?? false {
            shutdownText.isHidden = true
            shutdownButton.isHidden = true
        }
Josh Wisenbaker's avatar
Josh Wisenbaker committed
329
    }
330
    
Josh Wisenbaker's avatar
Josh Wisenbaker committed
331
    fileprivate func showResetUI() {
Josh Wisenbaker's avatar
Josh Wisenbaker committed
332 333 334 335
        os_log("Adjusting UI for change controls", log: uiLog, type: .debug)
        loginStack.isHidden = true
        signIn.isHidden = true
        signIn.isEnabled = false
336
        migrateBox.isHidden = true
Josh Wisenbaker's avatar
Josh Wisenbaker committed
337 338 339 340 341
        passwordChangeStack.isHidden = false
        passwordChangeButton.isHidden = false
        passwordChangeButton.isEnabled = true
        oldPassword.becomeFirstResponder()
    }
342
    
Joel Rennich's avatar
Joel Rennich committed
343
    fileprivate func authFail( _ message: String?=nil) {
344 345
        session = nil
        password.stringValue = ""
Joel Rennich's avatar
Joel Rennich committed
346
        alertText.stringValue = message ?? "Authentication Failed"
347 348
        loginStartedUI()
    }
349
    
Josh Wisenbaker's avatar
Josh Wisenbaker committed
350 351 352 353
    /// Simple toggle to change the state of the NoLo window UI between active and inactive.
    fileprivate func loginStartedUI() {
        signIn.isEnabled = !signIn.isEnabled
        signIn.isHidden = !signIn.isHidden
354
        
Josh Wisenbaker's avatar
Josh Wisenbaker committed
355 356 357
        username.isEnabled = !username.isEnabled
        password.isEnabled = !password.isEnabled
    }
358 359
    
    
360 361 362 363 364 365 366
    /// When the sign in button is clicked we check a few things.
    ///
    /// 1. Check to see if the username field is blank, bail if it is. If not, animate the UI and process the user strings.
    ///
    /// 2. Check the user shortname and see if the account already exists in DSLocal. If so, simply set the hints and pass on.
    ///
    /// 3. Create a `NoMADSession` and see if we can authenticate as the user.
Josh Wisenbaker's avatar
Josh Wisenbaker committed
367
    @IBAction func signInClick(_ sender: Any) {
368
        os_log("Sign In button clicked", log: uiLog, type: .debug)
369
        if username.stringValue.isEmpty {
370
            os_log("No username entered", log: uiLog, type: .default)
371 372
            return
        }
373 374 375 376 377
        
        // clear any alerts
        
        alertText.stringValue = ""
        
Josh Wisenbaker's avatar
Josh Wisenbaker committed
378
        loginStartedUI()
379 380
        prepareAccountStrings()
        if NoLoMechanism.checkForLocalUser(name: shortName) {
Josh Wisenbaker's avatar
Josh Wisenbaker committed
381
            os_log("Verify local user login for %{public}@", log: uiLog, type: .default, shortName)
382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
            
            if getManagedPreference(key: .DenyLocal) as? Bool ?? false {
                os_log("DenyLocal is enabled, looking for %{public}@ in excluded users", log: uiLog, type: .default, shortName)
                
                var exclude = false
                
                if let excludedUsers = getManagedPreference(key: .DenyLocalExcluded) as? [String] {
                    if excludedUsers.contains(shortName) {
                        os_log("Allowing local sign in via exclusions %{public}@", log: uiLog, type: .default, shortName)
                        exclude = true
                    }
                }
                
                if !exclude {
                    os_log("No exclusions for %{public}@, denying local login. Forcing network auth", log: uiLog, type: .default, shortName)
                    networkAuth()
                    return
                }
            }
            
Josh Wisenbaker's avatar
Josh Wisenbaker committed
402
            if NoLoMechanism.verifyUser(name: shortName, auth: passString) {
403 404 405 406
                os_log("Allowing local user login for %{public}@", log: uiLog, type: .default, shortName)
                setRequiredHintsAndContext()
                completeLogin(authResult: .allow)
                return
Josh Wisenbaker's avatar
Josh Wisenbaker committed
407 408
            } else {
                os_log("Could not verify %{public}@", log: uiLog, type: .default, shortName)
409
                authFail()
410
                return
Josh Wisenbaker's avatar
Josh Wisenbaker committed
411
            }
412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429
        } else {
            networkAuth()
        }
    }
    
    fileprivate func networkAuth() {
        session = NoMADSession.init(domain: domainName, user: shortName)
        os_log("NoMAD Login User: %{public}@, Domain: %{public}@", log: uiLog, type: .default, shortName, domainName)
        guard let session = session else {
            os_log("Could not create NoMADSession from SignIn window", log: uiLog, type: .error)
            return
        }
        session.useSSL = isSSLRequired
        session.userPass = passString
        session.delegate = self
        
        if let ignoreSites = getManagedPreference(key: .IgnoreSites) as? Bool {
            os_log("Ignoring AD sites", log: uiLog, type: .debug)
430
            
431 432 433 434 435
            session.siteIgnore = ignoreSites
        }
        
        if let ldapServers = getManagedPreference(key: .LDAPServers) as? [String] {
            os_log("Adding custom LDAP servers", log: uiLog, type: .debug)
436
            
437
            session.ldapServers = ldapServers
Josh Wisenbaker's avatar
Josh Wisenbaker committed
438
        }
439 440 441
        
        os_log("Attempt to authenticate user", log: uiLog, type: .debug)
        session.authenticate()
Josh Wisenbaker's avatar
Josh Wisenbaker committed
442
    }
443 444
    
    
Josh Wisenbaker's avatar
Josh Wisenbaker committed
445 446
    @IBAction func changePassowrd(_ sender: Any) {
        guard newPassword.stringValue == newPasswordConfirmation.stringValue else {
447
            os_log("New passwords didn't match", log: uiLog, type: .error)
448
            alertText.stringValue = "New passwords don't match"
Josh Wisenbaker's avatar
Josh Wisenbaker committed
449 450
            return
        }
451 452 453 454
        
        // set the passChanged flag
        
        passChanged = true
455
        
Josh Wisenbaker's avatar
Josh Wisenbaker committed
456 457
        //TODO: Terrible hack to be fixed once AD Framework is refactored
        password.stringValue = newPassword.stringValue
458
        
Josh Wisenbaker's avatar
Josh Wisenbaker committed
459 460
        session?.oldPass = oldPassword.stringValue
        session?.newPass = newPassword.stringValue
461
        
Josh Wisenbaker's avatar
Josh Wisenbaker committed
462
        os_log("Attempting password change for %{public}@", log: uiLog, type: .debug, shortName)
463 464 465 466 467 468 469
        
        // disable the fields
        
        oldPassword.isEnabled = false
        newPassword.isEnabled = false
        newPasswordConfirmation.isEnabled = false
        
Josh Wisenbaker's avatar
Josh Wisenbaker committed
470 471
        session?.changePassword()
    }
472 473
    
    
474 475
    /// Format the user and domain from the login window depending on the mode the window is in.
    ///
476
    /// I.e. are we picking a domain from a list, using a managed domain, or putting it on the user name with '@'.
477
    fileprivate func prepareAccountStrings() {
478
        os_log("Format user and domain strings", log: uiLog, type: .debug)
479 480 481 482
        
        var providedDomainName = ""
        
        shortName = username.stringValue
483
        
484
        if username.stringValue.range(of:"@") != nil {
485
            shortName = (username.stringValue.components(separatedBy: "@").first)!
486 487 488 489 490 491
            providedDomainName = username.stringValue.components(separatedBy: "@").last!.uppercased()
        }
        
        if !domain.isHidden {
            os_log("Using domain from picker", log: uiLog, type: .default)
            domainName = (domain.selectedItem?.title.uppercased())!
492 493
            return
        }
494
        
Josh Wisenbaker's avatar
Josh Wisenbaker committed
495 496 497 498
        if providedDomainName == domainName {
            os_log("Provided domain matches  managed domain", log: uiLog, type: .default)
            return
        }
499
        
500 501
        if !providedDomainName.isEmpty {
            os_log("Optional domain provided in text field: %{public}@", log: uiLog, type: .default, providedDomainName)
Josh Wisenbaker's avatar
Josh Wisenbaker committed
502 503
            if getManagedPreference(key: .AdditionalADDomains) as? Bool == true {
                os_log("Optional domain name allowed by AdditionalADDomains allow-all policy", log: uiLog, type: .default)
504 505 506
                domainName = providedDomainName
                return
            }
507
            
Josh Wisenbaker's avatar
Josh Wisenbaker committed
508
            if let optionalDomains = getManagedPreference(key: .AdditionalADDomains) as? [String] {
509
                guard optionalDomains.contains(providedDomainName) else {
Josh Wisenbaker's avatar
Josh Wisenbaker committed
510
                    os_log("Optional domain name not allowed by AdditionalADDomains whitelist policy", log: uiLog, type: .default)
511 512
                    return
                }
Josh Wisenbaker's avatar
Josh Wisenbaker committed
513
                os_log("Optional domain name allowed by AdditionalADDomains whitelist policy", log: uiLog, type: .default)
514 515 516
                domainName = providedDomainName
                return
            }
517
            
Josh Wisenbaker's avatar
Josh Wisenbaker committed
518
            os_log("Optional domain not name allowed by AdditionalADDomains policy (false or not defined)", log: uiLog, type: .default)
519
        }
520
        
521 522
        os_log("Using domain from managed domain", log: uiLog, type: .default)
        return
523
    }
524 525
    
    
526
    //MARK: - Login Context Functions
527
    
528
    /// Set the authorization and context hints. These are the basics we need to passthrough to the next mechanism.
529
    fileprivate func setRequiredHintsAndContext() {
530
        os_log("Setting hints for user: %{public}@", log: uiLog, type: .debug, shortName)
531
        setHint(type: .noMADUser, hint: shortName)
532
        setHint(type: .noMADPass, hint: passString)
533
        
534
        os_log("Setting context values for user: %{public}@", log: uiLog, type: .debug, shortName)
535 536
        setContextString(type: kAuthorizationEnvironmentUsername, value: shortName)
        setContextString(type: kAuthorizationEnvironmentPassword, value: passString)
Josh Wisenbaker's avatar
Josh Wisenbaker committed
537
    }
538 539
    
    
Josh Wisenbaker's avatar
Josh Wisenbaker committed
540 541 542 543
    /// Complete the NoLo process and either continue to the next Authorization Plugin or reset the NoLo window.
    ///
    /// - Parameter authResult:`Authorizationresult` enum value that indicates if login should proceed.
    fileprivate func completeLogin(authResult: AuthorizationResult) {
Josh Wisenbaker's avatar
Josh Wisenbaker committed
544 545 546 547 548 549 550 551 552 553
        switch authResult {
        case .allow:
            os_log("Complete login process with allow", log: uiLog, type: .debug)
        case .deny:
            os_log("Complete login process with deny", log: uiLog, type: .debug)
            window?.close()
        default:
            os_log("Complete login process with unknown", log: uiLog, type: .debug)
            window?.close()
        }
554
        os_log("Complete login process", log: uiLog, type: .debug)
555 556 557 558
        let error = mech?.fPlugin.pointee.fCallbacks.pointee.SetResult((mech?.fEngine)!, authResult)
        if error != noErr {
            os_log("Got error setting authentication result", log: uiLog, type: .error)
        }
Josh Wisenbaker's avatar
Josh Wisenbaker committed
559
        NSApp.stopModal()
Josh Wisenbaker's avatar
Josh Wisenbaker committed
560
    }
561 562
    
    
563
    //MARK: - Sleep, Restart, and Shut Down Actions
564
    
565 566
    @IBAction func sleepClick(_ sender: Any) {
        os_log("Sleeping system isn't supported yet", log: uiLog, type: .error)
Josh Wisenbaker's avatar
Josh Wisenbaker committed
567 568 569
        //        os_log("Setting sleep user", log: uiLog, type: .debug)
        //        setHint(type: .noMADUser, hint: SpecialUsers.noloSleep.rawValue)
        //        completeLogin(authResult: .allow)
570
    }
571
    
572 573 574 575 576
    @IBAction func restartClick(_ sender: Any) {
        os_log("Setting restart user", log: uiLog, type: .debug)
        setHint(type: .noMADUser, hint: SpecialUsers.noloRestart.rawValue)
        completeLogin(authResult: .allow)
    }
577
    
578 579 580 581 582
    @IBAction func shutdownClick(_ sender: Any) {
        os_log("Setting shutdown user", log: uiLog, type: .debug)
        setHint(type: .noMADUser, hint: SpecialUsers.noloShutdown.rawValue)
        completeLogin(authResult: .allow)
    }
583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602
    
    //MARK: - Migration Methods
    
    fileprivate func showPasswordSync() {
        // hide other possible boxes
        self.migrateBox.isHidden = true
        self.loginStack.isHidden = true
        self.signIn.isHidden = true
        self.signIn.isEnabled = true
        self.MigrateNo.isHidden = true
        self.migrateUsers.isHidden = true
        self.usernameLabel.isHidden = true
        
        // show migration box
        self.migrateBox.isHidden = false
        self.migrateSpinner.isHidden = false
        
        if self.didUpdateFail == true {
            self.migrateText.stringValue = "Invalid password. Try again."
        } else {
Joel Rennich's avatar
Joel Rennich committed
603
            self.migrateText.stringValue = getManagedPreference(key: .MessagePasswordSync) as? String ?? "Active Directory password does not match local password. Please enter your previous local password to update it."
604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701
        }
    }
    
    fileprivate func showMigration() {
        
        //RunLoop.main.perform {
        // hide other possible boxes
        os_log("Showing migration box", log: uiLog, type: .default)
        
        self.loginStack.isHidden = true
        self.signIn.isHidden = true
        self.signIn.isEnabled = true
        
        // show migration box
        self.migrateBox.isHidden = false
        self.migrateSpinner.isHidden = false
        self.migrateUsers.addItems(withTitles: self.localCheck.migrationUsers ?? [""])
        //}
    }
    
    @IBAction func clickMigrationOK(_ sender: Any) {
        RunLoop.main.perform {
            self.migrateSpinner.isHidden = false
            self.migrateSpinner.startAnimation(nil)
        }
        
        let migrateUIPass = self.migratePassword.stringValue
        if migrateUIPass.isEmpty {
            os_log("No password was entered", log: uiLog, type: .error)
            RunLoop.main.perform {
                self.migrateSpinner.isHidden = true
                self.migrateSpinner.stopAnimation(nil)
            }
            return
        }
        
        // Take a look to see if we are syncing passwords. Until the next refactor the easiest way to tell is if the picklist is hidden.
        if self.migrateUsers.isHidden {
            do {
                os_log("Password doesn't match existing local. Try to change local pass to match.", log: uiLog, type: .default)
                let localUser = try getLocalRecord(shortName)
                try localUser.changePassword(migrateUIPass, toPassword: passString)
                didUpdateFail = false
                passChanged = false
                os_log("Password sync worked, allowing login", log: uiLog, type: .default)
                setHint(type: .migratePass, hint: migrateUIPass)
                completeLogin(authResult: .allow)
                return
            } catch {
                os_log("Unable to sync local password to Network password. Reload and try again", log: uiLog, type: .error)
                didUpdateFail = true
                showPasswordSync()
                return
            }
        }
        guard let migrateToUser = self.migrateUsers.selectedItem?.title else {
            os_log("Could not select user to migrate from pick list.", log: uiLog, type: .error)
            return
        }
        do {
            os_log("Getting user record for %{public}@", log: uiLog, type: .default, migrateToUser)
            migrateUserRecord = try getLocalRecord(migrateToUser)
            os_log("Checking existing password for %{public}@", log: uiLog, type: .default, migrateToUser)
            if migrateUIPass != passString {
                os_log("No match. Upating local password for %{public}@", log: uiLog, type: .default, migrateToUser)
                try migrateUserRecord?.changePassword(migrateUIPass, toPassword: passString)
            } else {
                os_log("Okta and local passwords matched for %{public}@", log: uiLog, type: .default, migrateToUser)
            }
            // Mark the record to add an alias if required
            os_log("Setting hints for %{public}@", log: uiLog, type: .default, migrateToUser)
            self.setHint(type: .migrateUser, hint: migrateToUser)
            self.setHint(type: .migratePass, hint: migrateUIPass)
            os_log("Allowing login", log: uiLog, type: .default, migrateToUser)
            completeLogin(authResult: .allow)
        } catch {
            os_log("Migration failed with: %{public}@", log: uiLog, type: .error, error.localizedDescription)
            return
        }
        
        // if we are here, the password didn't work
        os_log("Unable to migrate user.", log: uiLog, type: .error)
        self.migrateSpinner.isHidden = true
        self.migrateSpinner.stopAnimation(nil)
        self.migratePassword.stringValue = ""
        self.completeLogin(authResult: .deny)
    }
    
    @IBAction func clickMigrationCancel(_ sender: Any) {
        passChanged = false
        didUpdateFail = false
        completeLogin(authResult: .deny)
    }
    
    @IBAction func clickMigrationNo(_ sender: Any) {
        // user doesn't want to migrate, so create a new account
        completeLogin(authResult: .allow)
    }
Josh Wisenbaker's avatar
Josh Wisenbaker committed
702 703
}

Josh Wisenbaker's avatar
Josh Wisenbaker committed
704

Josh Wisenbaker's avatar
Josh Wisenbaker committed
705 706 707
//MARK: - NoMADUserSessionDelegate
extension SignIn: NoMADUserSessionDelegate {
    
708
    func NoMADAuthenticationFailed(error: NoMADSessionError, description: String) {
709 710 711 712 713 714 715 716 717 718 719 720 721 722
        
        if passChanged {
            os_log("Password change failed.", log: uiLog, type: .default)
            oldPassword.isEnabled = true
            newPassword.isEnabled = true
            newPasswordConfirmation.isEnabled = true
            
            newPassword.stringValue = ""
            newPasswordConfirmation.stringValue = ""
            
            alertText.stringValue = "Password change failed"
            return
        }
        
723 724 725 726 727
        switch error {
        case .PasswordExpired:
            os_log("Password is expired or requires change.", log: uiLog, type: .default)
            showResetUI()
            return
Joel Rennich's avatar
Joel Rennich committed
728 729 730 731 732 733 734 735 736 737 738 739
        case .OffDomain :
            os_log("AD authentication failed, off domain.", log: uiLog, type: .default)
            
            if getManagedPreference(key: .LocalFallback) as? Bool ?? false {
                os_log("Local fallback enabled, passing off to local authentication", log: uiLog, type: .default)
                setRequiredHintsAndContext()
                completeLogin(authResult: .allow)
                return
            } else {
                authFail()
                return
            }
740 741
        default:
            os_log("NoMAD Login Authentication failed with: %{public}@", log: uiLog, type: .error, description)
742 743
            authFail()
            return
744
        }
745
    }
746 747
    
    
Josh Wisenbaker's avatar
Josh Wisenbaker committed
748
    func NoMADAuthenticationSucceded() {
749 750
        
        if passChanged {
751
            originalPass = passString
752 753 754
            // need to ensure the right password is stashed
            passString = newPassword.stringValue
            passChanged = false
755 756
            
            // check
757 758
        }
        
759
        os_log("Authentication succeded, requesting user info", log: uiLog, type: .default)
Josh Wisenbaker's avatar
Josh Wisenbaker committed
760 761
        session?.userInfo()
    }
762 763
    
    
Josh Wisenbaker's avatar
Josh Wisenbaker committed
764
    func NoMADUserInformation(user: ADUserRecord) {
765 766 767 768 769
        
        var allowedLogin = true
        
        os_log("Checking for DenyLogin groups", log: uiLog, type: .debug)
        
770 771
        if let allowedGroups = getManagedPreference(key: .DenyLoginUnlessGroupMember) as? [String] {
            os_log("Found a DenyLoginUnlessGroupMember key value: %{public}@ ", log: uiLog, type: .debug, allowedGroups.debugDescription)
772 773 774 775 776 777
            
            // set the allowed login to false for now
            
            allowedLogin = false
            
            user.groups.forEach { group in
778
                if allowedGroups.contains(group) {
779 780 781 782 783 784 785 786
                    allowedLogin = true
                    os_log("User is a member of %{public}@ group. Setting allowedLogin = true ", log: uiLog, type: .debug, group)
                }
            }
        }
        
        if allowedLogin {
            
787
            setHints(user: user)
788
            
789
            // check for any migration and local auth requirements
790
            
791 792 793 794 795 796
            switch localCheck.run(userToCheck: user.shortName, passToCheck: passString) {
                
            case .fullMigration:
                showMigration()
            case .syncPassword:
                // first check to see if we can resolve this ourselves
797
                os_log("Sync password called.", log: uiLog)
798 799 800 801 802 803
                
                if originalPass != nil {
                    os_log("Attempting to sync local pass.", log: uiLog, type: .default)
                    if localCheck.syncPass(oldPass: originalPass!) {
                        // password changed clean
                        completeLogin(authResult: .allow)
Joel Rennich's avatar
Joel Rennich committed
804
                        return
805 806
                    } else {
                        // unable to change the pass, let user fix
807
                        showPasswordSync()
808
                    }
Joel Rennich's avatar
Joel Rennich committed
809
                } else {
810
                    showPasswordSync()
811
                }
812
            case .errorSkipMigration, .skipMigration, .userMatchSkipMigration, .complete:
813 814
                completeLogin(authResult: .allow)
            }
815
            
816 817 818 819 820
        } else {
            authFail()
            alertText.stringValue = "Not authorized to login."
            showResetUI()
        }
Josh Wisenbaker's avatar
Josh Wisenbaker committed
821
    }
822 823 824 825 826 827 828 829 830 831 832 833 834 835
    
    fileprivate func setHints(user: ADUserRecord) {
        os_log("NoMAD Login Looking up info for: %{public}@", log: uiLog, type: .default, user.shortName)
        setRequiredHintsAndContext()
        setHint(type: .noMADFirst, hint: user.firstName)
        setHint(type: .noMADLast, hint: user.lastName)
        setHint(type: .noMADDomain, hint: domainName)
        setHint(type: .noMADGroups, hint: user.groups)
        setHint(type: .noMADFull, hint: user.cn)
        setHint(type: .kerberos_principal, hint: user.userPrincipal)
        
        // set the network auth time to be added to the user record
        setHint(type: .networkSignIn, hint: String(describing: Date.init().description))
    }
Josh Wisenbaker's avatar
Josh Wisenbaker committed
836
}
837

Josh Wisenbaker's avatar
Josh Wisenbaker committed
838

839 840
//MARK: - NSTextField Delegate
extension SignIn: NSTextFieldDelegate {
841
    public func controlTextDidChange(_ obj: Notification) {
842 843 844 845 846
        let passField = obj.object as! NSTextField
        passString = passField.stringValue
    }
}

Josh Wisenbaker's avatar
Josh Wisenbaker committed
847

848 849
//MARK: - ContextAndHintHandling Protocol
extension SignIn: ContextAndHintHandling {}
Josh Wisenbaker's avatar
Josh Wisenbaker committed
850 851

extension NSWindow {
852
    
Josh Wisenbaker's avatar
Josh Wisenbaker committed
853 854 855 856
    func shakeWindow(){
        let numberOfShakes      = 3
        let durationOfShake     = 0.25
        let vigourOfShake : CGFloat = 0.015
857
        
Josh Wisenbaker's avatar
Josh Wisenbaker committed
858 859
        let frame : CGRect = self.frame
        let shakeAnimation :CAKeyframeAnimation  = CAKeyframeAnimation()
860
        
Josh Wisenbaker's avatar
Josh Wisenbaker committed
861 862
        let shakePath = CGMutablePath()
        shakePath.move(to: CGPoint(x: frame.minX, y: frame.minY))
863
        
Josh Wisenbaker's avatar
Josh Wisenbaker committed
864 865 866 867
        for _ in 0...numberOfShakes-1 {
            shakePath.addLine(to: CGPoint(x: frame.minX - frame.size.width * vigourOfShake, y: frame.minY))
            shakePath.addLine(to: CGPoint(x: frame.minX + frame.size.width * vigourOfShake, y: frame.minY))
        }
868
        
Josh Wisenbaker's avatar
Josh Wisenbaker committed
869
        shakePath.closeSubpath()
870
        
Josh Wisenbaker's avatar
Josh Wisenbaker committed
871 872
        shakeAnimation.path = shakePath;
        shakeAnimation.duration = durationOfShake;
873
        
874
        self.animations = [NSAnimatablePropertyKey("frameOrigin"):shakeAnimation]
Josh Wisenbaker's avatar
Josh Wisenbaker committed
875 876
        self.animator().setFrameOrigin(self.frame.origin)
    }
877
    
Josh Wisenbaker's avatar
Josh Wisenbaker committed
878 879
}