Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

## master

### Added
- Replaces custom solution for getting auth credentials with [google auth library](https://github.com/googleapis/google-auth-library-swift) ([#36](https://github.com/AckeeCZ/ACKLocalization/pull/36), kudos to @babacros)

## 1.5.0

## Added
Expand Down
52 changes: 44 additions & 8 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,17 @@ let package = Package(
targets: ["ACKLocalization"]),
],
dependencies: [
.package(url:"https://github.com/vapor/jwt-kit", .upToNextMajor(from: "4.3.0")),
.package(
url: "https://github.com/googleapis/google-auth-library-swift",
.upToNextMajor(from: "0.5.2")
),
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages which this package depends on.
.target(
name: "ACKLocalizationCore",
dependencies: ["JWTKit"]),
dependencies: ["OAuth2"]),
.target(
name: "ACKLocalization",
dependencies: ["ACKLocalizationCore"]),
Expand Down
36 changes: 24 additions & 12 deletions Sources/ACKLocalizationCore/ACKLocalization.swift
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ public final class ACKLocalization {
/// Fetches content of given sheet from spreadsheet using given `serviceAccount`
///
/// If not `spreadsheetTabName` is provided, the first in the spreadsheet is used
public func fetchSheetValues(_ spreadsheetTabName: String?, spreadsheetId: String, serviceAccount: ServiceAccount) -> AnyPublisher<ValueRange, LocalizationError> {
public func fetchSheetValues(_ spreadsheetTabName: String?, spreadsheetId: String, serviceAccount: Data) -> AnyPublisher<ValueRange, LocalizationError> {
let sheetsAPI = self.sheetsAPI

return authAPI.fetchAccessToken(serviceAccount: serviceAccount)
Expand Down Expand Up @@ -326,9 +326,17 @@ public final class ACKLocalization {
do {
if let serviceAccountPath = config.serviceAccount {
let serviceAccount = try loadServiceAccount(from: serviceAccountPath)
return fetchSheetValues(config.spreadsheetTabName, spreadsheetId: config.spreadsheetID, serviceAccount: serviceAccount)
return fetchSheetValues(
config.spreadsheetTabName,
spreadsheetId: config.spreadsheetID,
serviceAccount: serviceAccount
)
} else if let apiKey = config.apiKey {
return fetchSheetValues(config.spreadsheetTabName, spreadsheetId: config.spreadsheetID, apiKey: apiKey)
return fetchSheetValues(
config.spreadsheetTabName,
spreadsheetId: config.spreadsheetID,
apiKey: apiKey
)
} else if let serviceAccountPath = ProcessInfo.processInfo.environment[Constants.serviceAccountPath] {
let serviceAccount = try loadServiceAccount(from: serviceAccountPath)
return fetchSheetValues(
Expand All @@ -338,16 +346,20 @@ public final class ACKLocalization {
)
} else if let apiKey = ProcessInfo.processInfo.environment[Constants.apiKey] {
let apiKey = APIKey(value: apiKey)
return fetchSheetValues(config.spreadsheetTabName, spreadsheetId: config.spreadsheetID, apiKey: apiKey)
return fetchSheetValues(
config.spreadsheetTabName,
spreadsheetId: config.spreadsheetID,
apiKey: apiKey
)
} else {
let errorMessage = """
Unable to load API key or service account path. Please check if:

- `apiKey` or `serviceAccount` attribute is provided in `localization.json` file
or
- `\(Constants.apiKey)` or `\(Constants.serviceAccountPath)` environment variable is set
"""

throw LocalizationError(message: errorMessage)
}
} catch {
Expand Down Expand Up @@ -385,16 +397,16 @@ public final class ACKLocalization {
}

/// Loads service account from given `config`
private func loadServiceAccount(from path: String) throws -> ServiceAccount {
private func loadServiceAccount(from path: String) throws -> Data {
guard let serviceAccountData = FileManager.default.contents(atPath: path) else {
throw LocalizationError(message: "Unable to load service account at " + path)
}

do {
return try JSONDecoder().decode(ServiceAccount.self, from: serviceAccountData)
} catch {
throw LocalizationError(message: "Unable to read service account from `" + path + "` - " + error.localizedDescription)

guard !serviceAccountData.isEmpty else {
throw LocalizationError(message: "Invalid service account data")
}

return serviceAccountData
}

/// Actually writes given `rows` to given `file`
Expand Down
41 changes: 0 additions & 41 deletions Sources/ACKLocalizationCore/Model/GoogleClaims.swift

This file was deleted.

69 changes: 32 additions & 37 deletions Sources/ACKLocalizationCore/Services/AuthAPIService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@

import Combine
import Foundation
import JWTKit
import OAuth2

/// Protocol wrapping a service that fetches an access token from further communication
public protocol AuthAPIServicing {
/// Fetch access token for given `serviceAccount`
func fetchAccessToken(serviceAccount: ServiceAccount) -> AnyPublisher<AccessToken, RequestError>
func fetchAccessToken(serviceAccount: Data) -> AnyPublisher<AccessToken, RequestError>
}

/// Service that fetches an access token from further communication
Expand All @@ -28,41 +28,36 @@ public struct AuthAPIService: AuthAPIServicing {
// MARK: - API calls

/// Fetch access token for given `serviceAccount`
public func fetchAccessToken(serviceAccount: ServiceAccount) -> AnyPublisher<AccessToken, RequestError> {
let jwt = try? self.jwt(
for: serviceAccount,
claims: claims(serviceAccount: serviceAccount, validFor: 60)
)
let requestData = AccessTokenRequest(assertion: jwt ?? "")
var request = URLRequest(url: serviceAccount.tokenURL)
request.httpMethod = "POST"
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
request.httpBody = try? JSONEncoder().encode(requestData)
public func fetchAccessToken(serviceAccount: Data) -> AnyPublisher<AccessToken, RequestError> {
Deferred {
Future<AccessToken, RequestError> { promise in
guard let tokenProvider = ServiceAccountTokenProvider(
credentialsData: serviceAccount,
scopes: ["https://www.googleapis.com/auth/spreadsheets.readonly"]
) else {
promise(.failure(.init(message: "Creating provider failed")))
return
}

return session.dataTaskPublisher(for: request)
.validate()
.decode(type: AccessToken.self, decoder: JSONDecoder())
.mapError(RequestError.init)
.eraseToAnyPublisher()
do {
try tokenProvider.withToken { token, error in
if let token = token, let accessToken = token.AccessToken {
let token = AccessToken(
accessToken: accessToken,
expiration: TimeInterval(token.ExpiresIn ?? 0),
type: token.TokenType ?? ""
)
promise(.success(token))
} else if let error {
promise(.failure(.init(underlyingError: error, message: "Retrieving access token failed")))
} else {
promise(.failure(.init(message: "Access token was not provided")))
}
}
} catch {
promise(.failure(.init(underlyingError: error, message: "Retrieving access token failed")))
}
}
}.eraseToAnyPublisher()
}

// MARK: - Private helpers

/// Create JWT token that will be sent to retrieve access token
private func jwt(for serviceAccount: ServiceAccount, claims: GoogleClaims) throws -> String {
let signers = JWTSigners()
try signers.use(.rs256(key: .private(pem: serviceAccount.privateKey)))
return try signers.sign(claims)
}

private func claims(serviceAccount sa: ServiceAccount, validFor interval: TimeInterval) -> GoogleClaims {
let now = Int(Date().timeIntervalSince1970)

return .init(
serviceAccount: sa,
scope: .readOnly,
exp: now + Int(interval),
iat: now
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@

import ACKLocalizationCore
import Combine
import Foundation

final class AuthAPIServiceMock: AuthAPIServicing {
func fetchAccessToken(serviceAccount: ServiceAccount) -> AnyPublisher<AccessToken, RequestError> {
func fetchAccessToken(serviceAccount: Data) -> AnyPublisher<AccessToken, RequestError> {
Empty().eraseToAnyPublisher()
}
}