File systemd_override.rb of Package smar-apparmor-profiles

# This file is included by generate_spec.rb

class OverrideFile
    attr_accessor :conditional

    def initialize profile_name

        # Matches if profile_name is a a collection of
        # key-value pairs.
        #
        # This means, the data is something like:
        #
        #    load_profile_by_systemd:
        #      user:
        #        pulseaudio:
        #          profile: pulseaudio
        #
        # Here key of `profile_name` would be “profile”
        # and value would be “pulseaudio”.
        if profile_name.respond_to? :each_pair
            @profile_name = profile_name["profile"]

            # Safeguard.
            raise "Empty profile name" if @profile_name.empty?

            @no_new_privs = profile_name["no_new_privs"]
        else
            @profile_name = profile_name
        end
    end

    def to_s
        return to_s_conditional if @conditional

        output = <<~EOF
            [Service]
            AppArmorProfile=#{@profile_name}
        EOF

        output += no_new_privs_to_s

        output
    end

    # Conditional profiles shouldn’t be used but where
    # absolutely needed (systemd-udevd load during boot
    # seems to be broken, so it needs to be conditional),
    # so log these.
    private def to_s_conditional
        verb "Conditional profile for #{@profile_name}"

        output = <<~EOF
            [Service]
            AppArmorProfile=-#{@profile_name}
        EOF

        output += no_new_privs_to_s

        output
    end

    private def no_new_privs_to_s
        return "" if @no_new_privs.nil?

        # If `NoNewPrivileges=yes`, systemd practically
        # won’t transition to the profile mentioned in
        # `AppArmorProfile=`.
        output = "NoNewPrivileges=#{ @no_new_privs ? "yes" : "no" }"
        output += "\n"

        output
    end
end

