bug-hurd
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

A deferred authorization-granting translator


From: Dr. Arne Babenhauserheide
Subject: A deferred authorization-granting translator
Date: Wed, 29 Dec 2021 12:56:23 +0100
User-agent: mu4e 1.6.10; emacs 27.2

Hi,


I created a project to ask for granting authorization when processes
access a file. It is built as a translator and a simple permission
granting program.

The translator can delegate permission-granting to the program via two
FIFO files. The goal is to create a simple replacement for the
use-case of polkit of granting privilege to a process to access some
resource after user-interaction with a permission-granting daemon.

This is the simplest structure I could devise for the use-case: The
whole system is implemented in about 150 lines of C for the translator
(building on hello-mt) and 30 lines of bash for the permission
granting program.

This code is supported by a small nlnet grant to provide one component
for sound in the Hurd: practical fine-grained access-control:
https://nlnet.nl/project/Hurd-Audio/

The original plan in that project was to add sound itself. I
retargeted it last year to access control to avoid running into
conflicts with the currently running rump-kernel work.


Code
----

The code for the translator is attached as patch against
https://git.savannah.gnu.org/cgit/hurd/hurd.git

It adds the trans/checkperm.c translator and adds it to trans/Makefile.

The code for the program is provided inline in this email in the
usage-example.


Usage Example
-------------

We restrict a the node /hello to require explicit permission for every
PID that does not have the group `user`. This notably does include
processes started by root.


### How it looks

**First shell** as root:

    settrans -cga /hello $(realpath ~/Dev/hurd/trans/checkperms) 
--groupname=user
    su - user --shell /bin/bash -c 'cat /hello'
    # ⇒ HELLOWORLD # user has the group user
    cat /hello # root does not have the group user, so 
               # this blocks until positive reply in the other shell
    
**Second shell** (run the program):

    Process 732 tries to access file /hello but is not in the required group 
user.
    USER       PID %CPU %MEM    SZ   RSS TT STAT   START     TIME COMMAND
    root       732  0.0  0.1  148M 3.55M p2 Sso  Mon 1AM  0:01.10 -bash
    Grant permission and add group "user" for 5 minutes? [y/N]> y

**First shell** as root:

    # ⇒ HELLOWORLD
    # only blocks once despite getting two reads from cat, 
    # because for the second read cat already has the group `user`.



### Trying it yourself

Setup the development environment with the code at ~/Dev similar to
https://www.draketo.de/software/hurd-development-environment


Compile and setup the translator:

    cd ~/Dev/hurd && \
    patch -p1 < checkperms.patch && \
    autoreconf -i  && \
    ./configure --without-parted && \
    make && \
    touch trans/checkperms.c && \
    CFLAGS="$CFLAGS -g" make && \
    echo HELLOWORLD > /hello && \
    settrans -cga /hello $(realpath ~/Dev/hurd/trans/checkperms) 
--groupname=user

Create the FIFOs:

    USER=root
    GROUP=user
    mkdir -p /run/$USER/request-permission
    mkdir -p /run/$USER/grant-permission
    mkfifo /run/$USER/request-permission/$GROUP
    mkfifo /run/$USER/grant-permission/$GROUP

Setup the permission-granting program in a separate shell:

    USER=root
    GROUP=user
    while true; do
      PID="$(cat /run/$USER/request-permission/$GROUP)"
      echo Process $PID tries to access file /hello but is not in the required 
