Configuration Provider


Enterprise applications often require an onboarding mechanism to retrieve initial configuration data, for example parameters required to connect to a backend service, such as a server URL and port number. The ConfigurationLoader class provides easy and consistent access to configuration data from multiple types of sources. A configuration loader acts as a container and processor of configuration providers - or classes that implement the ConfigurationProviding protocol. The SDK supports configuration providers that fetch data through a specified method, including:

For more information, see: Configure the App with a Configuration Provider.

Throughout this reference guide, the list of configuration providers contained in a configuration loader is referred to as the queue.

Below are suggested naming conventions to use for your basic app configuration parameters if it connects to a backend system connected to by SAP BTP:

  • backendURL – Host information of backend
  • applicationID – ID of the application on the Cloud Platform
  • applicationVersion – Version of the application

For provider implementations provided by SAP, see their respective sections in the SAPFoundation API reference.

Creating a ConfigurationLoader


You can initialize a ConfigurationLoader with a set of default parameters:

let myLoader = ConfigurationLoader()

To obtain configuration data, call loadConfiguration(). This causes the loader to begin processing its queue:

myLoader.loadConfiguration()

Although init(delegate:providers:outputHandler:) allows for this type of usage, use a non-default queue structure, or store retrieved configuration data in a specialized manner to receive queue processing information. These parameters are discussed in greater detail below.

Delegation Using a ConfigurationLoaderDelegate


Normally, you initialize the loader with a ConfigurationLoaderDelegate for its delegate argument. The delegate provides a mechanism for communicating information back to your application. For example, when a configuration loader encounters an error; when queue processing is complete; or when input is required. A configuration loader uses configurationProvider(_:result:) to notify the delegate that queue processing has finished. When this message is sent, provider contains the provider that obtained the configuration data and result are true. If no providers in the queue successfully obtained data, result is false and provider is the last object in the queue.

It is possible that a provider in the queue encounters an error. If this occurs, the loader sends the delegate configurationProvider(_:error:). provider contains the provider that encountered the error and error contains the Error object. The delegate may do something in response, such as log the error. After sending this message, the loader moves on to the next provider in its queue.

Some provider implementations may require input. In the case of DiscoveryServiceConfigurationProvider, a user’s email address is required to look up configuration data from the SAP Discovery Service. The loader sends configurationProvider(_:requestedInput:completionHandler:) if input was not provided with loadConfiguration(userInputs:). The required input is described as a Dictionary of Dictionary objects where the outer key contains the provider that needs the input, and the outer value is a Dictionary describing the needed item itself. The inner keys are the required items. This collection’s inner values can be filled in and provided through loadConfiguration(userInputs:). With this pattern, inputs can be gathered before queue processing begins (e.g., if the inputs are cached by the application), or in response to a failure to find configuration data (e.g., if the cached inputs are no longer valid).

Here is an example implementation of a ConfigurationLoaderDelegate that responds appropriately to a DiscoveryServiceConfigurationProvider‘s request for input. Comments show sample values of input received by the delegate and the response provided to loadConfiguration(userInputs:):

class MyClass: ConfigurationLoaderDelegate {
  func configurationProvider(_ provider: ConfigurationProviding, didCompleteWith result: Bool) {
    // Do something on completion
  }

  func configurationProvider(_ provider: ConfigurationProviding, didEncounter error: Error) {
    // Do something with the error
  }

  func configurationProvider(_ provider: ConfigurationProviding, requestedInput: [String: [String: Any]], completionHandler: @escaping (_ input: [String: [String: Any]]) -> ()) {
    // Structure `requestedInput`:
    // ["com.sap.configuration.provider.discoveryservice": ["emailAddress": ""]]

    var input =  [String: [String: Any]]()

    if requestedInput[ConfigurationProviderNames.DiscoveryService] ? [ConfigurationProviderInputKeys.emailAddress.rawValue] == nil {
      completionHandler(input)
      return
    }   

    self.presentEmailAddressPrompt { emailAddress in
      input[ConfigurationProviderNames.DiscoveryService] = [ConfigurationProviderInputKeys.emailAddress.rawValue: emailAddress as Any]
      completionHandler(input)
    }

    private func presentEmailAddressPrompt(completionHandler: @escaping(String?) -> ()) {
      // Implement here a UI that requests user's email address and finally call the completion handler
    }
  }
}

Using Your Own Provider Queue


