/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
 *
 * Copyright (C) 2014 Colin Walters <walters@verbum.org>
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA 02111-1307, USA.
 */

#include "config.h"

#ifdef HAVE_SELINUX
#include <selinux/selinux.h>
#include <selinux/label.h>
#endif

#include "otutil.h"
#include "libgsystem.h"

#include "ostree-sepolicy.h"
#include "ostree-bootloader-uboot.h"
#include "ostree-bootloader-syslinux.h"

/**
 * SECTION:libostree-sepolicy
 * @title: SELinux policy management
 * @short_description: Read SELinux policy and manage filesystem labels
 *
 * A #OstreeSePolicy object can load the SELinux policy from a given
 * root and perform labeling.
 */
struct OstreeSePolicy {
  GObject parent;

  GFile *path;

#ifdef HAVE_SELINUX
  GFile *selinux_policy_root;
  struct selabel_handle *selinux_hnd;
  char *selinux_policy_name;
#endif
};

typedef struct {
  GObjectClass parent_class;
} OstreeSePolicyClass;

static void initable_iface_init       (GInitableIface      *initable_iface);

enum {
  PROP_0,

  PROP_PATH
};

G_DEFINE_TYPE_WITH_CODE (OstreeSePolicy, ostree_sepolicy, G_TYPE_OBJECT,
                         G_IMPLEMENT_INTERFACE (G_TYPE_INITABLE, initable_iface_init))

static void
ostree_sepolicy_finalize (GObject *object)
{
  OstreeSePolicy *self = OSTREE_SEPOLICY (object);

  g_clear_object (&self->path);
#ifdef HAVE_SELINUX
  g_clear_object (&self->selinux_policy_root);
  g_clear_pointer (&self->selinux_policy_name, g_free);
  if (self->selinux_hnd)
    {
      selabel_close (self->selinux_hnd);
      self->selinux_hnd = NULL;
    }
#endif

  G_OBJECT_CLASS (ostree_sepolicy_parent_class)->finalize (object);
}