group $GROUP.
      ps-hurd -p $PID -aeux
      if [[ "$(read -e -p 'Grant permission and add group "'$GROUP'" for 5 
minutes? [y/N]> '; echo $REPLY)" == [Yy]* ]]; then
        addauth -p $PID -g $GROUP
        echo 0 > /run/$USER/grant-permission/$GROUP
        (sleep 300 && rmauth -p $PID -g $GROUP 2>/dev/null) &
      else
        echo 1 > /run/$USER/grant-permission/$GROUP
      fi
    done


Access the translator as user without the required group and with the group:

    su - user --shell /bin/bash -c 'head -n 1 /hello'
    head -n 1 /hello & 

Grant permission (currently need to do it for every line that is being read):

    USER=root
    GROUP=user
    cat /run/$USER/request-permission/$GROUP ; echo 0 > 
/run/$USER/grant-permission/$GROUP


Concept
-------

### The translator

The translator is started with a GROUP as argument. When the file is
accessed, the translator checks whether the process has the given
group. If it does, it returns data read from the underlying file.

If the process lacks the required group, the translator retrieves its
USER and PID and writes the PID into a FIFO located at

    /run/USER/request-permission/GROUP

Then it reads from 

    /run/USER/grant-permission/GROUP

It blocks until it gets a reply. If it reads a 0 (=success), it reads
from the file and returns the data.

### The permission granting program

The permission granting program reads the PID from

    /run/USER/request-permission/GROUP

retrieves information about the PID and asks the user whether to allow
the program.

If the USER answers no, the RET value is non-zero.

If the USER answers yes, the RET value is zero (0) 
and the program adds the GROUP to the process at PID (using addauth).

It also starts a daemon that will remove the group again after 5
minutes (modelled after the temporary permissions to run privileged
without password granted by sudo).

The program then writes the RET value into 

    /run/USER/grant-permission/GROUP

### What if the translator crashes?

If the translator crashes, the permissions return to those of the
underlying node. For every user except root this usually means that
the process does not have access to the file.

The failure-mode should therefore be safe.


Current limitations
-------------------

The current implementation only provides read-access, writing is
prevented. This is not an intrinsic limitation, only an implementation
artefact.

The underlying file is currently read by the translator and the data
returned to the reading process. To reduce delays, it could directly
delegate to the underlying file. With the long term goal to provide
multiplexing of access, for example for audio, reading via the
translator could be preferable, though.

Writing to and reading from the FIFOs is currently done with
`system()`. It would be nicer to move to an implementation that does
not rely on the system-shell.

Accesses from two different translators can currently race for the
reply. To fix this, the translator should write the PID and a random
LABEL into the request. The program should repeat that label for
replies to ensure that the reply and request can be matched. If
receiving a non-matching reply, it MUST be written into the grant
again after a random delay to enable a matching translator to
retrieve the grant.  
REQUEST: PID LABEL  
GRANT: RET LABEL (RET=0 is success)  
LABEL=$RANDOM

The system assumes having a single permission granting program per
user. For a setup with multiple unconnected sessions per user (like
several TTYs) the permission granting program needs to coordinate
between these.


Possibilities
-------------

The most important use-case for this translator is to make it easier
to start programs with reduced permissions and only add these when
required.

To setup deferred permissions for a single file, you can create a
group just for that file. Then each file can have its own permission
granting program. Having dedicated groups decouples authentication and
authorization while staying in the conventional *nix permissions
scheme.

You can also set this translator on a file that gets accessed first
when a process accesses a set of related files that all have the same
group. Since the authorization-program here adds the group for 5
minutes, the other files can afterwards be accessed, too.

Since the translator simply defers to a program, that program could do
any action to get authorization, including `curl`. Administrators for
a local network could therefore set up terminals for unprivileged
users that request permissions from a local server when accessing a
file. That way permissions can easily be coordinated over multiple
machines. (naturally this does not restrict root who can always use
settrans -g to get raw access to the file)


Personal note
-------------

There’s a magic in being asked on the second shell whether cat on the
first shell should be allowed to access a file for 5 minutes — and all
that in 150 lines of C and 30 lines of shell.

The Hurd is pretty cool! (but you knew that ;-) )

(though I have to admit that getting the translator to actually work
took ages)


Best wishes,
Arne

diff --git a/trans/Makefile b/trans/Makefile
--- a/trans/Makefile
+++ b/trans/Makefile
@@ -20,16 +20,16 @@ dir := trans
 makemode := servers
 
 targets = symlink firmlink ifsock magic null fifo new-fifo fwd crash \
-         password hello hello-mt streamio fakeroot proxy-defpager remap \
+         password hello hello-mt checkperms streamio fakeroot proxy-defpager 
remap \
          mtab
 SRCS = ifsock.c symlink.c magic.c null.c fifo.c new-fifo.c fwd.c \
-       crash.c firmlink.c password.c hello.c hello-mt.c streamio.c \
+       crash.c firmlink.c password.c hello.c hello-mt.c checkperms.c 
streamio.c \
        fakeroot.c proxy-defpager.c remap.c mtab.c
 OBJS = $(SRCS:.c=.o) fsysServer.o ifsockServer.o passwordServer.o \
        crashServer.o crash_replyUser.o msgServer.o \
        default_pagerServer.o default_pagerUser.o \
        device_replyServer.o elfcore.o startup_notifyServer.o
-HURDLIBS = ports netfs trivfs iohelp fshelp pipe ihash shouldbeinlibc
+HURDLIBS = ports netfs trivfs iohelp fshelp pipe ihash shouldbeinlibc ps
 LDLIBS += -lpthread
 password-MIGSFLAGS=\
     "-DIO_INTRAN=trivfs_protid_t trivfs_begin_using_protid (io_t)" \
@@ -81,6 +81,7 @@ symlink: fsysServer.o
 fakeroot: ../libnetfs/libnetfs.a
 fifo new-fifo: ../libpipe/libpipe.a
 crash fifo firmlink hello hello-mt ifsock magic mtab new-fifo null password 
proxy-defpager remap streamio: ../libtrivfs/libtrivfs.a
+checkperms: ../libtrivfs/libtrivfs.a ../libps/libps.a
 $(targets): ../libfshelp/libfshelp.a \
        ../libihash/libihash.a \
        ../libiohelp/libiohelp.a \
diff --git a/trans/hello-mt.c b/trans/checkperms.c
copy from trans/hello-mt.c
copy to trans/checkperms.c
--- a/trans/hello-mt.c
+++ b/trans/checkperms.c
@@ -1,4 +1,4 @@
-/* hello-mt.c - A trivial single-file translator, multithreaded version
+/* checkperms.c - A permission-checking and granting translator
    Copyright (C) 1998,99,2001,02,2006 Free Software Foundation, Inc.
 
    This program is free software; you can redistribute it and/or
@@ -18,6 +18,7 @@
 #define _GNU_SOURCE 1
 
 #include <hurd/trivfs.h>
+#include <idvec.h>
 #include <stdio.h>
 #include <stdlib.h>
 #include <argp.h>
@@ -27,17 +28,23 @@
 #include <fcntl.h>
 #include <sys/mman.h>
 #include <pthread.h>
+#include <portinfo.h>
+#include <grp.h>
+#include <ps.h>
+#include <portxlate.h>
 
 #include <version.h>
 
 #include "libtrivfs/trivfs_io_S.h"
 
-const char *argp_program_version = STANDARD_HURD_VERSION (hello-mt);
+const char *argp_program_version = STANDARD_HURD_VERSION (checkperms);
 
 /* The message we return when we are read.  */
-static const char hello[] = "Hello, world!\n";
+static const char hello[] = "Hello, perms!\n";
 static char *contents = (char *) hello;
 static size_t contents_len = sizeof hello - 1;
+static const char defaultgroupname[] = "audio";
+static char *groupname = (char *) defaultgroupname;
 
 /* This lock protects access to contents and contents_len.  */
 static pthread_rwlock_t contents_lock;
@@ -77,12 +84,143 @@ struct open
   off_t offs;
 };
 