providers contains the queue of configuration providers. When nil is passed for this argument, the default queue is constructed. The default queue is structured as follows:

  1. ManagedConfigurationProvider
  2. FileConfigurationProvider
  3. DiscoveryServiceConfigurationProvider
  4. JSONConfigurationProvider

Alternatively, a queue of a different size or ordering can be constructed by passing your own collection of providers for this parameter:

var providers = [ConfigurationProviding]()
providers.append(ManagedConfigurationProvider())
providers.append(DiscoveryServiceConfigurationProvider())

let myLoader = ConfigurationLoader(delegate: self,
                                    providers: providers,
                                    outputHandler: nil)

In this example, myLoader.providers looks like:

  1. ManagedConfigurationProvider
  2. DiscoveryServiceConfigurationProvider

Storing Configuration Data


outputHandler can be used to supply an implementation of the ConfigurationPersisting protocol, which allows configuration data to be stored using any desired method, such as writing the data to a preferred UserDefaults key or encrypting it before storing. It can also be nil (default), in which case the loader uses its own implementation of ConfigurationPersisting. The default implementation writes configuration data to the UserDefaults key com.sap.configuration.provider.configurationstore - similar to Apple’s method of writing a managed app’s configuration data to com.apple.configuration.managed.

If some ConfigurationPersisting-conforming object was supplied for outputHandler, configuration data will not be written to the UserDefaults key mentioned above, and instead that object’s persistConfiguration(_:) is called, allowing the configuration data to be consumed in any chosen way. Although the protocol does not require you to implement a method to read saved data, you can implement such a method to read the data at a later time.

Retrieving Configuration Data


Process the queue for configuration data by calling loadConfiguration().

  // Process queue with no input provided...
  myLoader.loadConfiguration()

  // ...or, as in the delegate example above,
  // provide some already-obtained input (if required)
  myLoader.loadConfiguration(userInputs: myUIDialogInput)

Providers in the queue are examined, in order, for configuration data, by calling the provider object’s provideConfiguration(input:) method.

If a provider requires input that was not already supplied, the delegate receives the configurationProvider(_:requestedInput:completionHandler:) message. If a provider encounters an error, the delegate receives the configurationProvider(_:error:) message.

If configuration data is found after executing any provider’s provideConfiguration(input:) method, or if every provider’s provideConfiguration(input:) has been executed with no configuration data found, queue processing ceases. If nil was provided for the outputHandler parameter, any found configuration data is written to UserDefaults under the key com.sap.configuration.provider.configurationstore, otherwise outputHandler’s implementation of persistConfiguration(_:) is called.

When queue processing ceases, the delegate receives the configurationProvider(_:result:) message.

See the Delegation Using a ConfigurationLoaderDelegate above for more information about the delegate methods.

URL Configuration Provider


The SDK supports two way of obtaining configuration from URL:

  • By Apple specific Universal Links
  • By traditional application(_:open:options:) application delegate method

In order to use universal links in your application, you need to configure the mobile application on your SAPcpms tenant and your native application.

Configuring the SAPcpms mobile application

  • Go to you mobileservices page and select your mobile application.
  • Navigate to the “Application Links” tab and fill in the required fields.

Configuring the native mobile application

  • Open your Xcode project and navigate to your project settings menu.
  • Select the Capabilities tab and enable the Associated Domains option.
  • Add a new domain as described in the Apple documentation: “applinks:<#your SAPcpms tenant host, without the http(s) prefix#>”.
  • Implement the necessary application delegate methods as described below.

Instantiate the provider in your application delegate “didFinishLaunchingWithOptions” method, before the return statement. This is important as the delegate call containing the universal link happens right after the return statement, and your provider must exists by that time. You must also register the created provider if your application is not onboarded yet. If your application supports SceneDelegate, register it as SceneDelegateObserver for the SceneDelegateDispatcher, otherwise as AppDelegateObserver for the AppDelegateDispatcher.

// If App life cycle is handled by AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // TODO: check if your application is onboarded or not.
    // Add the following lines if your application is NOT onboarded yet.
    let provider = URLConfigurationProvider()
    AppDelegateDispatcher.register(provider)
    return true
}

// If App life cycle is handled by SceneDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
    // TODO: check if your application is onboarded or not.
    // Add the following lines if your application is NOT onboarded yet.
    let provider = URLConfigurationProvider()
    SceneDelegateDispatcher.register(provider)
    return true
}

