NoLoMechanism.swift 13.6 KB
Newer Older
Josh Wisenbaker's avatar
Josh Wisenbaker committed
1 2 3 4 5 6 7 8 9
//
//  NoLoMechanism.swift
//  NoLo
//
//  Created by Joel Rennich on 9/18/17.
//  Copyright © 2017 Joel Rennich. All rights reserved.
//

import Security
10
import OpenDirectory
11
import os.log
12

13
/// Base class for authorization plugin mechanisms.
Josh Wisenbaker's avatar
Josh Wisenbaker committed
14
class NoLoMechanism: NSObject {
15 16

    ///  `string` is used to identify the authorization plugin context uniquely to this plugin
17
    let contextDomain: NSString = "menu.nomad.login"
18

19
    /// If there is an AD domain set via preferences it will be here in a `String`
20
    var managedDomain: String?
21 22 23
    
    /// If there is a SSL requirement set via preferences it will be here in a `Bool`. Defaults to `false`
    var isSSLRequired: Bool?
24

25
    /// A pointer to the MechanismRecord `struct`
26
    let mech: MechanismRecord?
27 28 29 30 31 32

    /// A convience property to access the `AuthorizationCallbacks` of the Authorization plug-in.
    let mechCallbacks: AuthorizationCallbacks

    /// A convience property to access the `AuthorizationEngineRef` of the Authorization Mechanism.
    let mechEngine: AuthorizationEngineRef
33 34

    //MARK: - Initializer
35 36 37 38

    /// Initializer that simply sets up the convience properties to access parts of the authorization plug-in.
    ///
    /// - Parameter mechanism: The base `AuthorizationPlugin` to be used.
39
    @objc init(mechanism: UnsafePointer<MechanismRecord>) {
40
        os_log("Initializing NoLoSwiftMech", log: noLoMechlog, type: .debug)
41
        self.mech = mechanism.pointee
42 43 44
        self.mechCallbacks = mechanism.pointee.fPlugin.pointee.fCallbacks.pointee
        self.mechEngine = mechanism.pointee.fEngine
        super.init()
45 46
        self.managedDomain = getManagedPreference(key: .ADDomain) as? String
        self.isSSLRequired = getManagedPreference(key: .LDAPOverSSL) as? Bool
47
        os_log("Initialization of NoLoSwiftMech complete", log: noLoMechlog, type: .debug)
48 49
    }

Josh Wisenbaker's avatar
Josh Wisenbaker committed
50
    
Josh Wisenbaker's avatar
Josh Wisenbaker committed
51 52
    var nomadUser: String? {
        get {
53
            guard let userName = getHint(type: .noMADUser) as? String else {
Josh Wisenbaker's avatar
Josh Wisenbaker committed
54 55
                return nil
            }
56
            os_log("Computed nomadUser accessed: %{public}@", log: noLoMechlog, type: .debug, userName)
57
            return userName
Josh Wisenbaker's avatar
Josh Wisenbaker committed
58 59
        }
    }
60 61 62 63 64 65 66 67 68 69
    
    var nomadDomain: String? {
        get {
            guard let domainName = getHint(type: .noMADDomain) as? String else {
                return nil
            }
            os_log("Computed nomadDomain accessed: %{public}@", log: noLoMechlog, type: .debug, domainName)
            return domainName
        }
    }
70

Josh Wisenbaker's avatar
Josh Wisenbaker committed
71 72
    var nomadPass: String? {
        get {
73
            guard let userPass = getHint(type: .noMADPass) as? String else {
Josh Wisenbaker's avatar
Josh Wisenbaker committed
74 75
                return nil
            }
76
            os_log("Computed nomadPass accessed: %@", log: noLoMechlog, type: .debug, userPass)
77 78 79 80 81 82
            return userPass
        }
    }

    var nomadFirst: String? {
        get {
83
            guard let firstName = getHint(type: .noMADFirst) as? String else {
84 85
                return nil
            }
86
            os_log("Computed nomadFirst accessed: %{public}@", log: noLoMechlog, type: .debug, firstName)
87 88 89 90 91 92
            return firstName
        }
    }