static void
ostree_sepolicy_set_property(GObject         *object,
                            guint            prop_id,
                            const GValue    *value,
                            GParamSpec      *pspec)
{
  OstreeSePolicy *self = OSTREE_SEPOLICY (object);

  switch (prop_id)
    {
    case PROP_PATH:
      /* Canonicalize */
      self->path = g_file_new_for_path (gs_file_get_path_cached (g_value_get_object (value)));
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static void
ostree_sepolicy_get_property(GObject         *object,
                            guint            prop_id,
                            GValue          *value,
                            GParamSpec      *pspec)
{
  OstreeSePolicy *self = OSTREE_SEPOLICY (object);

  switch (prop_id)
    {
    case PROP_PATH:
      g_value_set_object (value, self->path);
      break;
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
      break;
    }
}

static void
ostree_sepolicy_constructed (GObject *object)
{
  OstreeSePolicy *self = OSTREE_SEPOLICY (object);

  g_assert (self->path != NULL);

  G_OBJECT_CLASS (ostree_sepolicy_parent_class)->constructed (object);
}

static void
ostree_sepolicy_class_init (OstreeSePolicyClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->constructed = ostree_sepolicy_constructed;
  object_class->get_property = ostree_sepolicy_get_property;
  object_class->set_property = ostree_sepolicy_set_property;
  object_class->finalize = ostree_sepolicy_finalize;

  g_object_class_install_property (object_class,
                                   PROP_PATH,
                                   g_param_spec_object ("path",
                                                        "",
                                                        "",
                                                        G_TYPE_FILE,
                                                        G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
}

static gboolean
initable_init (GInitable     *initable,
               GCancellable  *cancellable,
               GError       **error)
{
#ifdef HAVE_SELINUX
  gboolean ret = FALSE;
  OstreeSePolicy *self = OSTREE_SEPOLICY (initable);
  gs_unref_object GFile *etc_selinux_dir = NULL;
  gs_unref_object GFile *policy_config_path = NULL;
  gs_unref_object GFile *policy_root = NULL;
  gs_unref_object GFileInputStream *filein = NULL;
  gs_unref_object GDataInputStream *datain = NULL;
  gboolean enabled = FALSE;
  char *policytype = NULL;
  const char *selinux_prefix = "SELINUX=";
  const char *selinuxtype_prefix = "SELINUXTYPE=";

  etc_selinux_dir = g_file_resolve_relative_path (self->path, "etc/selinux");
  if (!g_file_query_exists (etc_selinux_dir, NULL))
    {
      g_object_unref (etc_selinux_dir);
      etc_selinux_dir = g_file_resolve_relative_path (self->path, "usr/etc/selinux");
    }
  policy_config_path = g_file_get_child (etc_selinux_dir, "config");

  if (g_file_query_exists (policy_config_path, NULL))
    {
      filein = g_file_read (policy_config_path, cancellable, error);
      if (!filein)
        goto out;

      datain = g_data_input_stream_new ((GInputStream*)filein);

      while (TRUE)
        {
          gsize len;
          GError *temp_error = NULL;
          gs_free char *line = g_data_input_stream_read_line_utf8 (datain, &len,
                                                                   cancellable, &temp_error);
      
          if (temp_error)
            {
              g_propagate_error (error, temp_error);
              goto out;
            }

          if (!line)
            break;
      
          if (g_str_has_prefix (line, selinuxtype_prefix))
            {
              policytype = g_strstrip (g_strdup (line + strlen (selinuxtype_prefix))); 
              policy_root = g_file_get_child (etc_selinux_dir, policytype);
            }
          else if (g_str_has_prefix (line, selinux_prefix))
            {
              const char *enabled_str = line + strlen (selinux_prefix);
              if (g_ascii_strncasecmp (enabled_str, "enforcing", strlen ("enforcing")) == 0 ||
                  g_ascii_strncasecmp (enabled_str, "permissive", strlen ("permissive")) == 0)
                enabled = TRUE;
            }
        }
    }

  if (enabled)
    {
      g_setenv ("LIBSELINUX_DISABLE_PCRE_PRECOMPILED", "1", FALSE);
      if (selinux_set_policy_root (gs_file_get_path_cached (policy_root)) != 0)
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                       "selinux_set_policy_root(%s): %s",
                       gs_file_get_path_cached (etc_selinux_dir),
                       strerror (errno));
          goto out;
        }

      self->selinux_hnd = selabel_open (SELABEL_CTX_FILE, NULL, 0);
      if (!self->selinux_hnd)
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                       "With policy root '%s': selabel_open(SELABEL_CTX_FILE): %s",
                       gs_file_get_path_cached (etc_selinux_dir),
                       strerror (errno));
          goto out;
        }

      {
        char *con = NULL;
        if (selabel_lookup_raw (self->selinux_hnd, &con, "/", 0755) != 0)
          {
            g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                         "With policy root '%s': Failed to look up context of /: %s",
                         gs_file_get_path_cached (etc_selinux_dir),
                         strerror (errno));
            goto out;
          }
        freecon (con);
      }

      self->selinux_policy_name = g_strdup (policytype);
      self->selinux_policy_root = g_object_ref (etc_selinux_dir);
    }

  ret = TRUE;
 out:
  return ret;
#else
  return TRUE;
#endif
}

static void
ostree_sepolicy_init (OstreeSePolicy *self)
{
}

static void
initable_iface_init (GInitableIface *initable_iface)
{
  initable_iface->init = initable_init;
}

/**
 * ostree_sepolicy_new:
 * @path: Path to a root directory
 *
 * Returns: (transfer full): An accessor object for SELinux policy in root located at @path
 */
OstreeSePolicy*
ostree_sepolicy_new (GFile         *path,
                     GCancellable  *cancellable,
                     GError       **error)
{
  return g_initable_new (OSTREE_TYPE_SEPOLICY, cancellable, error, "path", path, NULL);
}

/**
 * ostree_sepolicy_get_path:
 * @self:
 *
 * Returns: (transfer none): Path to rootfs
 */
