Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[match] import cert key profiles path args #21723

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
137 changes: 93 additions & 44 deletions match/lib/match/importer.rb
Expand Up @@ -9,10 +9,13 @@
module Match
class Importer
def import_cert(params, cert_path: nil, p12_path: nil, profile_path: nil)
# Get and verify cert, p12 and profiles path
cert_path = ensure_valid_file_path(cert_path, "Certificate", ".cer")
p12_path = ensure_valid_file_path(p12_path, "Private key", ".p12")
profile_path = ensure_valid_file_path(profile_path, "Provisioning profile", ".mobileprovision or .provisionprofile", optional: true)
cert_path, p12_path, profile_path = resolve_inputs(params, cert_path, p12_path, profile_path)

if (cert_path.nil? && p12_path.nil? && profile_path.nil?) || (cert_path.nil? && !p12_path.nil?) || (!cert_path.nil? && p12_path.nil?)
bitsofinfo marked this conversation as resolved.
Show resolved Hide resolved
UI.user_error!("When using 'import' you must specify either both a certificate/private key and/or a provisioning profile!")
end

is_importing_cert_key = (cert_path && p12_path)

# Storage
storage = Storage.from_params(params)
Expand Down Expand Up @@ -65,49 +68,56 @@ def import_cert(params, cert_path: nil, p12_path: nil, profile_path: nil)
output_dir_certs = File.join(storage.prefixed_working_directory, "certs", cert_type.to_s)
output_dir_profiles = File.join(storage.prefixed_working_directory, "profiles", prov_type.to_s)

should_skip_certificate_matching = params[:skip_certificate_matching]
# In case there is no access to Apple Developer portal but we have the certificates, keys and profiles
if should_skip_certificate_matching
cert_name = File.basename(cert_path, ".*")
p12_name = File.basename(p12_path, ".*")
files_to_commit = []

# Make dir if doesn't exist
FileUtils.mkdir_p(output_dir_certs)
dest_cert_path = File.join(output_dir_certs, "#{cert_name}.cer")
dest_p12_path = File.join(output_dir_certs, "#{p12_name}.p12")
else
if (api_token = Spaceship::ConnectAPI::Token.from(hash: params[:api_key], filepath: params[:api_key_path]))
UI.message("Creating authorization token for App Store Connect API")
Spaceship::ConnectAPI.token = api_token
elsif !Spaceship::ConnectAPI.token.nil?
UI.message("Using existing authorization token for App Store Connect API")
else
UI.message("Login to App Store Connect (#{params[:username]})")
Spaceship::ConnectAPI.login(params[:username], use_portal: true, use_tunes: false, portal_team_id: params[:team_id], team_name: params[:team_name])
end
# only do certificate ops if they are actually being imported vs only provisioning profile
if is_importing_cert_key

# Need to get the cert id by comparing base64 encoded cert content with certificate content from the API responses
certs = Spaceship::ConnectAPI::Certificate.all(filter: { certificateType: certificate_type })
should_skip_certificate_matching = params[:skip_certificate_matching]
# Skip if we are not importing cert/keys (only profile) OR there is no access to Apple Developer portal but we have the certificates, keys and profiles
if should_skip_certificate_matching
cert_name = File.basename(cert_path, ".*")
p12_name = File.basename(p12_path, ".*")

# Base64 encode contents to find match from API to find a cert ID
cert_contents_base_64 = Base64.strict_encode64(File.binread(cert_path))
matching_cert = certs.find do |cert|
cert.certificate_content == cert_contents_base_64
# Make dir if doesn't exist
FileUtils.mkdir_p(output_dir_certs)
dest_cert_path = File.join(output_dir_certs, "#{cert_name}.cer")
dest_p12_path = File.join(output_dir_certs, "#{p12_name}.p12")
else
if (api_token = Spaceship::ConnectAPI::Token.from(hash: params[:api_key], filepath: params[:api_key_path]))
UI.message("Creating authorization token for App Store Connect API")
Spaceship::ConnectAPI.token = api_token
elsif !Spaceship::ConnectAPI.token.nil?
UI.message("Using existing authorization token for App Store Connect API")
else
UI.message("Login to App Store Connect (#{params[:username]})")
Spaceship::ConnectAPI.login(params[:username], use_portal: true, use_tunes: false, portal_team_id: params[:team_id], team_name: params[:team_name])
end

# Need to get the cert id by comparing base64 encoded cert content with certificate content from the API responses
certs = Spaceship::ConnectAPI::Certificate.all(filter: { certificateType: certificate_type })

# Base64 encode contents to find match from API to find a cert ID
cert_contents_base_64 = Base64.strict_encode64(File.binread(cert_path))
matching_cert = certs.find do |cert|
cert.certificate_content == cert_contents_base_64
end

