~kennylevinsen/pam_uaccess

54fbf043c63cc500b4850b0b4a12ea14078f2b53 — Kenny Levinsen 6 months ago master
Initial commit
4 files changed, 344 insertions(+), 0 deletions(-)

A LICENSE
A README.md
A meson.build
A pam_uaccess.c
A  => LICENSE +7 -0
@@ 1,7 @@
Copyright 2022 Kenny Levinsen

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

A  => README.md +63 -0
@@ 1,63 @@
# pam_uaccess

A PAM module that grants access to devices tagged "uaccess" in udev for the
duration of the users session.

Replaces (e)logind's uaccess feature. Requires udev rules that set the
'uaccess' tag.

Experimental.

## How to build and install

```sh
meson build --prefix /usr
ninja -C build
sudo ninja -C build install
```

The module will be installed to `/usr/lib/security/pam_uaccess.so`.

## How to use

To use the PAM module, it must be added to a relevant PAM stack:

```
session		optional	pam_uaccess.so
```

`getfacl` can be used to inspect files to see the added ACL. If no ACL is
present, ensure that udev is installed and running and ensure that udev rules
that will set 'uaccess' tags are in place and that they match your devices.

## Known issues

### Concurrent logins

pam_uaccess does not track the number of active logins of a user, and so the
grants made to a user will be removed when any session logs out, even if others
remain.

If this is a problem for your setup (it may not be - e.g., a single greetd
instance would not exhibit any issues with such setup), the skip_ungrant
argument can be specified to disable ungrant altogether:

```
session		optional	pam_uaccess.so	skip_ungrant
```

In this case, pam_uaccess will grant access to devices to a user on their first
login, and this access will persist until reboot or until the device is
removed.

### Hot-plug

pam_uaccess only operates on login, and does not grant access to new devices as
they are added.

This should not be a problem in most cases.

# How to discuss