    var nomadLast: String? {
        get {
93
            guard let lastName = getHint(type: .noMADLast) as? String else {
94 95
                return nil
            }
96
            os_log("Computed nomadLast accessed: %{public}@", log: noLoMechlog, type: .debug, lastName)
97
            return lastName
Josh Wisenbaker's avatar
Josh Wisenbaker committed
98 99
        }
    }
100 101 102 103 104 105 106 107 108 109 110
    
    var nomadGroups: [String]? {
        get {
            guard let userGroups = getHint(type: .noMADGroups) as? [String] else {
                os_log("noMADGroups value is empty", log: noLoMechlog, type: .debug)
                return nil
            }
            os_log("Computed nomadgroups accessed: %{public}@", log: noLoMechlog, type: .debug, userGroups)
            return userGroups
        }
    }
111

112 113 114 115 116 117 118 119 120 121 122 123 124 125 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 153 154 155

    var usernameContext: String? {
        get {
            var value : UnsafePointer<AuthorizationValue>? = nil
            var flags = AuthorizationContextFlags()
            var err: OSStatus = noErr
            err = mechCallbacks.GetContextValue(
                mechEngine, kAuthorizationEnvironmentUsername, &flags, &value)

            if err != errSecSuccess {
                return nil
            }

            guard let username = NSString.init(bytes: value!.pointee.data!,
                                               length: value!.pointee.length,
                                               encoding: String.Encoding.utf8.rawValue)
                else { return nil }

            return username.replacingOccurrences(of: "\0", with: "") as String
        }
    }

    var passwordContext: String? {
        get {
            var value : UnsafePointer<AuthorizationValue>? = nil
            var flags = AuthorizationContextFlags()
            var err: OSStatus = noErr
            err = mechCallbacks.GetContextValue(
                mechEngine, kAuthorizationEnvironmentPassword, &flags, &value)

            if err != errSecSuccess {
                return nil
            }
            guard let pass = NSString.init(bytes: value!.pointee.data!,
                                           length: value!.pointee.length,
                                           encoding: String.Encoding.utf8.rawValue)
                else { return nil }

            return pass.replacingOccurrences(of: "\0", with: "") as String
        }
    }


    
156
    //context value - create user
Josh Wisenbaker's avatar
Josh Wisenbaker committed
157
    func setUID(uid: Int) {
158
        os_log("Setting context hint for UID: %{public}@", log: noLoMechlog, type: .debug, uid)
Josh Wisenbaker's avatar
Josh Wisenbaker committed
159 160 161
        let flags = AuthorizationContextFlags(rawValue: AuthorizationContextFlags.RawValue(1 << 0))
        var data = uid_t.init(bitPattern: Int32(uid))
        var value = AuthorizationValue(length: MemoryLayout<uid_t>.size, data: UnsafeMutableRawPointer.init(&data))
162 163 164 165
        let error = mechCallbacks.SetContextValue(mechEngine, "uid", flags, &value)
        if error != noErr {
            logOSStatusErr(error, sender: "setUID")
        }
Josh Wisenbaker's avatar
Josh Wisenbaker committed
166
    }
167 168

    //context value - create user
Josh Wisenbaker's avatar
Josh Wisenbaker committed
169
    func setGID(gid: Int) {
170
        os_log("Setting context hint for GID: %{public}@", log: noLoMechlog, type: .debug, gid)
Josh Wisenbaker's avatar
Josh Wisenbaker committed
171 172 173
        let flags = AuthorizationContextFlags(rawValue: AuthorizationContextFlags.RawValue(1 << 0))
        var data = gid_t.init(bitPattern: Int32(gid))
        var value = AuthorizationValue(length: MemoryLayout<gid_t>.size, data: UnsafeMutableRawPointer.init(&data))
174 175 176 177
        let error = mechCallbacks.SetContextValue(self.mechEngine, "gid", flags, &value)
        if error != noErr {
            logOSStatusErr(error, sender: "setGID")
        }
Josh Wisenbaker's avatar
Josh Wisenbaker committed
178
    }
179 180

    // log only
Josh Wisenbaker's avatar
Josh Wisenbaker committed
181 182
    func getArguments() {
        var value : UnsafePointer<AuthorizationValueVector>? = nil
183 184 185 186
        let error = mechCallbacks.GetArguments(mechEngine, &value)
        if error != noErr {
            logOSStatusErr(error, sender: "getArguments")
        }    }
187 188

