Skip to content

Commit 008bbad

Browse files
Merge pull request #22 from omarzl/task/regenerate-profile
[Feature] Automatic profile regeneration
2 parents 8f3bd6c + 94521f6 commit 008bbad

23 files changed

+734
-39
lines changed

README.md

+4-2
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,7 @@ OPTIONS:
141141
OpenSSL documentation for this flag
142142
(https://www.openssl.org/docs/manmaster/man1/openssl-req.html):
143143
144-
Sets
145-
subject name for new request or supersedes the
144+
Sets subject name for new request or supersedes the
146145
subject name when processing a certificate request.
147146
148147
The arg must be formatted as
@@ -157,6 +156,9 @@ Sets
157156
specify the members of the set. Example:
158157
159158
/DC=org/DC=OpenSSL/DC=users/UID=123456+CN=JohnDoe
159+
--auto-regenerate
160+
Defines if the profile should be regenerated in case
161+
it already exists (optional)
160162
-h, --help Show help information.
161163
162164

Sources/SignHereLibrary/Commands/CreateProvisioningProfileCommand.swift

+72-10
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand {
3838
)
3939
case unableToCreateCSR(output: ShellOutput)
4040
case unableToImportIntermediaryAppleCertificate(certificate: String, output: ShellOutput)
41+
case profileNameMissing
4142

4243
var description: String {
4344
switch self {
@@ -101,6 +102,8 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand {
101102
- Output: \(output.outputString)
102103
- Error: \(output.errorString)
103104
"""
105+
case .profileNameMissing:
106+
return "--auto-regenerate flag requires that you include a profile name using the argument --profile-name"
104107
}
105108
}
106109
}
@@ -122,6 +125,7 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand {
122125
case intermediaryAppleCertificates = "intermediaryAppleCertificates"
123126
case certificateSigningRequestSubject = "certificateSigningRequestSubject"
124127
case profileName = "profileName"
128+
case autoRegenerate = "autoRegenerate"
125129
}
126130

127131
@Option(help: "The key identifier of the private key (https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests)")
@@ -182,6 +186,9 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand {
182186
""")
183187
internal var certificateSigningRequestSubject: String
184188

189+
@Flag(help: "Defines if the profile should be regenerated in case it already exists (optional)")
190+
internal var autoRegenerate = false
191+
185192
private let files: Files
186193
private let log: Log
187194
private let shell: Shell
@@ -228,7 +235,8 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand {
228235
certificateSigningRequestSubject: String,
229236
bundleIdentifierName: String?,
230237
platform: String,
231-
profileName: String?
238+
profileName: String?,
239+
autoRegenerate: Bool
232240
) {
233241
self.files = files
234242
self.log = log
@@ -252,6 +260,7 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand {
252260
self.bundleIdentifierName = bundleIdentifierName
253261
self.platform = platform
254262
self.profileName = profileName
263+
self.autoRegenerate = autoRegenerate
255264
}
256265

257266
internal init(from decoder: Decoder) throws {
@@ -286,18 +295,36 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand {
286295
certificateSigningRequestSubject: try container.decode(String.self, forKey: .certificateSigningRequestSubject),
287296
bundleIdentifierName: try container.decodeIfPresent(String.self, forKey: .bundleIdentifierName),
288297
platform: try container.decode(String.self, forKey: .platform),
289-
profileName: try container.decodeIfPresent(String.self, forKey: .profileName)
298+
profileName: try container.decodeIfPresent(String.self, forKey: .profileName),
299+
autoRegenerate: try container.decode(Bool.self, forKey: .autoRegenerate)
290300
)
291301
}
292302

293303
internal func run() throws {
294-
let privateKey: Path = .init(privateKeyPath)
295-
let csr: Path = try createCSR(privateKey: privateKey)
296304
let jsonWebToken: String = try jsonWebTokenService.createToken(
297305
keyIdentifier: keyIdentifier,
298306
issuerID: issuerID,
299307
secretKey: try files.read(Path(itunesConnectKeyPath))
300308
)
309+
let deviceIDs: Set<String> = try iTunesConnectService.fetchITCDeviceIDs(jsonWebToken: jsonWebToken)
310+
guard let profileName, let profile = try? fetchProvisioningProfile(jsonWebToken: jsonWebToken, name: profileName)
311+
else {
312+
try createProvisioningProfile(jsonWebToken: jsonWebToken, deviceIDs: deviceIDs)
313+
return
314+
}
315+
guard autoRegenerate, shouldRegenerate(profile: profile, with: deviceIDs)
316+
else {
317+
try save(profile: profile)
318+
log.append("The profile already exists")
319+
return
320+
}
321+
try deleteProvisioningProfile(jsonWebToken: jsonWebToken, id: profile.id)
322+
try createProvisioningProfile(jsonWebToken: jsonWebToken, deviceIDs: deviceIDs)
323+
}
324+
325+
private func createProvisioningProfile(jsonWebToken: String, deviceIDs: Set<String>) throws {
326+
let privateKey: Path = .init(privateKeyPath)
327+
let csr: Path = try createCSR(privateKey: privateKey)
301328
let tuple: (cer: Path, certificateId: String) = try fetchOrCreateCertificate(jsonWebToken: jsonWebToken, csr: csr)
302329
let cer: Path = tuple.cer
303330
let certificateId: String = tuple.certificateId
@@ -311,7 +338,6 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand {
311338
try importP12IdentityIntoKeychain(p12Identity: p12Identity, identityPassword: identityPassword)
312339
try importIntermediaryAppleCertificates()
313340
try updateKeychainPartitionList()
314-
let deviceIDs: Set<String> = try iTunesConnectService.fetchITCDeviceIDs(jsonWebToken: jsonWebToken)
315341
let profileResponse: CreateProfileResponse = try iTunesConnectService.createProfile(
316342
jsonWebToken: jsonWebToken,
317343
bundleId: try iTunesConnectService.determineBundleIdITCId(
@@ -325,11 +351,7 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand {
325351
profileType: profileType,
326352
profileName: profileName
327353
)
328-
guard let profileData: Data = .init(base64Encoded: profileResponse.data.attributes.profileContent)
329-
else {
330-
throw Error.unableToBase64DecodeProfile(name: profileResponse.data.attributes.name)
331-
}
332-
try files.write(profileData, to: .init(outputPath))
354+
try save(profile: profileResponse.data)
333355
log.append(profileResponse.data.id)
334356
}
335357

@@ -496,4 +518,44 @@ internal struct CreateProvisioningProfileCommand: ParsableCommand {
496518
)
497519
}
498520
}
521+
522+
private func fetchProvisioningProfile(jsonWebToken: String, name: String) throws -> ProfileResponseData? {
523+
try iTunesConnectService.fetchProvisioningProfile(
524+
jsonWebToken: jsonWebToken,
525+
name: name
526+
).first(where: { $0.attributes.name == name })
527+
}
528+
529+
private func deleteProvisioningProfile(jsonWebToken: String, id: String) throws {
530+
try iTunesConnectService.deleteProvisioningProfile(
531+
jsonWebToken: jsonWebToken,
532+
id: id
533+
)
534+
log.append("Deleted profile with id: \(id)")
535+
}
536+
537+
private func save(profile: ProfileResponseData) throws {
538+
guard let profileData: Data = .init(base64Encoded: profile.attributes.profileContent)
539+
else {
540+
throw Error.unableToBase64DecodeProfile(name: profile.attributes.name)
541+
}
542+
try files.write(profileData, to: .init(outputPath))
543+
}
544+
545+
private func shouldRegenerate(profile: ProfileResponseData, with deviceIDs: Set<String>) -> Bool {
546+
guard ProfileType(rawValue: profileType).usesDevices else { return false }
547+
let profileDevices = Set(profile.relationships.devices.data.map { $0.id })
548+
let shouldRegenerate = deviceIDs != profileDevices
549+
if shouldRegenerate {
550+
let missingDevices = deviceIDs.subtracting(profileDevices)
551+
log.append("The profile will be regenerated because it is missing the device(s): \(missingDevices.joined(separator: ", "))")
552+
}
553+
return shouldRegenerate
554+
}
555+
556+
mutating internal func validate() throws {
557+
if autoRegenerate, profileName == nil {
558+
throw Error.profileNameMissing
559+
}
560+
}
499561
}

Sources/SignHereLibrary/Models/CreateProfileResponse.swift

+1-16
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,5 @@
88
import Foundation
99

1010
internal struct CreateProfileResponse: Codable {
11-
struct CreateProfileResponseData: Codable {
12-
struct Attributes: Codable {
13-
var profileContent: String
14-
var uuid: String
15-
var name: String
16-
var platform: String
17-
var createdDate: Date
18-
var profileState: String
19-
var profileType: String
20-
var expirationDate: Date
21-
}
22-
var id: String
23-
var type: String
24-
var attributes: Attributes
25-
}
26-
var data: CreateProfileResponseData
11+
var data: ProfileResponseData
2712
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
//
2+
// GetProfilesResponse.swift
3+
// Models
4+
//
5+
// Created by Omar Zuniga on 29/05/24.
6+
//
7+
8+
import Foundation
9+
10+
internal struct GetProfilesResponse: Codable {
11+
var data: [ProfileResponseData]
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// CreateProfileResponse.swift
3+
// Models
4+
//
5+
// Created by Omar Zuniga on 29/05/24.
6+
//
7+
8+
import Foundation
9+
10+
struct ProfileResponseData: Codable {
11+
struct Attributes: Codable {
12+
var profileContent: String
13+
var uuid: String
14+
var name: String
15+
var platform: String
16+
var createdDate: Date
17+
var profileState: String
18+
var profileType: String
19+
var expirationDate: Date
20+
}
21+
struct Relationships: Codable {
22+
struct Devices: Codable {
23+
struct Data: Codable {
24+
var id: String
25+
var type: String
26+
}
27+
28+
var data: [Data]
29+
}
30+
var devices: Devices
31+
}
32+
var id: String
33+
var type: String
34+
var attributes: Attributes
35+
var relationships: Relationships
36+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//
2+
// ProfileType.swift
3+
// Models
4+
//
5+
// Created by Omar Zuniga on 29/05/24.
6+
//
7+
8+
import Foundation
9+
10+
enum ProfileType {
11+
case development
12+
case adHoc
13+
case appStore
14+
case inHouse
15+
case direct
16+
case unknown
17+
18+
init(rawValue: String) {
19+
switch rawValue {
20+
case let str where str.hasSuffix("_APP_DEVELOPMENT"): self = .development
21+
case let str where str.hasSuffix("_APP_ADHOC"): self = .adHoc
22+
case let str where str.hasSuffix("_APP_STORE"): self = .appStore
23+
case let str where str.hasSuffix("_APP_INHOUSE"): self = .inHouse
24+
case let str where str.hasSuffix("_APP_DIRECT"): self = .direct
25+
default: self = .unknown
26+
}
27+
}
28+
29+
var usesDevices: Bool {
30+
switch self {
31+
case .appStore: return false
32+
default: return true
33+
}
34+
}
35+
}

Sources/SignHereLibrary/Services/iTunesConnectService.swift

+38-1
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ internal protocol iTunesConnectService {
4242
jsonWebToken: String,
4343
id: String
4444
) throws
45+
func fetchProvisioningProfile(
46+
jsonWebToken: String,
47+
name: String
48+
) throws -> [ProfileResponseData]
4549
}
4650

4751
internal class iTunesConnectServiceImp: iTunesConnectService {
@@ -368,7 +372,7 @@ internal class iTunesConnectServiceImp: iTunesConnectService {
368372
let profileName = profileName ?? "\(certificateId)_\(profileType)_\(clock.now().timeIntervalSince1970)"
369373
var devices: CreateProfileRequest.CreateProfileRequestData.Relationships.Devices? = nil
370374
// ME: App Store profiles cannot use UDIDs
371-
if !["IOS_APP_STORE", "MAC_APP_STORE", "TVOS_APP_STORE", "MAC_CATALYST_APP_STORE"].contains(profileType) {
375+
if ProfileType(rawValue: profileType).usesDevices {
372376
devices = .init(
373377
data: deviceIDs.sorted().map {
374378
CreateProfileRequest.CreateProfileRequestData.Relationships.Devices.DevicesData(
@@ -440,6 +444,39 @@ internal class iTunesConnectServiceImp: iTunesConnectService {
440444
}
441445
}
442446

447+
func fetchProvisioningProfile(
448+
jsonWebToken: String,
449+
name: String
450+
) throws -> [ProfileResponseData] {
451+
var urlComponents: URLComponents = .init()
452+
urlComponents.scheme = Constants.httpsScheme
453+
urlComponents.host = Constants.itcHost
454+
urlComponents.path = "/v1/profiles"
455+
urlComponents.queryItems = [
456+
.init(name: "filter[name]", value: name),
457+
.init(name: "include", value: "devices")
458+
]
459+
guard let url: URL = urlComponents.url
460+
else {
461+
throw Error.unableToCreateURL(urlComponents: urlComponents)
462+
}
463+
var request: URLRequest = .init(url: url)
464+
request.setValue("Bearer \(jsonWebToken)", forHTTPHeaderField: "Authorization")
465+
request.setValue(Constants.applicationJSONHeaderValue, forHTTPHeaderField: "Accept")
466+
request.setValue(Constants.applicationJSONHeaderValue, forHTTPHeaderField: Constants.contentTypeHeaderName)
467+
request.httpMethod = "GET"
468+
let jsonDecoder: JSONDecoder = createITCApiJSONDecoder()
469+
let data: Data = try network.execute(request: request)
470+
do {
471+
return try jsonDecoder.decode(
472+
GetProfilesResponse.self,
473+
from: data
474+
).data
475+
} catch let decodingError as DecodingError {
476+
throw Error.unableToDecodeResponse(responseData: data, decodingError: decodingError)
477+
}
478+
}
479+
443480
private func createITCApiJSONDecoder() -> JSONDecoder {
444481
let jsonDecoder: JSONDecoder = .init()
445482
let dateFormatter: DateFormatter = .init()

0 commit comments

Comments
 (0)