Article

Changing Access Controls on User Data

Restrict access to or lift restrictions on a user's data at their request.

Overview

Users can ask you to prevent any further changes to their data stored by your app in CloudKit. Use the restrict API provided by CloudKit Web Services to honor those requests. You can lift restrictions at the user's request by calling the unrestrict API.

Identify Containers

To be sure that you'll be restricting changes and access to all of a user's data stored by your app, first cross-reference the list of containers your app has access to in Xcode and assemble a list of those containers' identifiers. This process is described in Identifying an App's Containers.

The example below stores containers in constants to be used later.

let defaultContainer = CKContainer.default()
let documents = CKContainer(identifier: "iCloud.com.example.myexampleapp.documents")
let settings = CKContainer(identifier: "iCloud.com.example.myexampleapp.settings")

Create Reusable API Tokens

The restrict API call requires a token each time you call the API. You create an API token once for each container in your app using the CloudKit Dashboard and reuse it in each API call to a specific container.

Generate a token in the CloudKit Dashboard by visiting the page for each container, then selecting API Access > New Token > Create Token. Tokens are specific to a deployment environment, so separate tokens are required for the production and development environments.

The example below stores tokens in a dictionary associated with each container for use later.

let containerAPITokens: [CKContainer: String] = [
    defaultContainer: "<# Token for the default container #>",
    documents: "<# Token for a custom container #>",
    settings: "<# Token for another custom container #>"
]

let containers = Array(containerAPITokens.keys)

Create Web Authentication Tokens

The restrict API call requires a new authentication token each time you call the API in addition to the reusable API token. The example below shows how to create that token by using a CKFetchWebAuthTokenOperation.

for container in containers {
    guard let apiToken = containerAPITokens[container] else {
        continue
    }
    
    let fetchAuthorization = CKFetchWebAuthTokenOperation(apiToken: apiToken)
    
    fetchAuthorization.fetchWebAuthTokenCompletionBlock = { webToken, error in
        guard let webToken = webToken, error == nil else { return }
        
        restrict(container: container, apiToken: apiToken, webToken: webToken) { error in
            guard error == nil else {
                 print("Restriction failed. Reason: ", error!)
                 return
            }
            print("Restriction succeeded.")
        }
    }
    
    container.privateCloudDatabase.add(fetchAuthorization)
}

Once the authentication token is received, you can immediately call the restrict API once for each container.

Restrict Data Access in Each Container

The example below defines the restrict(container:apiToken:webToken:completionHandler:) and requestRestriction(url:completionHandler:) methods used in the example above to build the network request for the restrict API.

func requestRestriction(url: URL, completionHandler: @escaping (Error?) -> Void) {
    let task = URLSession.shared.dataTask(with: url) { data, response, error in
        if let error = error {
            completionHandler(error)
            return
        }
        guard let httpResponse = response as? HTTPURLResponse,
            (200...299).contains(httpResponse.statusCode) else {
                completionHandler(RestrictError.failure)
                return
        }
        
        print("Restrict result", httpResponse)
        
        // Other than indicating success or failure, the `restrict` API doesn't return actionable data in its response.
        if data != nil {
            completionHandler(nil)
        } else {
            completionHandler(RestrictError.failure)
        }
    }
    task.resume()
}

/// A utility function that percent encodes a token for URL requests.
func encodeToken(_ token: String) -> String {
    return token.addingPercentEncoding(
        withAllowedCharacters: CharacterSet(charactersIn: "+/=").inverted
    ) ?? token
}

/// An error type that represents a failure in the `restrict` API call.
enum RestrictError: Error {
    case failure
}

func restrict(container: CKContainer, apiToken: String, webToken: String, completionHandler: @escaping (Error?) -> Void) {
    let webToken = encodeToken(webToken)
    
    let identifier = container.containerIdentifier!
    let env = "production" // Use "development" during development.
    let baseURL = "https://api.apple-cloudkit.com/database/1/"
    let apiPath = "\(identifier)/\(env)/private/users/restrict"
    let query = "?ckAPIToken=\(apiToken)&ckWebAuthToken=\(webToken)"
    
    let url = URL(string: "\(baseURL)\(apiPath)\(query)")!
    
    requestRestriction(url: url, completionHandler: completionHandler)
}

Lift Restrictions

When a user requests that you undo restrictions, you use the unrestrict API, which performs the opposite operation that the restrict API performs.

The example below shows a modification of the restrict(container:apiToken:webToken:completionHandler:) method used above that lifts restrictions instead of enabling them.

func unrestrict(container: CKContainer, apiToken: String, webToken: String, completionHandler: @escaping (Error?) -> Void) {
    let webToken = encodeToken(webToken)
    
    let identifier = container.containerIdentifier!
    let env = "production" // Use "development" during development.
    let baseURL = "https://api.apple-cloudkit.com/database/1/"
    let apiPath = "\(identifier)/\(env)/private/users/unrestrict"
    let query = "?ckAPIToken=\(apiToken)&ckWebAuthToken=\(webToken)"
    
    let url = URL(string: "\(baseURL)\(apiPath)\(query)")!
    
    requestRestriction(url: url, completionHandler: completionHandler)
}

A successful call to the unrestrict(container:apiToken:webToken: completionHandler:) function—indicated by a nil error parameter in the completion handler—means that your app's subsequent uses of CloudKit can access and modify user data once again.

See Also

User Data Management

Providing User Access to CloudKit Data

Give users access to the data your app stores on their behalf.

Responding to Requests to Delete Data

Provide options for users to delete their CloudKit data from your app.

Identifying an App's Containers

Use Xcode's project navigator to find the identifiers of active CloudKit containers.