    // log only
Josh Wisenbaker's avatar
Josh Wisenbaker committed
189 190
    func getTokens() {
        if #available(OSX 10.13, *) {
191
            var value : Unmanaged<CFArray>? = nil
192
            defer {value?.release()}
193 194 195 196
            let error = mechCallbacks.GetTokenIdentities(mechEngine, "" as CFTypeRef, &value)
            if error != noErr {
                logOSStatusErr(error, sender: "getTokens")
            }
Josh Wisenbaker's avatar
Josh Wisenbaker committed
197
        } else {
198
            os_log("Tokens are not supported on this version of macOS", log: noLoMechlog, type: .default)
199

Josh Wisenbaker's avatar
Josh Wisenbaker committed
200 201
        }
    }
202 203

    //MARK: - Mechanism Verdicts
Josh Wisenbaker's avatar
Josh Wisenbaker committed
204
    // Allow the login. End of the mechanism
205
    func allowLogin() {
206 207 208 209 210
        os_log("Allowing login", log: noLoMechlog, type: .default)
        let error = mechCallbacks.SetResult(mechEngine, .allow)
        if error != noErr {
            logOSStatusErr(error, sender: "allowLogin")
        }
Josh Wisenbaker's avatar
Josh Wisenbaker committed
211 212 213
    }
    
    // disallow login
214
    func denyLogin() {
215 216 217 218 219 220 221
        os_log("Denying login", log: noLoMechlog, type: .default)
        let error = mechCallbacks.SetResult(mechEngine, .deny)
        if error != noErr {
            logOSStatusErr(error, sender: "denyLogin")
        }
    }

222 223 224 225 226
    /// A simple method to send an OSStatus Error to the os.log error log with the name of the calling function.
    ///
    /// - Parameters:
    ///   - error: The `OSStatus` error to log.
    ///   - sender: A `String` to register as the sender of the error.
227 228
    func logOSStatusErr(_ error: OSStatus, sender: String) {
        os_log("Error setting %{public}@ context hint: %{public}@", log: noLoMechlog, type: .error, sender, error)
Josh Wisenbaker's avatar
Josh Wisenbaker committed
229
    }
Josh Wisenbaker's avatar
Josh Wisenbaker committed
230

231 232
    //MARK: - Directory Service Utilities

Josh Wisenbaker's avatar
Josh Wisenbaker committed
233 234 235 236
    /// Checks to see if a given user exits in the DSLocal OD node.
    ///
    /// - Parameter name: The shortname of the user to check as a `String`.
    /// - Returns: `true` if the user already exists locally. Otherwise `false`.
237
    class func checkForLocalUser(name: String) -> Bool {
238
        os_log("Checking for local username", log: noLoMechlog, type: .debug)
Josh Wisenbaker's avatar
Josh Wisenbaker committed
239 240 241
        var records = [ODRecord]()
        let odsession = ODSession.default()
        do {
242
            let node = try ODNode.init(session: odsession, type: ODNodeType(kODNodeTypeLocalNodes))
243
            let query = try ODQuery.init(node: node, forRecordTypes: kODRecordTypeUsers, attribute: kODAttributeTypeRecordName, matchType: ODMatchType(kODMatchEqualTo), queryValues: name, returnAttributes: kODAttributeTypeAllAttributes, maximumResults: 0)
Josh Wisenbaker's avatar
Josh Wisenbaker committed
244
            records = try query.resultsAllowingPartial(false) as! [ODRecord]
245
        } catch {
Josh Wisenbaker's avatar
Josh Wisenbaker committed
246
            let errorText = error.localizedDescription
247
            os_log("ODError while trying to check for local user: %{public}@", log: noLoMechlog, type: .error, errorText)
Josh Wisenbaker's avatar
Josh Wisenbaker committed
248 249
            return false
        }
250 251 252
        let isLocal = records.isEmpty ? false : true
        os_log("Results of local user check %{public}@", log: noLoMechlog, type: .debug, isLocal.description)
        return isLocal
Josh Wisenbaker's avatar
Josh Wisenbaker committed
253
    }