GFile *
ostree_sepolicy_get_path (OstreeSePolicy  *self)
{
  return self->path;
}

const char *
ostree_sepolicy_get_name (OstreeSePolicy *self)
{
#ifdef HAVE_SELINUX
  return self->selinux_policy_name;
#else
  return NULL;
#endif
}

/**
 * ostree_sepolicy_get_label:
 * @self: Self
 * @relpath: Path
 * @unix_mode: Unix mode
 * @out_label: (allow-none) (out) (transfer full): Return location for security context
 * @cancellable: Cancellable
 * @error: Error
 *
 * Store in @out_label the security context for the given @relpath and
 * mode @unix_mode.  If the policy does not specify a label, %NULL
 * will be returned.
 */
gboolean
ostree_sepolicy_get_label (OstreeSePolicy    *self,
                           const char       *relpath,
                           guint32           unix_mode,
                           char            **out_label,
                           GCancellable     *cancellable,
                           GError          **error)
{
#ifdef HAVE_SELINUX
  gboolean ret = FALSE;
  int res;
  char *con = NULL;

  if (self->selinux_hnd)
    {
      res = selabel_lookup_raw (self->selinux_hnd, &con, relpath, unix_mode);
      if (res != 0)
        {
          int errsv = errno;
          if (errsv != ENOENT)
            {
              ot_util_set_error_from_errno (error, errsv);
              goto out;
            }
        }
      else
        {
          /* Ensure we consistently allocate with g_malloc */
          *out_label = g_strdup (con);
          freecon (con);
        }
    }

  ret = TRUE;
 out:
  return ret;
#else
  return TRUE;
#endif
}

/**
 * ostree_sepolicy_restorecon:
 * @self: Self
 * @path: Path string to use for policy lookup
 * @info: (allow-none): File attributes
 * @target: Physical path to target file
 * @flags: Flags controlling behavior
 * @out_new_label: (allow-none) (out): New label, or %NULL if unchanged
 * @cancellable: Cancellable
 * @error: Error
 *
 * Reset the security context of @target based on the SELinux policy.
 */
gboolean
ostree_sepolicy_restorecon (OstreeSePolicy    *self,
                            const char       *path,
                            GFileInfo        *info,
                            GFile            *target,
                            OstreeSePolicyRestoreconFlags flags,
                            char            **out_new_label,
                            GCancellable     *cancellable,
                            GError          **error)
{
#ifdef HAVE_SELINUX
  gboolean ret = FALSE;
  gs_unref_object GFileInfo *src_info = NULL;
  gs_free char *label = NULL;
  gboolean do_relabel = TRUE;

  if (info != NULL)
    src_info = g_object_ref (info);
  else
    {
      src_info = g_file_query_info (target, "unix::mode",
                                    G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
                                    cancellable, error);
      if (!src_info)
        goto out;
    }

  if (flags & OSTREE_SEPOLICY_RESTORECON_FLAGS_KEEP_EXISTING)
    {
      char *existing_con = NULL;
      if (lgetfilecon_raw (gs_file_get_path_cached (target), &existing_con) > 0
          && existing_con)
        {
          do_relabel = FALSE;
          freecon (existing_con);
        }
    }

  if (do_relabel)
    {
      if (!ostree_sepolicy_get_label (self, path, 
                                      g_file_info_get_attribute_uint32 (src_info, "unix::mode"),
                                      &label,
                                      cancellable, error))
        goto out;

      if (!label)
        {
          if (!(flags & OSTREE_SEPOLICY_RESTORECON_FLAGS_ALLOW_NOLABEL))
            {
              g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
                           "No label found for '%s'", path);
              goto out;
            }
        }
      else
        {
          int res = lsetfilecon (gs_file_get_path_cached (target), label);
          if (res != 0)
            {
              ot_util_set_error_from_errno (error, errno);
              goto out;
            }
        }
    }

  ret = TRUE;
  gs_transfer_out_value (out_new_label, &label);
 out:
  return ret;
#else
  return TRUE;
#endif
}
