diff --git a/cmd/BUILD.bazel b/cmd/BUILD.bazel index ca58246..c46eb35 100644 --- a/cmd/BUILD.bazel +++ b/cmd/BUILD.bazel @@ -17,6 +17,7 @@ go_library( "sandbox.go", "tar2files.go", "verify.go", + "xattr.go", ], importpath = "github.com/rmohr/bazeldnf/cmd", visibility = ["//visibility:private"], @@ -31,6 +32,7 @@ go_library( "//pkg/repo", "//pkg/rpm", "//pkg/sat", + "//pkg/xattr", "@com_github_sassoftware_go_rpmutils//:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", "@com_github_spf13_cobra//:go_default_library", diff --git a/cmd/root.go b/cmd/root.go index f9d3f72..67d6b38 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -16,6 +16,7 @@ var rootCmd = &cobra.Command{ } func Execute() { + rootCmd.AddCommand(NewXATTRCmd()) rootCmd.AddCommand(NewSandboxCmd()) rootCmd.AddCommand(NewFetchCmd()) rootCmd.AddCommand(NewInitCmd()) diff --git a/cmd/rpm2tar.go b/cmd/rpm2tar.go index c35f8a1..aede9dd 100644 --- a/cmd/rpm2tar.go +++ b/cmd/rpm2tar.go @@ -12,10 +12,11 @@ import ( ) type rpm2tarOpts struct { - output string - input []string - symlinks map[string]string - capabilities map[string]string + output string + input []string + symlinks map[string]string + capabilities map[string]string + selinuxLabels map[string]string } var rpm2taropts = rpm2tarOpts{} @@ -79,13 +80,13 @@ func NewRpm2TarCmd() *cobra.Command { return fmt.Errorf("could not open rpm at %s: %v", i, err) } defer rpmStream.Close() - err = rpm.RPMToTar(rpmStream, tarWriter, true, cap) + err = rpm.RPMToTar(rpmStream, tarWriter, true, cap, rpm2taropts.selinuxLabels) if err != nil { return fmt.Errorf("could not convert rpm at %s: %v", i, err) } } } else { - err := rpm.RPMToTar(rpmStream, tarWriter, false, cap) + err := rpm.RPMToTar(rpmStream, tarWriter, false, cap, rpm2taropts.selinuxLabels) if err != nil { return fmt.Errorf("could not convert rpm : %v", err) } @@ -98,6 +99,7 @@ func NewRpm2TarCmd() *cobra.Command { rpm2tarCmd.Flags().StringArrayVarP(&rpm2taropts.input, "input", "i", []string{}, "location from where to read the rpm file (defaults to stdin)") rpm2tarCmd.Flags().StringToStringVarP(&rpm2taropts.symlinks, "symlinks", "s", map[string]string{}, "symlinks to add. Relative or absolute.") rpm2tarCmd.Flags().StringToStringVarP(&rpm2taropts.capabilities, "capabilities", "c", map[string]string{}, "capabilities of files (--capabilities=/bin/ls=cap_net_bind_service)") + rpm2tarCmd.Flags().StringToStringVar(&rpm2taropts.selinuxLabels, "selinux-labels", map[string]string{}, "selinux labels of files (--selinux-labels=/bin/ls=unconfined_u:object_r:default_t:s0)") // deprecated options rpm2tarCmd.Flags().StringToStringVar(&rpm2taropts.capabilities, "capabilties", map[string]string{}, "capabilities of files (-c=/bin/ls=cap_net_bind_service)") rpm2tarCmd.Flags().MarkDeprecated("capabilties", "use --capabilities instead") diff --git a/cmd/xattr.go b/cmd/xattr.go new file mode 100644 index 0000000..fe3e224 --- /dev/null +++ b/cmd/xattr.go @@ -0,0 +1,67 @@ +package main + +import ( + "archive/tar" + "os" + "strings" + + "github.com/rmohr/bazeldnf/pkg/xattr" + "github.com/spf13/cobra" +) + +type xattrOpts struct { + filePrefix string + tarFileInput string + tarFileOutput string + capabilities map[string]string + selinuxLabels map[string]string +} + +var xattropts = xattrOpts{} + +func NewXATTRCmd() *cobra.Command { + xattrCmd := &cobra.Command{ + Use: "xattr", + Short: "Modify xattrs on tar file members", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) (err error) { + capabilityMap := map[string][]string{} + for file, caps := range xattropts.capabilities { + split := strings.Split(caps, ":") + if len(split) > 0 { + capabilityMap["./"+strings.TrimPrefix(file, "/")] = split + } + } + labelMap := map[string]string{} + for file, label := range xattropts.selinuxLabels { + labelMap["./"+strings.TrimPrefix(file, "/")] = label + } + + streamInput := os.Stdin + if xattropts.tarFileInput != "" { + streamInput, err = os.Open(xattropts.tarFileInput) + if err != nil { + return err + } + defer streamInput.Close() + } + + streamOutput := os.Stdout + if xattropts.tarFileOutput != "" { + streamOutput, err = os.OpenFile(xattropts.tarFileOutput, os.O_WRONLY|os.O_CREATE, os.ModePerm) + if err != nil { + return err + } + } + tarWriter := tar.NewWriter(streamOutput) + defer tarWriter.Close() + return xattr.Apply(tar.NewReader(streamInput), tarWriter , capabilityMap, labelMap) + }, + } + + xattrCmd.Flags().StringVarP(&xattropts.tarFileInput, "input", "i", "", "location from where to read the tar file (defaults to stdin)") + xattrCmd.Flags().StringVarP(&xattropts.tarFileOutput, "output", "o", "", "where to write the file to (defaults to stdout)") + xattrCmd.Flags().StringToStringVarP(&xattropts.capabilities, "capabilities", "c", map[string]string{}, "capabilities of files (--capabilities=/bin/ls=cap_net_bind_service)") + xattrCmd.Flags().StringToStringVar(&xattropts.selinuxLabels, "selinux-labels", map[string]string{}, "selinux labels of files (--selinux-labels=/bin/ls=unconfined_u:object_r:default_t:s0)") + return xattrCmd +} diff --git a/deps.bzl b/deps.bzl index 2d808da..a5876af 100644 --- a/deps.bzl +++ b/deps.bzl @@ -14,10 +14,15 @@ load( "@bazeldnf//internal:rpmtree.bzl", _tar2files = "tar2files", ) +load( + "@bazeldnf//internal:xattrs.bzl", + _xattrs = "xattrs", +) rpm = _rpm rpmtree = _rpmtree tar2files = _tar2files +xattrs = _xattrs def bazeldnf_dependencies(): _maybe( diff --git a/internal/rpmtree.bzl b/internal/rpmtree.bzl index 02e025f..ade5300 100644 --- a/internal/rpmtree.bzl +++ b/internal/rpmtree.bzl @@ -30,7 +30,13 @@ def _rpm2tar_impl(ctx): capabilities = [] for k, v in ctx.attr.capabilities.items(): capabilities += [k + "=" + ":".join(v)] - args += ["-c", ",".join(capabilities)] + args += ["--capabilities", ",".join(capabilities)] + + if ctx.attr.selinux_labels: + selinux_labels = [] + for k, v in ctx.attr.selinux_labels.items(): + selinux_labels += [k + "=" + v] + args += ["--selinux-labels", ",".join(selinux_labels)] args += rpms @@ -71,6 +77,7 @@ _rpm2tar_attrs = { ), "symlinks": attr.string_dict(), "capabilities": attr.string_list_dict(), + "selinux_labels": attr.string_list_dict(), "out": attr.output(mandatory = True), } diff --git a/internal/xattrs.bzl b/internal/xattrs.bzl new file mode 100644 index 0000000..8fa1643 --- /dev/null +++ b/internal/xattrs.bzl @@ -0,0 +1,65 @@ +# Copyright 2014 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +def _xattrs_impl(ctx): + out = ctx.outputs.out + args = ["xattr", "-i", ctx.files.tar[0].path, "-o", out.path] + + if ctx.attr.capabilities: + capabilities = [] + for k, v in ctx.attr.capabilities.items(): + capabilities += [k + "=" + ":".join(v)] + args += ["--capabilities", ",".join(capabilities)] + + if ctx.attr.selinux_labels: + selinux_labels = [] + for k, v in ctx.attr.selinux_labels.items(): + selinux_labels += [k + "=" + v] + args += ["--selinux-labels", ",".join(selinux_labels)] + + ctx.actions.run( + inputs = ctx.files.tar, + outputs = [out], + arguments = args, + progress_message = "Enriching %s with xattrs" % ctx.label.name, + executable = ctx.executable._bazeldnf, + ) + + return [DefaultInfo(files = depset([ctx.outputs.out]))] + +_xattrs_attrs = { + "tar": attr.label(allow_single_file = True), + "_bazeldnf": attr.label( + executable = True, + cfg = "exec", + allow_files = True, + default = Label("//cmd:cmd"), + ), + "capabilities": attr.string_list_dict(), + "selinux_labels": attr.string_dict(), + "out": attr.output(mandatory = True), +} + +_xattrs = rule( + implementation = _xattrs_impl, + attrs = _xattrs_attrs, +) + +def xattrs(**kwargs): + basename = kwargs["name"] + tarname = basename + ".tar" + _xattrs( + out = tarname, + **kwargs + ) diff --git a/pkg/rpm/BUILD.bazel b/pkg/rpm/BUILD.bazel index ab59ba8..dcadfc7 100644 --- a/pkg/rpm/BUILD.bazel +++ b/pkg/rpm/BUILD.bazel @@ -11,6 +11,7 @@ go_library( visibility = ["//visibility:public"], deps = [ "//pkg/api", + "//pkg/xattr", "@com_github_sassoftware_go_rpmutils//:go_default_library", "@com_github_sassoftware_go_rpmutils//cpio:go_default_library", "@com_github_sirupsen_logrus//:go_default_library", diff --git a/pkg/rpm/cpio2tar.go b/pkg/rpm/cpio2tar.go index 00b25b1..647b365 100644 --- a/pkg/rpm/cpio2tar.go +++ b/pkg/rpm/cpio2tar.go @@ -23,20 +23,12 @@ import ( "io/ioutil" "time" + "github.com/rmohr/bazeldnf/pkg/xattr" "github.com/sassoftware/go-rpmutils/cpio" ) -const ( - capabilities_header = "SCHILY.xattr.security.capability" -) - -var cap_empty_bitmask = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} -var supported_capabilities = map[string][]byte{ - "cap_net_bind_service": {1, 0, 0, 2, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, -} - // Extract the contents of a cpio stream from and writes it as a tar file into the provided writer -func Tar(rs io.Reader, tarfile *tar.Writer, noSymlinksAndDirs bool, capabilities map[string][]string) error { +func Tar(rs io.Reader, tarfile *tar.Writer, noSymlinksAndDirs bool, capabilities map[string][]string, selinuxLabels map[string]string) error { hardLinks := map[int][]*tar.Header{} inodes := map[int]string{} @@ -54,18 +46,13 @@ func Tar(rs io.Reader, tarfile *tar.Writer, noSymlinksAndDirs bool, capabilities pax := map[string]string{} if caps, exists := capabilities[entry.Header.Filename()]; exists { - for _, cap := range caps { - if _, supported := supported_capabilities[cap]; !supported { - return fmt.Errorf("Requested capability '%s' for file '%s' is not supported", cap, entry.Header.Filename()) - } - if _, exists := pax[capabilities_header]; !exists { - pax[capabilities_header] = string(cap_empty_bitmask) - } - val := []byte(pax[capabilities_header]) - for i, b := range supported_capabilities[cap] { - val[i] = val[i] | b - } - pax[capabilities_header] = string(val) + if err := xattr.AddCapabilities(pax, caps); err != nil { + return fmt.Errorf("failed setting capabilities on %s: %v", entry.Header.Filename(), err) + } + } + if label, exists := selinuxLabels[entry.Header.Filename()]; exists { + if err := xattr.SetSELinuxLabel(pax, label); err != nil { + return fmt.Errorf("failed setting selinux label on %s: %v", entry.Header.Filename(), err) } } diff --git a/pkg/rpm/tar.go b/pkg/rpm/tar.go index a4d7922..e585bd4 100644 --- a/pkg/rpm/tar.go +++ b/pkg/rpm/tar.go @@ -15,7 +15,7 @@ import ( log "github.com/sirupsen/logrus" ) -func RPMToTar(rpmReader io.Reader, tarWriter *tar.Writer, noSymlinksAndDirs bool, capabilities map[string][]string) error { +func RPMToTar(rpmReader io.Reader, tarWriter *tar.Writer, noSymlinksAndDirs bool, capabilities map[string][]string, selinuxLabels map[string]string) error { rpm, err := rpmutils.ReadRpm(rpmReader) if err != nil { return fmt.Errorf("failed to read rpm: %s", err) @@ -24,7 +24,7 @@ func RPMToTar(rpmReader io.Reader, tarWriter *tar.Writer, noSymlinksAndDirs bool if err != nil { return fmt.Errorf("failed to open the payload reader: %s", err) } - return Tar(payloadReader, tarWriter, noSymlinksAndDirs, capabilities) + return Tar(payloadReader, tarWriter, noSymlinksAndDirs, capabilities, selinuxLabels) } func RPMToCPIO(rpmReader io.Reader) (*cpio.CpioStream, error) { diff --git a/pkg/rpm/tar_test.go b/pkg/rpm/tar_test.go index 40f496f..a31d083 100644 --- a/pkg/rpm/tar_test.go +++ b/pkg/rpm/tar_test.go @@ -48,7 +48,7 @@ func TestRPMToTar(t *testing.T) { g.Expect(err).ToNot(HaveOccurred()) defer tarWriter.Close() - err = RPMToTar(f, tar.NewWriter(tarWriter), false, nil) + err = RPMToTar(f, tar.NewWriter(tarWriter), false, nil, nil) g.Expect(err).ToNot(HaveOccurred()) tarWriter.Close() @@ -125,7 +125,7 @@ func TestTar2Files(t *testing.T) { defer pipeWriter.Close() go func() { - _ = RPMToTar(f, tar.NewWriter(pipeWriter), false, nil) + _ = RPMToTar(f, tar.NewWriter(pipeWriter), false, nil, nil) pipeWriter.Close() }() diff --git a/pkg/xattr/BUILD.bazel b/pkg/xattr/BUILD.bazel new file mode 100644 index 0000000..de1588c --- /dev/null +++ b/pkg/xattr/BUILD.bazel @@ -0,0 +1,16 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "xattr", + srcs = ["xattr.go"], + importpath = "github.com/rmohr/bazeldnf/pkg/xattr", + visibility = ["//visibility:public"], +) + +go_test( + name = "xattr_test", + srcs = ["xattr_test.go"], + data = glob(["testdata/**"]), + embed = [":xattr"], + deps = ["@com_github_onsi_gomega//:go_default_library"], +) diff --git a/pkg/xattr/testdata/xattr.tar b/pkg/xattr/testdata/xattr.tar new file mode 100644 index 0000000..019cac5 Binary files /dev/null and b/pkg/xattr/testdata/xattr.tar differ diff --git a/pkg/xattr/xattr.go b/pkg/xattr/xattr.go new file mode 100644 index 0000000..74915e2 --- /dev/null +++ b/pkg/xattr/xattr.go @@ -0,0 +1,84 @@ +package xattr + +import ( + "archive/tar" + "fmt" + "io" +) + +const ( + capabilities_header = "SCHILY.xattr.security.capability" + selinux_header = "SCHILY.xattr.security.selinux" +) + +var cap_empty_bitmask = []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} +var supported_capabilities = map[string][]byte{ + "cap_net_bind_service": {1, 0, 0, 2, 0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, +} + +func AddCapabilities(pax map[string]string, capabilities []string) error { + for _, cap := range capabilities { + if _, supported := supported_capabilities[cap]; !supported { + return fmt.Errorf("requested capability '%s' is not supported", cap) + } + if _, exists := pax[capabilities_header]; !exists { + pax[capabilities_header] = string(cap_empty_bitmask) + } + val := []byte(pax[capabilities_header]) + for i, b := range supported_capabilities[cap] { + val[i] = val[i] | b + } + pax[capabilities_header] = string(val) + } + return nil +} + +func SetSELinuxLabel(pax map[string]string, label string) error { + if label == "" { + return fmt.Errorf("label must not be empty, but got '%s'", label) + } + pax[selinux_header] = fmt.Sprintf("%s\x00",label) + return nil +} + +func Apply(reader *tar.Reader, writer *tar.Writer, capabilties map[string][]string, labels map[string]string) error { + for { + entry, err := reader.Next() + if err == io.EOF { + break + } else if err != nil { + return err + } + + if err := enrichEntry(entry, capabilties, labels); err != nil { + return err + } + + entry.Format = tar.FormatPAX + if err := writer.WriteHeader(entry); err != nil { + return err + } + if _, err := io.Copy(writer, reader); err != nil { + return err + } + } + return nil +} + +func enrichEntry(entry *tar.Header, capabilties map[string][]string, labels map[string]string) error { + if entry.PAXRecords == nil { + entry.PAXRecords = map[string]string{} + } + + if caps, exists := capabilties[entry.Name]; exists { + if err := AddCapabilities(entry.PAXRecords, caps); err != nil { + return err + } + } + if l, exists := labels[entry.Name]; exists { + if err := SetSELinuxLabel(entry.PAXRecords, l); err != nil { + return err + } + } + return nil +} diff --git a/pkg/xattr/xattr_test.go b/pkg/xattr/xattr_test.go new file mode 100644 index 0000000..779dab3 --- /dev/null +++ b/pkg/xattr/xattr_test.go @@ -0,0 +1,45 @@ +package xattr + +import ( + "archive/tar" + "fmt" + "io" + "os" + "testing" + + . "github.com/onsi/gomega" +) + +var g *GomegaWithT + +func TestSettingSELinuxLabel(t *testing.T) { + g = NewGomegaWithT(t) + referenceEntry, err := getHeader("blub") + g.Expect(err).ToNot(HaveOccurred()) + + generatedEntry := &tar.Header{Name: "blub"} + labels := map[string]string{"blub": "unconfined_u:object_r:user_home_t:s0", "somethingelse": "something"} + + g.Expect(enrichEntry(generatedEntry, nil, labels)).To(Succeed()) + + g.Expect(generatedEntry.PAXRecords[selinux_header]).To(Equal(referenceEntry.PAXRecords[selinux_header])) +} + +func getHeader(name string) (*tar.Header, error) { + f, err := os.Open("testdata/xattr.tar") + g.Expect(err).ToNot(HaveOccurred()) + defer f.Close() + r := tar.NewReader(f) + for { + entry, err := r.Next() + if err == io.EOF { + break + } else if err != nil { + g.Expect(err).ToNot(HaveOccurred()) + } + if entry.Name == name { + return entry, nil + } + } + return nil, fmt.Errorf("entry %v does not exist", name) +}