Skip to content

Commit

Permalink
Removes Authorization initializer with parameters, documentation impr…
Browse files Browse the repository at this point in the history
…ovements

This is a breaking change

The removed Authorization initializer was effectively duplicative of the requestRights function, which in this framework is trivial to call right after creating it. Use of this initializer already wasn't one of the recommend usage patterns in the documentation.

Significantly improves the documentation for LaunchdManager.bless(...).
  • Loading branch information
Josh Kaplan committed Nov 7, 2021
1 parent 18c3e07 commit ef63245
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 134 deletions.
29 changes: 15 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@ To see a runnable sample app using this framework, check out
[![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Ftrilemma-dev%2FBlessed%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/trilemma-dev/Blessed)

## Overview
Beyond making it easy to bless an executable, Blessed provides a complete Swift implementation of the non-deprecated
Beyond making it easy to bless a helper tool, Blessed provides a complete Swift implementation of the non-deprecated
portions of macOS's [Authorization Services](https://developer.apple.com/documentation/security/authorization_services)
and [Service Management](https://developer.apple.com/documentation/servicemanagement)
frameworks. At a high level this framework exposes three closely related capabilities:
1. Requesting a user grant permission for one or more rights via macOS's Security Server
2. Defining custom rights in the Policy Database
3. Using `launchd` to install executables which will run with root privileges
3. Using `launchd` to install helper tool executables which will run with root privileges

For completeness the Service Management capability to enable and disable login items via `launchd` is also included; see
`LaunchdManager.enableLoginItem(forBundleIdentifier:)` and `LaunchdManager.disableLoginItem(forBundleIdentifier:)`.
Expand All @@ -30,15 +30,15 @@ For completeness the Service Management capability to enable and disable login i
In some more advanced circumstances you may to want directly interact with macOS's Security Server via the
`Authorization` class.

If you only need to check if a user can perform an operation, use `checkRights(_:environment:options:)`
without needing to create an `Authorization` instance.
If you only need to check if a user can perform an operation, use `checkRights(_:environment:options:)` without needing
to create an `Authorization` instance.

Otherwise you'll typically want to initialize an instance via `init()` and then subsequently request
rights with `requestRights(_:environment:options:)` or `requestRightsAsync(_:environment:options:callback:)`.
Otherwise you'll typically want to initialize an instance via `init()` and then subsequently request rights with
`requestRights(_:environment:options:)` or `requestRightsAsync(_:environment:options:callback:)`.

## Defining Custom Rights
macOS's authorization system is built around the concept of rights. The Policy Database contains definitions for all
of the rights on the system and your application can add its own.
macOS's authorization system is built around the concept of rights. The Policy Database contains definitions for all of
the rights on the system and your application can add its own.

If an application defines its own rights it can then use these to self-restrict functionality. For details on *why* you
might want to do see, consider reading Apple's [Technical Note TN2095: Authorization for Everyone](https://developer.apple.com/library/archive/technotes/tn2095/_index.html#//apple_ref/doc/uid/DTS10003110)
Expand All @@ -52,16 +52,17 @@ let rules: Set<AuthorizationRightRule> = [CannedAuthorizationRightRules.authenti
try myCustomRight.createOrUpdateDefinition(rules: rules, descriptionKey: description)
```

The above example creates a right called "com.example.MyApp.special-action" which requires that the user authenticate
as an admin. How exactly the user does so is up to macOS; your application does not concern itself with this. (At the
time of this documentation being written this means the user needing to type in a password, but in the future Apple
could for example update their implementation of the `authenticateAsAdmin` rule to use Touch ID.) When the user is asked
to authenticate they will see the message "MyApp would like to perform a special action."
The above example creates a right called "com.example.MyApp.special-action" which requires that the user authenticate as
an admin. How exactly the user does so is up to macOS; your application does not concern itself with this. (At the time
of this documentation being written this means the user needing to type in a password, but in the future Apple could for
example update their implementation of the `authenticateAsAdmin` rule to use Touch ID.) When the user is asked to
authenticate they will see the message "MyApp would like to perform a special action."

There are several optional parameters not used in this example, see the documentation for details.

If you need to create a rule which is not solely composed of already existing rules, you must create an authorization
plug-in, which is not covered by this framework. See [Using Authorization Plug-ins](https://developer.apple.com/documentation/security/authorization_plug-ins/using_authorization_plug-ins)
plug-in, which is not covered by this framework. See
[Using Authorization Plug-ins](https://developer.apple.com/documentation/security/authorization_plug-ins/using_authorization_plug-ins)
for more information.

## Sandboxing
Expand Down
62 changes: 8 additions & 54 deletions Sources/Blessed/Authorization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ import Foundation
/// deserialized for convenient transference between processes.
///
/// ## Topics
/// ### Initializers
/// ### Initializer
/// - ``init()``
/// - ``init(rights:environment:options:)``
///
/// ### Rights
/// - ``requestRights(_:environment:options:)``
Expand All @@ -39,58 +38,13 @@ public class Authorization: Codable {
/// This reference is only valid during the lifetime of its enclosing ``Authorization`` instance's lifetime.
public let authorizationRef: AuthorizationRef

/// Creates a new default instance.
/// Creates a new instance with no rights.
///
/// For applications that require a one-time authorization, see ``init(rights:environment:options:)``.
public convenience init() throws {
try self.init(rights: [], environment: [], options: [])
}

/// Creates a new customized instance.
///
/// Authorizing rights with this initializer is most useful for applications that require a one-time authorization. Otherwise use ``init()`` and make subsequent
/// calls to ``requestRights(_:environment:options:)`` or
/// To request rights, use ``requestRights(_:environment:options:)`` or
/// ``requestRightsAsync(_:environment:options:callback:)``.
///
/// When ``AuthorizationOption/interactionAllowed`` is provided, user interaction will happen when required. Failing to provide this option will
/// result in this initializer throwing ``AuthorizationError/interactionNotAllowed`` when interaction is required.
///
/// Providing ``AuthorizationOption/extendRights`` will extend the currently available rights. If this option is provided and initialization
/// succeeds then all the rights requested were granted. If this option is not provided the operation will almost certainly succeed, but no attempt will be made to
/// make the requested rights available. Call ``Authorization/requestRights(_:environment:options:)`` or
/// ``Authorization/requestRightsAsync(_:environment:options:callback:)`` to figure out which of the requested rights were
/// granted.
///
/// Providing ``AuthorizationOption/partialRights`` will cause this initializer to succeed if only some of the requested rights were granted. Unless
/// this option is provided this initializer will throw an error if not all the requested rights could be obtained.
///
/// Providing ``AuthorizationOption/preAuthorize`` will preauthorize the requested rights so that at a later time the obtained rights can be used in a
/// different process. Rights which can't be preauthorized will be treated as if they were authorized for the sake of throwing an error (in other words if all rights
/// are either authorized or could not be preauthorized this initializer will still succeed).
///
/// The rights which could not be preauthorized are not currently authorized and may fail to authorize when a later call to
/// ``requestRights(_:environment:options:)`` or ``requestRightsAsync(_:environment:options:callback:)`` is
/// made, unless the ``AuthorizationOption/extendRights`` and ``AuthorizationOption/interactionAllowed`` options are provided.
/// Even then they might still fail if the user does not supply the correct credentials.
///
/// - Parameters:
/// - rights: A set of ``AuthorizationRight`` instances containing rights for which authorization is being requested. If the set is empty, this
/// instance can be valid, but will be authorized for nothing.
/// - environment: A set of ``AuthorizationEnvironmentEntry`` instances containing environment state used when making the authorization
/// decision. Can be an empty set if no environment state needs to be provided.
/// - options: A set of ``AuthorizationOption`` instances to configure this authorization. Can be an empty set if no options are needed.
public init(rights: Set<AuthorizationRight>,
environment: Set<AuthorizationEnvironmentEntry>,
options: Set<AuthorizationOption>) throws {
// This double-level nesting is necessary because the sets passed to the closures rely internally on the
// withUnsafeMutableBufferPointer() function which has a pointer only valid within the scope of the closure.
// All of the pointers need to be in scope when the AuthorizationCreate() call is made.
self.authorizationRef = try rights.withUnsafePointer { rightsPointer in
return try environment.withUnsafePointer { environmentPointer in
return try AuthorizationError.throwIfFailure { authorization in
AuthorizationCreate(rightsPointer, environmentPointer, options.asAuthorizationFlags(), &authorization)
}
}
public init() throws {
self.authorizationRef = try AuthorizationError.throwIfFailure { authorization in
AuthorizationCreate(nil, nil, [], &authorization)
}
}

Expand Down Expand Up @@ -136,7 +90,7 @@ public class Authorization: Codable {
private static func deserialize(from serialization: Data) throws -> AuthorizationRef {
// Convert data into authorization external form
var int8Array = [CChar](repeating: 0, count: kAuthorizationExternalFormLength)
for index in 0...kAuthorizationExternalFormLength - 1 {
for index in 0..<kAuthorizationExternalFormLength {
int8Array[index] = CChar(bitPattern: serialization[index])
}
let bytes = (int8Array[0], int8Array[1], int8Array[2], int8Array[3],
Expand Down Expand Up @@ -297,7 +251,7 @@ public class Authorization: Codable {
}
}

/// Retrieves supporting information such as the user name gathered during evaluation of authorization.
/// Retrieves supporting information such as the user name gathered while requesting rights.
///
/// Information provided via
/// [`SetContextValue`](https://developer.apple.com/documentation/security/authorizationcallbacks/1543148-setcontextvalue)
Expand Down
24 changes: 11 additions & 13 deletions Sources/Blessed/AuthorizationElements.swift
Original file line number Diff line number Diff line change
Expand Up @@ -190,18 +190,18 @@ public struct AuthorizationEnvironmentEntry: AuthorizationElement, Hashable {

/// Information about an ``Authorization``.
///
/// This struct cannot be initialized, it is returned by ``Authorization/retrieveInfo(tag:)``.
struct AuthorizationInfo: AuthorizationElement {
/// Package internal struct used by ``Authorization/retrieveInfo(tag:)``.
internal struct AuthorizationInfo: AuthorizationElement {
/// The name of this info.
public let name: String
let name: String
/// The value asociated with this info.
///
/// The specific format of this value can differ for each `AuthorizationInfo` instance, but in practice is commonly a UTF8 encoded C string.
public let value: ContiguousArray<CChar>
/// In practice this never appears to be used and so is not exposed.
internal let flags: UInt32
let value: ContiguousArray<CChar>
/// In practice this never appears to be used.
let flags: UInt32

internal init(name: String, value: ContiguousArray<CChar>, flags: UInt32) {
init(name: String, value: ContiguousArray<CChar>, flags: UInt32) {
self.name = name
self.value = value
self.flags = flags
Expand Down Expand Up @@ -265,18 +265,16 @@ internal extension AuthorizationItem {
}
}

return T.init(name: name, value: value, flags: self.flags)
return T(name: name, value: value, flags: self.flags)
}
}

internal extension AuthorizationItemSet {
func wrap<T>(type: T.Type) -> [T] where T:AuthorizationElement {
var items = [T]()
if self.count > 0 {
for index in 0...Int(self.count) - 1 {
if let item = self.items?.advanced(by: index).pointee {
items.append(item.wrap(type: type))
}
for index in 0..<Int(self.count) {
if let item = self.items?.advanced(by: index).pointee {
items.append(item.wrap(type: type))
}
}

Expand Down
26 changes: 9 additions & 17 deletions Sources/Blessed/Blessed.docc/Blessed.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ frameworks is provided along with additional convenience functions. At a high le
related capabilities:
1. Requesting a user grant permission for one or more rights via macOS's Security Server
2. Defining custom rights in the Policy Database
3. Using `launchd` to install executables which will run with root privileges
3. Using `launchd` to install helper tool executables which will run with root privileges

For completeness the Service Management capability to enable and disable login items via `launchd` is also included; see
``LaunchdManager/enableLoginItem(forBundleIdentifier:)`` and ``LaunchdManager/disableLoginItem(forBundleIdentifier:)``.

## Installing a Root Privileged Executable
## Installing a Root Privileged Helper Tool
Because in practice #1 is so often done in order to perform #3, this framework provides the
``LaunchdManager/authorizeAndBless(message:icon:)`` function which combines both into just one call:
```swift
Expand All @@ -28,8 +28,8 @@ try LaunchdManager.authorizeAndBless(message: message, icon: icon)
Both the `message` and `icon` parameters are optional. Defaults will be provided by macOS if they are not specified.

## Defining Custom Rights
macOS's authorization system is built around the concept of rights. The Policy Database contains definitions for all
of the rights on the system and your application can add its own.
macOS's authorization system is built around the concept of rights. The Policy Database contains definitions for all of
the rights on the system and your application can add its own.

If an application defines its own rights it can then use these to self-restrict functionality. For details on *why* you
might want to do see, consider reading Apple's [Technical Note TN2095: Authorization for Everyone](https://developer.apple.com/library/archive/technotes/tn2095/_index.html#//apple_ref/doc/uid/DTS10003110)
Expand All @@ -43,11 +43,11 @@ let rules: Set<AuthorizationRightRule> = [CannedAuthorizationRightRules.authenti
try myCustomRight.createOrUpdateDefinition(rules: rules, descriptionKey: description)
```

The above example creates a right called "com.example.MyApp.special-action" which requires that the user authenticate
as an admin. How exactly the user does so is up to macOS; your application does not concern itself with this. (At the
time of this documentation being written this means the user needing to type in a password, but in the future Apple
could for example update their implementation of the `authenticateAsAdmin` rule to use Touch ID.) When the user is asked
to authenticate they will see the message "MyApp would like to perform a special action."
The above example creates a right called "com.example.MyApp.special-action" which requires that the user authenticate as
an admin. How exactly the user does so is up to macOS; your application does not concern itself with this. (At the time
of this documentation being written this means the user needing to type in a password, but in the future Apple could for
example update their implementation of the `authenticateAsAdmin` rule to use Touch ID.) When the user is asked to
authenticate they will see the message "MyApp would like to perform a special action."

There are several optional parameters not used in this example, see
``AuthorizationRight/createOrUpdateDefinition(rules:authorization:descriptionKey:bundle:localeTableName:comment:)`` for
Expand Down Expand Up @@ -90,28 +90,20 @@ let sandboxed = try NSApplication.shared.isSandboxed()
```

## Topics

### Authorization

- ``Authorization``
- ``AuthorizationRight``
- ``AuthorizationEnvironmentEntry``
- ``AuthorizationOption``

### Authorization Policy Database

- ``AuthorizationRight``
- ``AuthorizationRightRule``
- ``CannedAuthorizationRightRules``
- ``AuthorizationRightDefinition``
- ``AuthorizationRightDefinitionClass``
- ``AuthorizationMechanism``

### Authorization Errors

- ``AuthorizationError``

### launchd Registration

- ``LaunchdManager``
- ``LaunchdError``
Loading

0 comments on commit ef63245

Please sign in to comment.