UI.user_error!("This certificate cannot be imported - the certificate contents did not match with any available on the Developer Portal") if matching_cert.nil?

# Make dir if doesn't exist
FileUtils.mkdir_p(output_dir_certs)
dest_cert_path = File.join(output_dir_certs, "#{matching_cert.id}.cer")
dest_p12_path = File.join(output_dir_certs, "#{matching_cert.id}.p12")
end

UI.user_error!("This certificate cannot be imported - the certificate contents did not match with any available on the Developer Portal") if matching_cert.nil?
files_to_commit = [dest_cert_path, dest_p12_path]

# Make dir if doesn't exist
FileUtils.mkdir_p(output_dir_certs)
dest_cert_path = File.join(output_dir_certs, "#{matching_cert.id}.cer")
dest_p12_path = File.join(output_dir_certs, "#{matching_cert.id}.p12")
# Copy files
IO.copy_stream(cert_path, dest_cert_path)
IO.copy_stream(p12_path, dest_p12_path)
end

files_to_commit = [dest_cert_path, dest_p12_path]

# Copy files
IO.copy_stream(cert_path, dest_cert_path)
IO.copy_stream(p12_path, dest_p12_path)
unless profile_path.nil?
FileUtils.mkdir_p(output_dir_profiles)
bundle_id = FastlaneCore::ProvisioningProfile.bundle_id(profile_path)
Expand All @@ -125,13 +135,52 @@ def import_cert(params, cert_path: nil, p12_path: nil, profile_path: nil)
storage.clear_changes if storage
end

def ensure_valid_file_path(file_path, file_description, file_extension, optional: false)
def resolve_inputs(params, cert_path, p12_path, profile_path)
if cert_path.nil?
cert_path = params[:import_certificate_file_path]
end
if p12_path.nil?
p12_path = params[:import_certificate_private_key_file_path]
end
if profile_path.nil?
profile_path = params[:import_provisioning_profile_file_path]
end
bitsofinfo marked this conversation as resolved.
Show resolved Hide resolved

# Get and verify cert, p12 and profiles path
if cert_path.nil?
cert_path = ensure_valid_file_path(cert_path, "Certificate", ".cer", optional: true, skip_prompt: params[:import_suppress_ui_file_path_prompts])
else
cert_path = ensure_valid_file_path(cert_path, "Certificate", ".cer", optional: true, skip_prompt: true)
end

if p12_path.nil?
p12_path = ensure_valid_file_path(p12_path, "Private key", ".p12", optional: true, skip_prompt: params[:import_suppress_ui_file_path_prompts])
else
p12_path = ensure_valid_file_path(p12_path, "Private key", ".p12", optional: true, skip_prompt: true)
end

if profile_path.nil?
profile_path = ensure_valid_file_path(profile_path, "Provisioning profile", ".mobileprovision or .provisionprofile", optional: true, skip_prompt: params[:import_suppress_ui_file_path_prompts])
else
profile_path = ensure_valid_file_path(profile_path, "Provisioning profile", ".mobileprovision or .provisionprofile", optional: true, skip_prompt: true)
end

return [cert_path, p12_path, profile_path]
end

def ensure_valid_file_path(file_path, file_description, file_extension, optional: false, skip_prompt: false)
bitsofinfo marked this conversation as resolved.
Show resolved Hide resolved
bitsofinfo marked this conversation as resolved.
Show resolved Hide resolved
optional_file_message = optional ? " or leave empty to skip this file" : ""
file_path ||= UI.input("#{file_description} (#{file_extension}) path#{optional_file_message}:")
raw_file_path = file_path
unless skip_prompt
bitsofinfo marked this conversation as resolved.
Show resolved Hide resolved
file_path ||= UI.input("#{file_description} (#{file_extension}) path#{optional_file_message}:")
end

if file_path
file_path = File.absolute_path(file_path) unless file_path == ""
bitsofinfo marked this conversation as resolved.
Show resolved Hide resolved
file_path = File.exist?(file_path) ? file_path : nil
end