+
+/* adapted from utils/portinfo.c::search_for_port: Locates the port
+   NAME from TASK in any other process and prints the mappings. */
+error_t
+search_for_pid (char **output, task_t task, mach_port_t name)
+{
+  error_t err;
+
+  /* These resources are freed in the function epilogue.  */
+  struct ps_context *context = NULL;
+  struct proc_stat_list *procset = NULL;
+
+  static process_t proc = MACH_PORT_NULL;
+  if (proc == MACH_PORT_NULL)
+    proc = getproc ();
+
+  pid_t pid;
+  err = proc_task2pid (proc, task, &pid);
+  if (err)
+    goto out;
+
+  /* Get a list of all processes.  */
+  err = ps_context_create (getproc (), &context);
+  if (err)
+    goto out;
+
+  err = proc_stat_list_create (context, &procset);
+  if (err)
+    goto out;
+
+  err = proc_stat_list_add_all (procset, 0, 0);
+  if (err)
+    goto out;
+
+  for (unsigned i = 0; i < procset->num_procs; i++)
+    {
+      /* Ignore the target process.  */
+      if (procset->proc_stats[i]->pid == pid)
+       continue;
+
+      task_t xlate_task = MACH_PORT_NULL;
+      err = proc_pid2task (proc, procset->proc_stats[i]->pid, &xlate_task);
+      if (err || xlate_task == MACH_PORT_NULL)
+       continue;
+
+      struct port_name_xlator *xlator = NULL;
+      err = port_name_xlator_create (task, xlate_task, &xlator);
+      if (err)
+       goto loop_cleanup;
+
+      mach_port_t translated_port;
+      mach_msg_type_name_t translated_type;
+        err = port_name_xlator_xlate (xlator,
+                                   name, 0,
+                                   &translated_port, &translated_type);
+      if (err)
+       goto loop_cleanup;
+
+      /* The port translation was successful, print more infos.  */
+      // output again, not called??
+      asprintf (output, "% 5i", procset->proc_stats[i]->pid);
+      goto out;
+
+    loop_cleanup:
+      if (xlate_task)
+       mach_port_deallocate (mach_task_self (), xlate_task);
+
+      if (xlator)
+       port_name_xlator_free (xlator);
+    }
+
+  err = 0;
+
+ out:
+  if (procset != NULL)
+    proc_stat_list_free (procset);
+
+  if (context != NULL)
+    ps_context_free (context);
+
+  return err;
+}
+
+
+int check_group (struct idvec *gids) {
+    /* Check whether the process has the checked group */
+  unsigned has_group = 0;
+  struct group _gr, *gr;
+  char buf[1024];
+  for (unsigned i = 0; i < gids->num; i++) {
+    if (getgrgid_r (gids->ids[i], &_gr, buf, sizeof buf, &gr) == 0 && gr)
+      {
+        if (strcmp(groupname, strdup (gr->gr_name)) == 0)
+          {
+            has_group += 1;
+          }
+      }
+  }
+  return has_group;
+}
+
+
+error_t request_auth (struct trivfs_protid *cred) {
+      /* specify the contents to show dynamically */
+  struct port_info info = cred->pi;
+  // struct rpc_info *rpcs = info.current_rpcs;
+  struct port_bucket *bucket = info.bucket;
+  mach_port_t portright = info.port_right;
+  
+  task_t task = mach_task_self ();
+  unsigned show = 0;
+  show |= PORTINFO_DETAILS;
+  const char otherinfo_arr[1024];
+  char *otherinfo = (char *) otherinfo_arr;
+  search_for_pid (&otherinfo, task, portright);
+  // for debugging:
+  // contents_len = asprintf(&dat, "%d\n%d\n%s\n%s\n%d\n", getpid (), 
bucket->count, otherinfo, idvec_gids_rep(cred->user->gids, 1, 1, ","), 
has_group);
+  char request_filename_arr[1024];
+  char *request_filename = (char *) request_filename_arr;
+  asprintf(&request_filename, "/run/%s/request-permission/%s", 
idvec_uids_rep(cred->user->uids, 0, 1, ","), groupname);
+  char grant_filename_arr[1024];
+  char *grant_filename = (char *) grant_filename_arr;
+  asprintf(&grant_filename, "/run/%s/grant-permission/%s", 
idvec_uids_rep(cred->user->uids, 0, 1, ","), groupname);
+  // FIXME: replace system(command) by proper io_write and port lookup (I did 
not get it working yet)
+  char command_arr[1024];
+  char *command = (char *) command_arr;
+  asprintf(&command, "echo \"%s\" > %s ; exit $(cat \"%s\")", otherinfo, 
request_filename, grant_filename);
+  return system(command);
+}
+
+
 void
 trivfs_modify_stat (struct trivfs_protid *cred, struct stat *st)
 {
   /* Mark the node as a read-only plain file. */
   st->st_mode &= ~(S_IFMT | ALLPERMS);
-  st->st_mode |= (S_IFREG | S_IRUSR | S_IRGRP | S_IROTH);
+  st->st_mode |= (S_IFREG | S_IRUSR | S_IRGRP); //  | S_IROTH);
   st->st_size = contents_len;  /* No need to lock for reading one word.  */
 }
 
