diff --git a/rules/ham.dep b/rules/ham.dep new file mode 100644 index 00000000000..bd591e01db2 --- /dev/null +++ b/rules/ham.dep @@ -0,0 +1,9 @@ +DEP_FILES := $(SONIC_COMMON_FILES_LIST) rules/ham.mk rules/ham.dep +DEP_FILES += $(SONIC_COMMON_BASE_FILES_LIST) + +$(SONIC_HAMD)_CACHE_MODE := GIT_CONTENT_SHA +$(SONIC_HAMD)_DEP_FLAGS := $(SONIC_COMMON_FLAGS_LIST) +$(SONIC_HAMD)_DEP_FILES := $(DEP_FILES) +$(SONIC_HAMD)_SMDEP_FILES := $(SMDEP_FILES) +$(SONIC_HAMD)_SMDEP_PATHS := $(SPATH) + diff --git a/rules/ham.mk b/rules/ham.mk new file mode 100644 index 00000000000..f03e52378eb --- /dev/null +++ b/rules/ham.mk @@ -0,0 +1,21 @@ +HAM_VERSION := 1.0.0 + +# Main package +HAM = ham_$(HAM_VERSION)_$(CONFIGURED_ARCH).deb +$(HAM)_SRC_PATH = $(SRC_PATH)/ham +$(HAM)_DEPENDS = $(LIBSWSSCOMMON_DEV) +$(HAM)_RDEPENDS = $(LIBSWSSCOMMON) + +SONIC_DPKG_DEBS += $(HAM) + +# Additional packages +LIBHAM_DEV = libham-dev_$(HAM_VERSION)_$(CONFIGURED_ARCH).deb +LIBHAM = libham_$(HAM_VERSION)_$(CONFIGURED_ARCH).deb +LIBNSS_HAM = libnss-ham_$(HAM_VERSION)_$(CONFIGURED_ARCH).deb +LIBNSS_SAC = libnss-sac_$(HAM_VERSION)_$(CONFIGURED_ARCH).deb + +$(eval $(call add_derived_package,$(HAM),$(LIBHAM_DEV))) +$(eval $(call add_derived_package,$(HAM),$(LIBHAM))) +$(eval $(call add_derived_package,$(HAM),$(LIBNSS_HAM))) +$(eval $(call add_derived_package,$(HAM),$(LIBNSS_SAC))) + diff --git a/sonic-slave-buster/Dockerfile.j2 b/sonic-slave-buster/Dockerfile.j2 index 7a970679a6f..7bfa9755929 100644 --- a/sonic-slave-buster/Dockerfile.j2 +++ b/sonic-slave-buster/Dockerfile.j2 @@ -271,6 +271,8 @@ RUN apt-get update && apt-get install -y \ # For initramfs shellcheck \ bash-completion \ +# For HAM + libdbus-c++-dev \ {%- if CONFIGURED_ARCH == "amd64" %} # For sonic vs image build dosfstools \ diff --git a/sonic-slave-stretch/Dockerfile.j2 b/sonic-slave-stretch/Dockerfile.j2 index 59a7044f385..452079f0b60 100644 --- a/sonic-slave-stretch/Dockerfile.j2 +++ b/sonic-slave-stretch/Dockerfile.j2 @@ -263,6 +263,8 @@ RUN apt-get update && apt-get install -y \ texi2html \ # For initramfs bash-completion \ +# For HAM + libdbus-c++-dev \ {%- if CONFIGURED_ARCH == "amd64" %} # For sonic vs image build dosfstools \ diff --git a/src/ham/.gitignore b/src/ham/.gitignore new file mode 100644 index 00000000000..690b7fc1d2c --- /dev/null +++ b/src/ham/.gitignore @@ -0,0 +1,7 @@ +*.d +*.o +*.so +*.so.2 +*.dbus-proxy.h +*.dbus-adaptor.h +hamd/hamd diff --git a/src/ham/Makefile b/src/ham/Makefile new file mode 100644 index 00000000000..041ce818874 --- /dev/null +++ b/src/ham/Makefile @@ -0,0 +1,44 @@ +.ONESHELL: +.SHELLFLAGS += -e + +TOPDIR := $(abspath .) +INSTALL := /usr/bin/install +SUB-DIRS := $(dir $(wildcard $(addsuffix /Makefile,$(shell find * -maxdepth 0 -type d | sort )))) + + +.PHONY: all +all: + @for dir in ${SUB-DIRS}; do \ + make -C $${dir}; \ + done + + +.PHONY: install +install: + rsync --archive --verbose --no-owner --no-group $(TOPDIR)/hamd/etc $(DESTDIR) + rsync --archive --verbose --no-owner --no-group $(TOPDIR)/libham/usr $(DESTDIR) + + # Create destination directories + $(INSTALL) -d $(DESTDIR)/usr/sbin/ + $(INSTALL) -d $(DESTDIR)/usr/bin/ + $(INSTALL) -d $(DESTDIR)/lib/x86_64-linux-gnu/ # NSS libraries MUST be under /lib and NOT /usr/lib + $(INSTALL) -d $(DESTDIR)/usr/lib/x86_64-linux-gnu/ + + # Copy files over to newly created directories + $(INSTALL) -D $(TOPDIR)/hamd/hamd $(DESTDIR)/usr/sbin/. + $(INSTALL) -D $(TOPDIR)/hamctl/hamctl $(DESTDIR)/usr/bin/. + $(INSTALL) -D $(TOPDIR)/libham/libham.so $(DESTDIR)/usr/lib/x86_64-linux-gnu/. + $(INSTALL) -D $(TOPDIR)/libnss_ham/libnss_ham.so.2 $(DESTDIR)/lib/x86_64-linux-gnu/. + $(INSTALL) -D $(TOPDIR)/libnss_sac/libnss_sac.so.2 $(DESTDIR)/lib/x86_64-linux-gnu/. + + +.PHONY: clean +clean: + @for dir in ${SUB-DIRS}; do \ + make -C $${dir} clean; \ + done + +.PHONY: cleanall +cleanall: + git clean -fdx + diff --git a/src/ham/README.md b/src/ham/README.md new file mode 100644 index 00000000000..badd019cdb6 --- /dev/null +++ b/src/ham/README.md @@ -0,0 +1,66 @@ +# Host Account Management + +## Introduction + +Host Account Management, or HAM, serves as the single source of truth for SONiC user accounts. HAM was created to solve several problems related to user accounts. Here's a summary: + +- Allow applications running in containers to retrieve user account information on the host using standard APIs such as `getpwnam()`, `getgrname()`, etc. +- Allow applications running in containers to create/modify/delete local user accounts on the host. +- Automatically assign UNIX user credentials (UID/GID) for users authenticated by RADIUS/TACACS+ that do not natively provide UNIX credentials. +- Allow additional credentials (such as keys or certificates) to be automatically created when a new user account is created. + +HAM was introduced in [AAA Improvements](https://github.com/Azure/SONiC/blob/master/doc/aaa/AAA%20Improvements/AAA%20Improvements.md). This document lists several challenges related to AAA (Authentication, Authorization, and Accounting) and how HAM helps mitigate some of these problems. This document provides details about the HAM software. + +## Components + +HAM provides the following components. + +| Package | Description | +| ----------------------------- | ------------------------------------------------------------ | +| `ham_[ver]_[arch].deb` | Contains `hamd`, the HAM daemon, and `hamctl` a companion utility program for `hamd`. | +| `libham_[ver]_[arch].deb` | Run-time library that primarily provides APIs to interact with `hamd` over D-Bus. | +| `libham-dev_[ver]_[arch].deb` | The counterpart development library to `libham_[version]_[arch].deb` | +| `libnss-ham_[ver]_[arch].deb` | NSS module that allows containerized apps to access user accounts on the host. | +| `libnss-sac_[ver]_[arch].deb` | NSS module for System-Assigned Credential | + +## HAM Daemon (`hamd`) + +The HAM daemon, `hamd`, is managed by `systemd`. The following `systemctl` commands are supported: + +| Command | Description | +| -------------------------------- | ------------------------------------------- | +| `systemctl start hamd.service` | Start `hamd` | +| `systemctl stop hamd.service` | Stop `hamd` | +| `systemctl restart hamd.service` | Restart `hamd`. Effectively a stop + start. | +| `systemctl reload hamd.service` | Reload configuration. | + +### Configuration file + +`hamd`'s configuration file is: `/etc/sonic/hamd/config`. This file can be modified while the daemon is running. The reload command described above can be issued to reload the configuration while the daemon is running. + +The configuration file content is self-documented (look in the file to see which parameters are configurable). + +### D-Bus interface + +The interface to `hamd` is D-Bus. The D-Bus introspect method should be called to determine what APIs are available. The D-Bus interface is defined by the IDL (interface description language) file: [org.SONiC.HostAccountManagement.xml](./shared/org.SONiC.HostAccountManagement.xml). + +#### D-Bus security + +Many of the D-Bus APIs are restricted. Some are restricted to `root` only, others are restricted to groups `hamd_accounts` and `hamd`. The security policy can be found in `/etc/dbus-1/system.d/org.SONiC.HostAccountManagement.conf`. + +## The hamctl utility + +This is a shell utility designed for "human" users. It should not be invoked by other programs. It is a companion program to `hamd`. The `hamctl` utility is mainly used for debugging purposes. It is self-documented. Simply type "`hamctl --help`" for help. + +## Roles to group mapping + +It is possible to add users of a given role to specific Linux groups. For example, one may want to map users with "`admin`" role to the "`sudo`" group. The mapping can be defined in: `/etc/sonic/hamd/group-mapping`. + +## Application-specific credentials + +One can tell `hamd` to create supplemental user credentials with the use of plug-in scripts. For example, if one would like all new users created by `hamd` to automatically be assigned an SSH key, one need only add a script to the directory `/etc/sonic/hamd/scripts/post-create/`. Scripts added to this directory will be invoked, in asciibetical order, when users are created. Similarly, when users are deleted, `hamd` will invoke the scripts in `/etc/sonic/hamd/scripts/pre-delete/`. Note that when deleting a user account, `hamd` always deletes the whole home directory. So, it is not necessary to provide a "`/pre-delete`" to delete files from the home directory since those will be removed anyway. + +The script syntax is defined in these two files. + +- `/etc/sonic/hamd/scripts/post-create/README` +- `/etc/sonic/hamd/scripts/pre-delete/README` \ No newline at end of file diff --git a/src/ham/debian/.gitignore b/src/ham/debian/.gitignore new file mode 100644 index 00000000000..77d4f4a0bd1 --- /dev/null +++ b/src/ham/debian/.gitignore @@ -0,0 +1,8 @@ +.debhelper/ +*.debhelper +*.debhelper.log +*.substvars +tmp/ +files +*/DEBIAN/* + diff --git a/src/ham/debian/changelog b/src/ham/debian/changelog new file mode 100644 index 00000000000..22c153a3026 --- /dev/null +++ b/src/ham/debian/changelog @@ -0,0 +1,5 @@ +ham (1.0.0) stable; urgency=medium + + * Initial release. + + -- Martin Belanger Wed, 23 Sep 2020 10:50:00 -0400 diff --git a/src/ham/debian/compat b/src/ham/debian/compat new file mode 100644 index 00000000000..ec635144f60 --- /dev/null +++ b/src/ham/debian/compat @@ -0,0 +1 @@ +9 diff --git a/src/ham/debian/control b/src/ham/debian/control new file mode 100644 index 00000000000..013ab04b056 --- /dev/null +++ b/src/ham/debian/control @@ -0,0 +1,58 @@ +Source: ham +Maintainer: Martin Belanger +Build-Depends: debhelper (>= 8.0.0), + dh-systemd, + libdbus-c++-dev, + libglib2.0-dev, + pkg-config +Vcs-Git: https://github.com/Azure/sonic-buildimage +Homepage: https://azure.github.io/SONiC/ +Standards-Version: 3.9.3 +Section: net + +Package: ham +Priority: extra +Architecture: amd64 +Depends: libdbus-c++-1-0v5, libglib2.0-0, libhiredis0.14, ${shlibs:Depends}, ${misc:Depends} +Description: SONiC Host Account Management + This package provides the daemon, hamd, which runs on the host and provides + user and group account management services. It allows applications running + in containers to retrieve account from the host. It can also allow users with + administrative privileges to create, modify, or delete user accounts on the + host. For example, an admin user running a CLI session in a container + could request hamd to create a new user account. + +Package: libnss-ham +Priority: extra +Architecture: amd64 +Depends: libdbus-c++-1-0v5, ${shlibs:Depends}, ${misc:Depends} +Description: SONiC Host Account Management NSS module + The HAM NSS module allows applications running in containers to retrieve + host's user accounts (e.g. /etc/passwd, /etc/group, and /etc/shadow). To do + that, the NSS module contacts the HAM daemon (hamd) running on the host over + DBus. THIS PACKAGE IS MEANT TO BE INSTALLED IN CONTAINERS ONLY! + +Package: libnss-sac +Priority: extra +Architecture: amd64 +Depends: libdbus-c++-1-0v5, ${shlibs:Depends}, ${misc:Depends} +Description: SONiC System Assigned Credentials NSS module + The SAC NSS module allows the automatic allocation of user credentials + when the standard methods (e.g. unix NSS module) fail to find credentials + for a user. This is to be used specifically by TACACS+ or RADIUS PAM modules + during user authentication. + +Package: libham +Priority: extra +Architecture: amd64 +Depends: libdbus-c++-1-0v5, ${shlibs:Depends}, ${misc:Depends} +Description: SONiC Host Account Management NSS module + This library contains functions to talk to the HAM daemon over D-Bus. + +Package: libham-dev +Priority: extra +Architecture: amd64 +Depends: libham ${shlibs:Depends}, ${misc:Depends} +Description: SONiC Host Account Management NSS module + This package provides the development headers to allow using library libham.so + provided by package libham. diff --git a/src/ham/debian/rules b/src/ham/debian/rules new file mode 100755 index 00000000000..c08b68a370a --- /dev/null +++ b/src/ham/debian/rules @@ -0,0 +1,7 @@ +#!/usr/bin/make -f +%: + dh $@ --with systemd + + +override_dh_shlibdeps: + dh_shlibdeps --dpkg-shlibdeps-params=--ignore-missing-info -l$(shell pwd)/build/cli/target/.libs/:$(shell pwd)/build/cli/.libs/ diff --git a/src/ham/debian/sonic-hamd.install b/src/ham/debian/sonic-hamd.install new file mode 100644 index 00000000000..2f318e37657 --- /dev/null +++ b/src/ham/debian/sonic-hamd.install @@ -0,0 +1,8 @@ +/etc/dbus-1/system.d/org.SONiC.HostAccountManagement.conf +/etc/sonic/hamd/config +/etc/sonic/hamd/group-mapping +/etc/sonic/hamd/scripts/post-create/README +/etc/sonic/hamd/scripts/pre-delete/README +/lib/systemd/system/hamd.service +/usr/sbin/hamd +/usr/bin/hamctl diff --git a/src/ham/debian/sonic-hamd.postinst b/src/ham/debian/sonic-hamd.postinst new file mode 100755 index 00000000000..aa400c0a1ef --- /dev/null +++ b/src/ham/debian/sonic-hamd.postinst @@ -0,0 +1,192 @@ +#!/bin/sh + +set -e + +action="$1" +#oldversion="$2" + +umask 022 + +migrate_roles_to_gecos="$(mktemp -t tmp.XXXXXXXXXX)" +# Use a trap to make sure the temporary file +# gets deleted even in the case of a kill or crashes +trap "rm -rf '${migrate_roles_to_gecos}'" EXIT +cat <<-EOF >>"${migrate_roles_to_gecos}" +#!/usr/bin/env python3 +# +# Script to reconcile user roles. In previous releases the roles were +# mapped to the primary group. Then, they were mapped to the supplementary +# groups. Now the roles are saved in the GECOS field. This script will +# make sure that existing users have their roles saved to the GECOS and +# not in the Linux groups. +# + +import grp +import pwd +import dbus + +def print_cmd(cmd, end=''): + print('{:<60}'.format(cmd), end=end) + +def print_cmd_result(success, errmsg): + print("[{}]".format("\x1b[0;32msuccess\x1b[0m" if success else "\x1b[0;31mfailed\x1b[0m: {}".format(errmsg))) + +def config_private_primary_group(user, uid): + ''' Create a personal user group (if one doesn't already exist). + The new group will have the same name as the user and the GID + will be the same as the user's UID (if possible). + ''' + import subprocess + + try: + grp.getgrnam(user) + except KeyError: + # A group by that name doesn't currently exist. Let's create it. + cmd = ['/usr/sbin/groupadd', '--gid', str(uid), user] + print_cmd(' '.join(cmd)) + cp = subprocess.run(cmd, stderr=subprocess.PIPE, universal_newlines=True) + print_cmd_result(cp.returncode == 0, cp.stderr) + if cp.returncode != 0: + # It probably failed because the GID specified is + # already in use. Let the system assign an ID. + cmd = ['/usr/sbin/groupadd', user] + print_cmd(' '.join(cmd)) + cp = subprocess.run(cmd, stderr=subprocess.PIPE, universal_newlines=True) + print_cmd_result(cp.returncode == 0, cp.stderr) + + cmd = ['/usr/sbin/usermod', '--gid', user, user] + print_cmd(' '.join(cmd)) + cp = subprocess.run(cmd, stderr=subprocess.PIPE, universal_newlines=True) + print_cmd_result(cp.returncode == 0, cp.stderr) + +def get_roles_from_gecos(pw_gecos): + ''' Retrieve current roles from GECOS (if any) + ''' + try: + gecos = pw_gecos.split(',') + if len(gecos) > 4: + others = gecos[4:] # Skip first 4 members of GECOS (FullName, Room, ...) + for other in others: + if other.startswith('roles='): + return set(other[6:].split(';')) + except: + pass + + return set() + +# Prepare a D-Bus interface to communicate with hamd +bus = dbus.SystemBus() +hamd = bus.get_object('org.SONiC.HostAccountManagement', '/org/SONiC/HostAccountManagement') +accounts = dbus.Interface(hamd, dbus_interface='ham.accounts') + +# Gather information for group 'admin' and 'operator' +gids = {} # key=gid, value=group-name -> value is "admin" or "operator" +user_roles = {} # key=user-name, value=list(group-name) -> value is a list containing "admin" and/or "operator" +for group in ('admin', 'operator'): + try: + ent = grp.getgrnam(group) + gr_mem = ent.gr_mem + gids[ent.gr_gid] = group + except KeyError: + gr_mem = [] + + for member in gr_mem: + user_roles.setdefault(member, []).append(group) + +# Parse all the existing users and fix their GECOS, GID, Groups, if needed. +for ent in pwd.getpwall(): + user = ent.pw_name + + # Check if this user's primary group is "admin" or "operator". If it is, + # add this user and its primary group to the dictionary. + # And since this user's primary group is "admin" or "operator" check + # whether we need to create a private group for this user, which will + # be use as this user's primary group instead of "admin" or "operator". + # In other words, "admin"/"operator" simply cannot be used as a user's + # primary group. This would create security issues between users that + # share the same primary group. + need_private_primary_group = False + group = gids.get(ent.pw_gid, None) + if group is not None: + user_roles.setdefault(user, []).append(group) + need_private_primary_group = group != user + + # Retrieve current roles from GECOS (if any) + cur_gecos_roles = get_roles_from_gecos(ent.pw_gecos) + + # Determine what the roles should be set to in the GECOS + new_gecos_roles = set(user_roles.get(user, [])) + new_gecos_roles.update(cur_gecos_roles) + + # Do we need to change the roles in the GECOS? + if new_gecos_roles != cur_gecos_roles: + # Let hamd update the roles for that user. This will + # set the roles in the GECOS, update the supplementary + # groups, and update the roles in the REDIS DB. + new_gecos_roles = list(new_gecos_roles) + print_cmd("ham.accounts.set_roles({}, {})".format(user, new_gecos_roles)) + success, errmsg = accounts.set_roles(user, new_gecos_roles) + print_cmd_result(success, errmsg) + + + # Do we need to change the Primary group? + # Each user should have their own, private, Primary group. + if need_private_primary_group: + config_private_primary_group(user, ent.pw_uid) +EOF + + +#**************************************** +group_create() { + group=$1 + shift + args=$* + if ! getent group "${group}" >/dev/null; then + echo "groupadd ${args} ${group}" + groupadd ${args} ${group} + fi +} + +#**************************************** +hamd_dbus_config() { + group_create hamd --system + group_create hamd_accounts --system + conf_file="/etc/dbus-1/system.d/org.SONiC.HostAccountManagement.conf" + chmod 644 ${conf_file} + if [ -f /usr/bin/dbus-send ]; then + /usr/bin/dbus-send --system --type=method_call --dest=org.freedesktop.DBus / org.freedesktop.DBus.ReloadConfig + fi +} + +#**************************************** +systemd_add() { + service=$1 + deb-systemd-helper unmask "${service}" >/dev/null || true + if deb-systemd-helper --quiet was-enabled "${service}"; then + deb-systemd-helper enable "${service}" >/dev/null || true + if [ -d /run/systemd/system ]; then + systemctl --system daemon-reload >/dev/null || true + deb-systemd-invoke start "${service}" >/dev/null || true + fi + else + deb-systemd-helper update-state "${service}" >/dev/null || true + fi +} + +#**************************************** +# configure +#**************************************** +if [ "${action}" = configure ]; then + + hamd_dbus_config + systemd_add hamd.service + echo "Migrating roles from Linux groups to GECOS" +# /usr/bin/python3 "${migrate_roles_to_gecos}" + +fi + +#DEBHELPER# + +exit 0 + + diff --git a/src/ham/debian/sonic-libham-dev.install b/src/ham/debian/sonic-libham-dev.install new file mode 100644 index 00000000000..1221c040d68 --- /dev/null +++ b/src/ham/debian/sonic-libham-dev.install @@ -0,0 +1 @@ +usr/include/ham/ham.h diff --git a/src/ham/debian/sonic-libham.install b/src/ham/debian/sonic-libham.install new file mode 100644 index 00000000000..df526fba6ff --- /dev/null +++ b/src/ham/debian/sonic-libham.install @@ -0,0 +1 @@ +usr/lib/x86_64-linux-gnu/libham.so diff --git a/src/ham/debian/sonic-libham.postinst b/src/ham/debian/sonic-libham.postinst new file mode 100755 index 00000000000..8bc6b63c7ed --- /dev/null +++ b/src/ham/debian/sonic-libham.postinst @@ -0,0 +1,21 @@ +#!/bin/sh +set -e + +action="$1" +oldversion="$2" + +umask 022 + +#**************************************** +# configure +#**************************************** +if [ "${action}" = configure ]; then + + ldconfig + +fi + +#DEBHELPER# + +exit 0 + diff --git a/src/ham/debian/sonic-libnss-ham.install b/src/ham/debian/sonic-libnss-ham.install new file mode 100644 index 00000000000..0b1dcd4dc8c --- /dev/null +++ b/src/ham/debian/sonic-libnss-ham.install @@ -0,0 +1 @@ +/lib/x86_64-linux-gnu/libnss_ham.so.2 diff --git a/src/ham/debian/sonic-libnss-ham.postinst b/src/ham/debian/sonic-libnss-ham.postinst new file mode 100755 index 00000000000..83852475fd2 --- /dev/null +++ b/src/ham/debian/sonic-libnss-ham.postinst @@ -0,0 +1,25 @@ +#!/bin/sh +set -e + +action="$1" +oldversion="$2" + +umask 022 + +#**************************************** +# configure +#**************************************** +if [ "${action}" = configure ]; then + + ldconfig + + # Add ham to /etc/nsswitch.conf (if not already) + if ! grep --silent ham /etc/nsswitch.conf; then + /bin/sed -i 's/^\(\(passwd\|group\|shadow\):.*\)$/\1 ham/' /etc/nsswitch.conf + fi +fi + +#DEBHELPER# + +exit 0 + diff --git a/src/ham/debian/sonic-libnss-sac.install b/src/ham/debian/sonic-libnss-sac.install new file mode 100644 index 00000000000..3d909388ef9 --- /dev/null +++ b/src/ham/debian/sonic-libnss-sac.install @@ -0,0 +1 @@ +/lib/x86_64-linux-gnu/libnss_sac.so.2 diff --git a/src/ham/debian/sonic-libnss-sac.postinst b/src/ham/debian/sonic-libnss-sac.postinst new file mode 100755 index 00000000000..8bc6b63c7ed --- /dev/null +++ b/src/ham/debian/sonic-libnss-sac.postinst @@ -0,0 +1,21 @@ +#!/bin/sh +set -e + +action="$1" +oldversion="$2" + +umask 022 + +#**************************************** +# configure +#**************************************** +if [ "${action}" = configure ]; then + + ldconfig + +fi + +#DEBHELPER# + +exit 0 + diff --git a/src/ham/hamctl/.gitignore b/src/ham/hamctl/.gitignore new file mode 100644 index 00000000000..8623ab27310 --- /dev/null +++ b/src/ham/hamctl/.gitignore @@ -0,0 +1 @@ +hamctl diff --git a/src/ham/hamctl/Makefile b/src/ham/hamctl/Makefile new file mode 100644 index 00000000000..6162f503b8c --- /dev/null +++ b/src/ham/hamctl/Makefile @@ -0,0 +1,75 @@ +# sudo apt-get install libsystemd-dev libdbus-c++-dev +TARGET := hamctl +SRCS := $(wildcard *.cpp) + +PKGS := libsystemd dbus-c++-glib-1 +LDLIBS := $(shell pkg-config --libs ${PKGS}) -lcrypt +CPPFLAGS := $(shell pkg-config --cflags ${PKGS}) -DCTL_NAME=\"${TARGET}\" +CFLAGS := -Wall -Werror -O3 +CXXFLAGS := -std=c++11 ${CFLAGS} +LL := g++ + +DBUS-GLUE := $(patsubst %.xml,%.dbus-proxy.h,$(wildcard ../shared/*.xml)) + +OBJS := $(patsubst %.cpp,%.o,$(filter %.cpp,${SRCS})) +DEPS := $(OBJS:.o=.d) + +ifeq (,$(strip $(filter $(MAKECMDGOALS),clean install uninstall package))) + ifneq (,$(strip ${DEPS})) + ${DEPS}: ${DBUS-GLUE} + -include ${DEPS} + endif +endif + +ifeq (,$(strip $(filter $(MAKECMDGOALS),clean install uninstall))) + +# ******************************************************************* +# Make all +.DEFAULT_GOAL := all +all: ${TARGET} + +# ******************************************************************* +# TARGET +${TARGET}: ${OBJS} ${DEPS} Makefile + @printf "%b[1;36m%s%b[0m\n" "\0033" "Linking: ${OBJS} -> $@" "\0033" + $(LL) ${LDFLAGS} -o $@ ${OBJS} $(LDLIBS) + @printf "%b[1;32m%s%b[0m\n\n" "\0033" "$@ Done!" "\0033" + +endif # (,$(strip $(filter $(MAKECMDGOALS),install debian))) + + +# ******************************************************************* +# Implicit rules: +%.o : %.cpp + @printf "%b[1;36m%s%b[0m\n" "\0033" "Compiling: $< -> $@" "\0033" + ${CXX} ${CPPFLAGS} ${CXXFLAGS} -c $< -o $@ + @printf "\n" + +%.d : %.cpp + @printf "%b[1;36m%s%b[0m\n" "\0033" "Dependency: $< -> $@" "\0033" + ${CXX} -MM -MG -MT '$@ $(@:.d=.o)' ${CPPFLAGS} ${CXXFLAGS} -o $@ $< + @printf "\n" + +# Implicit rule to generate DBus header files from XML +../shared/%.dbus-proxy.h: ../shared/%.xml + @printf "%b[1;36m%s%b[0m\n" "\0033" "dbusxx-xml2cpp $< --proxy=$@" "\0033" + @dbusxx-xml2cpp $< --proxy=$@ + +# ******************************************************************* +# ____ _ +# / ___| | ___ __ _ _ __ +# | | | |/ _ \/ _` | '_ \ +# | |___| | __/ (_| | | | | +# \____|_|\___|\__,_|_| |_| +# +# ******************************************************************* +RM_TARGET := ${TARGET} ./*.o ./*.d ${DBUS-GLUE} + +RM_LIST = $(strip $(wildcard ${RM_TARGET})) +.PHONY: clean +clean: +ifneq (,$(RM_LIST)) + rm -rf $(RM_LIST) +endif + + diff --git a/src/ham/hamctl/README.md b/src/ham/hamctl/README.md new file mode 100644 index 00000000000..74ff95d16cd --- /dev/null +++ b/src/ham/hamctl/README.md @@ -0,0 +1,5 @@ +# HAM Control + +hamctl is a companion utility program to the hamd daemon. + +Help is available by typing: `hamctl --help` diff --git a/src/ham/hamctl/hamctl.h b/src/ham/hamctl/hamctl.h new file mode 100644 index 00000000000..8c2159fff76 --- /dev/null +++ b/src/ham/hamctl/hamctl.h @@ -0,0 +1,46 @@ +// Host Account Management +#ifndef CTL_H +#define CTL_H + +#define VERSION "1.0.0" + +#include // DBus + +#include "../shared/org.SONiC.HostAccountManagement.dbus-proxy.h" + +class debug_proxy_c : public ham::debug_proxy, + public DBus::IntrospectableProxy, + public DBus::ObjectProxy +{ +public: + debug_proxy_c(DBus::Connection &connection, const char * dbus_bus_name_p, const char * dbus_obj_name_p) : + DBus::ObjectProxy(connection, dbus_obj_name_p, dbus_bus_name_p) + { + } +}; + +class accounts_proxy_c : public ham::accounts_proxy, + public DBus::IntrospectableProxy, + public DBus::ObjectProxy +{ +public: + accounts_proxy_c(DBus::Connection &connection, const char * dbus_bus_name_p, const char * dbus_obj_name_p) : + DBus::ObjectProxy(connection, dbus_obj_name_p, dbus_bus_name_p) + { + } +}; + +class sac_proxy_c : public ham::sac_proxy, + public DBus::IntrospectableProxy, + public DBus::ObjectProxy +{ +public: + sac_proxy_c(DBus::Connection &connection, const char * dbus_bus_name_p, const char * dbus_obj_name_p) : + DBus::ObjectProxy(connection, dbus_obj_name_p, dbus_bus_name_p) + { + } +}; + +extern DBus::Connection & get_dbusconn(); + +#endif /* CTL_H */ diff --git a/src/ham/hamctl/hamctl_accounts.cpp b/src/ham/hamctl/hamctl_accounts.cpp new file mode 100644 index 00000000000..94deb7819ff --- /dev/null +++ b/src/ham/hamctl/hamctl_accounts.cpp @@ -0,0 +1,407 @@ +// Host Account Management +#ifndef _GNU_SOURCE +# define _GNU_SOURCE // crypt.h +#endif +#include // getopt_long(), no_argument, struct option +#include // crypt_r() +#include // tcgetattr(), tcsetattr(), struct termios +#include // STDIN_FILENO +#include // rand(), srand() +#include // std::vector +#include // std::string +#include // std::endl +#include // std::cin, std::cout + +#include "hamctl.h" // get_dbusconn() +#include "hamctl_subsys.h" +#include "../shared/dbus-address.h" // DBUS_BUS_NAME_BASE +#include "../shared/utils.h" // split_any() + +#define FMT_RED "\x1b[0;31m" +#define FMT_NORMAL "\x1b[0m" +#define DEL 0x7F +#define ESC 0x1B + +typedef enum { normal, escaped, square, getpars } esc_state_t; + +#define ESCAPE_NPAR 8 +struct esc_data_c +{ + esc_state_t state = normal; + unsigned npar = 0; +}; + +/** + * @brief Eliminate escape sequences from the input. This may happen, for + * example, when the user presses the up/down/left/right arrows. + * + * Here's a list of the sequences supported by this algorithm. + * + * Esc 7 Save Cursor Position + * Esc 8 Restore Cursor Position + * Esc [ Pn ; Pn ; .. m Set attributes + * Esc [ Pn ; Pn H Cursor Position + * Esc [ Pn ; Pn f Cursor Position + * Esc [ Pn A Cursor Up + * Esc [ Pn B Cursor Down + * Esc [ Pn C Cursor Forward + * Esc [ Pn D Cursor Backward + * Esc [ Pn G Cursor Horizontal Absolute + * Esc [ Pn X Erase Characters + * Esc [ Ps J Erase in Display + * Esc [ Ps K Erase in Line + * + * Pn is a string of zero or more decimal digits. + * Ps is a selective parameter. + */ +static void escape_sequence(esc_data_c & esc, char c) +{ + if (esc.state == normal) + { + if (c == ESC) + esc.state = escaped; /* Starting new escape sequence. */ + return; + } + + if (esc.state == escaped) + { + esc.state = c == '[' ? square : normal; + return; + } + + if (esc.state == square) + { + esc.state = getpars; + esc.npar = 0; + if (c == '?') + return; + } + + if (esc.state == getpars) + { + if (c == ';' && esc.npar < (ESCAPE_NPAR - 1)) + { + esc.npar++; + return; + } + if (c >= '0' && c <= '9') + return; + } + + esc.state = normal; +} + +/** + * @brief Read password from stdin. The password gets obscured with '*' + * characters as it is being typed. + * + * @param[OUT] pw Where password will be saved. + */ +void read_pw(char *pw) +{ + char c; + unsigned i = 0; + esc_data_c esc_data; + + // Disable STDIN echo to obscure password while it is being typed + struct termios oflags, nflags; + + tcgetattr(STDIN_FILENO, &oflags); + nflags = oflags; + + nflags.c_lflag &= ~(ICANON | ECHO); + nflags.c_cc[VTIME] = 0; + nflags.c_cc[VMIN] = 1; + + (void)tcsetattr(STDIN_FILENO, TCSANOW/*TCSAFLUSH*/, &nflags); + + while (std::cin.get(c) && (c != '\n')) + { + if (esc_data.state != normal) + { + escape_sequence(esc_data, c); + continue; + } + + switch (c) + { + case '\b': + case DEL: // backspace + if (i > 0) + { + pw[--i] = '\0'; + std::cout << "\b \b"; + } + continue; + + case ESC: + escape_sequence(esc_data, c); + break; + + default: + if (isprint(c) && !isspace(c)) + { + pw[i++] = c; + std::cout << '*'; + } + } + } + std::cout << std::endl; + + // Restore STDIN config (i.e. echo) + (void)tcsetattr(STDIN_FILENO, TCSANOW, &oflags); + + pw[i] = '\0'; +} + +/** + * @brief Generate a random SHA-512 salt to be used when invoking crypt_r() + * + * @return The salt string in the form "$6$random-salt-string$" + */ +static std::string get_salt() +{ + srand(time(NULL)); // Seed the randomizer + + static const char valid_salts[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./"; + static const size_t valid_salts_sz = sizeof(valid_salts) - 1; + + std::string salt; + unsigned salt_len = 0; // Salt can be at most 16 chars long + + while (salt_len < 4) // Make sure salt is at least 4 chars long + salt_len = 1 + (rand() & 0xF); // Yields a value in the range 1..16 + + while (salt.length() < salt_len) + salt.push_back(valid_salts[rand() % valid_salts_sz]); + + salt.insert(0, "$6$"); + salt.push_back('$'); + + return salt; +} + +/** + * @brief Interactive prompt asking for password. As the password is + * entered it is obscured with '*' characters. The password will be + * requested twice to make sure there are no typos. + * + * @return The hashed password that can be used for the useradd/usermod + * --password option. + * + */ +static std::string get_hashed_pw() +{ + std::string hashed_pw = ""; + char clear_pw1[200]; + char clear_pw2[200]; + + printf("Enter new UNIX password: "); + read_pw(clear_pw1); + + printf("Retype new UNIX password: "); + read_pw(clear_pw2); + + if (streq(clear_pw1, clear_pw2)) + { + struct crypt_data data; + data.initialized = 0; + + char * hash_p = crypt_r(clear_pw1, get_salt().c_str(), &data); + if (hash_p != nullptr) + { + hashed_pw = hash_p; + } + else + { + fprintf(stderr, FMT_RED "Error! Unable to hash password" FMT_NORMAL "\n"); + } + + } + else + { + fprintf(stderr, FMT_RED "Error! Passwords do not match" FMT_NORMAL "\n"); + } + + memset(clear_pw1, 0, sizeof clear_pw1); + memset(clear_pw2, 0, sizeof clear_pw2); + + return hashed_pw; +} + +/** + * @brief Interactive prompt asking for the list of roles. + * + * @return List of roles as a std::vector. + */ +static std::vector< std::string > get_roles() +{ + char roles[1024]; + + printf("Enter comma-separated roles: "); + std::cin.getline(roles, sizeof roles); + + return split_any(roles, ", \t"); +} + +/** + * @brief hamctl's accounts command + * + * @param argc + * @param argv + * + * @return int + */ +static int accounts(int argc, char *argv[]) +{ + static const struct option options[] = + { + { "help", no_argument, NULL, 'h' }, + { "help", no_argument, NULL, '?' }, + {} + }; + + static const char * usage_p = + CTL_NAME " accounts [OPTIONS] [COMMANDS]\n" + "\n" + "This is used to manage user accounts\n" + "\n" + "OPTIONS:\n" + " -?,-h,--help print this message\n" + "\n" + "COMMANDS:\n" + " useradd [LOGIN] Add a user account.\n" + " userdel [LOGIN] Delete a user account.\n" + " passwd [LOGIN] Change a user's password.\n" + " set_roles [LOGIN] Change a user's roles.\n" + "\n" + "ARGUMENTS:\n" + " [LOGIN] User login name\n"; + + int c; + int rc = 0; + + while ((c = getopt_long(argc, argv, "h?", options, NULL)) >= 0) + { + switch (c) + { + case '?': + case 'h': printf("%s\n", usage_p); return 0; + default: return 1; + } + } + + const char * command_p = argv[optind]; + + if (command_p == NULL) + { + rc = 1; + fprintf(stderr, FMT_RED "Error! Missing command" FMT_NORMAL "\n"); + } + + if (rc == 0) + { + try + { + accounts_proxy_c acct(get_dbusconn(), DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE); + + if (0 == strcmp("useradd", command_p)) + { + if (argc < 3) + { + fprintf(stderr, "%s: Missing arguments. Try --help.\n", program_invocation_short_name); + return -1; + } + + const char * login_p = argv[2]; + std::string hashed_pw = get_hashed_pw(); + + rc = hashed_pw.length() == 0 ? 1 : 0; + if (rc == 0) + { + std::vector< std::string > roles = get_roles(); + + ::DBus::Struct< bool, std::string > rv = acct.useradd(login_p, roles, hashed_pw); + if (!rv._1) + { + rc = 1; + std::cout << rv._2 << '\n'; + } + } + } + else if (0 == strcmp("userdel", command_p)) + { + if (argc < 3) + { + fprintf(stderr, "%s: Missing arguments. Try --help.\n", program_invocation_short_name); + return -1; + } + + const char * login_p = argv[2]; + + ::DBus::Struct< bool, std::string > rv = acct.userdel(login_p); + if (!rv._1) + { + rc = 1; + std::cout << rv._2 << '\n'; + } + } + else if (0 == strcmp("passwd", command_p)) + { + if (argc < 3) + { + fprintf(stderr, "%s: Missing arguments. Try --help.\n", program_invocation_short_name); + return -1; + } + + const char * login_p = argv[2]; + std::string hashed_pw = get_hashed_pw(); + + rc = hashed_pw.length() == 0 ? 1 : 0; + if (rc == 0) + { + ::DBus::Struct< bool, std::string > rv = acct.passwd(login_p, hashed_pw); + if (!rv._1) + { + rc = 1; + std::cout << rv._2 << '\n'; + } + } + } + else if (0 == strcmp("set_roles", command_p)) + { + if (argc < 3) + { + fprintf(stderr, "%s: Missing arguments. Try --help.\n", program_invocation_short_name); + return -1; + } + + const char * login_p = argv[2]; + std::vector< std::string > roles = get_roles(); + + ::DBus::Struct< bool, std::string > rv = acct.set_roles(login_p, roles); + if (!rv._1) + { + rc = 1; + std::cout << rv._2 << '\n'; + } + } + else + { + rc = 1; + fprintf(stderr, FMT_RED "Error! Unknown command \"%s\"" FMT_NORMAL "\n", command_p); + } + } + catch (DBus::Error &ex) + { + rc = 1; + fprintf(stderr, "hamctl accounts - %s\n", ex.what()); + } + } + + return rc; +} + +const subsys_c subsys_accounts("accounts", "Accounts management commands", accounts, false); + + diff --git a/src/ham/hamctl/hamctl_dbus_connection.cpp b/src/ham/hamctl/hamctl_dbus_connection.cpp new file mode 100644 index 00000000000..0fab4dbcff9 --- /dev/null +++ b/src/ham/hamctl/hamctl_dbus_connection.cpp @@ -0,0 +1,23 @@ +#include // DBus +#include "hamctl.h" // get_dbusconn() + +DBus::Connection & get_dbusconn() +{ + static DBus::Connection * conn_p = nullptr; + if (conn_p == nullptr) + { + // DBus::BusDispatcher is a "main loop" construct that + // handles (i.e. dispatched) DBus messages. This should + // be defined as a singleton to avoid memory leaks. + static DBus::BusDispatcher dispatcher; + + // DBus::default_dispatcher must be initialized before DBus::Connection. + DBus::default_dispatcher = &dispatcher; + + static DBus::Connection conn = DBus::Connection::SystemBus(); + + conn_p = &conn; + } + + return *conn_p; +} diff --git a/src/ham/hamctl/hamctl_debug.cpp b/src/ham/hamctl/hamctl_debug.cpp new file mode 100644 index 00000000000..6d6d683bdfe --- /dev/null +++ b/src/ham/hamctl/hamctl_debug.cpp @@ -0,0 +1,100 @@ +// Host Account Management +#include +#include +#include +#include // DBUS_BUS_SYSTEM + +#include "hamctl.h" // get_dbusconn() +#include "hamctl_subsys.h" +#include "../shared/dbus-address.h" // DBUS_BUS_NAME_BASE + +#define FMT_RED "\x1b[0;31m" +#define FMT_NORMAL "\x1b[0m" + +/** + * @brief hamctl's debug command + * + * @param argc + * @param argv + * + * @return int + */ +static int debug(int argc, char *argv[]) +{ + static const struct option options[] = + { + { "help", no_argument, NULL, 'h' }, + { "help", no_argument, NULL, '?' }, + {} + }; + + static const char * usage_p = + CTL_NAME " debug [OPTIONS] [COMMANDS]\n" + "\n" + "This is used by software developers for debugging purposes\n" + "\n" + "OPTIONS:\n" + " -?,-h,--help print this message\n" + "\n" + "COMMANDS:\n" + " tron Trace on\n" + " troff Trace off\n" + " show Print debug information\n"; + + int c; + int rc = 0; + + while ((c = getopt_long(argc, argv, "h?", options, NULL)) >= 0) + { + switch (c) + { + case '?': + case 'h': printf("%s\n", usage_p); return 0; + default: return 1; + } + } + + const char * command_p = argv[optind]; + + if (command_p == NULL) + { + rc = 1; + fprintf(stderr, FMT_RED "Error! Missing command" FMT_NORMAL "\n"); + } + + if (rc == 0) + { + try + { + debug_proxy_c debug(get_dbusconn(), DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE); + + if (0 == strcmp("tron", command_p)) + { + std::cout << debug.tron() << '\n'; + } + else if (0 == strcmp("troff", command_p)) + { + std::cout << debug.troff() << '\n'; + } + else if (0 == strcmp("show", command_p)) + { + std::cout << debug.show() << '\n'; + } + else + { + rc = 1; + fprintf(stderr, FMT_RED "Error! Unknown command \"%s\"" FMT_NORMAL "\n", command_p); + } + } + catch (DBus::Error &ex) + { + rc = 1; + fprintf(stderr, "hamctl debug - %s\n", ex.what()); + } + } + + return rc; +} + +const subsys_c subsys_debug("debug", "Debug commands", debug, false); + diff --git a/src/ham/hamctl/hamctl_main.cpp b/src/ham/hamctl/hamctl_main.cpp new file mode 100644 index 00000000000..ef265643fc5 --- /dev/null +++ b/src/ham/hamctl/hamctl_main.cpp @@ -0,0 +1,146 @@ +// Host Account Management +#include + +#include // strcmp() +#include // getopt_long() +#include // EXIT_SUCCESS, EXIT_FAILURE +#include // program_invocation_short_name +#include /* geteuid() */ + +#include "hamctl_subsys.h" +#include "hamctl.h" + +#define N_ELEMENTS(arr) (sizeof (arr) / sizeof ((arr)[0])) + +static const struct subsys_c * subsystems[] = +{ + &subsys_accounts, + &subsys_debug, + &subsys_sac, +}; + +/** + * @brief Print the help text + * + * @param argc + * @param argv + * + * @return int + */ +static int help(int argc, char *argv[]) +{ + unsigned int i; + + fprintf(stderr, + CTL_NAME " [--help|-h|-?] [--version|-V] [SUBSYS] [OPTIONS] [COMMAND] [ARGS]\n" + "\n" + "Query or send control commands to the Host Account Management Daemon (hamd).\n" + "\n" + "SUBSYS:\n" + ); + for (i = 0; i < N_ELEMENTS(subsystems); i++) + { + if ((subsystems[i]->help_pm != NULL) && !subsystems[i]->hidden_m) + printf(" %-12s %s\n", subsystems[i]->name_pm, subsystems[i]->help_pm); + } + fprintf(stderr, + "\n" + "To get help for a particular subsystem use:\n" + CTL_NAME " [SUBSYS] --help\n" + ); + return 0; +} + +/** + * @brief Print the version ID + * + * @param argc + * @param argv + * + * @return int + */ +static int version(int argc, char *argv[]) +{ + printf("%s\n", VERSION); + return 0; +} + +/** + * @brief Run one of the commands + * + * @param command_r + * @param argc + * @param argv + * + * @return int + */ +static int run_command(const subsys_c & command_r, int argc, char *argv[]) +{ + return command_r.cmd_pm(argc, argv); +} + +/** + * @brief Main entry point + * + * @param argc + * @param argv + * + * @return int + */ +int main(int argc, char *argv[]) +{ + if (geteuid() != 0) + { + fprintf(stderr, CTL_NAME ": Permission denied. Root privileges required.\n"); + exit(EXIT_FAILURE); + } + + setvbuf(stdout, NULL, _IONBF, 0); // Make sure stdout is unbuffered + + static const struct option options[] = + { + { "help", no_argument, NULL, 'h' }, + { "help", no_argument, NULL, '?' }, + { "version", no_argument, NULL, 'V' }, + {} + }; + const char * command; + unsigned int i; + int c; + int rc = 1; + + while ((c = getopt_long(argc, argv, "+h?V", options, NULL)) >= 0) + { + switch (c) + { + case '?': + case 'h': return help(argc, argv); + case 'V': return version(argc, argv); + default: return 1; + } + } + + command = argv[optind]; + + if (command != NULL) + { + for (i = 0; i < N_ELEMENTS(subsystems); i++) + { + if (0 == strcmp(subsystems[i]->name_pm, command)) + { + argc -= optind; + argv += optind; + /* we need '0' here to reset the internal state */ + optind = 0; + rc = run_command(*subsystems[i], argc, argv); + goto out; + } + } + } + + fprintf(stderr, CTL_NAME ": missing or unknown command\n"); + rc = 2; + +out: + exit((0 == rc) ? EXIT_SUCCESS : EXIT_FAILURE); +} diff --git a/src/ham/hamctl/hamctl_sac.cpp b/src/ham/hamctl/hamctl_sac.cpp new file mode 100644 index 00000000000..2aae26ceba4 --- /dev/null +++ b/src/ham/hamctl/hamctl_sac.cpp @@ -0,0 +1,119 @@ +// Host Account Management +#include +#include +#include +#include // getpid() +#include // DBUS_BUS_SYSTEM + +#include "hamctl.h" // get_dbusconn() +#include "hamctl_subsys.h" +#include "../shared/dbus-address.h" // DBUS_BUS_NAME_BASE +#include "../shared/utils.h" // split_exact() + +#define FMT_RED "\x1b[0;31m" +#define FMT_NORMAL "\x1b[0m" + +/** + * @brief hamctl's debug command + * + * @param argc + * @param argv + * + * @return int + */ +static int sac(int argc, char *argv[]) +{ + static const struct option options[] = + { + { "help", no_argument, NULL, 'h' }, + { "help", no_argument, NULL, '?' }, + {} + }; + + static const char * usage_p = + CTL_NAME " sac [OPTIONS] [COMMANDS]\n" + "\n" + "This is used by software developers for debugging purposes\n" + "\n" + "OPTIONS:\n" + " -?,-h,--help print this message\n" + "\n" + "COMMANDS:\n" + " add_unconfirmed_user [LOGIN] Add a local unconfirmed user\n" + " user_confirm [LOGIN] [ROLES] Confirm the user\n" + "\n" + "ARGUMENTS:\n" + " [LOGIN] User login name\n" + " [ROLES] Comma-separated roles\n"; + + int c; + int rc = 0; + + while ((c = getopt_long(argc, argv, "h?", options, NULL)) >= 0) + { + switch (c) + { + case '?': + case 'h': printf("%s\n", usage_p); return 0; + default: return 1; + } + } + + const char * command_p = argv[optind]; + + if (command_p == NULL) + { + rc = 1; + fprintf(stderr, FMT_RED "Error! Missing command" FMT_NORMAL "\n"); + } + + if (rc == 0) + { + try + { + sac_proxy_c sac(get_dbusconn(), DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE); + + if (0 == strcmp("add_unconfirmed_user", command_p)) + { + if (argc < 3) + { + fprintf(stderr, "%s: Missing arguments. Try --help.\n", program_invocation_short_name); + return -1; + } + + const char * login_p = argv[2]; + std::cout << sac.add_unconfirmed_user(login_p, (uint32_t)getpid()) << '\n'; + } + else if (0 == strcmp("user_confirm", command_p)) + { + if (argc < 3) + { + fprintf(stderr, "%s: Missing arguments. Try --help.\n", program_invocation_short_name); + return -1; + } + + const char * login_p = argv[2]; + std::vector< std::string > roles; + + if (argc == 4) roles = split_exact(argv[3], ","); + + std::cout << sac.user_confirm(login_p, roles, "hamctl-manual-test") << '\n'; + } + else + { + rc = 1; + fprintf(stderr, FMT_RED "Error! Unknown command \"%s\"" FMT_NORMAL "\n", command_p); + } + } + catch (DBus::Error &ex) + { + rc = 1; + fprintf(stderr, "hamctl sac - %s\n", ex.what()); + } + } + + return rc; +} + +const subsys_c subsys_sac("sac", "System-assigned credentials commands", sac, false); + diff --git a/src/ham/hamctl/hamctl_subsys.h b/src/ham/hamctl/hamctl_subsys.h new file mode 100644 index 00000000000..479adbe8bfd --- /dev/null +++ b/src/ham/hamctl/hamctl_subsys.h @@ -0,0 +1,28 @@ +// Host Account Management +#ifndef SUBSYS_H +#define SUBSYS_H + +typedef int (* cmd_pt)(int argc, char *argv[]); + +class subsys_c +{ +public: + subsys_c(const char * name_p, const char * help_p, cmd_pt cmd_p, bool hidden=false) : + name_pm(name_p), + help_pm(help_p), + cmd_pm(cmd_p), + hidden_m(hidden) + { + } + + const char * name_pm; + const char * help_pm; + cmd_pt cmd_pm; + bool hidden_m; +}; + +extern const subsys_c subsys_accounts; +extern const subsys_c subsys_debug; +extern const subsys_c subsys_sac; + +#endif /* SUBSYS_H */ diff --git a/src/ham/hamd/.gitignore b/src/ham/hamd/.gitignore new file mode 100644 index 00000000000..b9954498c64 --- /dev/null +++ b/src/ham/hamd/.gitignore @@ -0,0 +1 @@ +./hamd diff --git a/src/ham/hamd/Makefile b/src/ham/hamd/Makefile new file mode 100644 index 00000000000..2ecaa01ef0c --- /dev/null +++ b/src/ham/hamd/Makefile @@ -0,0 +1,93 @@ +# Prerequisite: sudo apt-get install libsystemd-dev libdbus-c++-dev libglib2.0-dev +TARGET := hamd + +SRCS := $(wildcard *.c) $(wildcard *.cpp) + +PKGS := libsystemd glib-2.0 dbus-c++-glib-1 hiredis +LDLIBS := $(shell pkg-config --libs ${PKGS}) -lstdc++fs -lswsscommon +CPPFLAGS := $(shell pkg-config --cflags ${PKGS}) -DDAEMON_NAME=\"${TARGET}\" +CFLAGS := -Wall -Werror -O3 +CXXFLAGS := -std=c++14 ${CFLAGS} +LL := g++ + +DBUS-GLUE := $(patsubst %.xml,%.dbus-adaptor.h,$(wildcard ../shared/*.xml)) + +OBJS := \ + $(patsubst %.c,%.o,$(filter %.c,${SRCS})) \ + $(patsubst %.cpp,%.o,$(filter %.cpp,${SRCS})) +DEPS := $(OBJS:.o=.d) + +ifeq (,$(strip $(filter $(MAKECMDGOALS),clean install uninstall package))) + ifneq (,$(strip ${DEPS})) + ${DEPS}: ${DBUS-GLUE} + -include ${DEPS} + endif +endif + +ifeq (,$(strip $(filter $(MAKECMDGOALS),clean install uninstall))) + +# ******************************************************************* +# Make all +.DEFAULT_GOAL := all +all: ${TARGET} + +# ******************************************************************* +# TARGET +${TARGET}: ${OBJS} ${DEPS} Makefile + @printf "%b[1;36m%s%b[0m\n" "\0033" "Linking: ${OBJS} -> $@" "\0033" + $(LL) ${LDFLAGS} -o $@ ${OBJS} $(LDLIBS) + @printf "%b[1;32m%s%b[0m\n\n" "\0033" "$@ Done!" "\0033" + +endif # (,$(strip $(filter $(MAKECMDGOALS),install debian))) + + +# ******************************************************************* +# Implicit rules: +# ******************************************************************* +%.o : %.cpp + @printf "%b[1;36m%s%b[0m\n" "\0033" "Compiling: $< -> $@" "\0033" + ${CXX} ${CPPFLAGS} ${CXXFLAGS} -c $< -o $@ + @printf "\n" + +%.d : %.cpp + @printf "%b[1;36m%s%b[0m\n" "\0033" "Dependency: $< -> $@" "\0033" + ${CXX} -MM -MG -MT '$@ $(@:.d=.o)' ${CPPFLAGS} ${CXXFLAGS} -o $@ $< + @printf "\n" + +%.o : %.c + @printf "%b[1;36m%s%b[0m\n" "\0033" "Compiling: $< -> $@" "\0033" + ${CC} ${CPPFLAGS} ${CFLAGS} -c $< -o $@ + @printf "\n" + +%.d : %.c + @printf "%b[1;36m%s%b[0m\n" "\0033" "Dependency: $< -> $@" "\0033" + ${CC} -MM -MG -MT '$@ $(@:.d=.o)' ${CPPFLAGS} ${CFLAGS} -o $@ $< + @printf "\n" + +# Implicit rule to generate DBus header files from XML +../shared/%.dbus-adaptor.h: ../shared/%.xml + @printf "%b[1;36m%s%b[0m\n" "\0033" "dbusxx-xml2cpp $< --adaptor=$@" "\0033" + @dbusxx-xml2cpp $< --adaptor=$@ + # The following sed commands are used to add gcc warning suppression for: + # warning: variable 'ri' set but not used [-Wunused-but-set-variable] + sed -i '1s/^/#pragma GCC diagnostic push\n#pragma GCC diagnostic ignored "-Wunused-but-set-variable"/' $@ + sed -i '$$s/$$/\n#pragma GCC diagnostic pop/' $@ + +# ******************************************************************* +# ____ _ +# / ___| | ___ __ _ _ __ +# | | | |/ _ \/ _` | '_ \ +# | |___| | __/ (_| | | | | +# \____|_|\___|\__,_|_| |_| +# +# ******************************************************************* +RM_TARGET := ${TARGET} ./*.o ./*.d ${DBUS-GLUE} + +RM_LIST = $(strip $(wildcard ${RM_TARGET})) +.PHONY: clean +clean: +ifneq (,$(RM_LIST)) + rm -rf $(RM_LIST) +endif + + diff --git a/src/ham/hamd/README.md b/src/ham/hamd/README.md new file mode 100644 index 00000000000..d1640b879e3 --- /dev/null +++ b/src/ham/hamd/README.md @@ -0,0 +1,8 @@ +# Host Account Management Daemon (hamd) + +Hamd is the "single source of truth" for SONiC user accounts. Hamd does only one thing – manage user accounts. As such, hamd follows the UNIX philosophy, which stipulates "programs should do one thing and do it well." The UNIX philosophy provides several benefits, such as modularity and reusability, to name a few. + +Hamd provides a D-Bus interface used by programs (NSS, PAM, etc.) to create, modify, delete user accounts on the Host. The D-Bus interface is required to ensure synchronous operations, which is to say that hamd replies immediately to requests. + +Having a central process to handle user accounts allow consistent user account operations for all applications. Leaving it to applications to call `useradd` directly is not the right approach because various developers working on different applications may invoke `useradd` with inconsistent options and may not be aware of additional user account requirements. For example, users may need additional credentials such as certificates (TLS, REST, gRPC) or keys (SSH). Trusting that every developer will always create user accounts in a consistent way is simply not realistic. + diff --git a/src/ham/hamd/etc/sonic/hamd/config b/src/ham/hamd/etc/sonic/hamd/config new file mode 100644 index 00000000000..53215bfee23 --- /dev/null +++ b/src/ham/hamd/etc/sonic/hamd/config @@ -0,0 +1,46 @@ +# ============================================================================== +# Host Account Management Daemon (hand) configuration file + +# ============================================================================== +# The strategy used for options in the default hamd config shipped with +# SONiC is to specify options with their default value where +# possible, but leave them commented. Uncommented options override the +# default value. + +# ============================================================================== +# debug: Enable additional debug info to the syslog +# type: string +# range: [yes, no] +#debug=no + +# ============================================================================== +# Parameter: poll_period +# Daemon's polling period. Used for periodic house keeping tasks +# +# type: integer +# range: 0..N +# unit: seconds +#poll_period=30 + +# ============================================================================== +# Parameters: uid_min, uid_max +# Define the System-assigned credentials (SAC) min/max UID values. +# +# uid_min should be >= 1000 and lower than uid_max. +# uid_max should be > uid_min. +# +# The range uid_min..uid_max should be at least 200. That is, +# (1 + (uid_max - uid_min)) >= 200 +# +# type: uint32 +# range: 1000..(2^32 - 1) +#uid_min=5000 +#uid_max=59999 + +# ============================================================================== +# Parameter: shell +# Shell program used when creating new users. +# +# type: string +#shell=/usr/bin/sonic-launch-shell + diff --git a/src/ham/hamd/etc/sonic/hamd/group-mapping b/src/ham/hamd/etc/sonic/hamd/group-mapping new file mode 100644 index 00000000000..b346f03af9d --- /dev/null +++ b/src/ham/hamd/etc/sonic/hamd/group-mapping @@ -0,0 +1,16 @@ +# Hamd allows mapping role(s) or privilege-level(s) to Linux groups. This is +# particularly important for users that require access to the sudo command. +# +# This file contains a list of pairs, role:groups or priv-lvl:groups, telling +# hamd how to map particular roles or privilege-levels to Linux groups. +# +# Examples: +# +# ​sysadmin:sudo,adm,sys +# 15:sudo,adm,sys +# secadmin:adm +# netoperator:operator +# 0:operator​ + +admin:sudo,docker +operator:docker diff --git a/src/ham/hamd/etc/sonic/hamd/scripts/post-create/README b/src/ham/hamd/etc/sonic/hamd/scripts/post-create/README new file mode 100644 index 00000000000..18c08eb398c --- /dev/null +++ b/src/ham/hamd/etc/sonic/hamd/scripts/post-create/README @@ -0,0 +1,13 @@ +# After hamd adds a user to /etc/passwd with the useradd command, hamd +# invokes, in ASCIIbetical order, the scripts found in this directory. +# +# Hamd invokes each script as follows: +# +# [SCRIPT] [USER NAME] +# +# Example: +# SCRIPT bob +# +# Each SCRIPT should return 0 on success, and non-0 on failure. On failure, +# each SCRIPT should print the cause of the failure to the standard +# error (stderr). diff --git a/src/ham/hamd/etc/sonic/hamd/scripts/pre-delete/README b/src/ham/hamd/etc/sonic/hamd/scripts/pre-delete/README new file mode 100644 index 00000000000..bb480113de9 --- /dev/null +++ b/src/ham/hamd/etc/sonic/hamd/scripts/pre-delete/README @@ -0,0 +1,13 @@ +# Before hamd deletes a user from /etc/passwd with the userdel command, hamd +# invokes, in ASCIIbetical order, the scripts found in this directory. +# +# Hamd invokes each script as follows: +# +# [SCRIPT] [USER NAME] +# +# Examples: +# SCRIPT wyatt +# +# Each SCRIPT should return 0 on success, and non-0 on failure. On failure, +# each SCRIPT should print the cause of the failure to the standard +# error (stderr). diff --git a/src/ham/hamd/etc/systemd/system/hamd.service b/src/ham/hamd/etc/systemd/system/hamd.service new file mode 100644 index 00000000000..22a69eebfb6 --- /dev/null +++ b/src/ham/hamd/etc/systemd/system/hamd.service @@ -0,0 +1,22 @@ +[Unit] +Description=Host Account Management + +# hamd uses the REDIS database and must wait for it to be running +Requires=database.service +After=database.service + +# Make sure the NSS lookup infra is in place before starting hamd +After=nss-user-lookup.target + +[Service] +Type=dbus +BusName=org.SONiC.HostAccountManagement +ExecStart=/usr/sbin/hamd +ExecReload=/bin/kill -HUP $MAINPID + +# Resource Limitations +LimitCORE=infinity + +[Install] +WantedBy=multi-user.target + diff --git a/src/ham/hamd/file-utils.cpp b/src/ham/hamd/file-utils.cpp new file mode 100644 index 00000000000..a7ffa6559b9 --- /dev/null +++ b/src/ham/hamd/file-utils.cpp @@ -0,0 +1,166 @@ +#include +#include +#include // opendir(), readdir() +#include // stat() +#include // g_file_test(), etc... +#include // syslog(), LOG_WARNING +#include // LINE_MAX + +#include // std::sort() +#include // std::vector +#include // std::string +#include // std::map + +#include "file-utils.h" // is_regular_file(), sorted_filelist() +#include "../shared/utils.h" // startswith(), streq(), split_any() + +/** + * @brief Return an ASCII-sorted vector of filename entries in a given + * directory. If no path is specified, the current working directory + * is used. + * + * @param dir_path_p Oath to the directory + * + * @return Sorted vector containing the list of files. Always check the + * value of the global @errno variable after using this function to + * see if anything went wrong. (It will be zero if all is well.) + */ +std::vector sorted_filelist(const path_t & dir_path_r) +{ + std::vector result; + DIR * dp; + errno = 0; + dp = opendir(dir_path_r.empty() ? "." : dir_path_r.c_str()); + if (dp) + { + dirent * de; + while (true) + { + errno = 0; + de = readdir(dp); + if (de == NULL) break; + + path_t fullname = dir_path_r / de->d_name; + + if (g_file_test(fullname.c_str(), G_FILE_TEST_IS_REGULAR)) // Only keep regular files + result.push_back(fullname); + } + closedir(dp); + std::sort(result.begin(), result.end()); + } + return result; +} + +/** + * @brief Read text file containing a list of key:csv pairs (csv stands for + * comma-separated-values). + * + * This is used to read, for example, the group mapping file or the + * roles DB. + * + * The group mapping file contains a mapping of roles and/or + * privilege-levels to Linux groups. Here are a few examples: + * + * ​sysadmin:sudo,adm,sys + * 15:sudo,adm,sys + * secadmin:adm + * netoperator:operator + * 0:operator​ + * + * The roles DB file contains the list of users and their roles. + * Here are a few an examples: + * + * bob:sysadmin + * doug:netoperator + * jane:secadmin,netadmin + * + * @param fname_r Name of the file to read from. + * + * @return The output is a std::map. Note that the csv gets expanded. That + * is, the csv value is split at the commas (commas are + * removed), and the values are saved to a std::vector. + */ +std::map/*values*/> read_key_csv(const path_t & fname_r) +{ + std::map/*values*/> output; + + FILE * file = fopen(fname_r.c_str(), "re"); + if (file) + { + #define WHITESPACE " \t\n\r" + char line[LINE_MAX]; + size_t lineno = 0; + char * p; + char * s; + char * key; + char * csv; // comma-separated-value + while (nullptr != (p = fgets(line, sizeof line, file))) + { + lineno++; + + p += strspn(p, WHITESPACE); // Remove leading newline and spaces + if (*p == '#' || *p == '\0') continue; // Skip comments and empty lines + + // Delete trailing comments, spaces, tabs, and newline chars. + s = &p[strcspn(p, "#\n\r")]; + *s-- = '\0'; + while ((s >= p) && ((*s == ' ') || (*s == '\t'))) + { + *s-- = '\0'; + } + + if (*p == '\0') continue; // Check that there is still something left in the string + + csv = p; + key = strsep(&csv, ":"); + if (csv == nullptr) + { + syslog(LOG_WARNING, "read_key_csv() - Invalid syntax at line %lu in file %s", lineno, fname_r.c_str()); + continue; + } + + csv += strspn(csv, WHITESPACE); // Remove leading newline and spaces + key[strcspn(key, WHITESPACE)] = '\0'; // Remove trailing spaces + + output[key] = split_any(csv, ", \t"); + } + + fclose(file); + } + + return output; +} + +/** + * @brief Read the whole contents of a file into a std::string + * + * @param fname File to read + * @param verbose For debugging purposes + * + * @return A std::string containing the whole file's content. + */ +std::string get_file_contents(const path_t & fname_r, bool verbose) +{ + LOG_CONDITIONAL(verbose, LOG_DEBUG, "get_file_contents() - fname_r=%s", fname_r.c_str()); + + std::string contents; + std::FILE * fp = std::fopen(fname_r.c_str(), "re"); + + if (fp != nullptr) + { + std::fseek(fp, 0, SEEK_END); + contents.resize(std::ftell(fp)); + std::rewind(fp); + LOG_CONDITIONAL(verbose, LOG_DEBUG, "get_file_contents() - Reading %lu chars from %s", contents.size(), fname_r.c_str()); + if (std::fread(&contents[0], contents.size(), 1, fp) != 1) + syslog(LOG_ERR, "get_file_contents() - failed to read %lu bytes from %s", contents.size(), fname_r.c_str()); + + std::fclose(fp); + } + else + { + syslog(LOG_ERR, "get_file_contents() - failed to open %s", fname_r.c_str()); + } + + return contents; +} diff --git a/src/ham/hamd/file-utils.h b/src/ham/hamd/file-utils.h new file mode 100644 index 00000000000..9d25aad4897 --- /dev/null +++ b/src/ham/hamd/file-utils.h @@ -0,0 +1,41 @@ +#ifndef __FILE_UTILS_H__ +#define __FILE_UTILS_H__ + +#include +#include +#include + +// Portability check for "std::filesystem". Depending on the version +// of C++ that's available, "std::filesystem" is included as follows: +// - 2017 onwards: #include /* provides std::filesystem */ +// - 2011-2016: #include /* provides std::experimental::filesystem */ +// - Prior to 2011: Not available +#if __cplusplus >= 201703L /* 2017 onwards */ +# include + +#elif __cplusplus >= 201103L /* 2011 to 2016 */ +# include + +// Let's alias "std::experimental::filesystem" as "std::filesystem" +namespace std { + namespace filesystem = experimental::filesystem; +} + +#else /* Prior to 2011 */ +# error Missing C++ header files: "" or "" +#endif + +typedef std::filesystem::path path_t; +extern std::vector sorted_filelist(const path_t & dir_path_r = ""); +extern std::map/*values*/> read_key_csv(const path_t & fname_r); +extern std::string get_file_contents(const path_t & fname_r, bool verbose=false); + +#endif // __FILE_UTILS_H__ + + + + + + + + diff --git a/src/ham/hamd/hamd.cpp b/src/ham/hamd/hamd.cpp new file mode 100644 index 00000000000..d4b184d6164 --- /dev/null +++ b/src/ham/hamd/hamd.cpp @@ -0,0 +1,115 @@ +// Host Account Management +#include // getpid() +#include // std::string +#include // std::ostringstream + +#include "hamd.h" // hamd_c +#include "hamd_redis.h" // redis_c +#include "../shared/utils.h" // LOG_CONDITIONAL() + + +/** + * @brief DBus adaptor class constructor + * + * @param config_r Structure containing configuration parameters + * @param conn_r + */ +hamd_c::hamd_c(hamd_config_c & config_r, DBus::Connection & conn_r, redis_c & redis_r) : + DBus::ObjectAdaptor(conn_r, DBUS_OBJ_PATH_BASE), + config_rm(config_r), + poll_timer_m((double)config_rm.poll_period_sec_m, hamd_c::on_poll_timeout_cb, this), + redis_rm(redis_r) +{ + apply_config(); +} + +/** + * @brief This is called when the poll_timer_m expires. + * + * @param user_data_p Pointer to user data. In this case this point to the + * hamd_c object. + * @return bool + */ +bool hamd_c::on_poll_timeout_cb(gpointer user_data_p) +{ + static_cast(user_data_p)->on_poll_timeout(); + return true; // Return true to repeat timer +} + +void hamd_c::on_poll_timeout() +{ + rm_unconfirmed_users(); +} + +/** + * @brief reload configuration and apply to running daemon. + */ +void hamd_c::reload() +{ + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::reload()"); + config_rm.reload(); + apply_config(); +} + +/** + * @brief Apply the configuration to the running daemon + */ +void hamd_c::apply_config() +{ + if (config_rm.poll_period_sec_m > 0) + poll_timer_m.start((double)config_rm.poll_period_sec_m); + else + poll_timer_m.stop(); +} + +/** + * @brief This is called just before the destructor is called and is used + * to clean up all resources in use by the class instance. + */ +void hamd_c::cleanup() +{ + poll_timer_m.stop(); +} + + +/** + * @brief This is a DBus interface used to turn tracing on. This allows + * the daemon to run with verbosity turned on. + * + * @return std::string + */ +std::string hamd_c::tron() +{ + config_rm.tron_m = true; + return "Tracing is now ON"; +} + +/** + * @brief This is a DBus interface used to turn tracing off. This allows + * the daemon to run with verbosity turned off. + * + * @return std::string + */ +std::string hamd_c::troff() +{ + config_rm.tron_m = false; + return "Tracing is now OFF"; +} + +/** + * @brief This is a DBus interface used to retrieve daemon running info + * + * @return std::string + */ +std::string hamd_c::show() +{ + std::ostringstream oss; + oss << "Process data:\n" + << " PID = " << getpid() << '\n' + << " poll_timer_m = " << poll_timer_m << '\n' + << '\n' + << config_rm << '\n'; + + return oss.str(); +} + diff --git a/src/ham/hamd/hamd.h b/src/ham/hamd/hamd.h new file mode 100644 index 00000000000..e3753352b22 --- /dev/null +++ b/src/ham/hamd/hamd.h @@ -0,0 +1,78 @@ +// Host Account Management +#ifndef HAMD_H +#define HAMD_H + +#include // DBus::Connection +#include // gpointer +#include // std::string +#include // std::vector +#include // std::set +#include "timer.h" // gtimer_c +#include "hamd_config.h" // hamd_config_c + +#include "../shared/dbus-address.h" // DBUS_BUS_NAME_BASE +#include "../shared/org.SONiC.HostAccountManagement.dbus-adaptor.h" // Generated file + +struct redis_c; // forward declaration + +class hamd_c : public DBus::ObjectAdaptor, + public DBus::IntrospectableAdaptor, + public ham::accounts_adaptor, + public ham::name_service_adaptor, + public ham::sac_adaptor, + public ham::debug_adaptor +{ +public: + hamd_c(hamd_config_c & config_r, DBus::Connection & conn_r, redis_c & redis_r); + virtual ~hamd_c() {} + + // DBus "accounts" interface + virtual ::DBus::Struct< bool, std::string > useradd(const std::string& login, const std::vector< std::string >& roles, const std::string& hashed_pw); + virtual ::DBus::Struct< bool, std::string > userdel(const std::string& login); + virtual ::DBus::Struct< bool, std::string > passwd(const std::string& login, const std::string& hashed_pw); + virtual ::DBus::Struct< bool, std::string > set_roles(const std::string& login, const std::vector< std::string >& roles); + virtual ::DBus::Struct< bool, std::string > groupadd(const std::string& group); + virtual ::DBus::Struct< bool, std::string > groupdel(const std::string& group); + + // DBus "nss" interface + virtual ::DBus::Struct< bool, std::string, std::string, uint32_t, uint32_t, std::string, std::string, std::string > getpwnam(const std::string& name); + virtual ::DBus::Struct< bool, std::string, std::string, uint32_t, uint32_t, std::string, std::string, std::string > getpwuid(const uint32_t& uid); + virtual std::string getpwcontents(); + virtual ::DBus::Struct< bool, std::string, std::string, uint32_t, std::vector< std::string > > getgrnam(const std::string& name); + virtual ::DBus::Struct< bool, std::string, std::string, uint32_t, std::vector< std::string > > getgrgid(const uint32_t& gid); + virtual std::string getgrcontents(); + virtual ::DBus::Struct< bool, std::string, std::string, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, uint32_t > getspnam(const std::string& name); + + // DBus "sac" interface + virtual std::string add_unconfirmed_user(const std::string & login, const uint32_t & pid); + virtual std::string user_confirm(const std::string & login, const std::vector & roles, const std::string & auth_method); + + // DBus "debug" interface + virtual std::string tron(); + virtual std::string troff(); + virtual std::string show(); + + bool is_tron() const { return config_rm.tron_m; } + virtual void cleanup(); + void reload(); + void apply_config(); + +private: + hamd_config_c & config_rm; + gtimer_c poll_timer_m; + static bool on_poll_timeout_cb(gpointer user_data_p); // This callback functions must follow GSourceFunc signature. + void on_poll_timeout(); + void rm_unconfirmed_users() const; + std::string post_create_scripts(const std::string & login) const; + std::string pre_delete_scripts (const std::string & login) const; + + std::string create_user(const std::string& login, const std::vector< std::string >& roles, const std::string& hashed_pw); + std::string delete_user(const std::string& login); + + static std::set get_groups(const std::vector< std::string > & roles); + static std::string get_groups_as_string(const std::vector< std::string > & roles); + + redis_c & redis_rm; +}; + +#endif /* HAMD_H */ diff --git a/src/ham/hamd/hamd_accounts.cpp b/src/ham/hamd/hamd_accounts.cpp new file mode 100644 index 00000000000..940081fc7f5 --- /dev/null +++ b/src/ham/hamd/hamd_accounts.cpp @@ -0,0 +1,397 @@ +#include // fgetpwent() +#include // std::is_permutation() + +#include "hamd.h" // hamd_c +#include "hamd_gecos.h" // gecos_c() +#include "subprocess.h" // run() +#include "file-utils.h" // sorted_filelist(), read_key_csv() +#include "hamd_redis.h" // redis_c +#include "passwd_utils.h" // fgetpwnam() +#include "../shared/utils.h" // streq(), LOG_CONDITIONAL() + +/** + * @brief Create a new user + * + * @param login User's login name + * @param roles List of roles + * @param hashed_pw Hashed password. Must follow useradd's --password + * syntax. + * + * @return Empty string on success, Error message otherwise. + */ +std::string hamd_c::create_user(const std::string & login, + const std::vector< std::string > & roles, + const std::string & hashed_pw) +{ + gecos_c gecos; + gecos.set(roles_as_string(roles).c_str(), "", "local"); + + std::string cmd = "/usr/sbin/useradd" + " --create-home" + " --user-group" + " --password '" + hashed_pw + "'" + " --comment \"" + gecos.text() + '"'; + + if (!config_rm.shell().empty()) + cmd += " --shell " + config_rm.shell(); + + std::string groups = get_groups_as_string(roles); + if (!groups.empty()) + cmd += " --groups \"" + groups + '"'; + + cmd += ' ' + login; + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::create_user() - Create user \"%s\" [%s]", login.c_str(), cmd.c_str()); + + int rc; + std::string std_err; + std::string std_out; + std::tie(rc, std_out, std_err) = run(cmd); + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::create_user() - Create user \"%s\" rc=%d, stdout=%s, stderr=%s", + login.c_str(), rc, std_out.c_str(), std_err.c_str()); + + if (rc != 0) + { + if (std_err.empty()) + return "/usr/sbin/useradd failed with rc=" + std::to_string(rc); + + return "/usr/sbin/useradd failed with " + std_err; + } + + std::string errmsg = post_create_scripts(login); + if (!errmsg.empty()) // The errmsg will be empty on success + { + // Since we failed to run the port-create + // scripts, we now need to delete the user. + delete_user(login); + } + else + { + // Save roles to REDIS DB (a SONiC requirement) + redis_rm.set_roles(login, roles); + } + + return errmsg; +} + +/** + * @brief Delete a user + * + * @param login User's login name + * + * @return Empty string on success, Error message otherwise. + */ +std::string hamd_c::delete_user(const std::string & login) +{ + if (::getpwnam(login.c_str()) == nullptr) + { + // User doesn't exist...so success! + return ""; + } + + std::string pre_delete_msg = pre_delete_scripts(login); + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::delete_user() - pre_delete_scripts() %s", + pre_delete_msg.empty() ? "success" : pre_delete_msg.c_str()); + + std::string cmd = "/usr/sbin/userdel --force --remove " + login; + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::delete_user() - executing command \"%s\"", cmd.c_str()); + + int rc; + std::string std_err; + std::string std_out; + std::tie(rc, std_out, std_err) = run(cmd); + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::delete_user() - command returned rc=%d, stdout=%s, stderr=%s", + rc, std_out.c_str(), std_err.c_str()); + + if (rc == 0) + return ""; + + if (std_err.empty()) + return "/usr/sbin/userdel failed with rc=" + std::to_string(rc); + + return "/usr/sbin/userdel failed with " + std_err; +} + +/** + * @brief Create or Modify a user account + * + * @param login User's login name + * @param roles List of roles + * @param hashed_pw Hashed password. Must follow useradd's --password + * syntax. + */ +::DBus::Struct< bool, std::string > hamd_c::useradd(const std::string & login, + const std::vector< std::string > & roles, + const std::string & hashed_pw) +{ + if (fgetpwnam(login.c_str()) == nullptr) // User does not exist + { // Let's create it. + ::DBus::Struct< bool, std::string > ret; + ret._2 = create_user(login, roles, hashed_pw); + ret._1 = ret._2.empty(); + return ret; + } + + // User exists, so update password and roles + ::DBus::Struct< bool, /* success */ + std::string /* errmsg */ > ret; + ret = passwd(login, hashed_pw); + return ret._1/*success*/ ? set_roles(login, roles) : ret; +} + +/** + * @brief Delete a user account + */ +::DBus::Struct< bool, std::string > hamd_c::userdel(const std::string& login) +{ + ::DBus::Struct< bool, /* success */ + std::string /* errmsg */ > ret; + + redis_rm.del_roles(login); + + ret._2 = delete_user(login); + ret._1 = ret._2.empty(); + + return ret; +} + +/** + * @brief Change user password + */ +::DBus::Struct< bool, std::string > hamd_c::passwd(const std::string& login, const std::string& hashed_pw) +{ + std::string cmd = "/usr/sbin/usermod --password '" + hashed_pw + "' " + login; + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::passwd() - executing command \"%s\"", cmd.c_str()); + + int rc; + std::string std_err; + std::string std_out; + std::tie(rc, std_out, std_err) = run(cmd); + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::passwd() - command returned rc=%d, stdout=%s, stderr=%s", + rc, std_out.c_str(), std_err.c_str()); + + ::DBus::Struct< bool, /* success */ + std::string /* errmsg */ > ret; + + ret._1 = rc == 0; + ret._2 = rc == 0 ? "" : std_err; + + return ret; +} + +/** + * @brief Set user's roles (supplementary groups) + */ +::DBus::Struct< bool, std::string > hamd_c::set_roles(const std::string& login, const std::vector< std::string >& roles) +{ + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::set_roles() - login=\"%s\", roles=[ \"%s\" ]", + login.c_str(), join(roles.cbegin(), roles.cend(), "\", \"").c_str()); + + ::DBus::Struct< bool, /* success */ + std::string /* errmsg */ > ret; + + // First let's get the current roles configured in /etc/passwd + struct passwd * pwd = fgetpwnam(login.c_str()); + if ((NULL == pwd) || (NULL == pwd->pw_name) || (login != pwd->pw_name)) + { + // User does not exist + ret._2 = strfmt("No such user in /etc/passwd: %s", login.c_str()); + ret._1 = false; + } + else + { + std::string cmd; + + // Extract the current roles from the GECOS field. + gecos_c gecos(pwd->pw_gecos); + std::vector cur_roles = gecos.roles().members(); + + // Now let's check if the roles have changed. + if (std::is_permutation(roles.begin(), roles.end(), cur_roles.begin(), cur_roles.end())) + { + // The roles haven't changed compared to /etc/passwd + // We still need to run usermod for the --groups option (below), + // but we don't need to change the GECOS field. + cmd = "/usr/sbin/usermod"; + } + else + { + gecos.set(roles_as_string(roles).c_str()); + cmd = "/usr/sbin/usermod --comment \"" + gecos.text() + '"'; + } + + // No need to check if the groups have changed. + // usermod does that for us. + std::string groups = get_groups_as_string(roles); + cmd += " --groups \"" + groups + "\" " + login; + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::set_roles() - executing command \"%s\"", cmd.c_str()); + + int rc; + std::string std_err; + std::string std_out; + std::tie(rc, std_out, std_err) = run(cmd); + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::set_roles() - command returned rc=%d, stdout=%s, stderr=%s", + rc, std_out.c_str(), std_err.c_str()); + + if (rc == 0) + { + redis_rm.set_roles(login, roles); + ret._2 = ""; + ret._1 = true; + } + else + { + ret._1 = false; + ret._2 = std_err; + } + } + + return ret; +} + +/** + * @brief Create a group + */ +::DBus::Struct< bool, std::string > hamd_c::groupadd(const std::string& group) +{ + ::DBus::Struct< bool, std::string > ret; + ret._1 = false; + ret._2 = "Not implemented"; + return ret; +} + +/** + * @brief Delete a group + */ +::DBus::Struct< bool, std::string > hamd_c::groupdel(const std::string& group) +{ + ::DBus::Struct< bool, std::string > ret; + ret._1 = false; + ret._2 = "Not implemented"; + return ret; +} + +/** + * @brief Return the Linux groups associated with the @roles specified. + * + * @param roles List of roles + * + * @return A std::set of Linux groups. + */ +//#define MAP_ROLES_TO_GROUPS_AS_WELL +std::set hamd_c::get_groups(const std::vector< std::string > & roles) +{ + std::set groups; // Use std::set to eliminate duplicates + std::map/*groups*/> group_mapping = read_key_csv("/etc/sonic/hamd/group-mapping"); + + for (auto & role : roles) + { + std::copy(group_mapping[role].cbegin(), group_mapping[role].cend(), std::inserter(groups, groups.end())); +#ifdef MAP_ROLES_TO_GROUPS_AS_WELL + // Currently the roles are saved as Linux groups in /etc/group. + // In the next iteration the roles will be saved to the REDIS DB + // and this won't be needed. + groups.insert(role); +#endif + } + + return groups; +} + +/** + * @brief Return the Linux groups associated with the provided @roles. + * + * @param roles List of roles + * + * @return A string of comma-separated Linux groups (e.g. "sudo,docker"). + */ +std::string hamd_c::get_groups_as_string(const std::vector< std::string > & roles) +{ + std::set groups = get_groups(roles); + return join(groups.cbegin(), groups.cend(), ","); +} + +/** + * @brief Run the post-create scripts located in + * /etc/sonic/hamd/scripts/post-create/. + * + * @param login User's login name. + * + * @return Empty string on success, error message otherwise. + */ +std::string hamd_c::post_create_scripts(const std::string & login) const +{ + int rc; + std::string cmd; + std::string std_err; + std::string std_out; + + for (auto & file : sorted_filelist("/etc/sonic/hamd/scripts/post-create")) + { + if (file != "/etc/sonic/hamd/scripts/post-create/README") + { + if (g_file_test(file.c_str(), G_FILE_TEST_IS_EXECUTABLE)) + { + cmd = file + ' ' + login; + std::tie(rc, std_out, std_err) = run(cmd); + + if (rc != 0) + return "Failed to execute " + cmd + ". " + std_err; + } + else + { + syslog(LOG_WARNING, "\"%s\" is not executable", file.c_str()); + } + } + } + + return ""; +} + +/** + * @brief Run the post-create scripts located in + * /etc/sonic/hamd/scripts/pre-delete/. + * + * @param login User's login name. + * + * @return Empty string on success, error message otherwise. + */ +std::string hamd_c::pre_delete_scripts(const std::string & login) const +{ + int rc; + std::string cmd; + std::string std_err; + std::string std_out; + std::vector errmsgs; + + for (auto & file : sorted_filelist("/etc/sonic/hamd/scripts/pre-delete")) + { + if (file != "/etc/sonic/hamd/scripts/pre-delete/README") + { + if (g_file_test(file.c_str(), G_FILE_TEST_IS_EXECUTABLE)) + { + cmd = file + ' ' + login; + std::tie(rc, std_out, std_err) = run(cmd); + + if (rc != 0) + errmsgs.push_back("Failed to execute " + cmd + ". " + std_err); + } + else + { + syslog(LOG_WARNING, "\"%s\" is not executable", file.c_str()); + } + } + + } + + return join(errmsgs.cbegin(), errmsgs.cend(), ";"); +} diff --git a/src/ham/hamd/hamd_config.cpp b/src/ham/hamd/hamd_config.cpp new file mode 100644 index 00000000000..9c0fa45d01d --- /dev/null +++ b/src/ham/hamd/hamd_config.cpp @@ -0,0 +1,255 @@ +// Host Account Management +#include // g_option_context_new(), g_file_test(), etc... +#include // strtoll(), EXIT_SUCCESS +#include // syslog() +#include // LINE_MAX, LLONG_MIN, LLONG_MAX +#include // errno, EINVAL, ERANGE + +#include "hamd.h" // hamd_config_c +#include "../shared/utils.h" // true_false() + + +static long long numberize(const char * str_p, long long minval, long long maxval, const char ** errstr_pp = nullptr); + +//****************************************************************************** +hamd_config_c::hamd_config_c(int argc, char **argv) +{ + GOptionContext * ctx_p; + std::string verbose_help = "Print extra debug [" + std::string(true_false(tron_default_m)) + ']'; + std::string conf_file_help = "Configuration file [" + std::string(conf_file_default_pm) + ']'; + + static const GOptionEntry options[] = + { + { "verbose", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &tron_m, verbose_help.c_str(), nullptr }, + { "conf-file", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &conf_file_pm, conf_file_help.c_str(), nullptr }, + { nullptr, '\0', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, nullptr, nullptr, nullptr } + }; + + const std::string description = + "Configuration file parameters:\n" + " debug=[yes/no] Enable additional debug info to the syslog [" + std::string(true_false(tron_default_m, "yes", "no")) + "]\n" + + " poll_period=[sec] Daemon's polling period. Used for periodic house keeping tasks [" + std::to_string(poll_period_sec_default_m) + "s]\n" + + " uid_min=[uint32] System-assigned credentials minimum UID. Should be >= 1000 [" + std::to_string(sac_uid_min_default_m) + "]\n" + + " uid_max=[uint32] System-assigned credentials maximum UID. Should be > uid_min [" + std::to_string(sac_uid_max_default_m) + "]\n" + + " shell=[path] Shell to be assigned to new users [" + shell_default_m + "]"; + + ctx_p = g_option_context_new(nullptr); + g_option_context_set_summary(ctx_p, "Host Account Management Daemon (hamd)"); + g_option_context_set_description(ctx_p, description.c_str()); + g_option_context_add_main_entries (ctx_p, &options[0], nullptr); + g_option_context_parse (ctx_p, &argc, &argv, nullptr); + g_option_context_free (ctx_p); + + reload(); +} + +//****************************************************************************** +void hamd_config_c::reload() +{ + FILE * file = fopen(conf_file_pm, "re"); + if (file) + { + gint poll_period_sec = poll_period_sec_default_m; + gint sac_uid_min = sac_uid_min_default_m; + gint sac_uid_max = sac_uid_max_default_m; + bool tron = tron_default_m; + std::string shell = shell_default_m; + + #define WHITESPACE " \t\n\r" + char line[LINE_MAX]; + char * p; + char * s; + while (nullptr != (p = fgets(line, sizeof line, file))) + { + p += strspn(p, WHITESPACE); // Remove leading newline and spaces + if (*p == '#' || *p == '\0') continue; // Skip comments and empty lines + p[strcspn(p, "\n\r")] = '\0'; // Remove trailing newline chars + + if (nullptr != (s = startswith(p, "debug"))) + { + s += strspn(s, " \t="); // Skip leading spaces and equal sign (=) + tron = strneq(s, "yes", 3); + } + else if (nullptr != (s = startswith(p, "poll_period"))) + { + s += strspn(s, " \t="); // Skip leading spaces and equal sign (=) + const char * errstr_p = nullptr; + poll_period_sec = (gint)numberize(s, 0, G_MAXINT, &errstr_p); + if (errstr_p != nullptr) + { + syslog(LOG_ERR, "Error reading %s: poll_period %s (ignored)", conf_file_pm, errstr_p); + } + } + else if (nullptr != (s = startswith(p, "uid_min"))) + { + s += strspn(s, " \t="); // Skip leading spaces and equal sign (=) + const char * errstr_p = nullptr; + sac_uid_min = (gint)numberize(s, 1000, G_MAXUINT, &errstr_p); + if (errstr_p != nullptr) + { + syslog(LOG_ERR, "Error reading %s: uid_min %s (ignored)", conf_file_pm, errstr_p); + } + } + else if (nullptr != (s = startswith(p, "uid_max"))) + { + s += strspn(s, " \t="); // Skip leading spaces and equal sign (=) + const char * errstr_p = nullptr; + sac_uid_max = (gint)numberize(s, 1000, G_MAXUINT, &errstr_p); + if (errstr_p != nullptr) + { + syslog(LOG_ERR, "Error reading %s: uid_max %s (ignored)", conf_file_pm, errstr_p); + } + } + else if (nullptr != (s = startswith(p, "shell"))) + { + s += strspn(s, " \t="); // Skip leading spaces and equal sign (=) + shell = s; + } + } + + fclose(file); + + tron_m = tron; + poll_period_sec_m = poll_period_sec; + + if (sac_uid_min > sac_uid_max) + { + syslog(LOG_ERR, "Error reading %s: uid_max is less than uid_min", conf_file_pm); + } + else if ((1 + (sac_uid_max - sac_uid_min)) < 200) + { + syslog(LOG_ERR, "Error reading %s: uid_min..uid_max range too small (should be >= 200).", conf_file_pm); + } + else + { + sac_uid_min_m = sac_uid_min; + sac_uid_max_m = sac_uid_max; + sac_uid_range_m = 1 + (sac_uid_max_m - sac_uid_min_m); + } + + if (shell_m != shell) + { + // Make sure that the file exists + if (g_file_test(shell.c_str(), G_FILE_TEST_EXISTS)) + { + shell_m = shell; + } + else + { + syslog(LOG_ERR, "Error reading %s: shell=%s. File not found.", conf_file_pm, shell.c_str()); + } + } + } + + if (tron_m) + { + syslog(LOG_DEBUG, "hamd_config_c::reload()"); + syslog(LOG_DEBUG, " \\_ conf_file_pm = \"%s\"", conf_file_pm); + syslog(LOG_DEBUG, " \\_ tron_m = %s", true_false(tron_m)); + syslog(LOG_DEBUG, " \\_ poll_period_sec_m = %ds", poll_period_sec_m); + syslog(LOG_DEBUG, " \\_ sac_uid_min_m = %d", sac_uid_min_m); + syslog(LOG_DEBUG, " \\_ sac_uid_max_m = %d", sac_uid_max_m); + syslog(LOG_DEBUG, " \\_ sac_uid_range_m = %d", sac_uid_range_m); + } +} + +//****************************************************************************** +std::string hamd_config_c::to_string() const +{ + std::ostringstream oss; + + oss << "Running config:\n" + << " conf_file_pm = " << conf_file_pm << '\n' + << " poll_period_sec_m = " << std::to_string(poll_period_sec_m) << "s\n" + << " sac_uid_min_m = " << std::to_string(sac_uid_min_m) << '\n' + << " sac_uid_max_m = " << std::to_string(sac_uid_max_m) << '\n' + << " sac_uid_range_m = " << std::to_string(sac_uid_range_m) << '\n' + << " shell_m = " << shell_m << '\n' + << " tron_m = " << true_false(tron_m) << '\n' + << '\n' + << "Default config:\n" + << " conf_file_default_pm = " << conf_file_default_pm << '\n' + << " poll_period_sec_default_m = " << std::to_string(poll_period_sec_default_m) << "s\n" + << " sac_uid_min_default_m = " << std::to_string(sac_uid_min_default_m) << '\n' + << " sac_uid_max_default_m = " << std::to_string(sac_uid_max_default_m) << '\n' + << " shell_default_m = " << shell_default_m << '\n' + << " tron_default_m = " << (tron_default_m ? "true" : "false"); + + return oss.str(); +} + +//****************************************************************************** +std::ostream & operator<<(std::ostream & stream_r, const hamd_config_c & obj_r) +{ + stream_r << obj_r.to_string(); + return stream_r; +} + +//****************************************************************************** +static inline char * _startswith(const char *s, const char *prefix_p, size_t prefix_l) +{ + if (strneq(s, prefix_p, prefix_l)) return (char *)s + prefix_l - 1; + return nullptr; +} + +/** + * @brief Convert a "string" to an integer value. Handles overflow and/or + * underflow. + * + * @param str_p The string to convert + * @param minval Minimum acceptable value + * @param maxval Maximum acceptable value + * @param err_p A place where to return an error string indicating why + * the function failed. + * + * @return str_p converted to a long long. On failure 0 is returned. + */ +static long long numberize(const char * str_p, + long long minval, + long long maxval, + const char ** errstr_pp) +{ + #define OK 0 + #define INVALID 1 + #define TOOSMALL 2 + #define TOOLARGE 3 + + struct + { + const char * str; + int err; + } table[] = + { + { nullptr, errno }, // preserve current errno + { "invalid", EINVAL }, + { "too small", ERANGE }, + { "too large", ERANGE } + }; + + long long number = 0; + unsigned result = OK; + if (minval > maxval) + { + result = INVALID; + } + else + { + char * ep; + errno = 0; + number = strtoll(str_p, &ep, 10); + if (str_p == ep || *ep != '\0') + result = INVALID; + else if ((number == LLONG_MIN && errno == ERANGE) || number < minval) + result = TOOSMALL; + else if ((number == LLONG_MAX && errno == ERANGE) || number > maxval) + result = TOOLARGE; + } + + if (errstr_pp != nullptr) *errstr_pp = table[result].str; + errno = table[result].err; + + return result != OK ? 0 : number; +} + + + diff --git a/src/ham/hamd/hamd_config.h b/src/ham/hamd/hamd_config.h new file mode 100644 index 00000000000..eedceaeca40 --- /dev/null +++ b/src/ham/hamd/hamd_config.h @@ -0,0 +1,59 @@ +// Host Account Management +#ifndef HAMD_CONFIG_H +#define HAMD_CONFIG_H + +#include // uid_t +#include // uint64_t +#include // gint, gchar +#include // std::string +#include // std::ostream + +class hamd_config_c +{ +public: + hamd_config_c(int argc, char **argv); + + /** + * @brief Read configuration and update hamd_config_c object + */ + void reload(); + + uid_t uid_fit_into_range(uint64_t hash) const + { + return (uid_t)((hash % sac_uid_range_m) + sac_uid_min_m); + } + + /** + * @brief return the shell program to assign to new users. + */ + const std::string & shell() const {return shell_m;} + + std::string to_string() const; + bool is_tron() const { return tron_m; } + +private: + // PLEASE UPDATE etc/sonic/hamd/config + // WHEN MAKING CHANGES TO DEFAULTS + static const gint poll_period_sec_default_m = 30; + static const gint sac_uid_min_default_m = 5000; // System-Assigned IDs will be in the + static const gint sac_uid_max_default_m = 59999; // range [sac_uid_min_m..sac_uid_max_m] + static const bool tron_default_m = false; + const gchar * conf_file_default_pm = "/etc/sonic/hamd/config"; + std::string shell_default_m = "/usr/bin/sonic-launch-shell"; + +public: + bool tron_m = tron_default_m; + gint poll_period_sec_m = poll_period_sec_default_m; + + gint sac_uid_min_m = sac_uid_min_default_m; // System-Assigned IDs will be in the + gint sac_uid_max_m = sac_uid_max_default_m; // range [sac_uid_min_m..sac_uid_max_m] + +private: + const gchar * conf_file_pm = conf_file_default_pm; + std::string shell_m = shell_default_m; + gint sac_uid_range_m = 1 + (sac_uid_max_m - sac_uid_min_m); +}; + +std::ostream & operator<<(std::ostream & stream_r, const hamd_config_c & obj_r); + +#endif // HAMD_CONFIG_H diff --git a/src/ham/hamd/hamd_gecos.cpp b/src/ham/hamd/hamd_gecos.cpp new file mode 100644 index 00000000000..cf388fbbc08 --- /dev/null +++ b/src/ham/hamd/hamd_gecos.cpp @@ -0,0 +1,95 @@ +#include // std::string +#include // std::vector + +#include "hamd_gecos.h" // gecos_c +#include "../shared/utils.h" // join() + +gecos_c::gecos_c() +{ + // Allocate a minimum amount of members to account for + // Full Name, Room Address, Work Phone, Home Phone, + // and roles (i.e 5 elements). + members_m.resize(5, ""); + roles_index_m = 4; + roles_m.init(members_m[roles_index_m]); +} + +gecos_c::gecos_c(const char * pw_gecos) +{ + init(pw_gecos); +} + +void gecos_c::init(const char * pw_gecos) +{ + text_m = pw_gecos; + members_m = split_exact(pw_gecos, ",", 5/*min_vector_size*/); + roles_index_m = 4; + + bool roles_found = false; + for (size_t i = roles_index_m; i < members_m.size(); i++) + { + if (startswith(members_m[i].c_str(), "roles=")) + { + roles_found = true; + roles_index_m = i; + break; + } + } + + if (!roles_found) + { + if (*members_m.end() != "") + { + // If last element is not an empty string and + // we haven't found the "roles=" keyword, then + // we have other fields in the GECOS that are + // not the roles attribute. So we need to reserve + // an extra element at the end that we hold the roles. + members_m.push_back(""); + roles_index_m = members_m.size() - 1; + } + } + + roles_m.init(members_m[roles_index_m]); +} + +void gecos_c::set(const char * roles_p, + const char * full_name_p, + const char * room_addr_p, + const char * home_phone_p, + const char * work_phone_p) +{ + if (roles_p != nullptr) members_m[roles_index_m] = roles_p; + if (full_name_p != nullptr) members_m[0] = full_name_p; + if (room_addr_p != nullptr) members_m[1] = room_addr_p; + if (home_phone_p != nullptr) members_m[2] = home_phone_p; + if (work_phone_p != nullptr) members_m[3] = work_phone_p; + + // Find the last non-empty member of the "other" fields. + size_t work_phone_index = 3; + size_t index = members_m.size() - 1; + for (; index > work_phone_index; index--) + { + if (!members_m[index].empty()) break; + } + + auto end = members_m.cbegin() + index + 1; + text_m = join(members_m.cbegin(), end, ","); +} + +std::vector< std::string > roles_c::members() const +{ + char * p = startswith(roles_pm, "roles="); + if (p != nullptr) + { + return split_exact(p, ";"); + } + + std::vector< std::string > members; + return members; +} + +std::string roles_as_string(const std::vector< std::string > & roles) +{ + return "roles=" + join(roles.cbegin(), roles.cend(), ";"); +} diff --git a/src/ham/hamd/hamd_gecos.h b/src/ham/hamd/hamd_gecos.h new file mode 100644 index 00000000000..9f59de6e016 --- /dev/null +++ b/src/ham/hamd/hamd_gecos.h @@ -0,0 +1,89 @@ +#ifndef __HAMD_GECOS_H__ +#define __HAMD_GECOS_H__ + +#include // std::string +#include // std::vector + +class gecos_c; +class roles_c +{ +public: + const char * c_str() const { return roles_pm; } + std::vector< std::string > members() const; + +protected: + friend class gecos_c; + void init(const std::string & roles_r) { roles_pm = roles_r.c_str(); } + +private: + const char * roles_pm = "roles="; +}; + +/** + * @brief The GECOS is one of the fields of each entry in /etc/passwd. + * + * A typical entry in /etc/passwd looks like this: + * + * Login:pw:uid:gid:GECOS:HomeDir:Shell + * + * The GECOS field is a list of comma-separated sub-fields as follows: + * + * FullName,RoomAddress,WorkPhone,HomePhone,Other1,Other2,Other3,... + * + * User "roles" are specified as an "other" sub-field of the GECOS field. + * Roles are semi-colon separated to avoid confusion with the colon + * and comma already used as separators elsewhere in the /etc/passwd + * entry. + * + * roles=netadmin;secadmin;lord-of-the-vlans + * + * Example of a complete /etc/passwd entry: + * + * homer:x:1012:1012:Homer J. Simpson,,,,roles=secadmin;lord-of-the-vlans:/home/homer:/bin/bash + * + * The gecos_c class is a tool to make it easy to access the fields and + * sub-fields of the GECOS. + * + * @see + * -Wiki GECOS: https://en.wikipedia.org/wiki/Gecos_field + * -chfn(1): https://man7.org/linux/man-pages/man1/chfn.1@@shadow-utils.html + * -useradd(8): https://man7.org/linux/man-pages/man8/useradd.8.html + * -getpwnam(3): https://man7.org/linux/man-pages/man3/getpwnam.3.html + * + * @note We use the "RoomAddress" to identify where the user is located. + * E.g. "local", "tacacs+", "radius". + */ +class gecos_c +{ +public: + gecos_c(); + gecos_c(const char * pw_gecos); + void init(const char * pw_gecos); + + const char * full_name() const { return members_m[0].c_str(); } + const char * room_addr() const { return members_m[1].c_str(); } + const char * home_phone() const { return members_m[2].c_str(); } + const char * work_phone() const { return members_m[3].c_str(); } + + const roles_c & roles() const { return roles_m; } + + const char * c_str() const { return text_m.c_str(); } + std::string text() const { return text_m; } + + void set(const char * roles_r=nullptr, /* nullptr means leave unchanged */ + const char * full_name_p=nullptr, /* nullptr means leave unchanged */ + const char * room_addr_p=nullptr, /* nullptr means leave unchanged */ + const char * home_phone_p=nullptr, /* nullptr means leave unchanged */ + const char * work_phone_p=nullptr); /* nullptr means leave unchanged */ + +private: + std::vector< std::string > members_m; + std::string text_m = ",,,"; + roles_c roles_m; + size_t roles_index_m = 4; +}; + +std::string roles_as_string(const std::vector< std::string > & roles); + + +#endif // __HAMD_GECOS_H__ diff --git a/src/ham/hamd/hamd_main.cpp b/src/ham/hamd/hamd_main.cpp new file mode 100644 index 00000000000..1a25b62e626 --- /dev/null +++ b/src/ham/hamd/hamd_main.cpp @@ -0,0 +1,106 @@ +// Host Account Management +#include // g_main_loop_new(), g_main_context_default(), g_main_loop_run(), g_main_loop_unref(), g_main_loop_quit(), gboolean, etc... +#include // g_unix_signal_add() +#include // DBus::Glib::BusDispatcher +#include // EXIT_SUCCESS + +#include "hamd.h" // hamd_c +#include "hamd_config.h" // hamd_config_c +#include "hamd_redis.h" // redis_c +#include "../shared/utils.h" // LOG_CONDITIONAL() + +/** + * @brief This callback will be invoked when this process receives SIGINT + * or SIGTERM. + * + * @param data Pointer to the GMainLoop object + * + * @return G_SOURCE_REMOVE to indicate that this event is no longer needed + * and should be removed from the main loop. + */ +static gboolean terminationSignalCallback(gpointer data) +{ + GMainLoop * loop_p = static_cast(data); + g_main_loop_quit(loop_p); + return G_SOURCE_REMOVE; // No need to keep this event source since we're about to exit +} + +/** + * @brief This callback will be invoked when this process receives SIGHUP, + * which is usually triggered by "systemctl reload". This signal is + * used to tell the process to reload its configuration file and + * reconfigure itself without doing a complete shutdown-restart + * cycle. + * + * @param data Pointer to the hamd_c object + * + * @return G_SOURCE_CONTINUE to indicate that we want the main loop to keep + * processing this event. + */ +static gboolean reloadConfigCallback(gpointer data) +{ + hamd_c * hamd_p = static_cast(data); + hamd_p->reload(); + return G_SOURCE_CONTINUE; // Keep this event source. +} + +/** + * @brief Program entry point + * + * @param argc Argument count (i.e. argv array size) + * @param argv Array of argument strings passed to the program. + * + * @return int + */ +int main(int argc, char *argv[]) +{ + setvbuf(stdout, NULL, _IONBF, 0); // Set stdout buffering to unbuffered + + //putenv("DBUSXX_VERBOSE=1"); + + hamd_config_c config(argc, argv); + + LOG_CONDITIONAL(config.tron_m, LOG_DEBUG, "Creating a GMainLoop"); + GMainContext * main_ctx_p = g_main_context_default(); + GMainLoop * loop_p = g_main_loop_new(main_ctx_p, FALSE); + + // Set up a signal handler for handling SIGINT and SIGTERM. + g_unix_signal_add(SIGINT, terminationSignalCallback, loop_p); // CTRL-C + g_unix_signal_add(SIGTERM, terminationSignalCallback, loop_p); // systemctl stop + + // REDIS setup + LOG_CONDITIONAL(config.tron_m, LOG_DEBUG, "Creating REDIS infra"); + redis_c redis(config, main_ctx_p); + + // DBus setup + LOG_CONDITIONAL(config.tron_m, LOG_DEBUG, "Initializing the loop's dispatcher"); + DBus::Glib::BusDispatcher dispatcher; + DBus::default_dispatcher = &dispatcher; // DBus::default_dispatcher must be initialized before DBus::Connection. + dispatcher.attach(main_ctx_p); + + LOG_CONDITIONAL(config.tron_m, LOG_DEBUG, "Requesting System DBus connection \"" DBUS_BUS_NAME_BASE "\""); + DBus::Connection dbus_conn(DBus::Connection::SystemBus()); + dbus_conn.request_name(DBUS_BUS_NAME_BASE); + + // HAMD setup + hamd_c hamd(config, dbus_conn, redis); + g_unix_signal_add(SIGHUP, reloadConfigCallback, &hamd); // callback for "systemctl reload hamd" + + // Main loop actvation + LOG_CONDITIONAL(config.tron_m, LOG_DEBUG, "Entering main loop"); + g_main_loop_run(loop_p); + + LOG_CONDITIONAL(config.tron_m, LOG_DEBUG, "Cleaning up and exiting"); + + hamd.cleanup(); + redis.cleanup(); + + g_main_loop_unref(loop_p); + + LOG_CONDITIONAL(config.tron_m, LOG_DEBUG, "Exiting daemon."); + + fflush(stdout); + + exit(EXIT_SUCCESS); +} + diff --git a/src/ham/hamd/hamd_nss.cpp b/src/ham/hamd/hamd_nss.cpp new file mode 100644 index 00000000000..f461f7be1a4 --- /dev/null +++ b/src/ham/hamd/hamd_nss.cpp @@ -0,0 +1,195 @@ +#include // getpwnam(), getpid() +#include // getpwnam(), getpwuid() +#include // getgrnam(), getgrgid() +#include // getspnam() + +#include "hamd.h" // hamd_c +#include "file-utils.h" // get_file_contents() +#include "../shared/utils.h" // LOG_CONDITIONAL() + + +/** + * @brief D-Bus interface that provides the same functionality as POSIX + * getpwnam(). This gets invoked by the HAM NSS module installed in + * containers that need access to the host's user database. + */ +::DBus::Struct< bool, std::string, std::string, uint32_t, uint32_t, std::string, std::string, std::string > hamd_c::getpwnam(const std::string& name) +{ + ::DBus::Struct< bool, /* success */ + std::string, /* pw_name */ + std::string, /* pw_passwd */ + uint32_t, /* pw_uid */ + uint32_t, /* pw_gid */ + std::string, /* pw_gecos */ + std::string, /* pw_dir */ + std::string > /* pw_shell */ ret; + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::getpwnam(%s)", name.c_str()); + + struct passwd * p = ::getpwnam(name.c_str()); + + ret._1 = p != NULL; + if (ret._1) // success? + { + ret._2 = p->pw_name; + ret._3 = p->pw_passwd; + ret._4 = p->pw_uid; + ret._5 = p->pw_gid; + ret._6 = p->pw_gecos; + ret._7 = p->pw_dir; + ret._8 = p->pw_shell; + } + + return ret; +} + +/** + * @brief D-Bus interface that provides the same functionality as POSIX + * getpwuid(). This gets invoked by the HAM NSS module installed in + * containers that need access to the host's user database. + */ +::DBus::Struct< bool, std::string, std::string, uint32_t, uint32_t, std::string, std::string, std::string > hamd_c::getpwuid(const uint32_t& uid) +{ + ::DBus::Struct< bool, /* success */ + std::string, /* pw_name */ + std::string, /* pw_passwd */ + uint32_t, /* pw_uid */ + uint32_t, /* pw_gid */ + std::string, /* pw_gecos */ + std::string, /* pw_dir */ + std::string > /* pw_shell */ ret; + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::getpwuid(%u)", uid); + + struct passwd * p = ::getpwuid(uid); + + ret._1 = p != NULL; + if (ret._1) // success? + { + ret._2 = p->pw_name; + ret._3 = p->pw_passwd; + ret._4 = p->pw_uid; + ret._5 = p->pw_gid; + ret._6 = p->pw_gecos; + ret._7 = p->pw_dir; + ret._8 = p->pw_shell; + } + + return ret; +} + +/** + * @brief D-Bus interface that allows the HAM NSS module installed in + * containers to retrieve the whole /etc/passwd file from the host. + */ +std::string hamd_c::getpwcontents() +{ + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::getpwcontents()"); + return get_file_contents("/etc/passwd", is_tron()); +} + +/** + * @brief D-Bus interface that provides the same functionality as POSIX + * getgrnam(). This gets invoked by the HAM NSS module installed in + * containers that need access to the host's group database. + */ +::DBus::Struct< bool, std::string, std::string, uint32_t, std::vector< std::string > > hamd_c::getgrnam(const std::string& name) +{ + ::DBus::Struct< bool, /* success */ + std::string, /* gr_name */ + std::string, /* gr_passwd */ + uint32_t, /* gr_gid */ + std::vector< std::string > > /* gr_mem */ ret; + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::getgrnam(%s)", name.c_str()); + + struct group * p = ::getgrnam(name.c_str()); + + ret._1 = p != NULL; + if (ret._1) // success? + { + ret._2 = p->gr_name; + ret._3 = p->gr_passwd; + ret._4 = p->gr_gid; + + for (unsigned i = 0; p->gr_mem[i] != NULL; i++) + ret._5.push_back(p->gr_mem[i]); + } + + return ret; +} + +/** + * @brief D-Bus interface that provides the same functionality as POSIX + * getgrgid(). This gets invoked by the HAM NSS module installed in + * containers that need access to the host's group database. + */ +::DBus::Struct< bool, std::string, std::string, uint32_t, std::vector< std::string > > hamd_c::getgrgid(const uint32_t& gid) +{ + ::DBus::Struct< bool, /* success */ + std::string, /* gr_name */ + std::string, /* gr_passwd */ + uint32_t, /* gr_gid */ + std::vector< std::string > > /* gr_mem */ ret; + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::getgrgid(%u)", gid); + + struct group * p = ::getgrgid(gid); + + ret._1 = p != NULL; + if (ret._1) // success? + { + ret._2 = p->gr_name; + ret._3 = p->gr_passwd; + ret._4 = p->gr_gid; + + for (unsigned i = 0; p->gr_mem[i] != NULL; i++) + ret._5.push_back(p->gr_mem[i]); + } + + return ret; +} + +/** + * @brief D-Bus interface that allows the HAM NSS module installed in + * containers to retrieve the whole /etc/group file from the host. + */ +std::string hamd_c::getgrcontents() +{ + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::getgrcontents()"); + return get_file_contents("/etc/group", is_tron()); +} + +::DBus::Struct< bool, std::string, std::string, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t, uint32_t > hamd_c::getspnam(const std::string& name) +{ + ::DBus::Struct< bool, /* success */ + std::string, /* sp_namp */ + std::string, /* sp_pwdp */ + int32_t, /* sp_lstchg */ + int32_t, /* sp_min */ + int32_t, /* sp_max */ + int32_t, /* sp_warn */ + int32_t, /* sp_inact */ + int32_t, /* sp_expire */ + uint32_t > /* sp_flag */ ret; + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::getspnam(%s)", name.c_str()); + + struct spwd * p = ::getspnam(name.c_str()); + + ret._1 = p != NULL; + if (ret._1) // success? + { + ret._2 = p->sp_namp; + ret._3 = p->sp_pwdp; + ret._4 = p->sp_lstchg; + ret._5 = p->sp_min; + ret._6 = p->sp_max; + ret._7 = p->sp_warn; + ret._8 = p->sp_inact; + ret._9 = p->sp_expire; + ret._10 = p->sp_flag; + } + + return ret; +} diff --git a/src/ham/hamd/hamd_redis.cpp b/src/ham/hamd/hamd_redis.cpp new file mode 100644 index 00000000000..80ee393f5a6 --- /dev/null +++ b/src/ham/hamd/hamd_redis.cpp @@ -0,0 +1,306 @@ +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif + +#include /* memfd_create() */ +#include // fgetpwent() +#include // redisAsyncContext +#include // redisAsyncContext +#include // redis_source_new() +#include // GMainContext, gpointer +#include // std::system_error + +#include "../shared/utils.h" // join() +#include "../shared/missing-memfd_create.h" // memfd_create() if missing from sys/mman.h +#include "hamd_redis.h" // prototypes +#include "hamd_config.h" // hamd_config_c +#include "file-utils.h" // get_file_contents() +#include "hamd_gecos.h" // gecos_c() + +#include "swss/table.h" // swss::Table_async +#include "swss/dbconnector.h" // swss::DBConnector_async + +static inline std::string int_to_string(long long integer) +{ + return "int(" + std::to_string(integer) + ')'; +} + +static inline std::string string_to_string(const char * str_p) +{ + return "str(\"" + std::string(str_p) + "\")"; +} + +static inline std::string status_to_string(const char * str_p) +{ + return "status(" + std::string(str_p) + ')'; +} + +static inline std::string unknown_to_string(int type, const char * str_p) +{ + return "unknown(type=" + std::to_string(type) + ", \"" + std::string(str_p) + "\")"; +} + +/** + * @brief Convert a reply (%redisReply) of type array to a printable string + * + * @param reply The reply to convert + * + * @return std::string The printable string + */ +static std::string array_to_string(redisReply * reply_p) +{ + std::vector tokens; + std::string token; + + for (size_t i = 0; i < reply_p->elements; i++) + { + switch (reply_p->element[i]->type) + { + case REDIS_REPLY_ERROR: token = "err(" + std::string(reply_p->element[i]->str) + ')'; break; + case REDIS_REPLY_INTEGER: token = int_to_string(reply_p->element[i]->integer); break; + case REDIS_REPLY_STRING: token = string_to_string(reply_p->element[i]->str); break; + case REDIS_REPLY_STATUS: token = status_to_string(reply_p->element[i]->str); break; + case REDIS_REPLY_ARRAY: token = array_to_string(reply_p->element[i]); break; + default: token = unknown_to_string(reply_p->element[i]->type, reply_p->element[i]->str); + } + + tokens.push_back(token); + } + + if (tokens.empty()) + return "array[]"; + + return "array[" + join(tokens.cbegin(), tokens.cend(), ", ") + ']'; +} + +static std::string reply_as_text(redisAsyncContext * ac_p, redisReply * reply_p) +{ + std::string text; + + switch (reply_p->type) + { + case REDIS_REPLY_ERROR: text = "err(" + std::to_string(ac_p->err) + '-' + ac_p->errstr + ')'; break; + case REDIS_REPLY_INTEGER: text = int_to_string(reply_p->integer); break; + case REDIS_REPLY_STRING: text = string_to_string(reply_p->str); break; + case REDIS_REPLY_STATUS: text = status_to_string(reply_p->str); break; + case REDIS_REPLY_ARRAY: text = array_to_string(reply_p); break; + default: text = unknown_to_string(reply_p->type, reply_p->str); + } + + return text; +} + +//############################################################################## +redis_c::redis_c(const hamd_config_c & hamd_config_r, GMainContext * g_main_ctx_p) : + hamd_config_rm(hamd_config_r), + g_main_ctx_pm(g_main_ctx_p), + inloop_init_timer_m(0.01, redis_c::on_inloop_init_timeout_cb, this) +{ + inloop_init_timer_m.start(); +} + +bool redis_c::is_tron() const +{ + return hamd_config_rm.is_tron(); +} + +/** + * @brief This is called when the inloop_init_timer_m expires. + * + * @param user_data_p Pointer to user data. In this case this point to the + * hamd_c object. + * @return bool + */ +bool redis_c::on_inloop_init_timeout_cb(gpointer user_data_p) +{ + static_cast(user_data_p)->on_inloop_init_timeout(); + return false; // Return false for one-shot timer +} + +/** + * @brief This is called at the expiration of the inloop_init_timer_m. + * We use a timer to force the execution of this code as soon as + * the main event loop is started. It allows us to delay this + * initialization code until we're inside the event loop, as opposed + * to running it before the main loop in entered. + */ +void redis_c::on_inloop_init_timeout() +{ + DBConnector_async_glib db(g_main_ctx_pm, is_tron()); + if (db.valid()) + { + swss::Table_async user_table(db.obj(), "USER"); + roles_db_upload(user_table); + } +} + +/** + * @brief Set user's roles in the REDIS DB + * + * @param login_r User's login name + * @param roles_r List of roles + */ +void redis_c::set_roles(const std::string & login_r, const std::vector< std::string > & roles_r) +{ + DBConnector_async_glib db(g_main_ctx_pm, is_tron()); + if (db.valid()) + { + swss::Table_async user_table(db.obj(), "USER"); + set_roles(user_table, login_r, roles_r); + } +} + +/** + * @brief Remove user's roles from the REDIS DB + * + * @param login_r User's login name + */ +void redis_c::del_roles(const std::string & login_r) +{ + DBConnector_async_glib db(g_main_ctx_pm, is_tron()); + if (db.valid()) + { + swss::Table_async user_table(db.obj(), "USER"); + del_roles(user_table, login_r); + } +} + +/** + * @brief This function retrieves the users and their roles from + * /etc/passwd and uploads them to the REDIS DB. This is + * done every time hamd restarts. + */ +void redis_c::roles_db_upload(swss::Table_async & user_table_r) +{ + LOG_CONDITIONAL(is_tron(), LOG_INFO, "redis_c::roles_db_upload()"); + + // To limit the amount of time we spend accessing /etc/passwd, + // we retrieve the whole contents of /etc/passwd in one shot. + // Then we can parse the local copy. + std::string contents = get_file_contents("/etc/passwd"); + if (!contents.empty()) + { + // It is easier to parse each passwd entry with fgetpwent(). However, + // this API operates on a FILE *. So we create a temporary + // in-memory file to which we copy the contents of the real + // /etc/passwd file. + int fd = memfd_create("passwd", MFD_CLOEXEC); + if (fd == -1) + { + syslog(LOG_ERR, "redis_c::roles_db_upload() - Failed to create temp passwd file. errno=%d (%s)\n", errno, strerror(errno)); + return; + } + + // Convert File Descriptor to a "FILE *". This is needed for fgetpwent() + FILE * passwd_fp = fdopen(fd, "w+"); + if (passwd_fp == nullptr) + { + syslog(LOG_ERR, "redis_c::roles_db_upload() - fdopen() failed. errno=%d (%s)\n", errno, strerror(errno)); + return; + } + + fwrite(contents.c_str(), contents.length(), 1, passwd_fp); + rewind(passwd_fp); + + struct passwd * ent; + gecos_c gecos; + while (nullptr != (ent = fgetpwent(passwd_fp))) + { + if (nullptr != strstr(ent->pw_gecos, "roles=")) // Does the GECOS field contain roles? + { + gecos.init(ent->pw_gecos); + set_roles(user_table_r, ent->pw_name, gecos.roles().members()); + } + } + + fclose(passwd_fp); // This closes both passwd_fp and fd. + } +} + + +/** + * @brief Set user's roles in the REDIS DB + * + * @param login_r User's login name + * @param roles_r List of roles + */ +void redis_c::set_roles(swss::Table_async & user_table_r, const std::string & login_r, const std::vector< std::string > & roles_r) +{ + std::string cs_roles = join(roles_r.cbegin(), roles_r.cend(), ","); // Comma-separated roles + + LOG_CONDITIONAL(is_tron(), LOG_INFO, "redis_c::set_roles() - login_r=\"%s\", roles_r=\"%s\"", login_r.c_str(), cs_roles.c_str()); + + int rv = user_table_r.hset(redis_c::on_set_roles_cb, this, login_r, "roles@", cs_roles); + if (rv != 0) + { + syslog(LOG_ERR, "redis_c::set_roles() - user_table_m.hset() returned %d\n", rv); + } +} + +void redis_c::on_set_roles_cb(redisAsyncContext * ac_p, void * reply_p, void * user_data_p) +{ + static_cast(user_data_p)->on_set_roles(ac_p, static_cast(reply_p)); +} + +void redis_c::on_set_roles(redisAsyncContext * ac_p, redisReply * reply_p) +{ + LOG_CONDITIONAL(is_tron(), LOG_INFO, "redis_c::on_set_roles() - %s", reply_as_text(ac_p, reply_p).c_str()); +} + +/** + * @brief Remove user's roles from the REDIS DB + * + * @param login_r User's login name + */ +void redis_c::del_roles(swss::Table_async & user_table_r, const std::string & login_r) +{ + LOG_CONDITIONAL(is_tron(), LOG_INFO, "redis_c::del_roles() - login_r=\"%s\"", login_r.c_str()); + + int rv = user_table_r.hdel(redis_c::on_del_roles_cb, this, login_r, "roles@"); + if (rv != 0) + { + syslog(LOG_ERR, "redis_c::del_roles() - user_table_m.hdel() returned %d\n", rv); + } +} + +void redis_c::on_del_roles_cb(redisAsyncContext * ac_p, void * reply_p, void * user_data_p) +{ + static_cast(user_data_p)->on_del_roles(ac_p, static_cast(reply_p)); +} + +void redis_c::on_del_roles(redisAsyncContext * ac_p, redisReply * reply_p) +{ + LOG_CONDITIONAL(is_tron(), LOG_INFO, "redis_c::on_set_roles() - %s", reply_as_text(ac_p, reply_p).c_str()); +} + + + + +DBConnector_async_glib::DBConnector_async_glib(GMainContext * g_main_ctx_p, bool verbose) +{ + try + { + db_pm = new swss::DBConnector_async("APPL_DB", nullptr); + } + catch (std::system_error & ex) + { + syslog(LOG_ERR, "DBConnector_async_glib::DBConnector_async_glib() - %s", ex.what()); + db_pm = nullptr; + return; + } + + LOG_CONDITIONAL(verbose, LOG_INFO, + "DBConnector_async_glib::DBConnector_async_glib() - Connected to -> %s (%d) socket:%s", + db_pm->getDbName(), db_pm->getDbId(), db_pm->getSockAddr()); + + g_source_attach(redis_source_new(db_pm->context()), g_main_ctx_p); +} + +DBConnector_async_glib::~DBConnector_async_glib() +{ + if (db_pm != nullptr) + { + delete db_pm; + db_pm = nullptr; + } +} diff --git a/src/ham/hamd/hamd_redis.h b/src/ham/hamd/hamd_redis.h new file mode 100644 index 00000000000..05adba915d5 --- /dev/null +++ b/src/ham/hamd/hamd_redis.h @@ -0,0 +1,69 @@ +#ifndef __REDIS_H__ +#define __REDIS_H__ + +#include // GMainContext + +#include +#include + +#include "timer.h" // gtimer_c + +// Forward declarations +namespace swss +{ +class Table_async; +class DBConnector_async; +} + +class hamd_config_c; +struct redisReply; +typedef struct redisReply redisReply; +typedef struct redisAsyncContext redisAsyncContext; + +//############################################################################## +class redis_c +{ +public: + redis_c(const hamd_config_c & hamd_config_r, GMainContext * main_ctx_p); + + void cleanup() {} + bool is_tron() const; + + void set_roles(const std::string & login_r, const std::vector< std::string > & roles_r); + void del_roles(const std::string & login_r); + +private: + const hamd_config_c & hamd_config_rm; + GMainContext * g_main_ctx_pm = nullptr; // GLib main loop context + + gtimer_c inloop_init_timer_m; + static bool on_inloop_init_timeout_cb(gpointer user_data_p); // This callback functions must follow GSourceFunc signature. + void on_inloop_init_timeout(); + + void roles_db_upload(swss::Table_async & user_table_r); + + void set_roles(swss::Table_async & user_table_r, const std::string & login_r, const std::vector< std::string > & roles_r); + void del_roles(swss::Table_async & user_table_r, const std::string & login_r); + + static void on_set_roles_cb(redisAsyncContext * ac_p, void * reply_p, void * user_data); + void on_set_roles(redisAsyncContext * ac_p, redisReply * reply_p); + + static void on_del_roles_cb(redisAsyncContext * ac_p, void * reply_p, void * user_data); + void on_del_roles(redisAsyncContext * ac_p, redisReply * reply_p); +}; + + +class DBConnector_async_glib +{ +public: + DBConnector_async_glib(GMainContext * g_main_ctx_p, bool verbose=false); + ~DBConnector_async_glib(); + + bool valid() const { return db_pm != nullptr; } + swss::DBConnector_async & obj() const { return *db_pm; } + +private: + swss::DBConnector_async * db_pm = nullptr; +}; + +#endif // __REDIS_H__ diff --git a/src/ham/hamd/hamd_sac.cpp b/src/ham/hamd/hamd_sac.cpp new file mode 100644 index 00000000000..e5c05287e99 --- /dev/null +++ b/src/ham/hamd/hamd_sac.cpp @@ -0,0 +1,257 @@ +#include "hamd.h" // hamd_c +#include "hamd_gecos.h" // gecos_c() +#include "siphash24.h" // siphash24() +#include "subprocess.h" // run() +#include "hamd_redis.h" // redis_c +#include "passwd_utils.h" // fgetpwnam() +#include "../shared/utils.h" // LOG_CONDITIONAL(), strfmt(), join() + +#include // getpwnam() +#include // getpwnam(), getpwuid() +#include // getgrnam(), getgrouplist() +#include // syslog() +#include // std::set +#include // g_chdir() +#include // std::is_permutation() + +#define UNCONFIRMED_USER_MAGIC_STRING "Unconfirmed SAC user" +#define UNCONFIRMED_USER_MAGIC_STRING_LEN (sizeof(UNCONFIRMED_USER_MAGIC_STRING)-1) + + +/** + * @brief This is a DBus interface used by remote programs to add an + * unconfirmed user. + * + * @param login User's login name + * @param pid PID of the caller. + * + * @return Empty string on success, Error message otherwise. + */ +std::string hamd_c::add_unconfirmed_user(const std::string & login, + const uint32_t & pid) +{ + // First, let's check if there are any + // unconfirmed users that could be removed. + rm_unconfirmed_users(); + + // Second check if user already exists + if (NULL != fgetpwnam(login.c_str())) + { + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::add_unconfirmed_user: User \"%s\" already exist", login.c_str()); + return ""; + } + + // Next, add as an unconfirmed user. + + unsigned attempt_cnt; + uid_t candidate; + std::string name(login); + std::string full_cmd; + std::string base_cmd = "/usr/sbin/useradd" + " --create-home" + " --user-group" + " --comment \"" UNCONFIRMED_USER_MAGIC_STRING " " + std::to_string(pid) + '"'; + + if (!config_rm.shell().empty()) + base_cmd += " --shell " + config_rm.shell(); + + for (attempt_cnt = 0; attempt_cnt < 100; attempt_cnt++) /* Give up retrying eventually */ + { + // Find a unique UID in the range sac_uid_min_m..sac_uid_max_m. + // We use a hash function to always get the same ID for a given user + // name. Hash collisions (i.e. two user names with the same hash) will + // be handled by trying with a slightly different username. + candidate = config_rm.uid_fit_into_range(siphash24(name.c_str(), name.length())); + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "User \"%s\": attempt %d using name \"%s\", candidate UID=%lu", + login.c_str(), attempt_cnt, name.c_str(), (unsigned long)candidate); + + // Note: The range 60000-64999 is reserved on Debian platforms + // and should be avoided and the value 65535 is traditionally + // reserved as an "error" code as well as 65534 is reserved + // as the "nobody" user. + if (!((candidate >= 60000) && (candidate <= 64999)) && + (candidate != 65535) && + (candidate != 65534) && + !::getpwuid(candidate)) /* make sure not already allocated */ + { + full_cmd = base_cmd + " --uid " + std::to_string(candidate) + ' ' + login; + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "User \"%s\": executing \"%s\"", login.c_str(), full_cmd.c_str()); + + int rc; + std::string std_err; + std::string std_out; + std::tie(rc, std_out, std_err) = run(full_cmd); + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "SAC - User \"%s\": useradd returned rc=%d, stdout=%s, stderr=%s", + login.c_str(), rc, std_out.c_str(), std_err.c_str()); + + return rc == 0 ? "" : strfmt("SAC - User \"%s\": useradd failed rc=%d, stdout=%s, stderr=%s", + login.c_str(), rc, std_out.c_str(), std_err.c_str()); + } + else + { + // Try with a slightly different name + name = login + std::to_string(attempt_cnt); + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "SAC - User \"%s\": candidate UID=%lu already in use. Retry with name = \"%s\"", + login.c_str(), (unsigned long)candidate, name.c_str()); + } + } + + std::string errmsg = strfmt("SAC - User \"%s\": Unable to create user after %d attempts", + login.c_str(), attempt_cnt); + + syslog(LOG_ERR, "%s", errmsg.c_str()); + + return errmsg; +} + +/** + * @brief This is a DBus interface used by remote programs to confirm a + * user. + * + * @param login User's login name + * @param roles List of roles + * @param auth_method One of "tacacs+" or "radius" + * + * @return Empty string on success, Error message otherwise. + */ +std::string hamd_c::user_confirm(const std::string & login, + const std::vector & roles, + const std::string & auth_method) +{ + std::string errmsg; + std::string dbgstr; + if (is_tron()) + { + dbgstr = strfmt("SAC - hamd_c::user_confirm(login=\"%s\", roles=\"[%s]\", auth_method=\"%s\")", + login.c_str(), join(roles.cbegin(), roles.cend(), ", ").c_str(), auth_method.c_str()); + syslog(LOG_DEBUG, "%s", dbgstr.c_str()); + } + + // Check whether user already exists in /etc/passwd + struct passwd * pwd = fgetpwnam(login.c_str()); + if ((NULL == pwd) || (NULL == pwd->pw_name) || (login != pwd->pw_name)) + { + errmsg = strfmt("No such user in /etc/passwd: %s", login.c_str()); + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "%s: %s", dbgstr.c_str(), errmsg.c_str()); + return errmsg; + } + + // Check whether it is an unconfirmed user + bool unconfirmed_user = strneq(pwd->pw_gecos, UNCONFIRMED_USER_MAGIC_STRING, UNCONFIRMED_USER_MAGIC_STRING_LEN); + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "%s: unconfirmed_user=%s", dbgstr.c_str(), true_false(unconfirmed_user)); + + + gecos_c gecos(pwd->pw_gecos); + bool must_update_roles = true; + if (!unconfirmed_user) + { + // Check whether the roles need to be changed by extracting + // the current roles from the GECOS. + std::vector cur_roles = gecos.roles().members(); + + if (is_tron()) + { + syslog(LOG_DEBUG, "%s: cur roles=[%s]", dbgstr.c_str(), join(cur_roles.cbegin(), cur_roles.cend(), ", ").c_str()); + syslog(LOG_DEBUG, "%s: new roles=[%s]", dbgstr.c_str(), join(roles.cbegin(), roles.cend(), ", ").c_str()); + } + + must_update_roles = !std::is_permutation(roles.begin(), roles.end(), cur_roles.begin(), cur_roles.end()); + } + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "%s: must_update_roles=%s", + dbgstr.c_str(), true_false(must_update_roles)); + + if (!must_update_roles) + return ""; // We're all good. Nothing has changed. + + gecos.set(roles_as_string(roles).c_str(), nullptr, auth_method.c_str()); + std::string cmd = "/usr/sbin/usermod" + " --groups \"" + get_groups_as_string(roles) + "\"" + " --comment \"" + gecos.text() + "\" " + login; + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "%s: executing \"%s\"", dbgstr.c_str(), cmd.c_str()); + + int rc; + std::string std_err; + std::string std_out; + std::tie(rc, std_out, std_err) = run(cmd); + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "%s: usermod returned rc=%d, stdout=%s, stderr=%s", + dbgstr.c_str(), rc, std_out.c_str(), std_err.c_str()); + + if (rc != 0) + { + return strfmt("/usr/sbin/usermod returned rc=%d, stdout=%s, stderr=%s", + rc, std_out.c_str(), std_err.c_str()); + } + + if (unconfirmed_user) + { + // Only run port-create scripts for newly authenticated users. + errmsg = post_create_scripts(login); + if (!errmsg.empty()) // The errmsg will be empty on success + { + // Since we failed to run the post-create + // scripts, we now need to delete the user. + delete_user(login); + } + } + + if (!errmsg.empty()) // The errmsg will be empty on success + { + redis_rm.set_roles(login, roles); + } + + return errmsg; +} + +/** + * @brief Remove unconfirmed users from /etc/passwd. Unconfirmed users have + * the string "Unconfirmed sac user [PID]" in their GECOS string and + * the PID does not exist anymore. + */ +void hamd_c::rm_unconfirmed_users() const +{ + FILE * f = fopen("/etc/passwd", "re"); + if (f) + { + struct passwd * ent; + std::string base_cmd("/usr/sbin/userdel --remove "); + std::string full_cmd; + g_chdir("/proc"); + while (NULL != (ent = fgetpwent(f))) + { + const char * pid_p = startswith(ent->pw_gecos, UNCONFIRMED_USER_MAGIC_STRING); + if (NULL != pid_p) + { + if (!g_file_test(pid_p, G_FILE_TEST_EXISTS)) + { + // Directory does not exist, which means process does not + // exist either. Let's remove this user which was never + // confirmed by PAM authentification. + full_cmd = base_cmd + ent->pw_name; + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::rm_unconfirmed_users() - executing command \"%s\"", full_cmd.c_str()); + + int rc; + std::string std_err; + std::string std_out; + std::tie(rc, std_out, std_err) = run(full_cmd); + + LOG_CONDITIONAL(is_tron(), LOG_DEBUG, "hamd_c::rm_unconfirmed_users() - command returned rc=%d, stdout=%s, stderr=%s", + rc, std_out.c_str(), std_err.c_str()); + + if (rc != 0) + { + syslog(LOG_ERR, "User \"%s\": Failed to removed unconfirmed user UID=%d. %s", + ent->pw_name, ent->pw_uid, std_err.c_str()); + } + } + } + } + fclose(f); + } +} diff --git a/src/ham/hamd/passwd_utils.cpp b/src/ham/hamd/passwd_utils.cpp new file mode 100644 index 00000000000..11989374e51 --- /dev/null +++ b/src/ham/hamd/passwd_utils.cpp @@ -0,0 +1,36 @@ +#include "passwd_utils.h" +#include "../shared/utils.h" // streq() + +/** + * @brief Scan "/etc/passwd" looking for user. If found, return a pointer + * to a "struct passwd" containing all the data related to user. + * + * The reason for creating this function instead of using the + * stadard POSIX getpwnam(), is that this function doesn't use the + * underlying NSS infrastructure. Instead, it access /etc/passwd + * directly, which is what we need. + * + * @param user The user we're looking for + * + * @return If user found, return a pointer to a struct passwd. + */ +struct passwd * fgetpwnam(const char * user) +{ + struct passwd * pwd = NULL; + FILE * f = fopen("/etc/passwd", "re"); + if (f) + { + struct passwd * ent; + while (NULL != (ent = fgetpwent(f))) + { + if (streq(ent->pw_name, user)) + { + pwd = ent; + break; + } + } + fclose(f); + } + + return pwd; +} diff --git a/src/ham/hamd/passwd_utils.h b/src/ham/hamd/passwd_utils.h new file mode 100644 index 00000000000..f9a8901b671 --- /dev/null +++ b/src/ham/hamd/passwd_utils.h @@ -0,0 +1,8 @@ +#ifndef __PASSWD_UTILS_H__ +#define __PASSWD_UTILS_H__ + +#include // struct passwd + +extern struct passwd * fgetpwnam(const char * user); + +#endif // __PASSWD_UTILS_H__ diff --git a/src/ham/hamd/siphash24.c b/src/ham/hamd/siphash24.c new file mode 100644 index 00000000000..0884ccf0de1 --- /dev/null +++ b/src/ham/hamd/siphash24.c @@ -0,0 +1,193 @@ +/* + SipHash reference C implementation + + Written in 2012 by + Jean-Philippe Aumasson + Daniel J. Bernstein + + To the extent possible under law, the author(s) have dedicated all copyright + and related and neighboring rights to this software to the public domain + worldwide. This software is distributed without any warranty. + + You should have received a copy of the CC0 Public Domain Dedication along with + this software. If not, see . + + (Minimal changes made by Lennart Poettering, to make clean for inclusion in systemd) + (Refactored by Tom Gundersen to split up in several functions and follow systemd + coding style) +*/ + +#include +#include /* assert() */ + +#include "siphash24.h" + +static inline uint16_t unaligned_read_le16(const void *_u) +{ + const uint8_t *u = _u; + return (((uint16_t)u[1]) << 8) | ((uint16_t)u[0]); +} + +static inline uint32_t unaligned_read_le32(const void *_u) +{ + const uint8_t *u = _u; + return (((uint32_t)unaligned_read_le16(u + 2)) << 16) | + ((uint32_t)unaligned_read_le16(u)); +} + +static inline uint64_t unaligned_read_le64(const void *_u) +{ + const uint8_t *u = _u; + return (((uint64_t)unaligned_read_le32(u + 4)) << 32) | + ((uint64_t)unaligned_read_le32(u)); +} + +static inline uint64_t rotate_left(uint64_t x, uint8_t b) +{ + assert(b < 64); + return (x << b) | (x >> (64 - b)); +} + +static inline void sipround(struct siphash *state) +{ + assert(state); + + state->v0 += state->v1; + state->v1 = rotate_left(state->v1, 13); + state->v1 ^= state->v0; + state->v0 = rotate_left(state->v0, 32); + state->v2 += state->v3; + state->v3 = rotate_left(state->v3, 16); + state->v3 ^= state->v2; + state->v0 += state->v3; + state->v3 = rotate_left(state->v3, 21); + state->v3 ^= state->v0; + state->v2 += state->v1; + state->v1 = rotate_left(state->v1, 17); + state->v1 ^= state->v2; + state->v2 = rotate_left(state->v2, 32); +} + +void siphash24_init(struct siphash *state, const uint8_t k[16]) +{ + uint64_t k0, k1; + + assert(state); + assert(k); + + k0 = unaligned_read_le64(k); + k1 = unaligned_read_le64(k + 8); + + *state = (struct siphash){ + /* "somepseudorandomlygeneratedbytes" */ + .v0 = 0x736f6d6570736575ULL ^ k0, + .v1 = 0x646f72616e646f6dULL ^ k1, + .v2 = 0x6c7967656e657261ULL ^ k0, + .v3 = 0x7465646279746573ULL ^ k1, + .padding = 0, + .inlen = 0, + }; +} + +void siphash24_compress(const void *_in, size_t inlen, struct siphash *state) +{ + + const uint8_t *in = _in; + const uint8_t *end = in + inlen; + size_t left = state->inlen & 7; + uint64_t m; + + assert(in); + assert(state); + + /* Update total length */ + state->inlen += inlen; + + /* If padding exists, fill it out */ + if (left > 0) + { + for (; in < end && left < 8; in++, left++) + state->padding |= ((uint64_t)*in) << (left * 8); + + if (in == end && left < 8) + /* We did not have enough input to fill out the padding completely */ + return; + + state->v3 ^= state->padding; + sipround(state); + sipround(state); + state->v0 ^= state->padding; + + state->padding = 0; + } + + end -= (state->inlen % sizeof(uint64_t)); + + for (; in < end; in += 8) + { + m = unaligned_read_le64(in); + state->v3 ^= m; + sipround(state); + sipround(state); + state->v0 ^= m; + } + + left = state->inlen & 7; + switch (left) + { + case 7: state->padding |= ((uint64_t)in[6]) << 48; + case 6: state->padding |= ((uint64_t)in[5]) << 40; + case 5: state->padding |= ((uint64_t)in[4]) << 32; + case 4: state->padding |= ((uint64_t)in[3]) << 24; + case 3: state->padding |= ((uint64_t)in[2]) << 16; + case 2: state->padding |= ((uint64_t)in[1]) << 8; + case 1: state->padding |= ((uint64_t)in[0]); + case 0: break; + } +} + +uint64_t siphash24_finalize(struct siphash *state) +{ + uint64_t b; + + assert(state); + + b = state->padding | (((uint64_t)state->inlen) << 56); + + state->v3 ^= b; + sipround(state); + sipround(state); + state->v0 ^= b; + state->v2 ^= 0xff; + + sipround(state); + sipround(state); + sipround(state); + sipround(state); + + return state->v0 ^ state->v1 ^ state->v2 ^ state->v3; +} + +uint64_t siphash24_with_key(const void *in, size_t inlen, const uint8_t k[16]) +{ + struct siphash state; + + assert(in); + assert(k); + + siphash24_init(&state, k); + siphash24_compress(in, inlen, &state); + + return siphash24_finalize(&state); +} + +uint64_t siphash24(const void *in, size_t inlen) +{ + static const uint8_t hash_key[] = + { + 0x37, 0x53, 0x7e, 0x31, 0xcf, 0xce, 0x48, 0xf5, + 0x8a, 0xbb, 0x39, 0x57, 0x8d, 0xd9, 0xec, 0x59 + }; + + return siphash24_with_key(in, inlen, hash_key); +} diff --git a/src/ham/hamd/siphash24.h b/src/ham/hamd/siphash24.h new file mode 100644 index 00000000000..babe8d6df5a --- /dev/null +++ b/src/ham/hamd/siphash24.h @@ -0,0 +1,54 @@ +#ifndef __NSS_HAM_SIPHASH24_H +#define __NSS_HAM_SIPHASH24_H +/* + SipHash reference C implementation + + Written in 2012 by + Jean-Philippe Aumasson + Daniel J. Bernstein + + To the extent possible under law, the author(s) have dedicated all copyright + and related and neighboring rights to this software to the public domain + worldwide. This software is distributed without any warranty. + + You should have received a copy of the CC0 Public Domain Dedication along with + this software. If not, see . + + (Minimal changes made by Lennart Poettering, to make clean for inclusion in systemd) + (Refactored by Tom Gundersen to split up in several functions and follow systemd + coding style) +*/ + +#ifdef __cplusplus +extern "C" { +#endif + +#include +#include +#include +#include + +struct siphash +{ + uint64_t v0; + uint64_t v1; + uint64_t v2; + uint64_t v3; + uint64_t padding; + size_t inlen; +}; + +void siphash24_init(struct siphash *state, const uint8_t k[16]); +void siphash24_compress(const void *in, size_t inlen, struct siphash *state); +#define siphash24_compress_byte(byte, state) siphash24_compress((const uint8_t[]) { (byte) }, 1, (state)) + +uint64_t siphash24_finalize(struct siphash *state); + +uint64_t siphash24_with_key(const void *in, size_t inlen, const uint8_t k[16]); +uint64_t siphash24(const void *in, size_t inlen); + +#ifdef __cplusplus +} /* extern "C" */ +#endif + +#endif /* __NSS_HAM_SIPHASH24_H */ diff --git a/src/ham/hamd/subprocess.cpp b/src/ham/hamd/subprocess.cpp new file mode 100644 index 00000000000..25296052eeb --- /dev/null +++ b/src/ham/hamd/subprocess.cpp @@ -0,0 +1,124 @@ +#include +#include +#include +#include +#include +#include // recv(), MSG_DONTWAIT +#include // LINE_MAX +#include // std::string +#include // std::tuple + +#include "subprocess.h" // run() + +#define SHELL_PATH "/bin/sh" +#define SHELL_NAME "sh" + +class capture_pipe_c +{ +private: + #define RD_END 0 + #define WR_END 1 + + int stdout_pipe_fds[2] = { -1, -1 }; + int stderr_pipe_fds[2] = { -1, -1 }; + bool broken_m = true; + + void _close(int fd_id) + { + (void)close(stdout_pipe_fds[fd_id]); + stdout_pipe_fds[fd_id] = -1; + + (void)close(stderr_pipe_fds[fd_id]); + stderr_pipe_fds[fd_id] = -1; + } + + void _remap(int fd_id) + { + (void)dup2(stdout_pipe_fds[fd_id], STDOUT_FILENO); + _close(stdout_pipe_fds[fd_id]); // Not needed anymore since it's been mapped to STDOUT + + (void)dup2(stderr_pipe_fds[fd_id], STDERR_FILENO); + _close(stderr_pipe_fds[fd_id]); // Not needed anymore since it's been mapped to STDOUT + } + +public: + capture_pipe_c() + { + broken_m = (0 != pipe(stdout_pipe_fds)) || + (0 != pipe(stderr_pipe_fds)); + } + + ~capture_pipe_c() + { + _close(RD_END); // Close the reader end of both pipes + _close(WR_END); // Close the writer end of both pipes + } + + bool broken() const { return broken_m; } + + void running_as_child() + { + _remap(WR_END); // Remap stdout/stderr to the writer end of the two pipes + _close(RD_END); // Close the reader end of the pipes when running in the child process + } + + void running_as_parent() + { + _close(WR_END); // Close the writer end of the pipes when running in the parent process + } + + std::string stdout() + { + char buf[LINE_MAX]; + ssize_t len = read(stdout_pipe_fds[RD_END], buf, sizeof(buf)); + return (len > 0) ? std::string(buf, len) : ""; + } + + std::string stderr() + { + char buf[LINE_MAX]; + ssize_t len = read(stderr_pipe_fds[RD_END], buf, sizeof(buf)); + return (len > 0) ? std::string(buf, len) : ""; + } +}; + +std::tuple run(const std::string & cmd_r) +{ + capture_pipe_c pipe; // CREATE THE PIPE BEFORE FORKING!!!! + if (pipe.broken()) + return std::make_tuple(-1, "", "failed to create stdout/stderr capture pipe"); + + pid_t pid = fork(); + if (pid < (pid_t)0) // Did fork fail? + return std::make_tuple(-1, "", "failed to fork process"); + + if (pid == (pid_t)0) /* Child */ + { + pipe.running_as_child(); + + const char * new_argv[4]; + new_argv[0] = SHELL_NAME; + new_argv[1] = "-c"; + new_argv[2] = cmd_r.c_str(); + new_argv[3] = NULL; + + // Execute the shell + (void)execve(SHELL_PATH, (char *const *)new_argv, environ); + + exit(127); // exit the child + } + + /* Parent */ + pipe.running_as_parent(); + + int exit_status = -1; + if (TEMP_FAILURE_RETRY(waitpid(pid, &exit_status, 0)) != pid) + exit_status = -1; + + bool term_normal = WIFEXITED(exit_status); + if (!term_normal) + return std::make_tuple(-1, "", "abnormal command termination"); + + int rc = WEXITSTATUS(exit_status); + return std::make_tuple(rc, pipe.stdout(), pipe.stderr()); +} diff --git a/src/ham/hamd/subprocess.h b/src/ham/hamd/subprocess.h new file mode 100644 index 00000000000..5fdc6006305 --- /dev/null +++ b/src/ham/hamd/subprocess.h @@ -0,0 +1,8 @@ +#ifndef SUBPROCESS_H +#define SUBPROCESS_H +#include // std::string +#include // std::tuple + +std::tuple run(const std::string & cmd_r); + +#endif // SUBPROCESS_H diff --git a/src/ham/hamd/timer.cpp b/src/ham/hamd/timer.cpp new file mode 100644 index 00000000000..29a38d3a1ef --- /dev/null +++ b/src/ham/hamd/timer.cpp @@ -0,0 +1,190 @@ +// Host Account Management +#include +#include // rintf() +#include // std::ostream +#include // syslog() + +#include "timer.h" + +/** + * @brief See timer.h for description. + */ +gtimer_c::gtimer_c(double interval_sec, + bool (* user_cback_p)(void * user_data_p), + void * user_data_p, + int priority): + source_pm(NULL), + tid_m(0), + interval_sec_m(-1.0), + user_cback_pm(user_cback_p), + user_data_pm(user_data_p), + priority_m(priority), + g_timeout_source_new_funct_pm(g_timeout_source_new_seconds) +{ + //syslog(LOG_INFO, "gtimer_c::gtimer_c() - interval_sec=%f s", interval_sec); + set_timeout(interval_sec); +} + +/** + * @brief See timer.h for description. + */ +gtimer_c::~gtimer_c() +{ + stop(); +} + +/** + * @brief See timer.h for description. + */ +gboolean gtimer_c::callback(gpointer user_data_p) +{ + gtimer_c * p = reinterpret_cast(user_data_p); + bool r = p->user_cback_pm(p->user_data_pm); + gboolean ret; + + if (r) + { + ret = TRUE; + } + else + { + p->source_pm = NULL; + p->tid_m = 0; + ret = FALSE; + } + + return ret; +} + +/** + * @brief See timer.h for description. + */ +void gtimer_c::stop() +{ + //syslog(LOG_INFO, "gtimer_c::stop() - ENTER tid_m=%d", tid_m); + if (0 != tid_m) + { + g_source_remove(tid_m); + source_pm = NULL; + tid_m = 0; + } + //syslog(LOG_INFO, "gtimer_c::stop() - EXIT"); +} + +/** + * @brief See timer.h for description. + */ +void gtimer_c::start(double new_interval_sec, void * user_data_p) +{ + //syslog(LOG_INFO, "gtimer_c::start() - ENTER tid_m=%d", tid_m); + + if (new_interval_sec > 0) + { + set_timeout(new_interval_sec); + } + + if (NULL != user_data_p) + { + user_data_pm = user_data_p; + } + + if (active()) + { + //syslog(LOG_INFO, "gtimer_c::start() - Restarting interval_sec_m=%f s"); + g_source_set_ready_time(source_pm, g_source_get_time(source_pm) + (gint64)(interval_sec_m * 1000000)); + } + else + { + //syslog(LOG_INFO, "gtimer_c::start() - Starting new"); + source_pm = g_timeout_source_new_funct_pm(interval_m); + + if (G_PRIORITY_DEFAULT != priority_m) + g_source_set_priority(source_pm, priority_m); + + g_source_set_callback(source_pm, gtimer_c::callback, this, NULL); + tid_m = g_source_attach(source_pm, NULL); + g_source_unref(source_pm); + } + + //syslog(LOG_INFO, "gtimer_c::start() - EXIT tid_m=%d", tid_m); +} + +/** + * @brief See timer.h for description. + */ +void gtimer_c::clear() +{ + if (active()) + { + g_source_set_ready_time(source_pm, 0); + } +} + +/** + * @brief See timer.h for description. + */ +void gtimer_c::set_timeout(double new_interval_sec) +{ + //syslog(LOG_INFO, "gtimer_c::set_timeout() - ENTER new_interval_sec=%f", new_interval_sec); + if ((new_interval_sec >= 0) && (interval_sec_m != new_interval_sec)) + { + interval_sec_m = new_interval_sec; + if (rintf(new_interval_sec) == new_interval_sec) + { + //syslog(LOG_INFO, "gtimer_c::set_timeout() - use sec"); + g_timeout_source_new_funct_pm = g_timeout_source_new_seconds; + interval_m = (guint)new_interval_sec; + } + else + { + //syslog(LOG_INFO, "gtimer_c::set_timeout() - use msec"); + g_timeout_source_new_funct_pm = g_timeout_source_new; + interval_m = (guint)((new_interval_sec * 1000) + 0.5); + } + } + //syslog(LOG_INFO, "gtimer_c::set_timeout() - EXIT - interval_m=%d", interval_m); +} + +/** + * @brief See timer.h for description. + */ +void gtimer_c::set_user_data(void * user_data_p) +{ + if (NULL != user_data_p) + { + user_data_pm = user_data_p; + } +} + +/** + * @brief See timer.h for description. + */ +double gtimer_c::time_remaining() const +{ + if (active()) + { + gint64 delta_us = g_source_get_ready_time(source_pm) - g_source_get_time(source_pm); + if (delta_us > 0) + { + return (double)delta_us / 1000000.0; + } + } + + return 0; +} + +/** + * @brief See timer.h for description. + */ +std::ostream& operator<<(std::ostream & stream_r, const gtimer_c & timer_r) +{ + if (timer_r.active()) + { + stream_r << timer_r.time_remaining() << "s"; + } + else + { + stream_r << "off"; + } + return stream_r; +} diff --git a/src/ham/hamd/timer.h b/src/ham/hamd/timer.h new file mode 100644 index 00000000000..21e31d142c5 --- /dev/null +++ b/src/ham/hamd/timer.h @@ -0,0 +1,114 @@ +// Host Account Management +#ifndef TIMER_H +#define TIMER_H + +#include // GSourceFunc + +/** Prototype for functions passed to gtimer_c::gtimer_c(). + * + * This is the signature of the user provided callback function + * that should be called upon expiration of a timer. + * + * @param user_data_p Data passed to the callback function when + * timer expires. + * + * @return %false if the timer should be stopped. %true if + * timer should keep firing at regular interval. */ + +class gtimer_c +{ +public: + gtimer_c(double interval_sec, bool (* user_cback_p)(void * user_data_p), void * user_data_p, int priority=G_PRIORITY_DEFAULT); + ~gtimer_c(); + + /** @brief Stop timer + */ + void stop(); + + /** @brief Start (or restart) timer. If timer is already running it will + * be re-started with its original timeout. + * + * This method optionally allows you to change the default timeout + * by specifying new_timeout. + * + * @param new_interval_sec Timeout interval in seconds. + * @param user_data_p Data passed to the callback function when + * timer expires. + */ + void start(double new_interval_sec=-1.0, void * user_data_p=NULL); + + /** @brief Start timer only if it is not already running. Otherwise, leave + * timer alone. + * + * This method optionally allows you to change the default timeout + * by specifying new_timeout. + */ + void conditional_start(int new_interval_s=-1.0, void * user_data_p=NULL) + { + if (!active()) + start(new_interval_s, user_data_p); + } + + /** @brief Make a timer expire immediately. The reactor will immediately + * queue up the callback for execution. + */ + void clear(); + + /** @brief Set the timeout. This allows you to change the default + * timeout. An already running timer will continue with its old + * timeout unless start() is called again after setting new + * timeout. + * + * @param new_interval_sec Timeout interval in seconds. + */ + void set_timeout(double new_interval_sec); + + /** @brief Set callback arguments. This allows you to change the default + * cb_args. An already running timer will continue with its old + * cb_args unless start() is called again after setting new + * cb_args. + */ + void set_user_data(void * user_data_p); + + /** @brief Return how much time is remaining on a timer before it expires. + + @return Time remaining on a timer before it expires. + floating-point number in seconds + */ + double time_remaining() const; + + /** @brief Check whether a timer is currently running + * + * @return True if timer is running, false otherwise. + */ + bool active() const { return 0 != tid_m; } + + /** @brief Get the timeout interval. + * + * @return The timer interval as floating-point number in + * seconds. + */ + double get_interval_s() const { return interval_sec_m; } + +private: + + typedef GSource * (* new_source_func_pt) (guint interval); + + GSource * source_pm; + guint tid_m; + + double interval_sec_m; + guint interval_m; + bool (* user_cback_pm)(void * user_data_p); + void * user_data_pm; + int priority_m; + + new_source_func_pt g_timeout_source_new_funct_pm; + + static gboolean callback(gpointer user_data_p); // Must match GSourceFunc signature. +}; + +#include // std::ostream +std::ostream & operator<<(std::ostream & stream_r, const gtimer_c & timer_r); + +#endif /* TIMER_H */ diff --git a/src/ham/libham/Makefile b/src/ham/libham/Makefile new file mode 100644 index 00000000000..a003896d48a --- /dev/null +++ b/src/ham/libham/Makefile @@ -0,0 +1,76 @@ +# Prerequisite: sudo apt-get install libsystemd-dev libdbus-c++-dev +TARGET := libham.so +SRCS := $(wildcard *.cpp) + +#PKGS := libsystemd dbus-c++-glib-1 +PKGS := dbus-c++-glib-1 +LDFLAGS := -shared -Wl,-soname,${TARGET} +LDLIBS := $(shell pkg-config --libs ${PKGS}) +CPPFLAGS := $(shell pkg-config --cflags ${PKGS}) +CFLAGS := -fPIC -Wall -Werror -O3 +CXXFLAGS := -std=c++11 ${CFLAGS} +LL := g++ + +DBUS-GLUE := $(patsubst %.xml,%.dbus-proxy.h,$(wildcard ../shared/*.xml)) + +OBJS := $(patsubst %.cpp,%.o,$(filter %.cpp,${SRCS})) +DEPS := $(OBJS:.o=.d) + +ifeq (,$(strip $(filter $(MAKECMDGOALS),clean install uninstall package))) + ifneq (,$(strip ${DEPS})) + ${DEPS}: ${DBUS-GLUE} + -include ${DEPS} + endif +endif + + +ifeq (,$(strip $(filter $(MAKECMDGOALS),clean install uninstall))) +# ******************************************************************* +# Make all +# ******************************************************************* +.DEFAULT_GOAL := all +all: ${TARGET} + +${TARGET}: ${OBJS} Makefile + @printf "%b[1;36m%s%b[0m\n" "\0033" "Building $@" "\0033" + $(LL) ${LDFLAGS} -o $@ ${OBJS} $(LDLIBS) + @printf "%b[1;32m%s%b[0m\n\n" "\0033" "$@ Done!" "\0033" + +endif # (,$(strip $(filter $(MAKECMDGOALS),clean install uninstall))) + + +# ******************************************************************* +# Implicit rules: +# ******************************************************************* +%.o : %.cpp + @printf "%b[1;36m%s%b[0m\n" "\0033" "Compiling: $< -> $@" "\0033" + ${CXX} ${CPPFLAGS} ${CXXFLAGS} -c $< -o $@ + @printf "\n" + +%.d : %.cpp + @printf "%b[1;36m%s%b[0m\n" "\0033" "Dependency: $< -> $@" "\0033" + ${CXX} -MM -MG -MT '$@ $(@:.d=.o)' ${CPPFLAGS} ${CXXFLAGS} -o $@ $< + @printf "\n" + +# Implicit rule to generate DBus header files from XML +../shared/%.dbus-proxy.h: ../shared/%.xml + @printf "%b[1;36m%s%b[0m\n" "\0033" "dbusxx-xml2cpp $< --proxy=$@" "\0033" + @dbusxx-xml2cpp $< --proxy=$@ + +# ******************************************************************* +# ____ _ +# / ___| | ___ __ _ _ __ +# | | | |/ _ \/ _` | '_ \ +# | |___| | __/ (_| | | | | +# \____|_|\___|\__,_|_| |_| +# +# ******************************************************************* +RM_TARGET := ${TARGET} ./*.o ./*.d ${DBUS-GLUE} + +RM_LIST = $(strip $(wildcard ${RM_TARGET})) +.PHONY: clean +clean: +ifneq (,$(RM_LIST)) + rm -rf $(RM_LIST) +endif + diff --git a/src/ham/libham/README.md b/src/ham/libham/README.md new file mode 100644 index 00000000000..be12257a008 --- /dev/null +++ b/src/ham/libham/README.md @@ -0,0 +1,2 @@ +This is the HAM development library. It provides a suite of APIs to be used by +applications developers. \ No newline at end of file diff --git a/src/ham/libham/dbus-connection.cpp b/src/ham/libham/dbus-connection.cpp new file mode 100644 index 00000000000..cfb5f4a3a32 --- /dev/null +++ b/src/ham/libham/dbus-connection.cpp @@ -0,0 +1,23 @@ +#include // DBus +#include "dbus-proxy.h" // get_dbusconn() prototype + +DBus::Connection & get_dbusconn() +{ + static DBus::Connection * conn_p = nullptr; + if (conn_p == nullptr) + { + // DBus::BusDispatcher is a "main loop" construct that + // handles (i.e. dispatched) DBus messages. This should + // be defined as a singleton to avoid memory leaks. + static DBus::BusDispatcher dispatcher; + + // DBus::default_dispatcher must be initialized before DBus::Connection. + DBus::default_dispatcher = &dispatcher; + + static DBus::Connection conn = DBus::Connection::SystemBus(); + + conn_p = &conn; + } + + return *conn_p; +} diff --git a/src/ham/libham/dbus-proxy.h b/src/ham/libham/dbus-proxy.h new file mode 100644 index 00000000000..d2d9913caaa --- /dev/null +++ b/src/ham/libham/dbus-proxy.h @@ -0,0 +1,35 @@ +// Host Account Management +#ifndef DBUS_PROXY_H +#define DBUS_PROXY_H + +#include // DBus + +#include "../shared/org.SONiC.HostAccountManagement.dbus-proxy.h" + +class accounts_proxy_c : public ham::accounts_proxy, + public DBus::IntrospectableProxy, + public DBus::ObjectProxy +{ +public: + accounts_proxy_c(DBus::Connection &connection, const char * dbus_bus_name_p, const char * dbus_obj_name_p) : + DBus::ObjectProxy(connection, dbus_obj_name_p, dbus_bus_name_p) + { + } +}; + + +class sac_proxy_c : public ham::sac_proxy, + public DBus::IntrospectableProxy, + public DBus::ObjectProxy +{ +public: + sac_proxy_c(DBus::Connection &conn, const char * dbus_bus_name_p, const char * dbus_obj_name_p) : + DBus::ObjectProxy(conn, dbus_obj_name_p, dbus_bus_name_p) + { + } +}; + + +extern DBus::Connection & get_dbusconn(); + +#endif // DBUS_PROXY_H diff --git a/src/ham/libham/ham_groupadd.cpp b/src/ham/libham/ham_groupadd.cpp new file mode 100644 index 00000000000..5e6d9d4a29b --- /dev/null +++ b/src/ham/libham/ham_groupadd.cpp @@ -0,0 +1,29 @@ +// Host Account Management +#include "./usr/include/ham/ham.h" +#include "dbus-proxy.h" +#include "../shared/dbus-address.h" /* DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE */ + + +#include + +bool ham_groupadd(const char * group) +{ + bool success = false; + try + { + accounts_proxy_c acct(get_dbusconn(), DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE); + + ::DBus::Struct ret; + ret = acct.groupadd(group); + success = ret._1; + if (!success) + syslog(LOG_ERR, "ham_groupadd(group=\"%s\") - Error %s\n", group, ret._2.c_str()); + } + catch (DBus::Error & ex) + { + syslog(LOG_CRIT, "ham_groupadd(group=\"%s\" - DBus Exception %s\n", group, ex.what()); + } + + return success; +} + diff --git a/src/ham/libham/ham_groupdel.cpp b/src/ham/libham/ham_groupdel.cpp new file mode 100644 index 00000000000..d227950ffb8 --- /dev/null +++ b/src/ham/libham/ham_groupdel.cpp @@ -0,0 +1,29 @@ +// Host Account Management +#include "./usr/include/ham/ham.h" +#include "dbus-proxy.h" +#include "../shared/dbus-address.h" /* DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE */ + + +#include + +bool ham_groupdel(const char * group) +{ + bool success = false; + try + { + accounts_proxy_c acct(get_dbusconn(), DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE); + + ::DBus::Struct ret; + ret = acct.groupdel(group); + success = ret._1; + if (!success) + syslog(LOG_ERR, "ham_groupdel(group=\"%s\") - Error %s\n", group, ret._2.c_str()); + } + catch (DBus::Error & ex) + { + syslog(LOG_CRIT, "ham_groupdel(group=\"%s\") - Exception %s\n", group, ex.what()); + } + + return success; +} + diff --git a/src/ham/libham/ham_useradd.cpp b/src/ham/libham/ham_useradd.cpp new file mode 100644 index 00000000000..a20cb22ec9d --- /dev/null +++ b/src/ham/libham/ham_useradd.cpp @@ -0,0 +1,33 @@ +// Host Account Management +#include "./usr/include/ham/ham.h" +#include "dbus-proxy.h" +#include "../shared/dbus-address.h" /* DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE */ +#include "../shared/utils.h" /* split_any() */ + +#include + +bool ham_useradd(const char * login, const char * roles_p, const char * hashed_pw) +{ + bool success = false; + try + { + accounts_proxy_c acct(get_dbusconn(), DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE); + + std::vector< std::string > roles = split_any(roles_p, ", \t"); + + ::DBus::Struct ret; + + ret = acct.useradd(login, roles, hashed_pw); + success = ret._1; + if (!success) + syslog(LOG_ERR, "ham_useradd(login=\"%s\") - Error %s\n", login, ret._2.c_str()); + } + catch (DBus::Error & ex) + { + syslog(LOG_CRIT, "ham_useradd(login=\"%s\", roles_p=\"%s\" - Exception %s\n", login, roles_p, ex.what()); + } + + return success; +} + + diff --git a/src/ham/libham/ham_userdel.cpp b/src/ham/libham/ham_userdel.cpp new file mode 100644 index 00000000000..7e875cd34eb --- /dev/null +++ b/src/ham/libham/ham_userdel.cpp @@ -0,0 +1,30 @@ +// Host Account Management +#include "./usr/include/ham/ham.h" +#include "dbus-proxy.h" +#include "../shared/dbus-address.h" /* DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE */ + + +#include + +bool ham_userdel(const char * login) +{ + bool success = false; + try + { + accounts_proxy_c acct(get_dbusconn(), DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE); + + ::DBus::Struct ret; + ret = acct.userdel(login); + success = ret._1; + if (!success) + syslog(LOG_ERR, "ham_userdel(login=\"%s\") - Error %s\n", login, ret._2.c_str()); + } + catch (DBus::Error & ex) + { + syslog(LOG_CRIT, "ham_userdel(login=\"%s\" - Exception %s\n", login, ex.what()); + } + + return success; +} + + diff --git a/src/ham/libham/sac_user_confirm.cpp b/src/ham/libham/sac_user_confirm.cpp new file mode 100644 index 00000000000..a02778662cc --- /dev/null +++ b/src/ham/libham/sac_user_confirm.cpp @@ -0,0 +1,27 @@ +// System-Assigned Credentials (sac) +#include "./usr/include/ham/ham.h" +#include "dbus-proxy.h" +#include "../shared/dbus-address.h" /* DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE */ +#include "../shared/utils.h" /* split_any() */ + +#include + +bool sac_user_confirm(const char * login_p, const char * roles_p, const char * auth_method_p) +{ + bool success = false; + try + { + sac_proxy_c sac(get_dbusconn(), DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE); + + std::string errmsg = sac.user_confirm(login_p, split_any(roles_p, ", \t"), auth_method_p); + success = errmsg.empty(); // empty error message means success + if (!success) + syslog(LOG_ERR, "SAC - user_confirm() - User \"%s\": Error! %s", login_p, errmsg.c_str()); + } + catch (DBus::Error & ex) + { + syslog(LOG_ERR, "SAC - user_confirm() - User \"%s\": DBus Exception %s", login_p, ex.what()); + } + + return success; +} diff --git a/src/ham/libham/usr/include/ham/ham.h b/src/ham/libham/usr/include/ham/ham.h new file mode 100644 index 00000000000..2452b13cc43 --- /dev/null +++ b/src/ham/libham/usr/include/ham/ham.h @@ -0,0 +1,74 @@ +// Host Account Management +#ifndef __HAM_LIB_H +#define __HAM_LIB_H + +#include /* bool */ +#include /* gid_t, struct group */ +#include /* uid_t, struct passwd */ + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Add user to the host's /etc/passwd, /etc/group, /etc/shadow DBs. + * + * @param login Login name + * @param role List of comma-separated roles. E.g. + * "secadmin,netoperator". + * @param hashed_pw Hashed password. Must follow useradd's --password + * syntax. + * + * @return true on success, false otherwise. + */ +bool ham_useradd(const char * login, const char * roles, const char * hashed_pw); + +/** + * @brief Delete user from the host's /etc/passwd, /etc/group, /etc/shadow + * DBs. + * + * @param login Login name + * + * @return true on success, false otherwise. + */ +bool ham_userdel(const char * login); + +/** + * @brief Add a new group to the host's /etc/group DB. + * + * @param group Group name + * @param options + * + * @return true on success, false otherwise. + */ +bool ham_groupadd(const char * group); + +/** + * @brief Remove a group from the host's /etc/group DB. + * + * @param group Group name + * + * @return true on success, false otherwise. + */ +bool ham_groupdel(const char * group); + +/** + * @brief This is to be invoked by the PAM RADIUS and/or TACACS+ modules + * after a remote user has been authenticated. This tells the system + * that the System-assigned credentials (SAC) can be persisted in + * the local DB (/etc/passwd). + * + * @param login_p User's login name + * @param roles_p List of comma-separated roles for that user. + * @param auth_method_p Authentication method. E.g. "tacacs+", "radius". + * + * @return bool true if successful, false otherwise. + */ +bool sac_user_confirm(const char * login_p, const char * roles_p, const char * auth_method_p); + +#ifdef __cplusplus +} +#endif + +#endif // __HAM_LIB_H + diff --git a/src/ham/libnss_ham/Makefile b/src/ham/libnss_ham/Makefile new file mode 100644 index 00000000000..25fb07eb21d --- /dev/null +++ b/src/ham/libnss_ham/Makefile @@ -0,0 +1,73 @@ +# Prerequisite: sudo apt-get install libsystemd-dev libdbus-c++-dev +TARGET := libnss_ham.so.2 +SRCS := $(wildcard *.cpp) + +PKGS := dbus-c++-glib-1 +LDFLAGS := -shared -Wl,-soname,${TARGET} +LDLIBS := $(shell pkg-config --libs ${PKGS}) +CPPFLAGS := $(shell pkg-config --cflags ${PKGS}) +CFLAGS := -fPIC -Wall -Werror -O3 +CXXFLAGS := -std=c++11 ${CFLAGS} +LL := g++ + +DBUS-GLUE := $(patsubst %.xml,%.dbus-proxy.h,$(wildcard ../shared/*.xml)) + +OBJS := $(patsubst %.cpp,%.o,$(filter %.cpp,${SRCS})) +DEPS := $(OBJS:.o=.d) + +ifeq (,$(strip $(filter $(MAKECMDGOALS),clean install uninstall package))) + ifneq (,$(strip ${DEPS})) + ${DEPS}: ${DBUS-GLUE} + -include ${DEPS} + endif +endif + +ifeq (,$(strip $(filter $(MAKECMDGOALS),clean install uninstall))) + +# ******************************************************************* +# Make all +.DEFAULT_GOAL := all +all: ${TARGET} + +${TARGET}: ${OBJS} Makefile + @printf "%b[1;36m%s%b[0m\n" "\0033" "Building $@" "\0033" + $(LL) ${LDFLAGS} -o $@ ${OBJS} $(LDLIBS) + @printf "%b[1;32m%s%b[0m\n\n" "\0033" "$@ Done!" "\0033" + +endif # (,$(strip $(filter $(MAKECMDGOALS),clean install uninstall))) + +# ******************************************************************* +# Implicit rules: +%.o : %.cpp + @printf "%b[1;36m%s%b[0m\n" "\0033" "Compiling: $< -> $@" "\0033" + ${CXX} ${CPPFLAGS} ${CXXFLAGS} -c $< -o $@ + @printf "\n" + +%.d : %.cpp + @printf "%b[1;36m%s%b[0m\n" "\0033" "Dependency: $< -> $@" "\0033" + ${CXX} -MM -MG -MT '$@ $(@:.d=.o)' ${CPPFLAGS} ${CXXFLAGS} -o $@ $< + @printf "\n" + +# Implicit rule to generate DBus header files from XML +../shared/%.dbus-proxy.h: ../shared/%.xml + @printf "%b[1;36m%s%b[0m\n" "\0033" "dbusxx-xml2cpp $< --proxy=$@" "\0033" + @dbusxx-xml2cpp $< --proxy=$@ + +# ******************************************************************* +# ____ _ +# / ___| | ___ __ _ _ __ +# | | | |/ _ \/ _` | '_ \ +# | |___| | __/ (_| | | | | +# \____|_|\___|\__,_|_| |_| +# +# ******************************************************************* +RM_TARGET := ${TARGET} ./*.o ./*.d ${DBUS-GLUE} + +RM_LIST = $(strip $(wildcard ${RM_TARGET})) +.PHONY: clean +clean: +ifneq (,$(RM_LIST)) + rm -rf $(RM_LIST) +endif + + diff --git a/src/ham/libnss_ham/README.md b/src/ham/libnss_ham/README.md new file mode 100644 index 00000000000..98c464e1646 --- /dev/null +++ b/src/ham/libnss_ham/README.md @@ -0,0 +1,9 @@ +# HAM Name Service Switch Module + +This Name Service Switch (NSS) module is designed for containers that needs access to user accounts +on the host. It allows applications running in containers to use standard POSIX +APIs such as `getpwnam()` and retrieve user/group account information from the +host. + +The NSS module gets installed in the container and configure in +`/etc/nsswitch.conf`. \ No newline at end of file diff --git a/src/ham/libnss_ham/SYSLOG.h b/src/ham/libnss_ham/SYSLOG.h new file mode 100644 index 00000000000..87ab544c08c --- /dev/null +++ b/src/ham/libnss_ham/SYSLOG.h @@ -0,0 +1,29 @@ +#ifndef __SYSLOG_H__ +#define __SYSLOG_H__ + +#include // fprintf() +#include // syslog() + +//#define SYSLOG_TO_STDOUT + +#ifdef SYSLOG_TO_STDOUT +# define SYSLOG(LEVEL, args...) fprintf(stdout, args) +#else +# define SYSLOG(LEVEL, args...) \ + do \ + { \ + if (log_p != nullptr) fprintf(log_p, args); \ + else syslog(LEVEL, args); \ + } while (0) +#endif + +#define SYSLOG_CONDITIONAL(condition, LEVEL, args...) \ +do \ +{ \ + if (condition) \ + { \ + SYSLOG(LEVEL, args); \ + } \ +} while (0) + +#endif // __SYSLOG_H__ diff --git a/src/ham/libnss_ham/etc/sonic/hamd/libnss_ham.conf b/src/ham/libnss_ham/etc/sonic/hamd/libnss_ham.conf new file mode 100644 index 00000000000..9e0fb86eadc --- /dev/null +++ b/src/ham/libnss_ham/etc/sonic/hamd/libnss_ham.conf @@ -0,0 +1,23 @@ +# ============================================================================== +# Host Account Management (HAM) +# This is the configuration file for the HAM NSS module, which is used to +# communicate with the HAM Daemon (hamd) running on the host and retrieve +# user credentials from the host's DB (/etc/[passwd,group,shadow]) + +# ============================================================================== +# The strategy used for HAM options in the config shipped with SONiC is to +# specify options with their default value where possible, but leave them +# commented. Uncommented options override default values. + +# ============================================================================== +# debug= +# Enable additional debug info to the syslog +# values: [yes, no] +#debug=no + +# ============================================================================== +# log= +# Log file. By default logs are sent to the syslog. To send the logs to a +# file instead of the syslog, one can specify the file with this option. +#log= + diff --git a/src/ham/libnss_ham/libnss_ham.cpp b/src/ham/libnss_ham/libnss_ham.cpp new file mode 100644 index 00000000000..da7afd52221 --- /dev/null +++ b/src/ham/libnss_ham/libnss_ham.cpp @@ -0,0 +1,1224 @@ +/** + * This is an NSS module. It allows applications running in containers to + * access the user account information on the host. A service on the host + * called "hamd" for Host Account Management Daemon is contacted over DBus + * to retrieve user account information. + * + * This code is compiled as libnss_ham.so.2. In order to use it one must + * use the keyword "ham" in the NSS configuration file (i.e. + * /etc/nsswitch.conf) as follows: + * + * /etc/nsswitch.conf + * ================== + * + * passwd: compat ham <- To support getpwnam(), getpwuid(), getpwent() + * group: compat ham <- To support getgrnam(), getgrgid(), getgrent() + * shadow: compat ham <- To support getspnam() + + * The Naming Scheme of the NSS Modules + * ==================================== + * + * The name of each function consists of various parts: + * + * _nss_[service]_[function] + * + * [service] of course corresponds to the name of the module this function + * is found in. The [function] part is derived from the interface function + * in the C library itself. If the user calls the function gethostbyname() + * and the [service] used is "files" the function + * + * _nss_files_gethostbyname_r + * + * in the module + * + * libnss_files.so.2 + * + * is used. You see, what is explained above in not the whole truth. In + * fact the NSS modules only contain reentrant versions of the lookup + * functions. I.e., if the user would call the gethostbyname_r function + * this also would end in the above function. For all user interface + * functions the C library maps this call to a call to the reentrant + * function. For reentrant functions this is trivial since the interface is + * (nearly) the same. For the non-reentrant version the library keeps + * internal buffers which are used to replace the user supplied + * buffer. + * + * I.e., the reentrant functions can have counterparts. No service module + * is forced to have functions for all databases and all kinds to access + * them. If a function is not available it is simply treated as if the + * function would return unavail (see Actions in the NSS configuration). + * + * Error codes + * =========== + * + * In case the interface function has to return an error it is important + * that the correct error code is stored in *errnop. Some return status + * values have only one associated error code, others have more. + * + * enum nss_status errno Description + * =================== ======= ========================================== + * NSS_STATUS_TRYAGAIN EAGAIN One of the functions used ran temporarily + * out of resources or a service is + * currently not available. + * + * ERANGE The provided buffer is not large enough. + * The function should be called again with a + * larger buffer. + * + * NSS_STATUS_UNAVAIL ENOENT A necessary input file cannot be found. + * + * NSS_STATUS_NOTFOUND ENOENT The requested entry is not available. + * + * NSS_STATUS_NOTFOUND SUCCESS There are no entries. Use this to avoid + * returning errors for inactive services + * which may be enabled at a later time. This + * is not the same as the service being + * temporarily unavailable. + */ + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif +#include /* NSS_STATUS_SUCCESS, NSS_STATUS_NOTFOUND */ +#include /* uid_t, struct passwd */ +#include /* gid_t, struct group */ +#include /* struct spwd */ +#include /* stat() */ +#include /* getuid(), getgid() */ +#include /* getuid(), getgid() */ +#include /* errno, program_invocation_name */ +#include /* strdup() */ +#include /* open(), O_RDONLY */ +#include /* fopen(), fclose() */ +#include /* LINE_MAX */ +#include /* uint32_t */ +#include /* syslog() */ +#include /* memfd_create() */ + +#include /* std::string */ +#include /* std::vector */ +#include /* std::accumulate() */ +#include /* std::mutex, std::lock_guard */ + +#include "../shared/utils.h" /* strneq(), startswith(), cpy2buf(), join() */ + +#include "../shared/missing-memfd_create.h" /* memfd_create() if missing from sys/mman.h */ +#include "name_service_proxy.h" /* name_service_proxy_c */ +#include "SYSLOG.h" /* SYSLOG(), SYSLOG_CONDITIONAL */ + + +//############################################################################## +// Local configuration parameters +static FILE * log_p = nullptr; // Optional log file where log messages can be saved. The default is to log to syslog. +static bool verbose = false; // For debug only. +static char * cmdline_p = program_invocation_name; // For debug only. + +static char * kv_strip(char * p) +{ + #define WHITESPACE " \t\n\r" + + p[strcspn(p, "\n\r")] = '\0'; + + p += strspn(p, WHITESPACE); // Remove leading newline and spaces + if (*p == '#' || *p == '\0') // Skip comments and empty lines + { + *p = '\0'; + return p; + } + p[strcspn(p, "\n\r")] = '\0'; // Remove trailing newline chars + + // Delete trailing comments (including spaces/tabs that precede the #) + char * s = &p[strcspn(p, "#")]; + *s-- = '\0'; + while ((s >= p) && ((*s == ' ') || (*s == '\t'))) + { + *s-- = '\0'; + } + + return p; +} + +static char * kv_keymatch(const char * s, const char * key) +{ + char * value = startswith(s, key); + if (NULL != value) + { + switch (*value) + { + case ' ': // Make sure + case '\t': // key is a whole word. + case '=': // I.e. it should be followed by spaces, tabs, or a equal sign. + value += strspn(value, " \t="); // Skip leading spaces, tabs, and equal sign (=) + return value; + default:; + } + } + + return NULL; +} + +/** + * @brief Extract cmdline from /proc/self/cmdline. This is only needed for + * debugging purposes (i.e. when verbose==true) + */ +static void read_cmdline() +{ + cmdline_p = program_invocation_name; + + const char * fn = "/proc/self/cmdline"; + struct stat st; + if (stat(fn, &st) != 0) + return; + + int fd = open(fn, O_RDONLY); + if (fd == -1) + return; + + char buffer[LINE_MAX] = {0}; + size_t sz = sizeof(buffer); + size_t n = 0; + for(;;) + { + ssize_t r = read(fd, &buffer[n], sz-n); + if (r == -1) + { + if (errno == EINTR) continue; + break; + } + n += r; + if (n == sz) break; // filled the buffer + if (r == 0) break; // EOF + } + close(fd); + + if (n) + { + if (n == sz) n--; + buffer[n] = '\0'; + size_t i = n; + while (i--) + { + int c = buffer[i]; + if ((c < ' ') || (c > '~')) buffer[i] = ' '; + } + + // Delete trailing spaces, tabs, and newline chars. + char * p = &buffer[strcspn(buffer, "\n\r") - 1]; + while ((p >= buffer) && (*p == ' ' || *p == '\t' || *p == '\0')) + { + *p-- = '\0'; + } + } + + cmdline_p = strdup(buffer); + + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "cmdline = %s\n", cmdline_p); +} + +/** + * @brief Get configuration parameters for this module. Parameters are + * retrieved from the file "etc/sonic/hamd/libnss_ham.conf" + */ +static void read_config() +{ + bool debug = false; + std::string log_file; + static std::string current_log_file; + + FILE * file = fopen("/etc/sonic/hamd/libnss_ham.conf", "re"); + if (file) + { + #define WHITESPACE " \t\n\r" + char line[LINE_MAX]; + char * p; + char * s; + + while (NULL != (p = fgets(line, sizeof line, file))) + { + // Clean up string by removing leading/trailing blanks and new line + // characters. Also eliminate trailing comments, if any. + p = kv_strip(p); + if (*p == '\0') continue; // Check that there is still something left in the string + + if (NULL != (s = kv_keymatch(p, "debug"))) + { + if (strneq(s, "yes", 3)) + debug = true; + } + else if (NULL != (s = kv_keymatch(p, "log"))) + { + if (*s != '\0') + log_file = s; + } + } + + fclose(file); + } + + if (log_file != current_log_file) + { + if (log_p != nullptr) + { + if ((log_p != stderr) && (log_p != stdout)) + fclose(log_p); + + log_p = nullptr; + } + + if (!log_file.empty()) + { + log_p = fopen(log_file.c_str(), "w"); + if (log_p != nullptr) + current_log_file = log_file; + } + } + + verbose = debug; + + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "debug = true\n"); + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "log = %s\n", log_file.c_str()); +} + +/** + * @brief Retrieve current system monotonic clock as a 64-bit count in + * nano-sec. + */ +static uint64_t get_now_nsec() +{ + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + return (now.tv_sec * 1000000000ULL) + now.tv_nsec; +} + +static DBus::Connection & get_dbusconn() +{ + static DBus::Connection * conn_p = nullptr; + if (conn_p == nullptr) + { + // DBus::BusDispatcher is a "main loop" construct that + // handles (i.e. dispatched) DBus messages. This should + // be defined as a singleton to avoid memory leaks. + static DBus::BusDispatcher dispatcher; + + // DBus::default_dispatcher must be initialized before DBus::Connection. + DBus::default_dispatcher = &dispatcher; + + static DBus::Connection conn = DBus::Connection::SystemBus(); + + conn_p = &conn; + } + + return *conn_p; +} + + +#ifdef __cplusplus +extern "C" { +#endif +/*############################################################################## + * _ _ ____ ___ + * _ __ __ _ ___ _____ ____| | / \ | _ \_ _|___ + * | '_ \ / _` / __/ __\ \ /\ / / _` | / _ \ | |_) | |/ __| + * | |_) | (_| \__ \__ \\ V V / (_| | / ___ \| __/| |\__ \ + * | .__/ \__,_|___/___/ \_/\_/ \__,_| /_/ \_\_| |___|___/ + * |_| + * + *############################################################################*/ + +/** + * @brief Fill structure pointed to by @result with the data contained in + * the structure pointed to by @ham_data_r. + * + * @details The string fields pointed to by the members of the passwd + * structure are stored in @buffer of size buflen. In case @buffer + * has insufficient memory to hold the strings of struct passwd, + * the ENOMEM error will be reported. + * + * @param fn_p Name of the calling function + * @param msec How long it took to process the request + * @param success Whether the request was successful + * @param pw_name_r Value to be saved to result->pw_name + * @param pw_passwd_r Value to be saved to result->pw_passwd + * @param pw_uid Value to be saved to result->pw_uid + * @param pw_gid Value to be saved to result->pw_gid + * @param pw_gecos_r Value to be saved to result->pw_gecos + * @param pw_dir_r Value to be saved to result->pw_dir + * @param pw_shell_r Value to be saved to result->pw_shell + * @param result Pointer to destination where data gets copied + * @param buffer Pointer to memory where strings can be stored + * @param buflen Size of buffer + * @param errnop Pointer to where errno code can be written + * + * @return - If no entry was found, return NSS_STATUS_NOTFOUND and set + * errno=ENOENT. + * - If @buffer has insufficient memory to hold the strings of + * struct passwd then return NSS_STATUS_TRYAGAIN and set + * errno=ENOMEM. + * - Otherwise return NSS_STATUS_SUCCESS and set errno to 0. + */ +static enum nss_status pw_fill_result(const char * fn_p, + double msec, + bool success, + const std::string & pw_name_r, + const std::string & pw_passwd_r, + uid_t pw_uid, + gid_t pw_gid, + const std::string & pw_gecos_r, + const std::string & pw_dir_r, + const std::string & pw_shell_r, + struct passwd * result, + char * buffer, + size_t buflen, + int * errnop) +{ + if (!success) + { + SYSLOG(LOG_ERR, "%s() - [%u:%u] cmdline=\"%s\" exec time=%.3f ms. Failed!\n", fn_p, getuid(), getgid(), cmdline_p, msec); + *errnop = ENOENT; + return NSS_STATUS_NOTFOUND; + } + + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "%s() - [%u:%u] cmdline=\"%s\" exec time=%.3f ms. Success! pw_name=\"%s\", pw_passwd=\"%s\", pw_uid=%u, pw_gid=%u, pw_gecos=\"%s\", pw_dir=%s, pw_shell=\"%s\"\n", + fn_p, getuid(), getgid(), cmdline_p, msec, pw_name_r.c_str(), pw_passwd_r.c_str(), pw_uid, pw_gid, pw_gecos_r.c_str(), pw_dir_r.c_str(), pw_shell_r.c_str()); + + size_t name_l = pw_name_r.length() + 1; /* +1 to include NUL terminating char */ + size_t dir_l = pw_dir_r.length() + 1; + size_t shell_l = pw_shell_r.length() + 1; + size_t passwd_l = pw_passwd_r.length() + 1; + size_t gecos_l = pw_gecos_r.length() + 1; + if (buflen < (name_l + shell_l + dir_l + passwd_l + gecos_l)) + { + SYSLOG(LOG_ERR, "%s() - [%u:%u] cmdline=\"%s\" not enough memory for struct passwd data\n", fn_p, getuid(), getgid(), cmdline_p); + + if (errnop) *errnop = ENOMEM; + return NSS_STATUS_TRYAGAIN; + } + + result->pw_uid = pw_uid; + result->pw_gid = pw_gid; + result->pw_name = buffer; + result->pw_dir = cpy2buf(result->pw_name, pw_name_r.c_str(), name_l); + result->pw_shell = cpy2buf(result->pw_dir, pw_dir_r.c_str(), dir_l); + result->pw_passwd = cpy2buf(result->pw_shell, pw_shell_r.c_str(), shell_l); + result->pw_gecos = cpy2buf(result->pw_passwd, pw_passwd_r.c_str(), passwd_l); + cpy2buf(result->pw_gecos, pw_gecos_r.c_str(), gecos_l); + + if (errnop) *errnop = 0; + + return NSS_STATUS_SUCCESS; +} + +/** + * @brief Retrieve passwd info from Host Account Management daemon (hamd). + * + * @param name User name. + * @param result Where to write the result + * @param buffer Buffer used as a temporary pool where we can save + * strings. + * @param buflen Size of memory pointed to by buffer + * @param errnop Where to return the errno + * + * @return NSS_STATUS_SUCCESS, NSS_STATUS_NOTFOUND, or NSS_STATUS_TRYAGAIN. + */ +enum nss_status _nss_ham_getpwnam_r(const char * name, + struct passwd * result, + char * buffer, + size_t buflen, + int * errnop) +{ + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "_nss_ham_getpwnam_r() - [%u:%u] cmdline=\"%s\" name=\"%s\"\n", + getuid(), getgid(), cmdline_p, name); + + uint64_t before_nsec = verbose ? get_now_nsec() : 0ULL; + + ::DBus::Struct < + bool, /* _1: success */ + std::string, /* _2: pw_name */ + std::string, /* _3: pw_passwd */ + uint32_t, /* _4: pw_uid */ + uint32_t, /* _5: pw_gid */ + std::string, /* _6: pw_gecos */ + std::string, /* _7: pw_dir */ + std::string /* _8: pw_shell */ + > ham_data; + + try + { + name_service_proxy_c ns(get_dbusconn(), DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE); + ham_data = ns.getpwnam(name); + } + catch (DBus::Error & ex) + { + SYSLOG(LOG_CRIT, "_nss_ham_getpwnam_r() - [%u:%u] cmdline=\"%s\" Exception %s\n", getuid(), getgid(), cmdline_p, ex.what()); + *errnop = ENOENT; + return NSS_STATUS_NOTFOUND; + } + + double duration_msec = verbose ? (get_now_nsec() - before_nsec)/1000000.0 : 0.0; + + return pw_fill_result("_nss_ham_getpwnam_r", + duration_msec, + ham_data._1, + ham_data._2, + ham_data._3, + ham_data._4, + ham_data._5, + ham_data._6, + ham_data._7, + ham_data._8, + result, + buffer, + buflen, + errnop); +} + +/** + * @brief Retrieve passwd info from Host Account Management daemon (hamd). + * + * @param uid User ID. + * @param result Where to write the result + * @param buffer Buffer used as a temporary pool where we can save + * strings. + * @param buflen Size of memory pointed to by buffer + * @param errnop Where to return the errno + * + * @return NSS_STATUS_SUCCESS, NSS_STATUS_NOTFOUND, or NSS_STATUS_TRYAGAIN. + */ +enum nss_status _nss_ham_getpwuid_r(uid_t uid, + struct passwd * result, + char * buffer, + size_t buflen, + int * errnop) +{ + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "_nss_ham_getpwuid_r() - [%u:%u] cmdline=\"%s\" uid=%u\n", + getuid(), getgid(), cmdline_p, uid); + + uint64_t before_nsec = verbose ? get_now_nsec() : 0ULL; + + ::DBus::Struct < + bool, /* _1: success */ + std::string, /* _2: pw_name */ + std::string, /* _3: pw_passwd */ + uint32_t, /* _4: pw_uid */ + uint32_t, /* _5: pw_gid */ + std::string, /* _6: pw_gecos */ + std::string, /* _7: pw_dir */ + std::string /* _8: pw_shell */ + > ham_data; + + try + { + name_service_proxy_c ns(get_dbusconn(), DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE); + ham_data = ns.getpwuid(uid); + } + catch (DBus::Error & ex) + { + SYSLOG(LOG_CRIT, "_nss_ham_getpwuid_r() - [%u:%u] cmdline=\"%s\" Exception %s\n", getuid(), getgid(), cmdline_p, ex.what()); + *errnop = ENOENT; + return NSS_STATUS_NOTFOUND; + } + + double duration_msec = verbose ? (get_now_nsec() - before_nsec)/1000000.0 : 0.0; + + return pw_fill_result("_nss_ham_getpwuid_r", + duration_msec, + ham_data._1, + ham_data._2, + ham_data._3, + ham_data._4, + ham_data._5, + ham_data._6, + ham_data._7, + ham_data._8, + result, + buffer, + buflen, + errnop); +} + +/** + * @brief This function prepares the service for following operations (i.e. + * _nss_ham_endpwent and _nss_ham_getpwent_r). This function + * contacts the hamd service to retrieve the contents of the + * /etc/passwd file on the host and caches the contents to a local + * memory file. + * + * @return The return value should be NSS_STATUS_SUCCESS or according to + * the table above in case of an error (see NSS Modules Interface: + * https://www.gnu.org/software/libc/manual/html_node/NSS-Modules-Interface.html#NSS-Modules-Interface). + */ +static FILE * passwd_fp = nullptr; +static std::mutex passwd_mtx; // protects passwd_fp +enum nss_status _nss_ham_setpwent(void) +{ + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "_nss_ham_setpwent() - [%u:%u] cmdline=\"%s\"\n", getuid(), getgid(), cmdline_p); + + uint64_t before_nsec = verbose ? get_now_nsec() : 0ULL; + + const std::lock_guard lock(passwd_mtx); + if (passwd_fp == nullptr) + { + std::string contents; + + try + { + name_service_proxy_c ns(get_dbusconn(), DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE); + contents = ns.getpwcontents(); + } + catch (DBus::Error & ex) + { + SYSLOG(LOG_CRIT, "_nss_ham_setpwent() - [%u:%u] cmdline=\"%s\" Exception %s\n", getuid(), getgid(), cmdline_p, ex.what()); + return NSS_STATUS_TRYAGAIN; + } + + int fd = memfd_create("passwd", MFD_CLOEXEC); + if (fd == -1) + { + SYSLOG(LOG_ERR, "_nss_ham_setpwent() - [%u:%u] cmdline=\"%s\" Failed to create passwd cache file. errno=%d (%s)\n", + getuid(), getgid(), cmdline_p, errno, strerror(errno)); + return NSS_STATUS_TRYAGAIN; + } + + // Convert File Descriptor to a "FILE *". This is needed for fgetpwent() + passwd_fp = fdopen(fd, "w+"); + if (passwd_fp == nullptr) + { + SYSLOG(LOG_ERR, "_nss_ham_setpwent() - [%u:%u] cmdline=\"%s\" fdopen() failed. errno=%d (%s)\n", + getuid(), getgid(), cmdline_p, errno, strerror(errno)); + return NSS_STATUS_TRYAGAIN; + } + + fwrite(contents.c_str(), contents.length(), 1, passwd_fp); + } + + rewind(passwd_fp); + + double duration_msec = verbose ? (get_now_nsec() - before_nsec)/1000000.0 : 0.0; + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "_nss_ham_setpwent() - [%u:%u] cmdline=\"%s\" exec time=%.3f ms. Success!\n", getuid(), getgid(), cmdline_p, duration_msec); + + return NSS_STATUS_SUCCESS; +} + +/** + * @brief This function indicates that the caller is done with the cached + * contents of the host's /etc/passwd. + * + * @return There normally is no return value other than NSS_STATUS_SUCCESS. + */ +enum nss_status _nss_ham_endpwent(void) +{ + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "_nss_ham_endpwent() - [%u:%u] cmdline=\"%s\"\n", getuid(), getgid(), cmdline_p); + + const std::lock_guard lock(passwd_mtx); + + if (passwd_fp != nullptr) + { + fclose(passwd_fp); + passwd_fp = nullptr; + } + + return NSS_STATUS_SUCCESS; +} + +/** + * @brief Since this function will be called several times in a row to + * retrieve one entry after the other it must keep some kind of + * state. But this also means the functions are not really + * reentrant. They are reentrant only in that simultaneous calls to + * this function will not try to write the retrieved data in the + * same place; instead, it writes to the structure pointed to by the + * @result parameter. But the calls share a common state and in the + * case of a file access this means they return neighboring entries + * in the file. + * + * The buffer of length @buflen pointed to by @buffer can be used + * for storing some additional data for the result. It is not + * guaranteed that the same buffer will be passed for the next call + * of this function. Therefore one must not misuse this buffer to + * save some state information from one call to another. + * + * Before the function returns with a failure code, the + * implementation should store the value of the local errno variable + * in the variable pointed to be @errnop. This is important to + * guarantee the module working in statically linked programs. The + * stored value must not be zero. + * + * @param result Where to save result + * @param buffer Memory used to store additional data (e.g. strings) that + * cannot fit in result. + * @param buflen Size of @buffer + * @param errnop Where to save the errno code. + * + * @return The function shall return NSS_STATUS_SUCCESS as long as there + * are more entries. When the last entry was read it should return + * NSS_STATUS_NOTFOUND. When the buffer given as an argument is too + * small for the data to be returned NSS_STATUS_TRYAGAIN should be + * returned. When the service was not formerly initialized by a + * call to @_nss_ham_setpwent() all return values allowed for this + * function can also be returned here. + */ +enum nss_status _nss_ham_getpwent_r(struct passwd * result, char * buffer, size_t buflen, int * errnop) +{ + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "_nss_ham_getpwent_r() - [%u:%u] cmdline=\"%s\"\n", getuid(), getgid(), cmdline_p); + + struct passwd * ent = nullptr; + uint64_t before_nsec = verbose ? get_now_nsec() : 0ULL; + + do { // protected section begin + const std::lock_guard lock(passwd_mtx); + + if (passwd_fp == nullptr) + { + SYSLOG(LOG_ERR, "_nss_ham_getpwent_r() - [%u:%u] cmdline=\"%s\" passwd_fp=NULL\n", getuid(), getgid(), cmdline_p); + if (errnop != nullptr) *errnop = EIO; + return NSS_STATUS_TRYAGAIN; + } + + if (NULL == (ent = fgetpwent(passwd_fp))) + return NSS_STATUS_NOTFOUND; // no more entries + } while(0); // protected section end + + double duration_msec = verbose ? (get_now_nsec() - before_nsec)/1000000.0 : 0.0; + + return pw_fill_result("_nss_ham_getpwent_r", + duration_msec, + true, + ent->pw_name, + ent->pw_passwd, + ent->pw_uid, + ent->pw_gid, + ent->pw_gecos, + ent->pw_dir, + ent->pw_shell, + result, + buffer, + buflen, + errnop); +} + +/*############################################################################## + * _ ____ ___ + * __ _ _ __ ___ _ _ _ __ / \ | _ \_ _|___ + * / _` | '__/ _ \| | | | '_ \ / _ \ | |_) | |/ __| + * | (_| | | | (_) | |_| | |_) | / ___ \| __/| |\__ \ + * \__, |_| \___/ \__,_| .__/ /_/ \_\_| |___|___/ + * |___/ |_| + * + *############################################################################*/ + +/** + * @brief Fill structure pointed to by @result with the data contained in + * the structure pointed to by @ham_data_r. + * + * @details The string fields pointed to by the members of the group + * structure are stored in @buffer of size buflen. In case @buffer + * has insufficient memory to hold the strings of struct group, + * the ENOMEM error will be reported. + * + * @param fn_p Name of the calling function + * @param msec How long it took to process the request + * @param success Whether the request was successful + * @param gr_name_r Value to be saved to result->gr_name + * @param gr_passwd_r Value to be saved to result->gr_passwd + * @param gr_gid Value to be saved to result->gr_gid + * @param gr_mem_r Value to be saved to result->gr_mem + * @param result Pointer to destination where data gets copied + * @param buffer Pointer to memory where strings can be stored + * @param buflen Size of buffer + * @param errnop Pointer to where errno code can be written + * + * @return - If no entry was found, return NSS_STATUS_NOTFOUND and set + * errno=ENOENT. + * - If @buffer has insufficient memory to hold the strings of + * struct passwd then return NSS_STATUS_TRYAGAIN and set + * errno=ENOMEM. + * - Otherwise return NSS_STATUS_SUCCESS and set errno to 0. + */ +static enum nss_status gr_fill_result(const char * fn_p, + double msec, + bool success, + const std::string & gr_name_r, + const std::string & gr_passwd_r, + gid_t gr_gid, + const std::vector< std::string > & gr_mem_r, + struct group * result, + char * buffer, + size_t buflen, + int * errnop) +{ + if (!success) + { + SYSLOG(LOG_ERR, "%s() - [%u:%u] cmdline=\"%s\" exec time=%.3f ms. Failed!\n", fn_p, getuid(), getgid(), cmdline_p, msec); + *errnop = ENOENT; + return NSS_STATUS_NOTFOUND; + } + + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "%s() - [%u:%u] cmdline=\"%s\" exec time=%.3f ms. Success! gr_name=\"%s\", pw_passwd=\"%s\", gr_gid=%u, pw_mem=[\"%s\"]\n", + fn_p, getuid(), getgid(), cmdline_p, msec, gr_name_r.c_str(), gr_passwd_r.c_str(), gr_gid, join(gr_mem_r.begin(), gr_mem_r.end(), "\", \"").c_str()); + + size_t name_l = gr_name_r.length() + 1; /* +1 to include NUL terminating char */ + size_t passwd_l = gr_passwd_r.length() + 1; + size_t array_l = sizeof(char *) * (gr_mem_r.size() + 1); /* +1 for a nullptr sentinel */ + size_t strings_l = std::accumulate(gr_mem_r.begin(), gr_mem_r.end(), 0, [](size_t sum, const std::string& elem) {return sum + elem.length() + 1;}); /* +1 to include NUL terminating char */ + if (buflen < (name_l + passwd_l + array_l + strings_l)) + { + SYSLOG(LOG_ERR, "%s() - [%u:%u] cmdline=\"%s\" not enough memory for struct group data\n", fn_p, getuid(), getgid(), cmdline_p); + + if (errnop) *errnop = ENOMEM; + return NSS_STATUS_TRYAGAIN; + } + + result->gr_gid = gr_gid; + result->gr_mem = (char **)buffer; + result->gr_mem[0] = buffer + array_l; + for (unsigned i = 0; i < gr_mem_r.size(); i++) + result->gr_mem[i+1] = cpy2buf(result->gr_mem[i], gr_mem_r[i].c_str(), gr_mem_r[i].length() + 1); + + result->gr_name = result->gr_mem[gr_mem_r.size()]; + result->gr_mem[gr_mem_r.size()] = nullptr; // sentinel + + result->gr_passwd = cpy2buf(result->gr_name, gr_name_r.c_str(), name_l); + cpy2buf(result->gr_passwd, gr_passwd_r.c_str(), passwd_l); + + if (errnop) *errnop = 0; + + return NSS_STATUS_SUCCESS; +} + +/** + * @brief Retrieve group info from Host Account Management daemon (hamd). + * + * @param name Group name. + * @param result Where to write the result + * @param buffer Buffer used as a temporary pool where we can save + * strings. + * @param buflen Size of memory pointed to by buffer + * @param errnop Where to return the errno + * + * @return NSS_STATUS_SUCCESS, NSS_STATUS_NOTFOUND, or NSS_STATUS_TRYAGAIN. + */ +enum nss_status _nss_ham_getgrnam_r(const char * name, + struct group * result, + char * buffer, + size_t buflen, + int * errnop) +{ + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "_nss_ham_getgrnam_r() - [%u:%u] cmdline=\"%s\" name=\"%s\"\n", + getuid(), getgid(), cmdline_p, name); + + uint64_t before_nsec = verbose ? get_now_nsec() : 0ULL; + + ::DBus::Struct < + bool, /* _1: success */ + std::string, /* _2: gr_name */ + std::string, /* _3: gr_passwd */ + uint32_t, /* _4: gr_gid */ + std::vector< std::string > /* _5: gr_mem */ + > ham_data; + + try + { + name_service_proxy_c ns(get_dbusconn(), DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE); + ham_data = ns.getgrnam(name); + } + catch (DBus::Error & ex) + { + SYSLOG(LOG_CRIT, "_nss_ham_getgrnam_r() - [%u:%u] cmdline=\"%s\" Exception %s\n", + getuid(), getgid(), cmdline_p, ex.what()); + *errnop = ENOENT; + return NSS_STATUS_NOTFOUND; + } + + double duration_msec = verbose ? (get_now_nsec() - before_nsec)/1000000.0 : 0.0; + + return gr_fill_result("_nss_ham_getgrnam_r", + duration_msec, + ham_data._1, /* success */ + ham_data._2, /* gr_name */ + ham_data._3, /* gr_passwd */ + ham_data._4, /* gr_gid */ + ham_data._5, /* gr_mem */ + result, + buffer, + buflen, + errnop); +} + +/** + * @brief Retrieve group info from Host Account Management daemon (hamd). + * + * @param gid Group ID. + * @param result Where to write the result + * @param buffer Buffer used as a temporary pool where we can save + * strings. + * @param buflen Size of memory pointed to by buffer + * @param errnop Where to return the errno + * + * @return NSS_STATUS_SUCCESS, NSS_STATUS_NOTFOUND, or NSS_STATUS_TRYAGAIN. + */ +enum nss_status _nss_ham_getgrgid_r(gid_t gid, + struct group * result, + char * buffer, + size_t buflen, + int * errnop) +{ + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "_nss_ham_getgrgid_r() - [%u:%u] cmdline=\"%s\" gid=%u\n", + getuid(), getgid(), cmdline_p, gid); + + uint64_t before_nsec = verbose ? get_now_nsec() : 0ULL; + + ::DBus::Struct < + bool, /* _1: success */ + std::string, /* _2: gr_name */ + std::string, /* _3: gr_passwd */ + uint32_t, /* _4: gr_gid */ + std::vector< std::string > /* _5: gr_mem */ + > ham_data; + + try + { + name_service_proxy_c ns(get_dbusconn(), DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE); + ham_data = ns.getgrgid(gid); + } + catch (DBus::Error & ex) + { + SYSLOG(LOG_CRIT, "_nss_ham_getgrgid_r() - [%u:%u] cmdline=\"%s\" Exception %s\n", getuid(), getgid(), cmdline_p, ex.what()); + *errnop = ENOENT; + return NSS_STATUS_NOTFOUND; + } + + double duration_msec = verbose ? (get_now_nsec() - before_nsec)/1000000.0 : 0.0; + + return gr_fill_result("_nss_ham_getgrgid_r", + duration_msec, + ham_data._1, /* success */ + ham_data._2, /* gr_name */ + ham_data._3, /* gr_passwd */ + ham_data._4, /* gr_gid */ + ham_data._5, /* gr_mem */ + result, + buffer, + buflen, + errnop); +} + +/** + * @brief This function prepares the service for following operations (i.e. + * _nss_ham_endgrent and _nss_ham_getgrent_r). This function + * contacts the hamd service to retrieve the contents of the + * /etc/group file on the host and caches the contents to a local + * memory file. + * + * @return The return value should be NSS_STATUS_SUCCESS or according to + * the table above in case of an error (see NSS Modules Interface: + * https://www.gnu.org/software/libc/manual/html_node/NSS-Modules-Interface.html#NSS-Modules-Interface). + */ +static FILE * group_fp = nullptr; +static std::mutex group_mtx; // protects group_fp +enum nss_status _nss_ham_setgrent(void) +{ + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "_nss_ham_setgrent() - [%u:%u] cmdline=\"%s\"\n", getuid(), getgid(), cmdline_p); + + uint64_t before_nsec = verbose ? get_now_nsec() : 0ULL; + + const std::lock_guard lock(group_mtx); + if (group_fp == nullptr) + { + std::string contents; + + try + { + name_service_proxy_c ns(get_dbusconn(), DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE); + contents = ns.getgrcontents(); + } + catch (DBus::Error & ex) + { + SYSLOG(LOG_CRIT, "_nss_ham_setgrent() - [%u:%u] cmdline=\"%s\" Exception %s\n", getuid(), getgid(), cmdline_p, ex.what()); + return NSS_STATUS_TRYAGAIN; + } + + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "_nss_ham_setgrent() - [%u:%u] cmdline=\"%s\" received %lu bytes from hamd's getgrcontents()\n", getuid(), getgid(), cmdline_p, contents.size()); + + int fd = memfd_create("group", MFD_CLOEXEC); + if (fd == -1) + { + SYSLOG(LOG_ERR, "_nss_ham_setgrent() - [%u:%u] cmdline=\"%s\" Failed to create passwd cache file. errno=%d (%s)\n", + getuid(), getgid(), cmdline_p, errno, strerror(errno)); + return NSS_STATUS_TRYAGAIN; + } + + // Convert File Descriptor to a "FILE *". This is needed for fgetpwent() + group_fp = fdopen(fd, "w+"); + if (group_fp == nullptr) + { + SYSLOG(LOG_ERR, "_nss_ham_setgrent() - [%u:%u] cmdline=\"%s\" fdopen() failed. errno=%d (%s)\n", + getuid(), getgid(), cmdline_p, errno, strerror(errno)); + return NSS_STATUS_TRYAGAIN; + } + + if (fwrite(contents.c_str(), contents.length(), 1, group_fp) < 1) + { + SYSLOG(LOG_ERR, "_nss_ham_setgrent() - [%u:%u] cmdline=\"%s\" Failed to write to temporary file. errno=%d (%s)\n", + getuid(), getgid(), cmdline_p, errno, strerror(errno)); + } + } + + rewind(group_fp); + + double duration_msec = verbose ? (get_now_nsec() - before_nsec)/1000000.0 : 0.0; + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "_nss_ham_setgrent() - [%u:%u] cmdline=\"%s\" exec time=%.3f ms. Success!\n", getuid(), getgid(), cmdline_p, duration_msec); + + return NSS_STATUS_SUCCESS; +} + +/** + * @brief This function indicates that the caller is done with the cached + * contents of the host's /etc/group. + * + * @return There normally is no return value other than NSS_STATUS_SUCCESS. + */ +enum nss_status _nss_ham_endgrent(void) +{ + + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "_nss_ham_endgrent() - [%u:%u] cmdline=\"%s\"\n", getuid(), getgid(), cmdline_p); + + const std::lock_guard lock(group_mtx); + + if (group_fp != nullptr) + { + fclose(group_fp); + group_fp = nullptr; + } + return NSS_STATUS_SUCCESS; +} + +/** + * @brief Since this function will be called several times in a row to + * retrieve one entry after the other it must keep some kind of + * state. But this also means the functions are not really + * reentrant. They are reentrant only in that simultaneous calls to + * this function will not try to write the retrieved data in the + * same place; instead, it writes to the structure pointed to by the + * @result parameter. But the calls share a common state and in the + * case of a file access this means they return neighboring entries + * in the file. + * + * The buffer of length @buflen pointed to by @buffer can be used + * for storing some additional data for the result. It is not + * guaranteed that the same buffer will be passed for the next call + * of this function. Therefore one must not misuse this buffer to + * save some state information from one call to another. + * + * Before the function returns with a failure code, the + * implementation should store the value of the local errno variable + * in the variable pointed to be @errnop. This is important to + * guarantee the module working in statically linked programs. The + * stored value must not be zero. + * + * @param result Where to save result + * @param buffer Memory used to store additional data (e.g. strings) that + * cannot fit in result. + * @param buflen Size of @buffer + * @param errnop Where to save the errno code. + * + * @return The function shall return NSS_STATUS_SUCCESS as long as there + * are more entries. When the last entry was read it should return + * NSS_STATUS_NOTFOUND. When the buffer given as an argument is too + * small for the data to be returned NSS_STATUS_TRYAGAIN should be + * returned. When the service was not formerly initialized by a + * call to @_nss_ham_setgrent() all return values allowed for this + * function can also be returned here. + */ +enum nss_status _nss_ham_getgrent_r(struct group * result, char * buffer, size_t buflen, int * errnop) +{ + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "_nss_ham_getgrent() - [%u:%u] cmdline=\"%s\"\n", getuid(), getgid(), cmdline_p); + + struct group * ent = nullptr; + uint64_t before_nsec = verbose ? get_now_nsec() : 0ULL; + + do { // protected section begin + const std::lock_guard lock(group_mtx); + + if (group_fp == nullptr) + { + SYSLOG(LOG_ERR, "_nss_ham_getgrent_r() - [%u:%u] cmdline=\"%s\" group_fp=NULL\n", getuid(), getgid(), cmdline_p); + if (errnop != nullptr) *errnop = EIO; + return NSS_STATUS_TRYAGAIN; + } + + if (NULL == (ent = fgetgrent(group_fp))) + { + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "_nss_ham_getgrent() - [%u:%u] cmdline=\"%s\" reached end of entries\n", getuid(), getgid(), cmdline_p); + return NSS_STATUS_NOTFOUND; // no more entries + } + } while(0); // protected section end + + double duration_msec = verbose ? (get_now_nsec() - before_nsec)/1000000.0 : 0.0; + + std::vector < std::string > gr_mem; + for (unsigned i = 0; ent->gr_mem[i] != NULL; i++) + gr_mem.push_back(ent->gr_mem[i]); + + return gr_fill_result("_nss_ham_getgrent_r", + duration_msec, + true, + ent->gr_name, + ent->gr_passwd, + ent->gr_gid, + gr_mem, + result, + buffer, + buflen, + errnop); +} + +/*############################################################################## + * _ _ _ ____ ___ + * ___| |__ __ _ __| | _____ __ / \ | _ \_ _|___ + * / __| '_ \ / _` |/ _` |/ _ \ \ /\ / / / _ \ | |_) | |/ __| + * \__ \ | | | (_| | (_| | (_) \ V V / / ___ \| __/| |\__ \ + * |___/_| |_|\__,_|\__,_|\___/ \_/\_/ /_/ \_\_| |___|___/ + * + *############################################################################*/ + +/** + * @brief Invoke Host Account Management Daemon (hamd) over DBus to + * retrieve shadow password information for user @name. Upon receipt + * of hamd data, fill structure pointed to by @result. + * + * @details The string fields pointed to by the members of the spwd + * structure are stored in @buffer of size buflen. In case @buffer + * has insufficient memory to hold the strings of struct spwd, + * the ENOMEM error will be reported. + * + * @param name User name + * @param result Pointer to destination where data gets copied + * @param buffer Pointer to memory where strings can be stored + * @param buflen Size of buffer + * @param errnop Pointer to where errno code can be written + * + * @return - If no entry was found, return NSS_STATUS_NOTFOUND and set + * errno=ENOENT. + * - If @buffer has insufficient memory to hold the strings of + * struct passwd then return NSS_STATUS_TRYAGAIN and set + * errno=ENOMEM. + * - Otherwise return NSS_STATUS_SUCCESS and set errno to 0. + */ +enum nss_status _nss_ham_getspnam_r(const char * name, + struct spwd * result, + char * buffer, + size_t buflen, + int * errnop) +{ + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "_nss_ham_getspnam_r() - [%u:%u] cmdline=\"%s\" name=\"%s\"\n", + getuid(), getgid(), cmdline_p, name); + + uint64_t before_nsec = verbose ? get_now_nsec() : 0ULL; + + ::DBus::Struct < + bool, /* _1: success */ + std::string, /* _2: sp_namp */ + std::string, /* _3: sp_pwdp */ + int32_t, /* _4: sp_lstchg */ + int32_t, /* _5: sp_min */ + int32_t, /* _6: sp_max */ + int32_t, /* _7: sp_warn */ + int32_t, /* _8: sp_inact */ + int32_t, /* _9: sp_expire */ + uint32_t /* _10: sp_flag */ + > ham_data; + + try + { + name_service_proxy_c ns(get_dbusconn(), DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE); + ham_data = ns.getspnam(name); + } + catch (DBus::Error & ex) + { + SYSLOG(LOG_CRIT, "_nss_ham_getspnam_r() - [%u:%u] cmdline=\"%s\" Exception %s\n", getuid(), getgid(), cmdline_p, ex.what()); + *errnop = ENOENT; + return NSS_STATUS_NOTFOUND; + } + + double duration_msec = verbose ? (get_now_nsec() - before_nsec)/1000000.0 : 0.0; + bool success = ham_data._1; + + if (!success) + { + SYSLOG(LOG_ERR, "_nss_ham_getspnam_r() - [%u:%u] cmdline=\"%s\" exec time=%.3f ms. Failed!\n", getuid(), getgid(), cmdline_p, duration_msec); + *errnop = ENOENT; + return NSS_STATUS_NOTFOUND; + } + + std::string & sp_namp_r = ham_data._2; + std::string & sp_pwdp_r = ham_data._3; + long sp_lstchg = ham_data._4; + long sp_min = ham_data._5; + long sp_max = ham_data._6; + long sp_warn = ham_data._7; + long sp_inact = ham_data._8; + long sp_expire = ham_data._9; + unsigned long sp_flag = ham_data._10; + + SYSLOG_CONDITIONAL(verbose, LOG_DEBUG, "_nss_ham_getspnam_r() - [%u:%u] cmdline=\"%s\" exec time=%.3f ms. Success! sp_namp=\"%s\", sp_pwdp=\"%s\", sp_lstchg=%ld, sp_min=%ld, sp_max=%ld, sp_warn=%ld, sp_inact=%ld, sp_expire=%ld, sp_flag=%lu\n", + getuid(), getgid(), cmdline_p, duration_msec, sp_namp_r.c_str(), sp_pwdp_r.c_str(), sp_lstchg, sp_min, sp_max, sp_warn, sp_inact, sp_expire, sp_flag); + + size_t sp_namp_l = sp_namp_r.length() + 1; /* +1 to include NUL terminating char */ + size_t sp_pwdp_l = sp_pwdp_r.length() + 1; + if (buflen < (sp_namp_l + sp_pwdp_l)) + { + SYSLOG(LOG_ERR, "_nss_ham_getspnam_r() - [%u:%u] cmdline=\"%s\" not enough memory for struct spwd data\n", getuid(), getgid(), cmdline_p); + + if (errnop) *errnop = ENOMEM; + return NSS_STATUS_TRYAGAIN; + } + + result->sp_namp = buffer; + result->sp_pwdp = cpy2buf(result->sp_namp, sp_namp_r.c_str(), sp_namp_l); + cpy2buf(result->sp_pwdp, sp_pwdp_r.c_str(), sp_pwdp_l); + result->sp_lstchg = sp_lstchg; + result->sp_min = sp_min; + result->sp_max = sp_max; + result->sp_warn = sp_warn; + result->sp_inact = sp_inact; + result->sp_expire = sp_expire; + result->sp_flag = sp_flag; + + if (errnop) *errnop = 0; + + return NSS_STATUS_SUCCESS; +} + +//############################################################################## + +/** + * @brief Initalize module singletons on entry. + */ +void __attribute__((constructor)) __module_enter(void) +{ + read_config(); + if (verbose) + read_cmdline(); +} + +/** + * @brief Module clean up on exit + */ +void __attribute__((destructor)) __module_exit(void) +{ + if ((cmdline_p != nullptr) && (cmdline_p != program_invocation_name)) + { + free(cmdline_p); + cmdline_p = nullptr; + } + + if ((log_p != nullptr) && (log_p != stderr) && (log_p != stdout)) + { + fclose(log_p); + log_p = nullptr; + } +} + +#ifdef __cplusplus +} +#endif diff --git a/src/ham/libnss_ham/name_service_proxy.h b/src/ham/libnss_ham/name_service_proxy.h new file mode 100644 index 00000000000..6622288fb3f --- /dev/null +++ b/src/ham/libnss_ham/name_service_proxy.h @@ -0,0 +1,20 @@ +#ifndef __NAME_SERVICE_PROXY_H__ +#define __NAME_SERVICE_PROXY_H__ + +#include /* DBus */ +#include "../shared/org.SONiC.HostAccountManagement.dbus-proxy.h" +#include "../shared/dbus-address.h" /* DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE */ + +class name_service_proxy_c : public ham::name_service_proxy, + public DBus::IntrospectableProxy, + public DBus::ObjectProxy +{ +public: + name_service_proxy_c(DBus::Connection &conn, const char * dbus_bus_name_p, const char * dbus_obj_name_p) : + DBus::ObjectProxy(conn, dbus_obj_name_p, dbus_bus_name_p) + { + } +}; + + +#endif // __NAME_SERVICE_PROXY_H__ diff --git a/src/ham/libnss_ham/test/Makefile b/src/ham/libnss_ham/test/Makefile new file mode 100644 index 00000000000..6d5b14ca182 --- /dev/null +++ b/src/ham/libnss_ham/test/Makefile @@ -0,0 +1,86 @@ +#***************************************************************************** +# +# AUTHOR: Martin Belanger +# +#*****************************************************************************/ +PROGRAM := nsshamtest + +PROGRAM_SRC := $(wildcard *.c) + +PROGRAM_OBJ := $(patsubst %.c,%.o,$(filter %.c,$(PROGRAM_SRC))) +PROGRAM_DEP := $(PROGRAM_OBJ:.o=.d) + +CC := gcc +#LDFLAGS := $(shell pkg-config --libs libsystemd) +LDFLAGS := +LL := gcc +CFLAGS := -g -O3 -Wall + +ifeq (,$(strip $(filter $(MAKECMDGOALS),clean install uninstall package))) + ifneq (,$(strip $(PROGRAM_DEP))) + -include $(PROGRAM_DEP) + endif +endif + + +# ******************************************************************* +# INCLUDES: +# ******************************************************************* +# NOTE: D-Bus header files are located in: +# /usr/include/dbus-c++-1/dbus-c++/dbus.h +# /usr/include/dbus-1.0/dbus/dbus.h +#INCLUDES := $(sort $(shell pkg-config --cflags libsystemd)) +INCLUDES := + + +# ******************************************************************* +# Implicit rules: +# ******************************************************************* +%.o : %.c + @printf "%b[1;36m%s%b[0m\n" "\0033" "Compiling: $< -> $@" "\0033" + $(CC) $(CFLAGS) $(INCLUDES) -c $< -o $@ + @printf "\n" + +%.d : %.c + @printf "%b[1;36m%s%b[0m\n" "\0033" "Dependency: $< -> $@" "\0033" + $(CC) -MM -MG -MT '$@ $(@:.d=.o)' $(CFLAGS) $(INCLUDES) -o $@ $< + @printf "\n" + +##################################################################### +##################################################################### + +ifeq (,$(strip $(filter $(MAKECMDGOALS),install package))) + +# ******************************************************************* +# Make all +# ******************************************************************* +.DEFAULT_GOAL := all +all: $(PROGRAM) + +# ******************************************************************* +# PROGRAM +# ******************************************************************* +$(PROGRAM): $(PROGRAM_OBJ) $(PROGRAM_DEP) Makefile $(PICS-LIB) + @printf "%b[1;36m%s%b[0m\n" "\0033" "Linking: $(PROGRAM_OBJ) -> $@" "\0033" + $(LL) $(CFLAGS) -o $@ $(PROGRAM_OBJ) $(LDFLAGS) + @printf "%b[1;32m%s%b[0m\n\n" "\0033" "$@ Done!" "\0033" + +endif # (,$(strip $(filter $(MAKECMDGOALS),install debian))) + +##################################################################### +##################################################################### + +# ******************************************************************* +# CLEAN +# ******************************************************************* +RM_PROGRAM := $(PROGRAM) ./*.o ./*.d + +RM_LIST = $(strip $(wildcard $(RM_PROGRAM))) +.PHONY: clean +clean: + @printf "%b[1;36m%s%b[0m\n" "\0033" "Cleaning" "\0033" +ifneq (,$(RM_LIST)) + rm -rf $(RM_LIST) +endif + + diff --git a/src/ham/libnss_ham/test/nsshamtest.c b/src/ham/libnss_ham/test/nsshamtest.c new file mode 100644 index 00000000000..4c7e311b709 --- /dev/null +++ b/src/ham/libnss_ham/test/nsshamtest.c @@ -0,0 +1,117 @@ +// Prior to running this, the hamd daemon must be running +// and the NSS ham module need to be installed in /lib/x86_64-linux-gnu/ +// as follows: +// sudo cp libnss_ham.so.2 /lib/x86_64-linux-gnu/. + + +#include +#include /* getpwnam() */ +#include /* getgrnam(), getgrouplist() */ +#include /* syslog(), LOG_ERR */ +#include /* sysconf() */ +#include /* bool */ +#include /* malloc(), exit() */ +#include /* printf() */ + +/** + * @brief Test whether a user is a member of a role. + * + * @details The role_member() function tests whether a user has been + * assigned to a particular role. + * + * @param[in] user - The user name + * @param[in] role - The role + * + * @return bool true on success, false otherwise. + */ +bool role_member(const char * user, const char * role) +{ + struct passwd * pwd = getpwnam(user); + if (pwd == NULL) + { + syslog(LOG_ERR, "role_member() - Unknown user %s", user); + return false; + } + gid_t primary_group = pwd->pw_gid; + + struct group * grp = getgrnam(role); + if (grp == NULL) + { + syslog(LOG_ERR, "role_member() - Unknown role (i.e. group) %s", role); + return false; + } + gid_t role_gid = grp->gr_gid; + + // Check primary Group ID + if (primary_group == role_gid) + return true; + + // Check supplementary Group IDs + int ngroups = (int)sysconf(_SC_NGROUPS_MAX) + 1; + size_t memsize = ngroups * sizeof(gid_t); + gid_t * groups = (gid_t *)malloc(memsize); + if (groups == NULL) + { + syslog(LOG_ERR, "role_member() - malloc() error. size = %lu", memsize); + return false; + } + + bool is_member = false; + if (getgrouplist(user, primary_group, groups, &ngroups) == -1) + { + syslog(LOG_ERR, "role_member() - getgrouplist() failed; ngroups = %d\n", ngroups); + } + else + { + for (int i = 0; i < ngroups; i++) + { + if (role_gid == groups[i]) + { + is_member = true; + break; + } + } + } + + free(groups); + + return is_member; +} + +int main(int argc, char *argv[]) +{ + __nss_configure_lookup("passwd", "ham"); + __nss_configure_lookup("group", "ham"); + __nss_configure_lookup("shadow", "ham"); + + const char * users[] = + { + "mbelanger", + "syslog", + "pipi", + NULL + }; + + const char * roles[] = + { + "sudo", + "adm", + "caca", + NULL + }; + + const char * role; + const char * user; + unsigned i, j; + for (i = 0, user = users[0]; user != NULL; user = users[++i]) + { + for (j = 0, role = roles[0]; role != NULL; role = roles[++j]) + { + printf("%-9s %s member of %s\n", user, role_member(user, role) ? "\033[32;1mis\033[0m " : "\033[31;1mis not\033[0m", role); + } + } + + exit(EXIT_SUCCESS); +} + + diff --git a/src/ham/libnss_sac/Makefile b/src/ham/libnss_sac/Makefile new file mode 100644 index 00000000000..4002e6fc6ff --- /dev/null +++ b/src/ham/libnss_sac/Makefile @@ -0,0 +1,73 @@ +# Prerequisite: sudo apt-get install libsystemd-dev libdbus-c++-dev +TARGET := libnss_sac.so.2 +SRCS := $(wildcard *.cpp) + +PKGS := dbus-c++-glib-1 +LDFLAGS := -shared -Wl,-z,nodelete -Wl,-soname,${TARGET} -Wl,--version-script=libnss_sac.sym +LDLIBS := $(shell pkg-config --libs ${PKGS}) +CPPFLAGS := $(shell pkg-config --cflags ${PKGS}) +CFLAGS := -fPIC -Wall -Werror -O3 +CXXFLAGS := -std=c++11 ${CFLAGS} +LL := g++ + +DBUS-GLUE := $(patsubst %.xml,%.dbus-proxy.h,$(wildcard ../shared/*.xml)) + +OBJS := $(patsubst %.cpp,%.o,$(filter %.cpp,${SRCS})) +DEPS := $(OBJS:.o=.d) + +ifeq (,$(strip $(filter $(MAKECMDGOALS),clean install uninstall package))) + ifneq (,$(strip ${DEPS})) + ${DEPS}: ${DBUS-GLUE} + -include ${DEPS} + endif +endif + +ifeq (,$(strip $(filter $(MAKECMDGOALS),clean install uninstall))) + +# ******************************************************************* +# Make all +.DEFAULT_GOAL := all +all: ${TARGET} + +${TARGET}: ${OBJS} Makefile libnss_sac.sym + @printf "%b[1;36m%s%b[0m\n" "\0033" "Building $@" "\0033" + $(LL) ${LDFLAGS} -o $@ ${OBJS} $(LDLIBS) + @printf "%b[1;32m%s%b[0m\n\n" "\0033" "$@ Done!" "\0033" + +endif # (,$(strip $(filter $(MAKECMDGOALS),clean install uninstall))) + +# ******************************************************************* +# Implicit rules: +%.o : %.cpp + @printf "%b[1;36m%s%b[0m\n" "\0033" "Compiling: $< -> $@" "\0033" + ${CXX} ${CPPFLAGS} ${CXXFLAGS} -c $< -o $@ + @printf "\n" + +%.d : %.cpp + @printf "%b[1;36m%s%b[0m\n" "\0033" "Dependency: $< -> $@" "\0033" + ${CXX} -MM -MG -MT '$@ $(@:.d=.o)' ${CPPFLAGS} ${CXXFLAGS} -o $@ $< + @printf "\n" + +# Implicit rule to generate DBus header files from XML +../shared/%.dbus-proxy.h: ../shared/%.xml + @printf "%b[1;36m%s%b[0m\n" "\0033" "dbusxx-xml2cpp $< --proxy=$@" "\0033" + @dbusxx-xml2cpp $< --proxy=$@ + +# ******************************************************************* +# ____ _ +# / ___| | ___ __ _ _ __ +# | | | |/ _ \/ _` | '_ \ +# | |___| | __/ (_| | | | | +# \____|_|\___|\__,_|_| |_| +# +# ******************************************************************* +RM_TARGET := ${TARGET} ./*.o ./*.d ${DBUS-GLUE} + +RM_LIST = $(strip $(wildcard ${RM_TARGET})) +.PHONY: clean +clean: +ifneq (,$(RM_LIST)) + rm -rf $(RM_LIST) +endif + + diff --git a/src/ham/libnss_sac/README.md b/src/ham/libnss_sac/README.md new file mode 100644 index 00000000000..5d4f2281a8e --- /dev/null +++ b/src/ham/libnss_sac/README.md @@ -0,0 +1,12 @@ +# System-Assigned Credentials (SAC) + +The SAC Name Service Switch (NSS) module automatically assigns a `UID` and primary `GID` to RADIUS/TACACS+ +users. + +This is used to fill a gap with protocols such as RADIUS and TACACS+ that were +never designed for UNIX-like systems. These protocols have no concept of User +IDs (`UID`) and Group IDs (`GID`), which are essential to any program running on +UNIX-like systems. + +This NSS module gets installed on the host and configured in `/etc/nsswitch.conf`. + diff --git a/src/ham/libnss_sac/etc/sonic/hamd/libnss_sac.conf b/src/ham/libnss_sac/etc/sonic/hamd/libnss_sac.conf new file mode 100644 index 00000000000..c8c7a6829f2 --- /dev/null +++ b/src/ham/libnss_sac/etc/sonic/hamd/libnss_sac.conf @@ -0,0 +1,49 @@ +# ============================================================================== +# System-Assigned Credentials (SAC) +# This is the configuration file for the SAC NSS module. +# SAC is part of Host Account Management (HAM) and is meant to be used +# with the HAM Daemon (hamd). + +# ============================================================================== +# The strategy used for SAC options in the config shipped with SONiC is to +# specify options with their default value where possible, but leave them +# commented. Uncommented options override default values. + +# ============================================================================== +# debug= +# Enable additional debug info to the syslog +# values: [yes, no] +#debug=no + +# ============================================================================== +# log= +# Log file. By default logs are sent to the syslog. To send the logs to a +# file instead of the syslog, one can specify the file with this option. +#log= + +# ============================================================================== +# programs= +# This SAC NSS module is only needed when we want to authenticate users +# with methods such as RADIUS or TACACS+ that don't natively support Linux +# credentials (i.e. UID, GID). For such methods, SAC will automatically +# assign credentials in the /etc/passwd and /etc/group files. +# +# Only programs used to log into SONiC Linux should be allowed to use SAC. +# Depending on the method used to log into the system we can expect one of +# the following program names: +# +# Login method Program name +# ================ ============ +# ssh sshd +# telnet, console login +# Shell's su su +# +# This option may be specified more than once or multiple comma-separated +# program names may be specified in one option in which case all listed +# program names from all "programs=" options will be used. +#programs=sshd,login,su + + + + + diff --git a/src/ham/libnss_sac/libnss_sac.cpp b/src/ham/libnss_sac/libnss_sac.cpp new file mode 100644 index 00000000000..a7bbbd1c7c8 --- /dev/null +++ b/src/ham/libnss_sac/libnss_sac.cpp @@ -0,0 +1,625 @@ +/** + * System-Assigned Credentials (SAC) + * + * This NSS module is only needed when users are logged into the system + * with an authentication protocol such as RADIUS or TACACS+. These + * protocols were not designed for UNIX-like systems and do not provide + * a standard way of defining UNIX credentials (UID and GID). In + * particular, RADIUS doesn't separate the authorization function from + * authentication, which makes it impossible to retrieve user credentials + * separate from the authentication process. In other words, in order to + * get the UID associated with a given user (i.e. getpwnam) one needs + * to provide the password for that user. This simply doesn't work well + * with Linux. In fact, sshd calls getpwnam() before it even tries to + * authenticate the user and if this API cannot find the credentials the + * login will fail. + * + * This NSS module solves this problem by automatically allocating + * credentials (UID and primary GID) to users that are not found by any + * other NSS methods. We call these credentials "System-Assigned + * Credentials" or SAC for short. These credentials are only allocated + * temporarily and won't become permanent until the user has passed + * authentication. If the user fails to authenticate, then the temporary + * credentials get deleted. Once the user has successfully authenticated, + * the credentials become permanent in the /etc/passwd file. + * + * There are 3 components to SAC. The SAC NSS module, the Host Account + * Management Daemon (hamd), and the RADIUS and/or TACACS+ PAM modules. + * + * System overview: + * + * The following is a highly simplified representation of what happens + * when a user logs into the system over SSH. + * + * ssh sshd NSS-SAC hamd PAM RADIUS/TACACS+ + * | . . . . Server + * | . . . . + * +---->+ . . . . + * | . . . + * . [1] +--getpwnam()--->+ . . . + * . | . . + * . . +---DBus-->+ . . + * . . . | . . + * . . . useradd() [2] . . + * . . . temporarily . . + * . . . to /etc/passwd . . + * . . | . . + * . . +<--DBus---+ . . + * . | . . . + * . [3] +<--(uid, gid)---+ . . . + * . | . + * . +-pam_autheticate()----------------------->+ [4] . + * . . | + * . . . +---auth------>+ + * . . . | + * . . . +<--pass/fail--+ [5] + * . . DBus: | . + * . . +<--pass/fail--+ . + * . . | . . + * . . [6] persist/remove . . + * . . from /etc/passwd . + * . . | + * . . +--DBus:done-->+ + * . | + * . +<---------------success/fail--------------+ [7] + * . | + * . [8] +----exec(shell) / exit() + * . + * + * [1] When an ssh session is initiated, one of the first things that sshd + * does is to get the credentials (uid/gid) for the user that is + * trying to log in. This is done by calling getpwnam(username), which + * invokes the NSS modules configured in /etc/nsswitch.conf in the + * order they appear in the file. libnss_sac should be configured as + * the last method in /etc/nsswitch.conf. When all other NSS methods + * have failed to find credentials for the user, libnss_sac gets + * invoked. The NSS SAC module then invokes hamd over DBus to + * temporarily allocate credentials to the user. + * + * [2] The daemon hamd creates temporary credentials for the user. We use + * the GECOS filed to mark the credentials as "unconfirmed", which + * simply means that the user hasn't been authenticated yet. The GECOS + * will also have the PID of the process that initiated the session. + * The PID can then be used later to identify "unconfirmed" + * credentials that are no longer valid. That is, if hamd finds an + * entry marked as "unconfirmed", but the PID associted to that entry + * no longer exists, then hamd will conclude that this user was never + * confirmed and can safely be removed from the /etc/passwd file. + * + * [3] After the temporary credentials are returned to sshd, sshd can + * start the authentication phase using pam_authenticate(). + * + * [4] pam_authenticate() triggers the PAM layer library to query the + * user's password and verify it against the different PAM modules + * configured in /etc/pam.d/[application]. In this example + * [application] would be the /etc/pam.d/sshd. In that file there may + * be several authentication methods that will be tried including + * local (i.e. pam_unix), RADIUS, and/or TACACS+. For the sake of this + * example we will refer to RADIUS and/or TACACS+ servers as a AAA + * server. At this point the PAM module will contact the AAA server + * to authenticate the user. + * + * [5] The AAA server verifies the user provided password with what it has + * in its DB. Depending on whether the password matches the AAA server + * returns pass or fail to the PAM module. + * + * [6] If authentication is successful, the PAM module contacts hamd to + * confirm the user and make the credentials permanent in /etc/passwd. + * This simply means that the "unconfirmed" marker in the GECOS field + * can be cleared. If authentication has failed, the PAM module will + * contact hamd to tell it to remove the user credentials from + * /etc/passwd. + * + * [7] The PAM module finally returns success/fail to sshd + * + * [8] Upon successful authentication sshd will start the shell configured + * in the passwd struct returned by getpwnam() earlier. This has been + * set by hamd. By default, hamd will have set it to start a kish + * shell, but hamd is configurable so a different could be configured + * if need be (see hamd code for details). + * + * Restricting applications that are allowed to invoke SAC + * ======================================================= + * For additional security, only certain applications will be allowed to + * automatically create credentials. Those are applications that are + * typically used for login such as sshd, login, su, etc. This is + * configurable through file /etc/sonic/hamd/libnss_sac.conf (see macro + * SAC_CONFIG_FILE below). + * + * This code is compiled as libnss_sac.so.2. In order to use it one must + * use the keyword "sac" in the NSS configuration file (i.e. + * /etc/nsswitch.conf) as follows: + * + * /etc/nsswitch.conf + * ================== + * + * passwd: compat sac <- To support getpwnam() + */ + +#ifndef _GNU_SOURCE +# define _GNU_SOURCE +#endif +#include /* NSS_STATUS_SUCCESS, NSS_STATUS_NOTFOUND */ +#include /* syslog() */ +#include /* fopen(), fclose() */ +#include /* errno, program_invocation_name */ +#include /* uid_t, struct passwd */ +#include /* stat() */ +#include /* getpid() */ +#include /* getpid(), access() */ +#include /* strdup() */ +#include /* open(), O_RDONLY */ +#include /* LINE_MAX */ + +#include /* std::string */ +#include /* std::vector */ +#include /* std::find() */ + +#include "prototypes.h" /* _nss_sac_getpwnam_r() */ +#include "sac_proxy.h" /* sac_proxy_c */ +#include "../shared/utils.h" /* strneq(), startswith(), cpy2buf(), join() */ + +#define SAC_CONFIG_FILE "/etc/sonic/hamd/libnss_sac.conf" +#define SAC_ENABLE_FILE "/etc/sonic/hamd/libnss_sac.enable" // Presence of this file enables SAC + +//#define WITH_PYTHON + +static FILE * log_p = nullptr; +static bool verbose = false; +static char * cmdline_p = program_invocation_name; +static std::vector default_programs = { "sshd", "login", "su" }; +static std::vector programs(default_programs); +#ifdef WITH_PYTHON +static std::vector default_pyscripts; +static std::vector pyscripts(default_pyscripts); +#endif + + +#define SYSLOG(LEVEL, args...) \ +do \ +{ \ + if (verbose) \ + { \ + if (log_p != nullptr) fprintf(log_p, args); \ + else syslog(LEVEL, args); \ + } \ +} while (0) + + +static bool sac_enabled() +{ + // The presence of the file SAC_ENABLE_FILE + // indicates whether SAC is enabled. + return access(SAC_ENABLE_FILE, F_OK) != -1; +} + +static char * kv_strip(char * p) +{ + #define WHITESPACE " \t\n\r" + + p[strcspn(p, "\n\r")] = '\0'; + + p += strspn(p, WHITESPACE); // Remove leading newline and spaces + if (*p == '#' || *p == '\0') // Skip comments and empty lines + { + *p = '\0'; + return p; + } + p[strcspn(p, "\n\r")] = '\0'; // Remove trailing newline chars + + // Delete trailing comments (including spaces/tabs that precede the #) + char * s = &p[strcspn(p, "#")]; + *s-- = '\0'; + while ((s >= p) && ((*s == ' ') || (*s == '\t'))) + { + *s-- = '\0'; + } + + return p; +} + +static char * kv_keymatch(const char * s, const char * key) +{ + char * value = startswith(s, key); + if (NULL != value) + { + switch (*value) + { + case ' ': // Make sure + case '\t': // key is a whole word. + case '=': // I.e. it should be followed by spaces, tabs, or a equal sign. + value += strspn(value, " \t="); // Skip leading spaces, tabs, and equal sign (=) + return value; + default:; + } + } + + return NULL; +} + +/** + * @brief Extract cmdline from /proc/self/cmdline. This is only needed for + * debugging purposes + */ +static void read_cmdline() +{ + const char *fn = "/proc/self/cmdline"; + struct stat st; + if (stat(fn, &st) != 0) return; + + int fd = open(fn, O_RDONLY); + if (fd == -1) return; + + char buffer[LINE_MAX] = {0}; + size_t sz = sizeof(buffer); + size_t n = 0; + for (;;) + { + ssize_t r = read(fd, &buffer[n], sz - n); + if (r == -1) + { + if (errno == EINTR) continue; + break; + } + n += r; + if (n == sz) break; // filled the buffer + if (r == 0) break; // EOF + } + close(fd); + + if (n) + { + if (n == sz) n--; + buffer[n] = '\0'; + size_t i = n; + while (i--) + { + int c = buffer[i]; + if ((c < ' ') || (c > '~')) buffer[i] = ' '; + } + + // Delete trailing spaces, tabs, and newline chars. + char *p = &buffer[strcspn(buffer, "\n\r")]; + while ((p >= buffer) && (*p == ' ' || *p == '\t' || *p == '\0')) + { + *p-- = '\0'; + } + } + + cmdline_p = strdup(buffer); +} + +/** + * @brief Get configuration parameters for this module. Parameters are + * retrieved from the file #SAC_CONFIG_FILE + */ +static void read_config() +{ + FILE *file = fopen(SAC_CONFIG_FILE, "re"); + if (file) + { + char line[LINE_MAX]; + char * p; + char * s; + + std::vector new_programs; +#ifdef WITH_PYTHON + std::vector new_pyscripts; +#endif + + while (NULL != (p = fgets(line, sizeof line, file))) + { + // Clean up string by removing leading/trailing blanks and new line + // characters. Also eliminate trailing comments, if any. + p = kv_strip(p); + if (*p == '\0') continue; // Check that there is still something left in the string + + if (NULL != (s = kv_keymatch(p, "debug"))) + { + if (strneq(s, "yes", 3)) verbose = true; + } + else if (NULL != (s = kv_keymatch(p, "log"))) + { + if (*s != '\0') + { + if (log_p != nullptr) + { + if ((log_p != stderr) && (log_p != stdout)) + { + fclose(log_p); + } + log_p = nullptr; + } + log_p = fopen(s, "w"); + } + } + if (NULL != (s = kv_keymatch(p, "programs"))) + { + // 'programs' is a list of comma-separated program names. + // So we need to split the list into its components + std::vector prog_names = split_any(s, ", \t"); + for (auto &prog : prog_names) new_programs.push_back(trim(prog)); + + } +#ifdef WITH_PYTHON + if (NULL != (s = kv_keymatch(p, "python_scripts"))) + { + // 'python_scripts' is a list of comman-separated python script names. + // So we need to split the list into its components + std::vector scripts = split_any(s, ", \t"); + for (auto & script : scripts) + new_pyscripts.push_back(trim(script)); + } +#endif + } + + fclose(file); + + programs = new_programs.empty() ? default_programs : new_programs; +#ifdef WITH_PYTHON + pyscripts = new_pyscripts.empty() ? default_pyscripts : new_pyscripts; +#endif + + if (verbose) + { + SYSLOG(LOG_DEBUG, "NSS sac: verbose = true\n"); + SYSLOG(LOG_DEBUG, "NSS sac: programs = [%s]\n", join(programs.cbegin(), programs.cend()).c_str()); +#ifdef WITH_PYTHON + SYSLOG(LOG_DEBUG, "NSS sac: pyscripts = [%s]\n", join(pyscripts.cbegin(), pyscripts.cend()).c_str()); +#endif + } + } + + if (verbose) + { + read_cmdline(); + SYSLOG(LOG_DEBUG, "NSS sac: cmdline = %s\n", cmdline_p); + SYSLOG(LOG_DEBUG, "NSS sac: sac_enabled() = %s\n", true_false(sac_enabled())); + SYSLOG(LOG_DEBUG, "NSS sac: program = %s\n", program_invocation_short_name); + SYSLOG(LOG_DEBUG, "NSS sac: config file = %s\n", SAC_CONFIG_FILE); + } +} + +static DBus::Connection & get_dbusconn() +{ + static DBus::Connection * conn_p = nullptr; + if (conn_p == nullptr) + { + // DBus::BusDispatcher is a "main loop" construct that + // handles (i.e. dispatched) DBus messages. This should + // be defined as a singleton to avoid memory leaks. + static DBus::BusDispatcher dispatcher; + + // DBus::default_dispatcher must be initialized before DBus::Connection. + DBus::default_dispatcher = &dispatcher; + + static DBus::Connection conn = DBus::Connection::SystemBus(); + + conn_p = &conn; + } + + return *conn_p; +} + +/** + * @brief This NSS module is only needed when we want to login users with a + * AAA method such as RADIUS or TACACS+. This means that only the + * programs used to log into the system should be allowed to + * proceed. Depending on the method used to log into the system we + * can expect one of the following program names: + * + * Login method Program name + * ================ ============ + * ssh sshd + * telnet, console login + * bash's su su + * + * @return bool: true if the calling program is one of the few programs + * used to let AAA users log into the system. false otherwise. + */ +static bool is_program_allowed_to_alloc_creds() +{ +#ifdef WITH_PYTHON + // Look for python scripts that are allowed to invoke SAC + if (strneq(program_invocation_short_name, "python", strlen("python"))) + { + for (auto & pyscript : pyscripts) + { + if (NULL != strstr(cmdline, pyscript.c_str())) + return true; + } + + return false; + } +#endif + + // Check if program_invocation_short_name is in programs list. + return std::find(programs.cbegin(), programs.cend(), program_invocation_short_name) != programs.cend(); +} + +/** + * @brief Scan file (@fname) looking for user. If found, return a pointer + * to a "struct passwd" containing all the data related to user. + * + * @param fname E.g. /etc/passwd + * @param user The user we're looking for + * + * @return If user found, return a pointer to a struct passwd. + */ +static struct passwd* fgetpwname(const char *fname, const char *user) +{ + struct passwd *pwd = NULL; + FILE *f = fopen(fname, "re"); + if (f) + { + struct passwd *ent; + while (NULL != (ent = fgetpwent(f))) + { + if (streq(ent->pw_name, user)) + { + pwd = ent; + break; + } + } + fclose(f); + } + + return pwd; +} + + +/** + * @brief Fill structure pointed by result with the data contained in the + * structure pointed to by pwd. + * + * @param pwd Pointer to the source of the data to transfer to result + * @param name User name + * @param result Pointer to destination where data get copied + * @param buffer Pointer to a chunk of memory where strings can be + * allocated from + * @param buflen Size of buffer + * @param errnop Pointer to where errno code can be written + * + * @return If there is not enough memory in buffer to copy the strings + * return NSS_STATUS_TRYAGAIN. Otherwise return NSS_STATUS_SUCCESS. + */ +static nss_status fill_result(struct passwd * pwd, + const char * name, + struct passwd * result, + char * buffer, + size_t buflen, + int * errnop) +{ + size_t name_l = strlen(name) + 1; /* +1 to include NUL terminating char */ + size_t dir_l = strlen(pwd->pw_dir) + 1; + size_t shell_l = strlen(pwd->pw_shell) + 1; + size_t passwd_l = strlen(pwd->pw_passwd) + 1; + if (buflen < (name_l + shell_l + dir_l + passwd_l)) + { + if (errnop) *errnop = ENOMEM; + return NSS_STATUS_TRYAGAIN; + } + + result->pw_uid = pwd->pw_uid; + result->pw_gid = pwd->pw_gid; + result->pw_name = buffer; + result->pw_dir = cpy2buf(result->pw_name, name, name_l); + result->pw_shell = cpy2buf(result->pw_dir, pwd->pw_dir, dir_l); + result->pw_passwd = cpy2buf(result->pw_shell, pwd->pw_shell, shell_l); + result->pw_gecos = cpy2buf(result->pw_passwd, pwd->pw_passwd, passwd_l); + + // Check if there is enough room left in buffer for GECOS. + // If not just, just assign "AAA user" as default. + size_t gecos_l = strlen(pwd->pw_gecos) + 1; + if ((size_t)((buffer + buflen) - result->pw_gecos) >= gecos_l) cpy2buf(result->pw_gecos, pwd->pw_gecos, gecos_l); + else result->pw_gecos = (char *)"AAA user"; + + if (errnop) *errnop = 0; + + return NSS_STATUS_SUCCESS; +} + + +#ifdef __cplusplus +extern "C" { +#endif +/** + * @brief Automatically create user credentials for AAA authenticated + * users. + * + * @param name User name. + * @param result Where to write the result + * @param buffer Buffer used as a temporary pool where we can save + * strings. + * @param buflen Size of memory pointed to by buffer + * @param errnop Where to return the errno + * + * @return NSS_STATUS_SUCCESS on success, NSS_STATUS_NOTFOUND otherwise. + */ +enum nss_status _nss_sac_getpwnam_r(const char * name, + struct passwd * result, + char * buffer, + size_t buflen, + int * errnop) +{ + if (!sac_enabled()) return NSS_STATUS_NOTFOUND; + + // Just to be sure, let's check if user is already in /etc/passwd + struct passwd *pwd = fgetpwname("/etc/passwd", name); + if (pwd) + { + if (verbose) SYSLOG(LOG_DEBUG, "NSS sac: _nss_sac_getpwnam_r() - User \"%s\": user found in /etc/passwd\n", name); + return fill_result(pwd, name, result, buffer, buflen, errnop); + } + + // Only allow certain programs to automatically allocated credentials. + // The list of programs is configurable through /etc/ + + bool program_allowed = is_program_allowed_to_alloc_creds(); + if (verbose) SYSLOG(LOG_DEBUG, "NSS sac: _nss_sac_getpwnam_r() - User \"%s\": invoked from \"%s\" with creds UID=%u GID=%u. program_allowed=%s\n", + name, cmdline_p, getuid(), getgid(), program_allowed ? "yes" : "no"); + + /* We should only let login programs continue. */ + if (!program_allowed) return NSS_STATUS_NOTFOUND; + + try + { + // Create the DBus interface + sac_proxy_c sac(get_dbusconn(), DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE); + + std::string errmsg = sac.add_unconfirmed_user(name, getpid()); + bool ok = errmsg.empty(); + if (ok) + { + pwd = fgetpwname("/etc/passwd", name); + if (pwd) + { + return fill_result(pwd, name, result, buffer, buflen, errnop); + } + } + + if (verbose) SYSLOG(LOG_DEBUG, "NSS sac: _nss_sac_getpwnam_r() - User \"%s\": Exiting with Try Again due to: %s, pwd=%p\n", + name, errmsg.c_str(), pwd); + } + catch (DBus::Error &ex) + { + SYSLOG(LOG_ERR, "NSS sac: _nss_sac_getpwnam_r() - User \"%s\": Exiting with Try Again due to: %s\n", + name, ex.what()); + } + + *errnop = EBUSY; + return NSS_STATUS_TRYAGAIN; +} + +/** + * @brief Initalize module singletons on entry. + * + * cmdline_p contains the command line of the program that invoked + * the NSS module. Used for debug purposes only. + */ +void __attribute__((constructor)) __module_enter(void) +{ + read_config(); +} + +/** + * @brief Module clean up on exit + */ +void __attribute__((destructor)) __module_exit(void) +{ + if ((cmdline_p != nullptr) && (cmdline_p != program_invocation_name)) + { + free(cmdline_p); + cmdline_p = nullptr; + } + + if ((log_p != nullptr) && (log_p != stderr) && (log_p != stdout)) + { + fclose(log_p); + log_p = nullptr; + } +} + +#ifdef __cplusplus +} +#endif diff --git a/src/ham/libnss_sac/libnss_sac.postinst b/src/ham/libnss_sac/libnss_sac.postinst new file mode 100644 index 00000000000..60c4184c07b --- /dev/null +++ b/src/ham/libnss_sac/libnss_sac.postinst @@ -0,0 +1,25 @@ +#!/bin/sh +set -e + +action="$1" +oldversion="$2" + +umask 022 + +#**************************************** +# configure +#**************************************** +if [ "${action}" = configure ]; then + + ldconfig + + # Add ham to /etc/nsswitch.conf (if not already) + if ! grep --silent sac /etc/nsswitch.conf; then + /bin/sed -i 's/^\(passwd:.*\)$/\1 sac/' /etc/nsswitch.conf + fi +fi + +#DEBHELPER# + +exit 0 + diff --git a/src/ham/libnss_sac/libnss_sac.sym b/src/ham/libnss_sac/libnss_sac.sym new file mode 100644 index 00000000000..876a248246a --- /dev/null +++ b/src/ham/libnss_sac/libnss_sac.sym @@ -0,0 +1,7 @@ + +{ +global: + _nss_sac_getpwnam_r; + +local: *; +}; diff --git a/src/ham/libnss_sac/prototypes.h b/src/ham/libnss_sac/prototypes.h new file mode 100644 index 00000000000..fddc45597f2 --- /dev/null +++ b/src/ham/libnss_sac/prototypes.h @@ -0,0 +1,13 @@ +#ifndef _PROTOTYPES_H_ +#define _PROTOTYPES_H_ + +extern "C" { + +enum nss_status _nss_sac_getpwnam_r(const char * name, + struct passwd * result, + char * buffer, + size_t buflen, + int * errnop); +} + +#endif diff --git a/src/ham/libnss_sac/sac_proxy.h b/src/ham/libnss_sac/sac_proxy.h new file mode 100644 index 00000000000..82900c4c0ae --- /dev/null +++ b/src/ham/libnss_sac/sac_proxy.h @@ -0,0 +1,20 @@ +#ifndef __SAC_PROXY_H__ +#define __SAC_PROXY_H__ + +#include /* DBus */ +#include "../shared/dbus-address.h" /* DBUS_BUS_NAME_BASE, DBUS_OBJ_PATH_BASE */ +#include "../shared/org.SONiC.HostAccountManagement.dbus-proxy.h" + +class sac_proxy_c : public ham::sac_proxy, + public DBus::IntrospectableProxy, + public DBus::ObjectProxy +{ +public: + sac_proxy_c(DBus::Connection &connection, const char *dbus_bus_name_p, const char *dbus_obj_name_p) : + DBus::ObjectProxy(connection, dbus_obj_name_p, dbus_bus_name_p) + { + } +}; + + +#endif // __SAC_PROXY_H__ diff --git a/src/ham/shared/dbus-address.h b/src/ham/shared/dbus-address.h new file mode 100644 index 00000000000..53be4d2a227 --- /dev/null +++ b/src/ham/shared/dbus-address.h @@ -0,0 +1,8 @@ +// Host Account Management +#ifndef DBUS_ADDRESS_H +#define DBUS_ADDRESS_H + +#define DBUS_BUS_NAME_BASE "org.SONiC.HostAccountManagement" +#define DBUS_OBJ_PATH_BASE "/org/SONiC/HostAccountManagement" + +#endif /* DBUS_ADDRESS_H */ diff --git a/src/ham/shared/missing-memfd_create.h b/src/ham/shared/missing-memfd_create.h new file mode 100644 index 00000000000..dfbd804c1b4 --- /dev/null +++ b/src/ham/shared/missing-memfd_create.h @@ -0,0 +1,36 @@ +#ifndef __MISSING_MEMFD_CREATE_H__ +#define __MISSING_MEMFD_CREATE_H__ + +// The memfd_create syscall has been available since Linux 3.17 (2014-10-05) +// However, the API memfd_create() was only added to glibc 2.27 (2018-02-01) +// As of 2020-01-15, SONiC is running with Linux 4.9 (2016-12-11) and +// glibc 2.24 (2016-08-05), which is why we need the following. +#if !__GLIBC_PREREQ(2,27) + +# ifndef _GNU_SOURCE +# define _GNU_SOURCE +# endif +# include +# include /* syscall(), SYS_memfd_create */ + +# ifdef SYS_memfd_create + static inline int memfd_create(const char *name, unsigned int flags) + { + return syscall(SYS_memfd_create, name, flags); + } +# else // SYS_memfd_create +# include /* errno, ENOSYS */ + static inline int memfd_create(const char *name, unsigned int flags) + { + errno = ENOSYS; + return -1; + } +# endif // SYS_memfd_create + +# ifndef MFD_CLOEXEC +# define MFD_CLOEXEC 0x0001U +# endif + +#endif // !__GLIBC_PREREQ(2,27) + +#endif // __MISSING_MEMFD_CREATE_H__ diff --git a/src/ham/shared/org.SONiC.HostAccountManagement.xml b/src/ham/shared/org.SONiC.HostAccountManagement.xml new file mode 100644 index 00000000000..11249a7f354 --- /dev/null +++ b/src/ham/shared/org.SONiC.HostAccountManagement.xml @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ham/shared/utils.h b/src/ham/shared/utils.h new file mode 100644 index 00000000000..9aa1484255d --- /dev/null +++ b/src/ham/shared/utils.h @@ -0,0 +1,197 @@ +// Host Account Management +#ifndef __UTILS_H +#define __UTILS_H + +#include /* strcmp(), strncmp() */ +#include /* syslog() */ + +#define LOG_CONDITIONAL(condition, args...) do { if (condition) {syslog(args);} } while(0) + +#define streq(a,b) (strcmp((a),(b)) == 0) +#define strneq(a,b,n) (strncmp((a),(b),(n)) == 0) + +/** + * @brief Checks that a string starts with a given prefix. + * + * @param s The string to check + * @param prefix A string that s could be starting with + * + * @return If s starts with prefix then return a pointer inside s right + * after the end of prefix. + * NULL otherwise + */ +static inline char * startswith(const char *s, const char *prefix) +{ + size_t l = strlen(prefix); + if (strncmp(s, prefix, l) == 0) return (char *)s + l; + + return NULL; +} + +/** + * Copy string to buffer + * + * @param dest Where to copy srce to + * @param srce String to be copied + * @param len Number of characters to copy. + * + * @return a pointer to the location in dest after the NUL terminating + * character of the string that was copied. + */ +static inline char * cpy2buf(char * dest, const char * srce, size_t len) +{ + memcpy(dest, srce, len); + return dest + len; +} + + + + +#ifdef __cplusplus +# include /* std::string */ +# include /* std::ostringstream, std::istringstream */ +# include /* std::vector */ +# include /* std::unique_ptr */ + + inline const char * true_false(bool x, const char * pos_p = "true", const char * neg_p = "false") { return (x) ? pos_p : neg_p; } + + /** + * This is an equivalent to Python's ''.join(). + * + * @example + * + * static std::vector v = {"a", "b", "c"}; + * std::string s = join(v.begin(), v.end(), ", ", "."); + * // Result: "a, b, c." + * + * @return std::string + */ + template + std::string join(InputIt begin, + InputIt end, + const std::string & separator = ", ") + { + std::ostringstream ss; + + if (begin != end) + ss << *begin++; + + while (begin != end) + { + ss << separator; + ss << *begin++; + } + + return ss.str(); + } + + /** + * @brief Return a list (vector) of the sub-strings of %s that are + * delimited by any of the bytes in %any. + * + * @param s - The string to split + * @param any - A set of characters that will be used as delimiters. + * @param min_vector_size - The minimum size of the returned vector. This + * can be used to append empty strings at the end + * of the returned vector to increase its length + * to @min_vector_size. + * + * @return std::vector + */ + static inline std::vector split_any(const std::string& s, const char * any=";", size_t min_vector_size=0) + { + std::vector tokens; + + char s_copy[s.length() + 1]; + memcpy(s_copy, s.data(), sizeof s_copy); // Copy whole string including nul char at the end. + + char * next = &s_copy[0]; + char * token = strsep(&next, any); + while (token != nullptr) + { + tokens.push_back(token); + if (next != nullptr) + next += strspn(next, any); // Skip all delimiters + token = strsep(&next, any); + } + + if (tokens.size() < min_vector_size) + tokens.resize(min_vector_size, ""); + + return tokens; + } + + /** + * Returns a list (vector) of the sub-strings of @s that are delimited by + * the %exact string. + * + * @param s - The string to split + * @param exact - The exact delimiter. + * @param min_vector_size - The minimum size of the returned vector. This + * can be used to append empty strings at the end + * of the returned vector to increase its length + * to @min_vector_size. + * + * @return std::vector + */ + static inline std::vector split_exact(const std::string& s, const char * exact=";", size_t min_vector_size=0) + { + std::vector tokens; + + size_t s_len = s.length(); + char s_copy[s_len + 1]; + memcpy(s_copy, s.data(), s_len + 1); // Copy whole string including nul char at the end. + + size_t delim_len = strlen(exact); + char * next = &s_copy[0]; + char * end = &s_copy[s_len + 1]; // Point at the terminating nul + char * delim; + + while ((next < end) && (nullptr != (delim = strstr(next, exact)))) + { + delim[0] = '\0'; + tokens.push_back(next); + next = delim + delim_len; + } + if (next < end) + tokens.push_back(next); + + if (tokens.size() < min_vector_size) + tokens.resize(min_vector_size, ""); + + return tokens; + } + + /** + * @brief Remove leading and trailing characters + * + * @param str + * @param whitespace + * + * @return The trimmed string + */ + static inline std::string trim(const std::string & str, + const std::string & whitespace = " \t") + { + const auto strBegin = str.find_first_not_of(whitespace); + if (strBegin == std::string::npos) + return ""; // no content + + const auto strEnd = str.find_last_not_of(whitespace); + const auto strRange = strEnd - strBegin + 1; + + return str.substr(strBegin, strRange); + } + + template + std::string strfmt(const std::string &format, VArgs&& ... vargs) + { + size_t buf_size = std::snprintf(nullptr, 0, format.c_str(), std::forward(vargs)...) + 1; + std::unique_ptr buffer(new char[buf_size]); + std::snprintf(&buffer[0], buf_size, format.c_str(), vargs ...); + return std::string(buffer.get(), buffer.get() + buf_size - 1); + } + +#endif // __cplusplus + +#endif /* __UTILS_H */