class Match::Nuke

rubocop:disable Metrics/ClassLength

Attributes

certs[RW]
encryption[RW]
files[RW]
params[RW]
profiles[RW]
safe_remove_certs[RW]
storage[RW]
type[RW]

Public Instance Methods

filter_by_cert() click to toggle source
# File match/lib/match/nuke.rb, line 170
def filter_by_cert
  # Force will continue to revoke and delete all certificates and profiles
  return if self.params[:force] || !UI.interactive?
  return if self.certs.count < 2

  # Print table showing certificates that can be revoked
  puts("")
  rows = self.certs.each_with_index.collect do |cert, i|
    cert_expiration = cert.expiration_date.nil? ? "Unknown" : Time.parse(cert.expiration_date).strftime("%Y-%m-%d")
    [i + 1, cert.name, cert.id, cert.class.to_s.split("::").last, cert_expiration]
  end
  puts(Terminal::Table.new({
    title: "Certificates that can be #{removed_or_revoked_message}".green,
    headings: ["Option", "Name", "ID", "Type", "Expires"],
    rows: FastlaneCore::PrintTable.transform_output(rows)
  }))
  puts("")

  UI.important("By default, all listed certificates and profiles will be nuked")
  if UI.confirm("Do you want to only nuke specific certificates and their associated profiles?")
    input_indexes = UI.input("Enter the \"Option\" number(s) from the table above? (comma-separated)").split(',')

    # Get certificates from option indexes
    self.certs = input_indexes.map do |index|
      self.certs[index.to_i - 1]
    end.compact

    if self.certs.empty?
      UI.user_error!("No certificates were selected based on option number(s) entered")
    end

    # Do profile selection logic
    cert_ids = self.certs.map(&:id)
    self.profiles = self.profiles.select do |profile|
      profile_cert_ids = profile.certificates.map(&:id)
      (cert_ids & profile_cert_ids).any?
    end

    # Do file selection logic
    self.files = self.files.select do |f|
      found = false

      ext = File.extname(f)
      filename = File.basename(f, ".*")

      # Attempt to find cert based on filename
      if ext == ".cer" || ext == ".p12"
        found ||= self.certs.any? do |cert|
          filename == cert.id.to_s
        end
      end

      # Attempt to find profile matched on UUIDs in profile
      if ext == ".mobileprovision" || ext == ".provisionprofile"
        storage_uuid = FastlaneCore::ProvisioningProfile.uuid(f)

        found ||= self.profiles.any? do |profile|
          tmp_file = Tempfile.new
          tmp_file.write(Base64.decode64(profile.profile_content))
          tmp_file.close

          # Compare profile uuid in storage to profile uuid on developer portal
          portal_uuid = FastlaneCore::ProvisioningProfile.uuid(tmp_file.path)
          storage_uuid == portal_uuid
        end
      end

      found
    end
  end
end
nuke_it_now!() click to toggle source
# File match/lib/match/nuke.rb, line 293
def nuke_it_now!
  UI.header("Deleting #{self.profiles.count} provisioning profiles...") unless self.profiles.count == 0
  self.profiles.each do |profile|
    UI.message("Deleting profile '#{profile.name}' (#{profile.id})...")
    begin
      profile.delete!
    rescue => ex
      UI.message(ex.to_s)
    end
    UI.success("Successfully deleted profile")
  end

  removing_or_revoking_message = self.safe_remove_certs ? "Removing" : "Revoking"
  UI.header("#{removing_or_revoking_message} #{self.certs.count} certificates...") unless self.certs.count == 0
  self.certs.each do |cert|
    if self.safe_remove_certs
      UI.message("Certificate '#{cert.name}' (#{cert.id}) will be removed from repository without revoking it")
      next
    end

    UI.message("Revoking certificate '#{cert.name}' (#{cert.id})...")
    begin
      cert.delete!
    rescue => ex
      UI.message(ex.to_s)
    end
    UI.success("Successfully deleted certificate")
  end

  files_to_delete = delete_files! if self.files.count > 0
  files_to_delete ||= []

  self.encryption.encrypt_files if self.encryption

  if files_to_delete.count > 0
    # Now we need to save all this to the storage too, if needed
    message = ["[fastlane]", "Nuked", "files", "for", type.to_s].join(" ")
    self.storage.save_changes!(files_to_commit: [],
                               files_to_delete: files_to_delete,
                               custom_message: message)
  else
    UI.message("Your storage had no files to be deleted. This happens when you run `nuke` with an empty storage. Nothing to be worried about!")
  end