@@ -128,12 +266,16 @@ trivfs_S_io_read (struct trivfs_protid *
                  loff_t offs, mach_msg_type_number_t amount)
 {
   struct open *op;
+  error_t err;
+
+  const char contents_array[amount];
+  char *dat = (char *) contents_array;
 
   /* Deny access if they have bad credentials. */
   if (! cred)
     return EOPNOTSUPP;
   else if (! (cred->po->openmodes & O_READ))
-    return EBADF;
+     return EBADF;
 
   op = cred->po->hook;
 
@@ -145,37 +287,52 @@ trivfs_S_io_read (struct trivfs_protid *
 
   pthread_rwlock_rdlock (&contents_lock);
 
-  /* Prune the amount they want to read. */
-  if (offs > contents_len)
-    offs = contents_len;
-  if (offs + amount > contents_len)
-    amount = contents_len - offs;
+  /* Check whether the process has the checked group */
+  unsigned has_group = check_group(cred->user->gids);
+  if (has_group == 0)
+      err = request_auth (cred);
+
+  if (has_group > 0 || !err) 
+    // TODO: delegate to or hand over the underlying node directly on lookup 
to reduce delays
+     err = io_read (cred->realnode,
+                  &dat, data_len, offs, *data_len);
 
-  if (amount > 0)
+  if (!err)
     {
-      /* Possibly allocate a new buffer. */
-      if (*data_len < amount)
-       *data = mmap (0, amount, PROT_READ|PROT_WRITE, MAP_ANON, 0, 0);
-      if (*data == MAP_FAILED)
+      char *contents = (char *) dat;
+      contents_len = strlen(dat);
+  
+      /* Prune the amount they want to read. */
+      if (offs > contents_len)
+       offs = contents_len;
+      if (offs + amount > contents_len)
+       amount = contents_len - offs;
+
+      if (amount > 0)
        {
-         pthread_mutex_unlock (&op->lock);
-         pthread_rwlock_unlock (&contents_lock);
-         return ENOMEM;
-       }
+         /* Possibly allocate a new buffer. */
+         if (*data_len < amount)
+           *data = mmap (0, amount, PROT_READ|PROT_WRITE, MAP_ANON, 0, 0);
+         if (*data == MAP_FAILED)
+           {
+             pthread_mutex_unlock (&op->lock);
+             pthread_rwlock_unlock (&contents_lock);
+             return ENOMEM;
+           }
 
-      /* Copy the constant data into the buffer. */
-      memcpy ((char *) *data, contents + offs, amount);
+         /* Copy the constant data into the buffer. */
+         memcpy ((char *) *data, contents + offs, amount);
 
-      /* Update the saved offset.  */
-      op->offs += amount;
+         /* Update the saved offset.  */
+         op->offs += amount;
+       }
     }
-
   pthread_mutex_unlock (&op->lock);
 
   pthread_rwlock_unlock (&contents_lock);
 
   *data_len = amount;
-  return 0;
+  return err;
 }
 
 