class SystemdOverride
    SYSTEMD_OVERRIDES_DIR_NAME = "systemd_overrides"
    SYSTEMD_OVERRIDES_DIR = "#{__dir__}/#{SYSTEMD_OVERRIDES_DIR_NAME}"
    SYSTEMD_OVERRIDES_TAR = "systemd_overrides.tar"

    at_exit do
        if Dir.exist? SYSTEMD_OVERRIDES_DIR
            FileUtils.rm_r SYSTEMD_OVERRIDES_DIR
        end
    end

    def initialize
        Dir.mkdir SYSTEMD_OVERRIDES_DIR
    end

    def finalize
        return unless Dir.exists? SYSTEMD_OVERRIDES_DIR
        return unless systemd_overrides_changed?

        # Try to create reproducible tar.
        # NOTE: --mtime is not specified here, so archive actually will be different in
        # each generation, but that can be added to reproduce archive from certain time.
        tar_command = "tar"
        tar_command = "#{tar_command} --sort=name --owner=0 --group=0 --numeric-owner"
        tar_command = "#{tar_command} --pax-option=exthdr.name=%d/PaxHeaders/%f,delete=atime,delete=ctime"
        tar_command = "#{tar_command} -cf #{SYSTEMD_OVERRIDES_TAR} systemd_overrides/"
        debug "Executing “#{tar_command}”."
        result = `#{tar_command}`
        unless $?.success?
            puts "Failed to execute “#{tar_command}”. Output:"
            puts result
            exit 21
        end

        File.delete "#{SYSTEMD_OVERRIDES_TAR}.xz" if File.exist? "#{SYSTEMD_OVERRIDES_TAR}.xz"
        # Using single thread should make xz archives reproducible across systems.
        # Though since I always regenerate the files, metadata won’t match, so there will always
        # be new archive.
        # But since this is so small, this has no harm.
        xz_command = "xz --threads=1 #{SYSTEMD_OVERRIDES_TAR}"
        `#{xz_command}`

        # Now there should be archive systemd_overrides.tar.xz.
    end

    def install_lines_for_load_profile_by_systemd(definition,
                                                  package,
                                                  service_name,
                                                  profile_name,
                                                  user: false,
                                                  conditional: false)
        return if redirect_to_service definition, package, service_name, profile_name

        write_systemd_override_file package, service_name, profile_name, user: user, conditional: conditional

        # Matches if profile_name is a a collection of
        # key-value pairs.
        #
        # This means, the data is something like:
        #
        #    load_profile_by_systemd:
        #      user:
        #        pulseaudio:
        #          profile: pulseaudio
        #
        # Here key of `profile_name` would be “profile”
        # and value would be “pulseaudio”.
        if profile_name.respond_to? :each_pair
            profile_name = profile_name["profile"]
        end

        if user
            override_file = Pathname.new("systemd_overrides") / "user" / package["name"] / service_name / "#{profile_name}.conf"
            unitdir_tag = "%{_userunitdir}"
        else
            override_file = Pathname.new("systemd_overrides") / package["name"] / service_name / "#{profile_name}.conf"
            unitdir_tag = "%{_unitdir}"
        end
        definition << "mkdir -p %{buildroot}#{unitdir_tag}/#{service_name}.service.d/"
        definition << "mv #{override_file} %{buildroot}#{unitdir_tag}/#{service_name}.service.d/"
    end

    # Returns true if service name is something like “user”.
    private def redirect_to_service definition, package, service_name, profile_name
        if service_name == "system"
            profile_name.each do |service_name, profile_name|
                install_lines_for_load_profile_by_systemd definition, package, service_name, profile_name
            end
            return true
        end
        if service_name == "system_conditional"
            profile_name.each do |service_name, profile_name|
                install_lines_for_load_profile_by_systemd definition, package, service_name, profile_name, conditional: true
            end
            return true
        end
        if service_name == "user"
            profile_name.each do |service_name, profile_name|
                install_lines_for_load_profile_by_systemd definition, package, service_name, profile_name, user: true
            end
            return true
        end
        if service_name == "user_conditional"
            profile_name.each do |service_name, profile_name|
                install_lines_for_load_profile_by_systemd definition, package, service_name, profile_name, user: true, conditional: true
            end
            return true
        end

        false
    end

    private def write_systemd_override_file package, service_name, profile_name, user: false, conditional: false
        if user
            package_dir = Pathname.new(SYSTEMD_OVERRIDES_DIR) / "user" / package["name"]
            Dir.mkdir package_dir.parent unless Dir.exist? package_dir.parent
        else
            package_dir = Pathname.new(SYSTEMD_OVERRIDES_DIR) / package["name"]
        end
        Dir.mkdir package_dir unless Dir.exist? package_dir

        service_dir = package_dir / service_name
        Dir.mkdir service_dir unless Dir.exist? service_dir

        profile = generate_service_override_conf profile_name, conditional: conditional

        # Matches if profile_name is a a collection of
        # key-value pairs.
        #
        # This means, the data is something like:
        #
        #    load_profile_by_systemd:
        #      user:
        #        pulseaudio:
        #          profile: pulseaudio
        #
        # Here key of `profile_name` would be “profile”
        # and value would be “pulseaudio”.
        if profile_name.respond_to? :each_pair
            profile_name = profile_name["profile"]
        end

        debug "Writing #{service_dir / "#{profile_name}.conf"}"
        File.open(service_dir / "#{profile_name}.conf", "w") do |f|
            f.write profile
        end
    end

    private def generate_service_override_conf profile_name, conditional: false
        override = OverrideFile.new profile_name
        override.conditional = conditional
        override.to_s
    end

    private def systemd_overrides_changed?
        # First check if there is missing files.

        files = Dir.glob "#{SYSTEMD_OVERRIDES_DIR_NAME}/**/*", base: __dir__
        files = files.delete_if do |path|
            File.directory? path
        end
        missing_files_command = "tar"
        missing_files_args = %W{ tf #{SYSTEMD_OVERRIDES_TAR}.xz } + files
        debug "Executing missing_files_command: “#{missing_files_command} #{missing_files_args.join(" ")}”."

        # err: :close == Don’t output command STDERR to generate_spec.rb’s STDERR.
        #
        # HINT: Use latter for debugging.
        if DEBUG
            result = system(missing_files_command, *missing_files_args)
        else
            result = system(missing_files_command, *missing_files_args, out: File::NULL, err: File::NULL)
        end

        debug "missing_files_command exitstatus=#{$?.exitstatus}, result=#{result}"

        if $?.exitstatus == 2
            # A file is missing from overrides archive.
            return true
        end

        if $?.exitstatus != 0
            puts "ERROR: Got an exit code from tar that is not handled: #{$?.exitstatus}"
            exit 23
        end

        # Then try to compare contents.
        #
        # This is a bit flaky.

        compare_command = %{env LANG=C tar --compare --file='#{SYSTEMD_OVERRIDES_TAR}.xz'}
        compare_command = %{#{compare_command} | grep -v 'Uid differs$' | grep -v 'Gid differs$'}
        compare_command = %{#{compare_command} | grep -v 'Mod time differs$'}

        debug "Executing compare_command: “#{compare_command}”."

        output = nil

        IO.pipe do |read_io, write_io|
            result = system(compare_command, out: write_io, err: write_io)
            write_io.close

            output = read_io.read
        end

        debug "compare_command exitstatus=#{$?.exitstatus}, result=#{result}"

        # 0 is everything is same, 1 means some files differs.
        unless [0, 1].include? $?.exitstatus
            puts "Failed to execute compare command. Result:"
            puts
            puts output
            exit 22
        end

        debug "compare_command output: #{output}"

        # If yes, there is no changed content in the archive.
        if output.empty?
            return false
        end

        true
    end
end