end
prepare_list() click to toggle source

Collect all the certs/profiles

# File match/lib/match/nuke.rb, line 122
def prepare_list
  UI.message("Fetching certificates and profiles...")
  cert_type = Match.cert_type_sym(type)
  cert_types = [cert_type]

  prov_types = []
  prov_types = [:development] if cert_type == :development
  prov_types = [:appstore, :adhoc, :developer_id] if cert_type == :distribution
  prov_types = [:enterprise] if cert_type == :enterprise

  # Get all iOS and macOS profile
  self.profiles = []
  prov_types.each do |prov_type|
    types = Match.profile_types(prov_type)
    self.profiles += Spaceship::ConnectAPI::Profile.all(filter: { profileType: types.join(",") }, includes: "certificates")
  end

  # Gets the main and additional cert types
  cert_types += (params[:additional_cert_types] || []).map do |ct|
    Match.cert_type_sym(ct)
  end

  # Gets all the certs form the cert types
  self.certs = []
  self.certs += cert_types.map do |ct|
    certificate_type(ct).flat_map do |cert|
      Spaceship::ConnectAPI::Certificate.all(filter: { certificateType: cert })
    end
  end.flatten

  # Finds all the .cer and .p12 files in the file storage
  certs = []
  keys = []
  cert_types.each do |ct|
    certs += self.storage.list_files(file_name: ct.to_s, file_ext: "cer")
    keys += self.storage.list_files(file_name: ct.to_s, file_ext: "p12")
  end

  # Finds all the iOS and macOS profiles in the file storage
  profiles = []
  prov_types.each do |prov_type|
    profiles += self.storage.list_files(file_name: prov_type.to_s, file_ext: "mobileprovision")
    profiles += self.storage.list_files(file_name: prov_type.to_s, file_ext: "provisionprofile")
  end

  self.files = certs + keys + profiles
end
print_tables() click to toggle source

Print tables to ask the user

run(params, type: nil) click to toggle source
# File match/lib/match/nuke.rb, line 29
def run(params, type: nil)
  self.params = params
  self.type = type

  update_optional_values_depending_on_storage_type(params)

  spaceship_login

  self.storage = Storage.from_params(params)
  self.storage.download

  # After the download was complete
  self.encryption = Encryption.for_storage_mode(params[:storage_mode], {
    git_url: params[:git_url],
    s3_bucket: params[:s3_bucket],
    s3_skip_encryption: params[:s3_skip_encryption],
    working_directory: storage.working_directory,
    force_legacy_encryption: params[:force_legacy_encryption]
  })
  self.encryption.decrypt_files if self.encryption

  had_app_identifier = self.params.fetch(:app_identifier, ask: false)
  self.params[:app_identifier] = '' # we don't really need a value here
  FastlaneCore::PrintTable.print_values(config: params,
                                     hide_keys: [:app_identifier],
                                         title: "Summary for match nuke #{Fastlane::VERSION}")

  self.safe_remove_certs = params[:safe_remove_certs] || false

  prepare_list
  filter_by_cert
  print_tables

  if params[:readonly]
    UI.user_error!("`fastlane match nuke` doesn't delete anything when running with --readonly enabled")
  end

  if (self.certs + self.profiles + self.files).count > 0
    unless params[:skip_confirmation]
      UI.error("---")
      remove_or_revoke_message = self.safe_remove_certs ? "remove" : "revoke"
      UI.error("Are you sure you want to completely delete and #{remove_or_revoke_message} all the")
      UI.error("certificates and delete provisioning profiles listed above? (y/n)")
      UI.error("Warning: By nuking distribution, both App Store and Ad Hoc profiles will be deleted") if type == "distribution"
      UI.error("Warning: The :app_identifier value will be ignored - this will delete all profiles for all your apps!") if had_app_identifier
      UI.error("---")
      print_safe_remove_certs_hint
    end
    if params[:skip_confirmation] || UI.confirm("Do you really want to nuke everything listed above?")
      nuke_it_now!
      UI.success("Successfully cleaned your account ♻️")
    else
      UI.success("Cancelled nuking #thanks 🏠 👨 ‍👩 ‍👧")
    end
  else
    UI.success("No relevant certificates or provisioning profiles found, nothing to nuke here :)")
  end
