From 2ceb9d64c14d84233a9e9cc869bb302bfa249ff0 Mon Sep 17 00:00:00 2001 From: Nick Cooke <36927374+ncooke3@users.noreply.github.com> Date: Thu, 16 Jul 2020 11:13:04 -0400 Subject: [PATCH] Remote Config Console API for Testing (#6009) * Added Remote Config Console API and some tests * Added workflow scripts * Update path in remote config workflow * Update path in remote config workflow 2.0 * Adding print statements to debug * Update generate_access_token.sh * Style and script cleanup * Updated README and script * reordering workflow tests for faster dev * Updated generate_access_token.sh * Removed unneeded part of path * Set env var in GHA rather than script * Fix env var in GHA * Fix env var in GHA 2.0 * Refactored actions into one script * removed back slash * Removed unneeded workflow step * Refactored script and added comments * Minor tweaks * Added new line * well nvm that tweak * Reordered workflow steps back to original order * Only generate access token for ios runs * Fixed readme, organized tests, improved RemoteConfigConsole * Fixed test * reordering tests for dev * Removed README.md * Added local dev mode for script * Reset the ordering of tests to original order * Update README * Update README again * Trailing space * trailing space..again * Cleanup up cloned repo in script * Resolved feedback * Style * Gotta love trailing spaces * Resolved feedback 2.0 * Resolved feedback 3.0 * Changed param type for update APIs * Fixed type in updateRemoteConfigValue method --- .github/workflows/remoteconfig.yml | 4 + FirebaseRemoteConfig.podspec | 3 +- .../Tests/SwiftAPI/APITests.swift | 190 +++++++++++++- .../Tests/SwiftAPI/AccessToken.json | 5 + .../Tests/SwiftAPI/RemoteConfigConsole.swift | 239 ++++++++++++++++++ .../Tests/SwiftAPI/Resources/README.md | 54 ++++ scripts/generate_access_token.sh | 94 +++++++ .../ServiceAccount.json.gpg | Bin 0 -> 1735 bytes 8 files changed, 580 insertions(+), 9 deletions(-) create mode 100644 FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json create mode 100644 FirebaseRemoteConfig/Tests/SwiftAPI/RemoteConfigConsole.swift create mode 100644 FirebaseRemoteConfig/Tests/SwiftAPI/Resources/README.md create mode 100755 scripts/generate_access_token.sh create mode 100644 scripts/gha-encrypted/RemoteConfigSwiftAPI/ServiceAccount.json.gpg diff --git a/.github/workflows/remoteconfig.yml b/.github/workflows/remoteconfig.yml index dd8b5a879e..c696cdbee9 100644 --- a/.github/workflows/remoteconfig.yml +++ b/.github/workflows/remoteconfig.yml @@ -30,6 +30,10 @@ jobs: - name: Install Secret GoogleService-Info.plist run: scripts/decrypt_gha_secret.sh scripts/gha-encrypted/RemoteConfigSwiftAPI/GoogleService-Info.plist.gpg \ FirebaseRemoteConfig/Tests/SwiftAPI/GoogleService-Info.plist "$plist_secret" + - name: Generate Access Token for RemoteConfigConsoleAPI in IntegrationTests + if: matrix.target == 'iOS' + run: scripts/generate_access_token.sh "$plist_secret" scripts/gha-encrypted/RemoteConfigSwiftAPI/ServiceAccount.json.gpg + FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json - name: BuildAndUnitTest # can be replaced with pod lib lint with CocoaPods 1.10 run: scripts/third_party/travis/retry.sh scripts/build.sh RemoteConfig ${{ matrix.target }} unit - name: Fake Console API Tests diff --git a/FirebaseRemoteConfig.podspec b/FirebaseRemoteConfig.podspec index 89e4e6aa30..669271a16c 100644 --- a/FirebaseRemoteConfig.podspec +++ b/FirebaseRemoteConfig.podspec @@ -87,7 +87,8 @@ app update. swift_api.pod_target_xcconfig = { 'SWIFT_OBJC_BRIDGING_HEADER' => '$(PODS_TARGET_SRCROOT)/FirebaseRemoteConfig/Tests/FakeUtils/Bridging-Header.h' } - swift_api.resources = 'FirebaseRemoteConfig/Tests/SwiftAPI/GoogleService-Info.plist' + swift_api.resources = 'FirebaseRemoteConfig/Tests/SwiftAPI/GoogleService-Info.plist', + 'FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json' swift_api.dependency 'OCMock' end diff --git a/FirebaseRemoteConfig/Tests/SwiftAPI/APITests.swift b/FirebaseRemoteConfig/Tests/SwiftAPI/APITests.swift index 015ac4a1c6..7ce3aa3ef2 100644 --- a/FirebaseRemoteConfig/Tests/SwiftAPI/APITests.swift +++ b/FirebaseRemoteConfig/Tests/SwiftAPI/APITests.swift @@ -17,11 +17,37 @@ import FirebaseCore import XCTest +/// String constants used for testing. +private enum Constants { + static let key1 = "Key1" + static let jedi = "Jedi" + static let sith = "Sith_Lord" + static let value1 = "Value1" + static let obiwan = "Obi-Wan" + static let yoda = "Yoda" + static let darthSidious = "Darth Sidious" +} + class APITests: APITestBase { + var console: RemoteConfigConsole! + override func setUp() { super.setUp() if APITests.useFakeConfig { - fakeConsole.config = ["Key1": "Value1"] + fakeConsole.config = [Constants.key1: Constants.value1] + } else { + console = RemoteConfigConsole() + console.updateRemoteConfigValue(Constants.obiwan, forKey: Constants.jedi) + } + } + + override func tearDown() { + super.tearDown() + + // If using RemoteConfigConsole, reset remote config values. + if !APITests.useFakeConfig { + console.removeRemoteConfigValue(forKey: Constants.sith) + console.removeRemoteConfigValue(forKey: Constants.jedi) } } @@ -34,7 +60,7 @@ class APITests: APITestBase { XCTAssertEqual(status, RemoteConfigFetchStatus.success) self.config.activate { _, error in XCTAssertNil(error) - XCTAssertEqual(self.config["Key1"].stringValue, "Value1") + XCTAssertEqual(self.config[Constants.key1].stringValue, Constants.value1) expectation.fulfill() } } @@ -50,7 +76,7 @@ class APITests: APITestBase { XCTAssertEqual(status, RemoteConfigFetchStatus.success) self.config.activate { _, error in XCTAssertNil(error) - XCTAssertEqual(self.config["Key1"].stringValue, "Value1") + XCTAssertEqual(self.config[Constants.key1].stringValue, Constants.value1) expectation.fulfill() } } @@ -63,7 +89,7 @@ class APITests: APITestBase { if let error = error { XCTFail("Fetch and Activate Error \(error)") } - XCTAssertEqual(self.config["Key1"].stringValue, "Value1") + XCTAssertEqual(self.config[Constants.key1].stringValue, Constants.value1) expectation.fulfill() } waitForExpectations() @@ -82,7 +108,7 @@ class APITests: APITestBase { if let error = error { print("Activate Error \(error)") } - XCTAssertEqual(self.config["Key1"].stringValue, "Value1") + XCTAssertEqual(self.config[Constants.key1].stringValue, Constants.value1) expectation.fulfill() } } @@ -98,7 +124,7 @@ class APITests: APITestBase { if let error = error { XCTAssertEqual((error as NSError).code, RemoteConfigError.internalError.rawValue) } - XCTAssertEqual(self.config["Key1"].stringValue, "Value1") + XCTAssertEqual(self.config[Constants.key1].stringValue, Constants.value1) expectation2.fulfill() } } @@ -117,7 +143,7 @@ class APITests: APITestBase { self.config.activate { changed, error in XCTAssertTrue(!APITests.useFakeConfig || changed) XCTAssertNil(error) - XCTAssertEqual(self.config["Key1"].stringValue, "Value1") + XCTAssertEqual(self.config[Constants.key1].stringValue, Constants.value1) expectation.fulfill() } } @@ -131,13 +157,161 @@ class APITests: APITestBase { self.config.activate { changed, error in XCTAssertFalse(changed) XCTAssertNil(error) - XCTAssertEqual(self.config["Key1"].stringValue, "Value1") + XCTAssertEqual(self.config[Constants.key1].stringValue, Constants.value1) expectation2.fulfill() } } waitForExpectations() } + func testFetchAndActivateUnchangedConfig() throws { + guard APITests.useFakeConfig == false else { return } + + let expectation = self.expectation(description: #function) + + XCTAssertEqual(config.settings.minimumFetchInterval, 0) + + let serialQueue = DispatchQueue(label: "\(#function)Queue") + let group = DispatchGroup() + group.enter() + serialQueue.async { + // Represents pre-fetch occuring sometime in past. + self.config.fetch { status, error in + XCTAssertNil(error, "Fetch Error \(error!)") + XCTAssertEqual(status, .success) + group.leave() + } + } + + serialQueue.async { + group.wait() + group.enter() + // Represents a `fetchAndActivate` being made to pull latest changes from Remote Config. + self.config.fetchAndActivate { status, error in + XCTAssertNil(error, "Fetch & Activate Error \(error!)") + // Since no updates to remote config have occurred we use the `.successUsingPreFetchedData`. + XCTAssertEqual(status, .successUsingPreFetchedData) + // The `lastETagUpdateTime` should either be older or the same time as `lastFetchTime`. + if let lastFetchTime = try? XCTUnwrap(self.config.lastFetchTime) { + XCTAssertLessThanOrEqual(Double(self.config.settings.lastETagUpdateTime), + Double(lastFetchTime.timeIntervalSince1970)) + } else { + XCTFail("Could not unwrap lastFetchTime.") + } + + expectation.fulfill() + } + } + + waitForExpectations() + } + + // MARK: - RemoteConfigConsole Tests + + func testFetchConfigThenUpdateConsoleThenFetchAgain() { + guard APITests.useFakeConfig == false else { return } + + let expectation = self.expectation(description: #function) + + config.fetchAndActivate { status, error in + XCTAssertNil(error, "Fetch & Activate Error \(error!)") + + if let configValue = self.config.configValue(forKey: Constants.jedi).stringValue { + XCTAssertEqual(configValue, Constants.obiwan) + } else { + XCTFail("Could not unwrap config value for key: \(Constants.jedi)") + } + expectation.fulfill() + } + waitForExpectations() + + // Synchronously update the console. + console.updateRemoteConfigValue(Constants.yoda, forKey: Constants.jedi) + + let expectation2 = self.expectation(description: #function + "2") + config.fetchAndActivate { status, error in + XCTAssertNil(error, "Fetch & Activate Error \(error!)") + + if let configValue = self.config.configValue(forKey: Constants.jedi).stringValue { + XCTAssertEqual(configValue, Constants.yoda) + } else { + XCTFail("Could not unwrap config value for key: \(Constants.jedi)") + } + + expectation2.fulfill() + } + waitForExpectations() + } + + func testFetchConfigThenAddValueOnConsoleThenFetchAgain() { + guard APITests.useFakeConfig == false else { return } + + // Ensure no Sith Lord has been written to Remote Config yet. + let expectation = self.expectation(description: #function) + + config.fetchAndActivate { status, error in + XCTAssertNil(error, "Fetch & Activate Error \(error!)") + + XCTAssertTrue(self.config.configValue(forKey: Constants.sith).dataValue.isEmpty) + + expectation.fulfill() + } + waitForExpectations() + + // Synchronously update the console + console.updateRemoteConfigValue(Constants.darthSidious, forKey: Constants.sith) + + // Verify the Sith Lord can now be fetched from Remote Config. + let expectation2 = self.expectation(description: #function + "2") + + config.fetchAndActivate { status, error in + XCTAssertNil(error, "Fetch & Activate Error \(error!)") + + if let configValue = self.config.configValue(forKey: Constants.sith).stringValue { + XCTAssertEqual(configValue, Constants.darthSidious) + } else { + XCTFail("Could not unwrap config value for key: \(Constants.sith)") + } + + expectation2.fulfill() + } + waitForExpectations() + } + + func testFetchConfigThenDeleteValueOnConsoleThenFetchAgain() { + guard APITests.useFakeConfig == false else { return } + + let expectation = self.expectation(description: #function) + + config.fetchAndActivate { status, error in + XCTAssertNil(error, "Fetch & Activate Error \(error!)") + + if let configValue = self.config.configValue(forKey: Constants.jedi).stringValue { + XCTAssertEqual(configValue, Constants.obiwan) + } else { + XCTFail("Could not unwrap config value for key: \(Constants.jedi)") + } + expectation.fulfill() + } + waitForExpectations() + + // Synchronously delete value on the console. + console.removeRemoteConfigValue(forKey: Constants.jedi) + + let expectation2 = self.expectation(description: #function + "2") + config.fetchAndActivate { status, error in + XCTAssertNil(error, "Fetch & Activate Error \(error!)") + + XCTAssertTrue(self.config.configValue(forKey: Constants.jedi).dataValue.isEmpty, + "Remote config should have been deleted.") + + expectation2.fulfill() + } + waitForExpectations() + } + + // MARK: - Private Helpers + private func waitForExpectations() { let kFIRStorageIntegrationTestTimeout = 10.0 waitForExpectations(timeout: kFIRStorageIntegrationTestTimeout, diff --git a/FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json b/FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json new file mode 100644 index 0000000000..c798f43e92 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json @@ -0,0 +1,5 @@ +{ + "token_type":"Bearer", + "expires_in":3599, + "access_token":"Generated by ./scripts/generate_access_token.sh" +} diff --git a/FirebaseRemoteConfig/Tests/SwiftAPI/RemoteConfigConsole.swift b/FirebaseRemoteConfig/Tests/SwiftAPI/RemoteConfigConsole.swift new file mode 100644 index 0000000000..2a4043a1ed --- /dev/null +++ b/FirebaseRemoteConfig/Tests/SwiftAPI/RemoteConfigConsole.swift @@ -0,0 +1,239 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +class RemoteConfigConsole { + private let projectID: String + private var latestConfig: [String: Any]! + + public var requestTimeout: TimeInterval = 10 + + private var consoleURL: URL { + let api = "https://firebaseremoteconfig.googleapis.com" + let endpoint = "/v1/projects/\(projectID)/remoteConfig" + return URL(string: api + endpoint)! + } + + private lazy var accessToken: String = { + guard let fileURL = Bundle(for: type(of: self)) + .url(forResource: "AccessToken", withExtension: "json") else { + fatalError("Could not find AccessToken.json in bundle.") + } + guard let data = try? Data(contentsOf: fileURL), + let json = try? JSONSerialization.jsonObject(with: data, options: .allowFragments), + let jsonDict = json as? [String: Any], + let accessToken = jsonDict["access_token"] as? String else { + fatalError("Could not retrieve access token.") + } + return accessToken + }() + + /// Synchronously fetches and returns currently active Remote Config, if it exists. + public var activeRemoteConfig: [String: Any]? { + var config: [String: Any]? + perform(configRequest: .get) { latestConfigJSON in + config = latestConfigJSON + } + if let config = config { + saveConfig(config) + } + return config + } + + /// Exposing this initializer allows us to create`RemoteConfigConsole` instances without + /// depending on a `GoogleService-Info.plist`. + init(projectID: String) { + self.projectID = projectID + syncWithConsole() + } + + /// This initializer will attempt to read from a `GoogleService-Info.plist` to set `projectID`. + convenience init() { + let currentBundle = Bundle(for: type(of: self)) + let projectID = currentBundle.plistValue( + forKey: "PROJECT_ID", + fromPlist: "GoogleService-Info.plist" + ) + self.init(projectID: projectID! as! String) + } + + // MARK: - Public API + + /// Update Remote Config with multiple String key value pairs. + /// - Parameter parameters: Dictionary representation of config key value pairs. + public func updateRemoteConfig(with parameters: [String: CustomStringConvertible]) { + var updatedConfig: [String: Any] = latestConfig + + let latestParameters = latestConfig["parameters"] as? [String: Any] + var updatedParameters = latestParameters ?? [String: Any]() + for (key, value) in parameters { + updatedParameters.updateValue(["defaultValue": ["value": value.description]], forKey: key) + } + updatedConfig.updateValue(updatedParameters, forKey: "parameters") + + publish(config: updatedConfig) + } + + /// Updates a Remote Config value for a given key. + /// - Parameters: + /// - value: Use strings, numbers, and booleans to represent Remote Config values. + /// - key: The corresponding string key that maps to the given value. + public func updateRemoteConfigValue(_ value: CustomStringConvertible, forKey key: String) { + var updatedConfig: [String: Any] = latestConfig + + let latestParameters = latestConfig["parameters"] as? [String: Any] + if var parameters = latestParameters { + parameters.updateValue(["defaultValue": ["value": value.description]], forKey: key) + updatedConfig.updateValue(parameters, forKey: "parameters") + + } else { + updatedConfig.updateValue( + [key: ["defaultValue": ["value": value.description]]], + forKey: "parameters" + ) + } + + publish(config: updatedConfig) + } + + public func removeRemoteConfigValue(forKey key: String) { + var updatedConfig: [String: Any] = latestConfig + + let latestParameters = latestConfig["parameters"] as? [String: Any] + if var parameters = latestParameters { + parameters.removeValue(forKey: key) + updatedConfig.updateValue(parameters, forKey: "parameters") + } + + publish(config: updatedConfig) + } + + public func clearRemoteConfig() { + var updatedConfig: [String: Any]! = latestConfig + updatedConfig.removeValue(forKey: "parameters") + publish(config: updatedConfig) + } + + // MARK: - Networking + + private enum ConfigRequest { + case get, put(_ data: Data) + + var httpMethod: String { + switch self { + case .get: return "GET" + case .put(data: _): return "PUT" + } + } + + var httpBody: Data? { + switch self { + case .get: return nil + case let .put(data: data): return data + } + } + + var httpHeaderFields: [String: String]? { + switch self { + case .get: return nil + case .put(data: _): + return ["Content-Type": "application/json; UTF8", "If-Match": "*"] + } + } + + func secureRequest(url: URL, with token: String, _ timeout: TimeInterval = 10) -> URLRequest { + var request = URLRequest(url: url, timeoutInterval: timeout) + request.httpMethod = self.httpMethod + request.allHTTPHeaderFields = self.httpHeaderFields + request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization") + request.httpBody = self.httpBody + return request + } + } + + /// Performs a given `ConfigRequest` synchronously. + private func perform(configRequest: ConfigRequest, + _ completion: (([String: Any]?) -> Void)? = nil) { + let request = configRequest.secureRequest(url: consoleURL, with: accessToken, requestTimeout) + + let semaphore = DispatchSemaphore(value: 0) + + let task = URLSession.shared.dataTask(with: request) { data, response, error in + // Signal the semaphore when this scope is escaped. + defer { semaphore.signal() } + + guard let data = data else { + print(String(describing: error)) + return + } + + let json = try? JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed) + + if let response = response as? HTTPURLResponse, + let json = json as? [String: Any] { + if response.statusCode >= 400 { + print("RemoteConfigConsole Error: \(String(describing: json["error"]!))") + } + } + + completion?(json as? [String: Any]) + } + + task.resume() + semaphore.wait() + } + + /// Publishes a config object to the live console and updates `latestConfig`. + private func publish(config: [String: Any]) { + let configData = data(withConfig: config) + perform(configRequest: .put(configData)) + saveConfig(config) + } + + // MARK: - Private Helpers + + /// Creates an optional Data object given a config object. + /// Used for serializing config objects before posting them to live console. + private func data(withConfig config: [String: Any]) -> Data { + let dictionary = NSDictionary(dictionary: config, copyItems: true) + let data = try! JSONSerialization.data(withJSONObject: dictionary, options: .fragmentsAllowed) + return data + } + + /// Perform a synchronous sync with remote config console. + private func syncWithConsole() { + if let consoleConfig = activeRemoteConfig { + latestConfig = consoleConfig + } else { + fatalError("Could not sync with console.") + } + } + + /// A more intuitively named setter for `latestConfig`. + private func saveConfig(_ config: [String: Any]) { + latestConfig = config + } +} + +// MARK: - Extensions + +extension Bundle { + func plistValue(forKey key: String, fromPlist plist: String) -> Any? { + guard let plistURL = url(forResource: plist, withExtension: "") else { + print("Could not find plist file \(plist) in bundle.") + return nil + } + let plistDictionary = NSDictionary(contentsOf: plistURL) + return plistDictionary?.object(forKey: key) + } +} diff --git a/FirebaseRemoteConfig/Tests/SwiftAPI/Resources/README.md b/FirebaseRemoteConfig/Tests/SwiftAPI/Resources/README.md new file mode 100644 index 0000000000..ad61b30ff3 --- /dev/null +++ b/FirebaseRemoteConfig/Tests/SwiftAPI/Resources/README.md @@ -0,0 +1,54 @@ +# Remote Config Console API + +[`RemoteConfigConsole.swift`](https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfig/Tests/SwiftAPI/RemoteConfigConsole.swift) +provides a simple API for interacting with an app's Remote Config on the +Firebase console. + +## Local Development Setup +1. Create a Firebase project on the Firebase Console and download +the `GoogleService-Info.plist`. +2. Navigate to your project's settings. Click on the **Service accounts** tab and +then download a private key by clicking the blue button that says "Generate new private key". +Rename it `ServiceAccount.json`. +3. Within the `firebase-ios-sdk`, run: +```bash +./scripts/generate_access_token.sh local_dev PATH/TO/ServiceAccount.json FirebaseRemoteConfig/Tests/SwiftAPI/AccessToken.json +``` +4. Generate the `FirebaseRemoteConfig` project: +```bash +pod gen FirebaseRemoteConfig.podspec --local-sources=./ --auto-open --platforms=ios +``` +5. Copy the `GoogleService-Info.plist` you downloaded earlier into the generated +Xcode project. + +🚀 Everything is ready to go! Run the tests in the `swift-api-tests` target. + + +## How it works + +While the `RemoteConfigConsole` API basically just makes simple network calls, +we need to include an `access token` so our requests do the proper "handshake" with the Firebase console. + +### Firebase Service Account Private Key +This private key is needed to create an access token with the valid parameters +that authorizes our requests to programmatically make changes to remote config on the Firebase console. + +The private key can be located on the Firebase console and navigate to your project's settings. To download, +click on the **Service accounts** tab and and then generate the private key by clicking +the blue button that says "Generate new private key". + +### Create the Access Token +We use Google's [Auth Library for Swift](https://github.com/googleapis/google-auth-library-swift) +to generate the access token. There are a few example use cases provided. We use the +[`TokenSource`](https://github.com/googleapis/google-auth-library-swift/blob/master/Sources/Examples/TokenSource/main.swift) +example. + +For the access token to be generated, the `GOOGLE_APPLICATION_CREDENTIALS` env var should be set to point to where the +Firebase project's service account key is stored. This is set in the +[`generate_access_token.sh`](https://github.com/firebase/firebase-ios-sdk/blob/master/scripts/generate_access_token.sh) +script. + +### Remote Config API Tests +There is a [section](https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfig/Tests/SwiftAPI/APITests.swift#L210) +of tests in [`APITests.swift`](https://github.com/firebase/firebase-ios-sdk/blob/master/FirebaseRemoteConfig/Tests/SwiftAPI/APITests.swift) +showcasing the `RemoteConfigConsole` in action. diff --git a/scripts/generate_access_token.sh b/scripts/generate_access_token.sh new file mode 100755 index 0000000000..6bc791b43c --- /dev/null +++ b/scripts/generate_access_token.sh @@ -0,0 +1,94 @@ +#!/bin/bash + +# Copyright 2020 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# This script generates access tokens that are needed to make admin API +# calls to the Firebase Console. +# +# The script takes three arguments: +# - GHA_SECRET: The password for decrypting GitHub secrets. +# - SERVICE_ACCOUNT: The path to the encrypted service account secret. +# - OUTPUT: The path to file where generated access token will be stored. +# +# This script uses Google's Swift Auth Client Library. +# - https://github.com/googleapis/google-auth-library-swift +# +# Generated tokens are `JSON` in the form: +# { +# "token_type":"Bearer", +# "expires_in":3599, +# "access_token":"1234567890ABCDEFG" +# } + +GHA_SECRET="$1" # Pass in `local_dev` if the SERVICE_ACCOUNT does not to be decrypted. +SERVICE_ACCOUNT="$2" +OUTPUT="$3" + +echo "GHA_SECRET: ***" +echo "SERVICE_ACCOUNT: ${SERVICE_ACCOUNT}" +echo "OUTPUT: ${OUTPUT}" + +if [[ ! -f $SERVICE_ACCOUNT ]]; then + echo ERROR: Cannot find encrypted secret at $SERVICE_ACCOUNT, aborting. + exit 1 +fi + +if [[ ! -f $OUTPUT ]]; then + echo ERROR: Cannot find $OUTPUT, aborting. + exit 1 +fi + +# The access token is generated using a downloaded Service Account JSON file from a +# Firebase Project. This can be downloaded from Firebase console under 'Project Settings'. +# +# The following stores the decrypted service account JSON file in `$HOME/.credentials/` and points +# the GOOGLE_APPLICATION_CREDENTIALS env var to it. +SERVICE_ACCOUNT_FILE=$(basename $SERVICE_ACCOUNT .gpg) + +echo "Creating ~/.credentials/ directory" +mkdir -p ~/.credentials/ + +if [[ $GHA_SECRET == "local_dev" ]]; then + echo "Local Development Mode" + echo "Copying ${SERVICE_ACCOUNT_FILE} to ~/.credentials/" + cp $SERVICE_ACCOUNT ~/.credentials/ +else + echo "Decrypting ${SERVICE_ACCOUNT_FILE}.gpg" + scripts/decrypt_GHA_SECRET.sh $SERVICE_ACCOUNT ~/.credentials/$SERVICE_ACCOUNT_FILE "$plist_secret" +fi + +echo "::set-env name=GOOGLE_APPLICATION_CREDENTIALS::${HOME}/.credentials/${SERVICE_ACCOUNT_FILE}" +export GOOGLE_APPLICATION_CREDENTIALS="${HOME}/.credentials/${SERVICE_ACCOUNT_FILE}" + +# Clone Google's Swift Auth Client Library and use it to generate a token. +# The generated token is piped to the specified OUTPUT file. +git clone https://github.com/googleapis/google-auth-library-swift.git +cd google-auth-library-swift +git checkout --quiet 7b1c9cd4ffd8cb784bcd8b7fd599794b69a810cf # Working main branch as of 7/9/20. +make -f Makefile + +# Prepend OUTPUT path with ../ since we cd'd into `google-auth-library-swift`. +swift run TokenSource > ../$OUTPUT + +# Remove cloned Swift Auth Client Library. +cd .. +rm -rf google-auth-library-swift + +if grep -q "access_token" $OUTPUT; then + echo "Token successfully generated and placed at ${OUTPUT}" +else + echo "ERROR: $(cat $OUTPUT)" + exit 1 +fi diff --git a/scripts/gha-encrypted/RemoteConfigSwiftAPI/ServiceAccount.json.gpg b/scripts/gha-encrypted/RemoteConfigSwiftAPI/ServiceAccount.json.gpg new file mode 100644 index 0000000000000000000000000000000000000000..19b54e4f476ce38cd409eaab06e4e91f1123a674 GIT binary patch literal 1735 zcmV;&1~~bQ4Fm}T0zo|MPOd-Y3+>YC0qh@vHOD@x_0LgM*sMU%KBoPIZ0(02!p4;oiB*No*nNe6HSmIaZ=I>XyM!@gB5^2H6EcUT zsXpr!TefoMlOu0_3nqPLoPxjVyAI==kA*4n^900-ehle?RJJ21j}*+#p@T%Dop zY|l~L0c%&=T7Us}uhW=|f0%nEJndpb!XFvj?wt z(iZHSV`Fbe^PmfxRA4I&T@EgRDM??4bdpW~yVZHdyRz-)tgu{m)q=D#6q2&aKU9^j zAhZ(EuEzuZLX*GNH1WP*x4EM@RoHD9mzZ9N=cX|!8G5--PJ+McrQVEVQDX!k$G5d^ zerw6YN>d!7uS=H4Ei0E+5SysByIUq~A=P`yT@5`4pSyn2gy0;28@ySB9AlR#?|tYr z4coBT{B0RL*XYk;zm6mU;qPALpe*V&@+aWHw4HAjE-01oIBrZ(#l3{!qD@|V7Ohiq zfiGV*hbT0ye}byKFSvp=8iwl&SA!8qe92jfuHl1hozQ-@+WgR7oYPBo5|gz;X9L8& z#$APQ_mh#S#aW*1+mVHD9oAO&Ur7&$$Wy0>4wpG(C|B^;I0AD)7V zeXd0P#`imiHyW1hrcFG6q@)PfJHPq>)QCP=W;BTtc2TAh|cHh@bU>p?*C1rLOyVX0&tP;_%jTI?$S% z2VSX<778DCC-Xv9v=`~+?E>uNK2Iz<{i{I&Y%F}@TE{}6(7t#?qWVa|dZ7#7YgzzB97hLlh{lIYhTyZ#+L z?l-7~V&}o56gp`R&~a1cRF5al)TJi&LZvF>=}`G2ccPBfz^#CJWvl3k4RayWC43~F zHHD!SUda9PPP!lCj7~?pbv+~SR0_Y8KagI+`<)mDB=*?se0_g32jV^S25zNe`C3G4 zk`oA-7u1$g>{$5ex3^Vx6$D=>ZD_xOStMU1_6Qt1HRRa#H5KgzEq&l^XVleWl;yeFf_9>o-CVB1ff>e;`|$vJ5LR3@)NU`@VZn`Xzir5V_>wR__faza(2 zOIy(qR-xR|>6LL_AtMNGz#V#^w}rS4R-7bO_~M+Rvz>Z~Af2ha4Y)(;u?jfQ!zx)M zShwIcj#$2qJNf5-)9b^_jJNA3Egnm>bhlXWZT{>tF7%&bV8@>V`LTVYi&0n~eG_z)f;#Y%5ckk6N8 z-Y&r`D=K!<*j#&wck;P`6+(E461j{VWni*T7WU^?xVPKdGvd1B6TXCF#5 z%1tq%%QR)e-~#h!F4jUyF~XQ-ol%hTq$f2n#LP(gAwtJRBdT!QK~}SazDhA1WF{#j z30VEzJ#kbPh-4AnCN0$G*gG4SguYnUO#*HS5$dY+H>WN%$Y;B^mcs|10Ki*@l}|kW znk}@dqh@q@w*fyK%jAc50r3qpiTR{kr`pzfkzGl+w^ett;mr0rGzAGwR%lUC38}H2 zbm8|CC$K0d=NVqjHU!c2FHl(2G43ruw}NxJWhT4#{GFw1N{P#~$q^)Q>%=Dr2183` z@8fFPWPP+fR1DWWsG%G;-0f6S8ZJCEEZ!yu2VW~(4&q@6QFl@i;xT_m5(O<2uzx>m zq_pcLdoIm(XNTymgJ`Lg8*_dS;NVYf7NZtN>8jg_2ZLI=s4TGQ7)HCXodx2s=3w&7 zAIhAH98#)nt3OF3L&TJ*JaMR0L7$}Vsffu?$&u5uRTaLT5wvV?z5c0jM$o8u5SvvN zWT%xyngmtrEU#LhK%|ptU>dSKNx_ZV{dm|A1reab?$51b4J9MKxZ5%y#@r39_ d^Gb_lYLTaniHCu3O-Y`F)-OG=$=swx4Ap(-Ub_GQ literal 0 HcmV?d00001