This is important as the delegate call containing the universal link happens right after the return statement, and your provider must exist by that time. If your app supports Scenedelegate, in the scene(:willConnectTo:options:) and scene(:willContinueUserActivityWithType:) methods, forward the call to the SceneDelegateDispatcher. Otherwise in case of AppDelegate, in the application(_:continue:restorationHandler:) forward the call to the AppDelegateDispatcher.

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    return AppDelegateDispatcher.application(application, continue: userActivity, restorationHandler: restorationHandler)
}

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions){        
    SceneDelegateDispatcher.scene(scene, willConnectTo: session, options: connectionOptions)
}

func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
    SceneDelegateDispatcher.scene(scene, continue: userActivity)
}

At the end of the onboarding process, if successful, unregister the provider from the AppDelegateDispatcher

...
// At this point we have onboarded successfully
let provider = <#Acquire reference to the URLConfigurationProvider created in the didFinishLaunchingWithOptions application delegate method#>
AppDelegateDispatcher.unregister(provider)
SceneDelegateDispatcher.unregister(provider)
...

Security considerations

The parameters that are passed in to the app via the Universal Link are not guaranteed to be correct. An attacker could form malicious URLs that would make the app connect to the attacker’s system by replacing the link parameters and making users tap on the fraud link. To avoid such phishing attacks, end users need to be made aware that they should only tap on links coming from a trusted source. In addition, we recommend using onboarding via Universal Link in combination with OAuth2 authentication and running the authentication in an ASAuthenticationSession using the SDK’s ASWebAuthenticationSessionPresenter. This way, the operating system prompts the user before opening the authentication flow and shows the actual host that they will be connected to. This mitigates the risk of an attacker taking end users to hostile servers for authentication in order to steal user passwords.

  • A ConfigurationLoader acts as a container of configuration providers (objects that conform to the ConfigurationProviding protocol), and provides an interface to access them. This class acts as a mediator between configuration providers and the application, providing callback mechanisms through a ConfigurationLoaderDelegate. In addition, an alternate method to save data may be supplied by an object that implements the ConfigurationPersisting protocol.

    See more

    Declaration

    Swift

    open class ConfigurationLoader
  • Delegate pattern used by ConfigurationLoader.

    See more

    Declaration

    Swift

    public protocol ConfigurationLoaderDelegate : AnyObject
  • Configuration provider protocol used to save data in some specified way implemented in the persistConfiguration(_:) method. Classes that implement this protocol may be passed as a desired outputHandler to a ConfigurationLoader during initialization. When ConfigurationLoader processes its queue and finds configuration data, the data will be passed to the supplied ConfigurationPersisting object’s persistConfiguration(_:) method as the configurationData parameter.

    If you choose to receive configuration data in this way, it is up to you to save the data (or not) - it will not be saved or retained by the ConfigurationLoader that supplied the data. By extension, you will also need a method to retrieve data that has been saved using your persistConfiguration(_:) implementation if your app will be utilizing that data later on. An example use case for a class implementing this protocol would be if a developer wishes to encrypt configuration data before writing it to some location, or wishes to write the data to a location other than ConfigurationLoader‘s implementation of this protocol (which is in UserDefaults under ConfigurationProviderUserDefaultsKey).

    See more

    Declaration

    Swift

    public protocol ConfigurationPersisting
  • Dictionary keys used when describing input required by a configuration provider.

    See more

    Declaration

    Swift

    public struct ConfigurationProviderInputKeys : RawRepresentable
  • SAP default (built-in) configuration provider names.

    See more

    Declaration

    Swift

    public struct ConfigurationProviderNames
  • Default UserDefaults location where configuration data is written when nil is passed for the ouputHandler parameter in ConfigurationLoader.init(delegate:providers:outputHandler:). This is similar to the pattern used by Apple for managed app configuration data.

    Declaration

    Swift

    public let ConfigurationProviderUserDefaultsKey: String
  • Definition that configuration providers must adhere to.

    See more

    Declaration

    Swift

    public protocol ConfigurationProviding
  • Describes a structure that can be assembled from SAP BTP configuration.

    See more

    Declaration

    Swift

    public protocol DiscoveryServiceConfigurable
  • Built-in configuration provider that obtains data from the SAP Discovery Service.

    Examples

    Example for obtaining configuration from the global Discovery Service with an email address when using domain-based configuration.

    let provider = DiscoveryServiceConfigurationProvider(applicationID: "")
    let (providerSuccess, configuration, _) = provider.provideConfiguration(input:  [ConfigurationProviderInputKeys.emailAddress.rawValue : "example@sap.com"])
    if providerSuccess {
        // use configuration
    }
    

    Example for obtaining configuration from the global Discovery Service with an onboarding code when using onboardingCode-based configuration.

    let provider = DiscoveryServiceConfigurationProvider(applicationID: "")
    let (providerSuccess, configuration, _) = provider.provideConfiguration(input:  [ConfigurationProviderInputKeys.onboardingCode.rawValue : "12345678"])
    if providerSuccess {
        // use configuration
    }
    

    Further Documentation

    See Enabling Applications to Discover Configurations and Using the Configuration Discovery Service for domain-based configuration.

    See Discovery Service - Onboarding Codes for onboarding-code-based configuration.

    See more

    Declaration

    Swift

    open class DiscoveryServiceConfigurationProvider : ConfigurationProviding
  • Built-in configuration provider that obtains data from values in the application’s property list file. By default, configuration data will be read from ConfigurationProvider.plist. Alternatively, you can specify your own property list file by passing the name (sans extension) for the configurationFilename parameter when calling init(_:).

    See more

    Declaration

    Swift

    open class FileConfigurationProvider : ConfigurationProviding
  • Built-in configuration provider that obtains data supplied via application defined user interaction.

    See more

    Declaration

    Swift

    open class JSONConfigurationProvider : ConfigurationProviding
  • Built-in configuration provider that obtains data from an MDM managed application. A managed application may have data written under the UserDefaults key com.apple.configuration.managed. This is the location chosen by Apple for managed applications. For more information, see Managed App Configuration Sample Code.

    See more

    Declaration

    Swift

    open class ManagedConfigurationProvider : ConfigurationProviding
  • This class is used with the ConfigurationLoader class. Uses the Apple Universal Links functionality to obtain and provide the configuration information for the application to use. If your app supports Appdelegate to handle App life cycle, application(_:continue:restorationHandler:) AppDelegate method acquire the necessary URL as described in Apple documentation. Use the AppDelegateDispatcher class to broadcast the invocation of said delegate for this class.

    The corresponding AppDelegate method is invoked right after the return of didFinishLaunchingWithOptions. This configuration provider needs to exist by that time, so instantiate it before returning from that method. You need to register and unregister this provider as AppDelegateObserving to avoid memory leaks - see code example below.

     // ... in the UIApplicationDelegate file
     func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
        AppDelegateDispatcher.register(provider)
        let result = AppDelegateDispatcher.application(application, continue: userActivity, restorationHandler: restorationHandler)
        AppDelegateDispatcher.unregister(provider)
        return result
     }
    

    In case of SceneDelegate, scene(_:openURLContexts:) SceneDelegate method acquire the necessary URL as described in Apple documentation. Use the SceneDelegateDispatcher class to broadcast the invocation of said delegate for this class.

    The corresponding SceneDelegate method is invoked right after the return of didFinishLaunchingWithOptions. This configuration provider needs to exist by that time, so instantiate it before returning from that method. You need to register and unregister this provider as SceneDelegateObserving to avoid memory leaks - see code example below.

     // ... in the UIWindowSceneDelegate file
    
     func scene(_ scene: UIScene, continue userActivity: NSUserActivity){
         SceneDelegateDispatcher.register(provider)
         SceneDelegateDispatcher.scene(scene, continue: userActivity)
         SceneDelegateDispatcher.unregister(provider)
     }
     func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
         SceneDelegateDispatcher.register(provider)
         SceneDelegateDispatcher.scene(scene, willConnectTo: session, options: connectionOptions)
         SceneDelegateDispatcher.unregister(provider)
    }
    
    See more

    Declaration

    Swift

    open class URLConfigurationProvider : ConfigurationProviding, AppDelegateObserving
    extension URLConfigurationProvider: SceneDelegateObserving
  • Set of errors that can occur during the configuration parsing.

    • invalid: the provided URL is invalid, or has invalid parts
    • missing: the provided URL has missing arguments
    • missingConfiguration: the application was not launched with a configuration url
    See more

    Declaration

    Swift

    public enum URLConfigurationProviderError : Error
    extension URLConfigurationProviderError: SAPError