ensure
  self.storage.clear_changes if self.storage
end
spaceship_login() click to toggle source
# File match/lib/match/nuke.rb, line 98
def spaceship_login
  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
    Spaceship::ConnectAPI.login(params[:username], use_portal: true, use_tunes: false, portal_team_id: params[:team_id], team_name: params[:team_name])
  end

  if Spaceship::ConnectAPI.client.in_house? && (type == "distribution" || type == "enterprise")
    UI.error("---")
    UI.error("⚠️ Warning: This seems to be an Enterprise account!")
    unless self.safe_remove_certs
      UI.error("By nuking your account's distribution, all your apps deployed via ad-hoc will stop working!") if type == "distribution"
      UI.error("By nuking your account's enterprise, all your in-house apps will stop working!") if type == "enterprise"
    end
    UI.error("---")
    print_safe_remove_certs_hint
    UI.user_error!("Enterprise account nuke cancelled") unless UI.confirm("Do you really want to nuke your Enterprise account?")
  end
end
update_optional_values_depending_on_storage_type(params) click to toggle source

Be smart about optional values here Depending on the storage mode, different values are required

# File match/lib/match/nuke.rb, line 92
def update_optional_values_depending_on_storage_type(params)
  if params[:storage_mode] != "git"
    params.option_for_key(:git_url).optional = true
  end
end

Private Instance Methods

certificate_type(type) click to toggle source

The kind of certificate we’re interested in

# File match/lib/match/nuke.rb, line 362
def certificate_type(type)
  case type.to_sym
  when :mac_installer_distribution
    return [
      Spaceship::ConnectAPI::Certificate::CertificateType::MAC_INSTALLER_DISTRIBUTION
    ]
  when :distribution
    return [
      Spaceship::ConnectAPI::Certificate::CertificateType::MAC_APP_DISTRIBUTION,
      Spaceship::ConnectAPI::Certificate::CertificateType::IOS_DISTRIBUTION,
      Spaceship::ConnectAPI::Certificate::CertificateType::DISTRIBUTION
    ]
  when :development
    return [
      Spaceship::ConnectAPI::Certificate::CertificateType::MAC_APP_DEVELOPMENT,
      Spaceship::ConnectAPI::Certificate::CertificateType::IOS_DEVELOPMENT,
      Spaceship::ConnectAPI::Certificate::CertificateType::DEVELOPMENT
    ]
  when :enterprise
    return [
      Spaceship::ConnectAPI::Certificate::CertificateType::IOS_DISTRIBUTION
    ]
  else
    raise "Unknown type '#{type}'"
  end
end
delete_files!() click to toggle source
# File match/lib/match/nuke.rb, line 340
def delete_files!
  UI.header("Deleting #{self.files.count} files from the storage...")

  return self.files.collect do |file|
    UI.message("Deleting file '#{File.basename(file)}'...")

    # Check if the profile is installed on the local machine
    if file.end_with?("mobileprovision")
      parsed = FastlaneCore::ProvisioningProfile.parse(file)
      uuid = parsed["UUID"]
      path = Dir[File.join(FastlaneCore::ProvisioningProfile.profiles_path, "#{uuid}.mobileprovision")].last
      File.delete(path) if path
    end

    File.delete(file)
    UI.success("Successfully deleted file")

    file
  end
end
print_safe_remove_certs_hint() click to toggle source

Helpers for ‘safe_remove_certs`

removed_or_revoked_message() click to toggle source
# File match/lib/match/nuke.rb, line 396
def removed_or_revoked_message
  self.safe_remove_certs ? "removed" : "revoked"
end