Go to #kennylevinsen @ irc.libera.chat to discuss, or use
[~kennylevinsen/public-inbox@lists.sr.ht](https://lists.sr.ht/~kennylevinsen/public-inbox).

A  => meson.build +26 -0
@@ 1,26 @@
project(
	'pam_uaccess',
	'c',
	version: '0.1',
	license: 'MIT',
	meson_version: '>=0.58.0',
	default_options: [
		'c_std=c11',
		'warning_level=3',
		'werror=true',
	],
)

cc = meson.get_compiler('c')
shared_library(
	'pam_uaccess',
	['pam_uaccess.c'],
	dependencies: [
		cc.find_library('pam'),
		dependency('libacl'),
		dependency('libudev'),
	],
	name_prefix: '',
	install_dir: 'lib/security',
	install: true)


A  => pam_uaccess.c +248 -0
@@ 1,248 @@
#include <pwd.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include <syslog.h>
#include <sys/types.h>
#include <unistd.h>

#include <security/pam_modules.h>
#include <security/pam_appl.h>
#include <security/pam_ext.h>
#include <acl/libacl.h>
#include <libudev.h>

static struct udev_enumerate *enumerate_uaccess(struct udev *udev) {
        struct udev_enumerate *enumerator = udev_enumerate_new(udev);
        if (!enumerator) {
                return NULL;
        }

        udev_enumerate_add_match_tag(enumerator, "uaccess");
        if (udev_enumerate_scan_devices(enumerator) < 0) {
                udev_enumerate_unref(enumerator);
                return NULL;
        }

        return enumerator;
}

static int acl_find_uid(acl_t acl, uid_t uid, acl_entry_t *ret) {
        acl_entry_t idx;
        int res = acl_get_entry(acl, ACL_FIRST_ENTRY, &idx);
        while (res > 0) {
                acl_tag_t tag;
                if (acl_get_tag_type(idx, &tag) < 0) {
                        return -1;
                }
                if (tag == ACL_USER) {
                        uid_t *user = acl_get_qualifier(idx);
                        if (user == NULL) {
                                return -1;
                        }
                        bool match = *user == uid;
                        acl_free(user);
                        if (match) {
                                *ret = idx;
                                return 0;
                        }
                }
                res = acl_get_entry(acl, ACL_NEXT_ENTRY, &idx);
        }
        if (res < 0) {
                return -1;
        }
        *ret = NULL;
        return 0;
}

static void ungrant_access(const char *path, uid_t uid) {
        acl_t acl = acl_get_file(path, ACL_TYPE_ACCESS);
        if (acl == NULL) {
                return;
        }

        acl_entry_t entry;
        if (acl_find_uid(acl, uid, &entry) == -1) {
                goto error;
        }
        acl_delete_entry(acl, entry);
        if (acl_calc_mask(&acl) < 0) {
                goto error;
        }
        if (acl_set_file(path, ACL_TYPE_ACCESS, acl) < 0) {
                goto error;
        }

error:
        acl_free(acl);
        return;
}

static void grant_access(const char *path, uid_t uid) {
        acl_t acl = acl_get_file(path, ACL_TYPE_ACCESS);
        if (acl == NULL) {
                return;
        }

        acl_entry_t entry;
        if (acl_find_uid(acl, uid, &entry) == -1) {
                goto error;
        }
        if (entry == NULL) {
                if (acl_create_entry(&acl, &entry) < 0) {
                        goto error;
                }
                if (acl_set_tag_type(entry, ACL_USER) < 0) {
                        goto error;
                }
                if (acl_set_qualifier(entry, &uid) < 0) {
                        goto error;
                }
        }

        acl_permset_t permset;
        if (acl_get_permset(entry, &permset) < 0) {
                goto error;
        }
        if (acl_add_perm(permset, ACL_READ | ACL_WRITE) < 0) {
                goto error;
        }
        if (acl_calc_mask(&acl) < 0) {
                goto error;
        }
        if (acl_set_file(path, ACL_TYPE_ACCESS, acl) < 0) {
                goto error;
        }

error:
        acl_free(acl);
        return;
}

int pam_sm_open_session(pam_handle_t *pamh, int flags, int argc, const char **argv) {
        (void)flags;
        (void)argc;
        (void)argv;

        if (geteuid() != 0) {
                pam_syslog(pamh, LOG_ERR, "pam_uaccess can only be used by a root process");
                return PAM_SESSION_ERR;
        }

        for (int idx = 0; idx < argc; idx++) {
                if (strcmp(argv[idx], "skip_ungrant") == 0) {
                        // Used in pam_sm_close_session
                } else {
                        pam_syslog(pamh, LOG_ERR, "Unknown argument to pam_uaccess: %s", argv[idx]);
                        return PAM_SESSION_ERR;
                }
        }

        const char *user;
        if (pam_get_user(pamh, &user, NULL) != PAM_SUCCESS) {
                return PAM_USER_UNKNOWN;
        }

        const struct passwd *pw = getpwnam(user);
        if (pw == NULL) {
                return PAM_USER_UNKNOWN;
        }

        struct udev *udev = udev_new();
        if (udev == NULL) {
                pam_syslog(pamh, LOG_ERR, "Could not create udev instance");
                return PAM_SESSION_ERR;
        }

        struct udev_enumerate *enumerator = enumerate_uaccess(udev);
        if (enumerator == NULL) {
                udev_unref(udev);
                pam_syslog(pamh, LOG_ERR, "Could not create udev enumerator");
                return PAM_SESSION_ERR;
        }

        if (udev_enumerate_get_list_entry(enumerator) == NULL) {
                udev_enumerate_unref(enumerator);
                udev_unref(udev);
                pam_syslog(pamh, LOG_ERR, "Could not find any devices tagged with uaccess");
                return PAM_SESSION_ERR;
        }

        struct udev_list_entry *entry;
        udev_list_entry_foreach(entry, udev_enumerate_get_list_entry(enumerator)) {
                const char *path = udev_list_entry_get_name(entry);
                struct udev_device *dev = udev_device_new_from_syspath(udev, path);
                grant_access(udev_device_get_devnode(dev), pw->pw_uid);
                udev_device_unref(dev);
        }

        udev_enumerate_unref(enumerator);
        udev_unref(udev);

        return PAM_SUCCESS;
}

int pam_sm_close_session(pam_handle_t *pamh, int flags, int argc, const char **argv) {
        (void)flags;
        (void)argc;
        (void)argv;

        if (geteuid() != 0) {
                pam_syslog(pamh, LOG_ERR, "pam_uaccess can only be used by a root process");
                return PAM_SESSION_ERR;
        }

        for (int idx = 0; idx < argc; idx++) {
                if (strcmp(argv[idx], "skip_ungrant") == 0) {
                        return PAM_SUCCESS;
                } else {
                        pam_syslog(pamh, LOG_ERR, "Unknown argument to pam_uaccess: %s", argv[idx]);
                        return PAM_SESSION_ERR;
                }
        }

        const char *user;
        if (pam_get_user(pamh, &user, NULL) != PAM_SUCCESS) {
                return PAM_USER_UNKNOWN;
        }

        const struct passwd *pw = getpwnam(user);
        if (pw == NULL) {
                return PAM_USER_UNKNOWN;
        }

        struct udev *udev = udev_new();
        if (udev == NULL) {
                pam_syslog(pamh, LOG_ERR, "Could not create udev instance");
                return PAM_SESSION_ERR;
        }

        struct udev_enumerate *enumerator = enumerate_uaccess(udev);
        if (enumerator == NULL) {
                udev_unref(udev);
                pam_syslog(pamh, LOG_ERR, "Could not create udev enumerator");
                return PAM_SESSION_ERR;
        }

        if (udev_enumerate_get_list_entry(enumerator) == NULL) {
                udev_enumerate_unref(enumerator);
                udev_unref(udev);
                pam_syslog(pamh, LOG_ERR, "Could not find any devices tagged with uaccess");
                return PAM_SESSION_ERR;
        }

        struct udev_list_entry *entry;
        udev_list_entry_foreach(entry, udev_enumerate_get_list_entry(enumerator)) {
                const char *path = udev_list_entry_get_name(entry);
                struct udev_device *dev = udev_device_new_from_syspath(udev, path);
                ungrant_access(udev_device_get_devnode(dev), pw->pw_uid);
                udev_device_unref(dev);
        }

        udev_enumerate_unref(enumerator);
        udev_unref(udev);

        return PAM_SUCCESS;
}