Josh Wisenbaker's avatar
Josh Wisenbaker committed
254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271

    class func verifyUser(name: String, auth: String) -> Bool {
        os_log("Finding user record", log: noLoMechlog, type: .debug)
        var records = [ODRecord]()
        let odsession = ODSession.default()
        var isValid = false
        do {
            let node = try ODNode.init(session: odsession, type: ODNodeType(kODNodeTypeLocalNodes))
            let query = try ODQuery.init(node: node, forRecordTypes: kODRecordTypeUsers, attribute: kODAttributeTypeRecordName, matchType: ODMatchType(kODMatchEqualTo), queryValues: name, returnAttributes: kODAttributeTypeAllAttributes, maximumResults: 0)
            records = try query.resultsAllowingPartial(false) as! [ODRecord]
            isValid = ((try records.first?.verifyPassword(auth)) != nil)
        } catch {
            let errorText = error.localizedDescription
            os_log("ODError while trying to check for local user: %{public}@", log: noLoMechlog, type: .error, errorText)
            return false
        }
        return isValid
    }
272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298

    /// Gets shortname from a UUID
    ///
    /// - Parameters:
    ///   - uuid: the uuid of the user to check as a `String`.
    /// - Returns: shortname of the user or nil.
    class func getShortname(uuid: String) -> String? {

        os_log("Checking for username from UUID", log: noLoMechlog, type: .debug)
        var records = [ODRecord]()
        let odsession = ODSession.default()
        do {
            let node = try ODNode.init(session: odsession, type: ODNodeType(kODNodeTypeLocalNodes))
            let query = try ODQuery.init(node: node, forRecordTypes: kODRecordTypeUsers, attribute: kODAttributeTypeGUID, matchType: ODMatchType(kODMatchEqualTo), queryValues: uuid, returnAttributes: kODAttributeTypeAllAttributes, maximumResults: 0)
            records = try query.resultsAllowingPartial(false) as! [ODRecord]
        } catch {
            let errorText = error.localizedDescription
            os_log("ODError while trying to check for local user: %{public}@", log: noLoMechlog, type: .error, errorText)
            return nil
        }

        if records.count != 1 {
            return nil
        } else {
            return records.first?.recordName
        }
    }
Joel Rennich's avatar
Joel Rennich committed
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337
    
    /// Updates a timestamp on a local account
    ///
    /// - Parameters:
    ///   - name: the shortname of the user to check as a `String`.
    ///   - time: The time to add  as a `String`.
    /// - Returns: `true` if time attribute can be added, false if not.
    class func updateSignIn(name: String, time: AnyObject ) -> Bool {
        os_log("Checking for local username", log: noLoMechlog, type: .default)
        var records = [ODRecord]()
        let odsession = ODSession.default()
        do {
            let node = try ODNode.init(session: odsession, type: ODNodeType(kODNodeTypeLocalNodes))
            let query = try ODQuery.init(node: node, forRecordTypes: kODRecordTypeUsers, attribute: kODAttributeTypeRecordName, matchType: ODMatchType(kODMatchEqualTo), queryValues: name, returnAttributes: kODAttributeTypeAllAttributes, maximumResults: 0)
            records = try query.resultsAllowingPartial(false) as! [ODRecord]
        } catch {
            let errorText = error.localizedDescription
            os_log("ODError while trying to check for local user: %{public}@", log: noLoMechlog, type: .error, errorText)
            return false
        }
        
        let isLocal = records.isEmpty ? false : true
        os_log("Results of local user check %{public}@", log: noLoMechlog, type: .default, isLocal.description)
        
        if !isLocal {
            return isLocal
        }
        
        // now to update the attribute
        
        do {
            try records.first?.setValue(time, forAttribute: kODAttributeNetworkSignIn)
        } catch {
            os_log("Unable to add sign in time to record", log: noLoMechlog, type: .error)
            return false
        }
        
        return true
    }
Josh Wisenbaker's avatar
Josh Wisenbaker committed
338
}
339

Josh Wisenbaker's avatar
Josh Wisenbaker committed
340

341 342


343 344
//MARK: - ContextAndHintHandling Protocol
extension NoLoMechanism: ContextAndHintHandling {}