file_path = File.absolute_path(file_path) unless file_path == ""
file_path = File.exist?(file_path) ? file_path : nil
UI.user_error!("#{file_description} does not exist at path: #{file_path}") unless !file_path.nil? || optional
UI.user_error!("#{file_description} does not exist at path: #{raw_file_path}") unless !file_path.nil? || optional
file_path
end
end
Expand Down
21 changes: 21 additions & 0 deletions match/lib/match/options.rb
Expand Up @@ -355,6 +355,27 @@ def self.available_options
description: "Skips setting the partition list (which can sometimes take a long time). Setting the partition list is usually needed to prevent Xcode from prompting to allow a cert to be used for signing",
type: Boolean,
default_value: false),
FastlaneCore::ConfigItem.new(key: :import_certificate_file_path,
env_name: "MATCH_IMPORT_CERTIFICATE_FILE_PATH",
description: "File path of the certificate (.cer) to be imported. Only works with match import action",
default_value: nil,
optional: true),
FastlaneCore::ConfigItem.new(key: :import_certificate_private_key_file_path,
env_name: "MATCH_IMPORT_CERTIFICATE_PRIVATE_KEY_FILE_PATH",
description: "File path of the certificate's private key (.p12) to be imported. Only works with match import action",
default_value: nil,
optional: true),
FastlaneCore::ConfigItem.new(key: :import_provisioning_profile_file_path,
env_name: "MATCH_IMPORT_PROVISIONING_PROFILE_FILE_PATH",
description: "File path of the provisioning profile (.mobileprovision or .provisionprofile) to be imported. Only works with match import action",
default_value: nil,
optional: true),
FastlaneCore::ConfigItem.new(key: :import_suppress_ui_file_path_prompts,
env_name: "MATCH_IMPORT_SUPPRESS_UI_FILE_PATH_PROMPTS",
description: "Suppress any prompting for certificate, key or provisioning profile file paths by the UI if not provided via other config items. Only works with match import action",
default_value: false,
type: Boolean,
optional: true),

# other
FastlaneCore::ConfigItem.new(key: :verbose,
Expand Down
68 changes: 68 additions & 0 deletions match/spec/importer_spec.rb
Expand Up @@ -101,6 +101,74 @@ def developer_id_test_values
Match::Importer.new.import_cert(config, cert_path: cert_path, p12_path: p12_path)
end

it "imports a .mobileprovision (iOS provision) into the match repo without a a .cert and .p12" do
repo_dir = Dir.mktmpdir
setup_fake_storage(repo_dir, config)

# UI will prompt 2x for cert/key which we are not providing
expect(UI).to receive(:input).and_return("")
expect(UI).to receive(:input).and_return("")

# because no cert/key provided ConnectAPI operations will be skipped
expect(Spaceship::ConnectAPI).not_to receive(:login)
expect(Spaceship::ConnectAPI::Certificate).not_to receive(:all)
expect(fake_storage).to receive(:save_changes!).with(
files_to_commit: [
File.join(repo_dir, "profiles", "appstore", "AppStore_tools.fastlane.app.mobileprovision")
]
)

expect(fake_storage).to receive(:clear_changes)

Match::Importer.new.import_cert(config, cert_path: nil, p12_path: nil, profile_path: ios_profile_path)
end

it "imports a .provisionprofile (osx provision) into the match repo without a a .cert and .p12" do
repo_dir = Dir.mktmpdir
setup_fake_storage(repo_dir, config)

# UI will prompt 2x for cert/key which we are not providing
expect(UI).to receive(:input).and_return("")
expect(UI).to receive(:input).and_return("")

# because no cert/key provided ConnectAPI operations will be skipped
expect(Spaceship::ConnectAPI).not_to receive(:login)
expect(Spaceship::ConnectAPI::Certificate).not_to receive(:all)
expect(fake_storage).to receive(:save_changes!).with(
files_to_commit: [
File.join(repo_dir, "profiles", "appstore", "AppStore_tools.fastlane.app.provisionprofile")
]
)

expect(fake_storage).to receive(:clear_changes)

Match::Importer.new.import_cert(config, cert_path: nil, p12_path: nil, profile_path: osx_profile_path)
end

it "imports a .provisionprofile (osx provision) into the match repo without a a .cert and .p12 with UI prompt supression" do
repo_dir = Dir.mktmpdir
setup_fake_storage(repo_dir, config)

# supress prompting
config[:import_suppress_ui_file_path_prompts] = true

# UI should not prompt 2x for cert/key which we are not providing
expect(UI).not_to receive(:input)

# because no cert/key provided ConnectAPI operations will be skipped
expect(Spaceship::ConnectAPI).not_to receive(:login)
expect(Spaceship::ConnectAPI::Certificate).not_to receive(:all)
expect(fake_storage).to receive(:save_changes!).with(
files_to_commit: [
File.join(repo_dir, "profiles", "appstore", "AppStore_tools.fastlane.app.provisionprofile")
]
)

expect(fake_storage).to receive(:clear_changes)

Match::Importer.new.import_cert(config, cert_path: nil, p12_path: nil, profile_path: osx_profile_path)
end

it "imports a .cert and .p12 when the type is set to developer_id" do
repo_dir = Dir.mktmpdir
developer_id_config = FastlaneCore::Configuration.create(Match::Options.available_options, developer_id_test_values)
Expand Down