@@ -188,30 +345,34 @@ trivfs_S_io_seek (struct trivfs_protid *
   struct open *op;
   error_t err = 0;
   if (! cred)
-    return EOPNOTSUPP;
+    return (error_t) EOPNOTSUPP;
 
   op = cred->po->hook;
+  pthread_mutex_lock (&op->lock);
+  unsigned has_group = check_group(cred->user->gids);
+  if (has_group == 0)
+      err = request_auth (cred);
 
-  pthread_mutex_lock (&op->lock);
-
-  switch (whence)
+  if (has_group > 0 || !err) 
     {
-    case SEEK_CUR:
-      offs += op->offs;
-      goto check;
-    case SEEK_END:
-      offs += contents_len;
-    case SEEK_SET:
-    check:
-      if (offs >= 0)
+      switch (whence)
        {
-         *new_offs = op->offs = offs;
-         break;
+       case SEEK_CUR:
+         offs += op->offs;
+         goto check;
+       case SEEK_END:
+         offs += contents_len;
+       case SEEK_SET:
+       check:
+         if (offs >= 0)
+           {
+             *new_offs = op->offs = offs;
+             break;
+           }
+       default:
+         err = EINVAL;
        }
-    default:
-      err = EINVAL;
     }
-
   pthread_mutex_unlock (&op->lock);
 
   return err;
@@ -232,7 +393,7 @@ void (*trivfs_peropen_destroy_hook) (str
 
 static const struct argp_option options[] =
 {
-  {"contents", 'c', "STRING",  0, "Specify the contents of the virtual file"},
+  {"groupname",        'n', "STRING",  0, "Specify the group to check for"},
   {0}
 };
 
@@ -248,12 +409,13 @@ parse_opt (int opt, char *arg, struct ar
     case ARGP_KEY_ERROR:
       break;
 
-    case 'c':
+    case 'n':
       {
        char *new = strdup (arg);
        if (new == NULL)
          return ENOMEM;
        pthread_rwlock_wrlock (&contents_lock);
+    groupname = new;
        if (contents != hello)
          free (contents);
        contents = new;
@@ -278,10 +440,10 @@ trivfs_append_args (struct trivfs_contro
   char *c;
 
   s = open_memstream (&opt, &opt_len);
-  fprintf (s, "--contents='");
+  fprintf (s, "--groupname='");
 
   pthread_rwlock_rdlock (&contents_lock);
-  for (c = contents; *c; c++)
+  for (c = groupname; *c; c++)
     switch (*c)
       {
       case 0x27: /* Single quote.  */
@@ -333,7 +495,7 @@ main (int argc, char **argv)
     error (1, 0, "Must be started as a translator");
 
   /* Reply to our parent */
-  err = trivfs_startup (bootstrap, 0, 0, 0, 0, 0, &fsys);
+  err = trivfs_startup (bootstrap, O_READ, 0, 0, 0, 0, &fsys);
   mach_port_deallocate (mach_task_self (), bootstrap);
   if (err)
     error (3, err, "trivfs_startup");
-- 
Unpolitisch sein
heißt politisch sein,
ohne es zu merken.
draketo.de

Attachment: signature.asc
Description: PGP signature


reply via email to

[Prev in Thread] Current Thread [Next in Thread]