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:
DiscoveryServiceConfigurationProvider
queries the SAP Discovery Service for data available to a given domain and app identifier. See Enabling Applications to Discover Configurations and Using the Configuration Discovery Service.ManagedConfigurationProvider
uses an MDM/EMM service that pushes data to the app.
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:
ManagedConfigurationProvider
FileConfigurationProvider
DiscoveryServiceConfigurationProvider
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:
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
See moreConfigurationLoader
acts as a container of configuration providers (objects that conform to theConfigurationProviding
protocol), and provides an interface to access them. This class acts as a mediator between configuration providers and the application, providing callback mechanisms through aConfigurationLoaderDelegate
. In addition, an alternate method to save data may be supplied by an object that implements theConfigurationPersisting
protocol.Declaration
Swift
open class ConfigurationLoader
-
Delegate pattern used by
See moreConfigurationLoader
.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 desiredoutputHandler
to aConfigurationLoader
during initialization. WhenConfigurationLoader
processes its queue and finds configuration data, the data will be passed to the suppliedConfigurationPersisting
object’spersistConfiguration(_:)
method as theconfigurationData
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
See moreConfigurationLoader
that supplied the data. By extension, you will also need a method to retrieve data that has been saved using yourpersistConfiguration(_:)
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 thanConfigurationLoader
‘s implementation of this protocol (which is inUserDefaults
underConfigurationProviderUserDefaultsKey
).Declaration
Swift
public protocol ConfigurationPersisting
-
See moreDictionary
keys used when describing input required by a configuration provider.Declaration
Swift
public struct ConfigurationProviderInputKeys : RawRepresentable
-
SAP default (built-in) configuration provider names.
See moreDeclaration
Swift
public struct ConfigurationProviderNames
-
Default
UserDefaults
location where configuration data is written whennil
is passed for theouputHandler
parameter inConfigurationLoader.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 moreDeclaration
Swift
public protocol ConfigurationProviding
-
Describes a structure that can be assembled from SAP BTP configuration.
See moreDeclaration
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 moreDeclaration
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
See moreConfigurationProvider.plist
. Alternatively, you can specify your own property list file by passing the name (sans extension) for theconfigurationFilename
parameter when callinginit(_:)
.Declaration
Swift
open class FileConfigurationProvider : ConfigurationProviding
-
Built-in configuration provider that obtains data supplied via application defined user interaction.
See moreDeclaration
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
See moreUserDefaults
keycom.apple.configuration.managed
. This is the location chosen by Apple for managed applications. For more information, see Managed App Configuration Sample Code.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 theAppDelegateDispatcher
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 theSceneDelegateDispatcher
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.
See more// ... 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) }
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
Declaration
Swift
public enum URLConfigurationProviderError : Error
extension URLConfigurationProviderError: SAPError