diff options
author | Liam Hopkins <liamh@google.com> | 2018-12-14 12:44:47 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-12-14 12:44:47 -0800 |
commit | f773905cc0a70927c7180dd60d939fbf21264c92 (patch) | |
tree | a8aa77f094f896d6689fcee711eb490822b6b1f0 /packages/google-compute-engine-oslogin | |
parent | 091c4251a0d5e4af7c006af747251af7d7bcee62 (diff) | |
download | google-compute-image-packages-f773905cc0a70927c7180dd60d939fbf21264c92.tar.gz |
Repo layout changes (#688)
Diffstat (limited to 'packages/google-compute-engine-oslogin')
30 files changed, 3820 insertions, 0 deletions
diff --git a/packages/google-compute-engine-oslogin/Makefile b/packages/google-compute-engine-oslogin/Makefile new file mode 100644 index 0000000..c838166 --- /dev/null +++ b/packages/google-compute-engine-oslogin/Makefile @@ -0,0 +1,154 @@ +SHELL = /bin/sh + +BASENAME = oslogin +NAME = google-compute-engine-$(BASENAME) +MAJOR = 1 +MINOR = 4 +REVISION = 3 + +LIBNSS_CACHE_OSLOGIN = libnss_cache_$(BASENAME) +LIBNSS_CACHE_OSLOGIN_NAME = libnss_cache_$(NAME)-$(MAJOR).$(MINOR).$(REVISION).so +LIBNSS_CACHE_OSLOGIN_SONAME = $(LIBNSS_CACHE_OSLOGIN).so.2 +NSS_LIBRARY_NAME = libnss_$(NAME)-$(MAJOR).$(MINOR).$(REVISION).so +NSS_LIBRARY_SONAME = libnss_$(BASENAME).so.2 +NSS_INSTALL_PATH = /lib +PAM_INSTALL_PATH = /lib/security +AUTHKEYS_INSTALL_PATH = /usr/bin + +JSON_INCLUDE_PATH = /usr/include/json-c +INCLUDE_FLAGS = -I$(JSON_INCLUDE_PATH) + +CXX ?= g++ +CXXFLAGS += -fPIC# -Wall +CC ?= gcc +PAMFLAGS = $(LDFLAGS) $(INCLUDE_FLAGS) -shared +NSSFLAGS = $(LDFLAGS) $(INCLUDE_FLAGS) -shared -Wl,-soname,$(NSS_LIBRARY_SONAME) +LIBNSSFLAGS = $(LDFLAGS) -Wall -Wstrict-prototypes -fPIC -g +LIBNSS_SO_FLAGS = $(LIBNSSFLAGS) -shared -Wl,-soname,$(LIBNSS_CACHE_OSLOGIN_SONAME) + +# UTILS +UTILS_DIR = utils +UTILS_SRC = $(UTILS_DIR)/$(BASENAME)_utils.cc +UTILS = $(UTILS_DIR)/$(BASENAME)_utils.o + +# AUTHORIZED KEYS +AUTHKEYS_DIR = authorized_keys +AUTHKEYS_SRC = $(AUTHKEYS_DIR)/authorized_keys.cc +AUTHKEYS_BIN = google_authorized_keys + +# NSS +NSS = nss_$(BASENAME) +NSS_DIR = nss_module +NSS_SRC = $(NSS_DIR)/$(NSS).cc + +# NSS CACHE +NSS_CACHE = nss_cache +NSS_CACHE_DIR = nss_cache +NSS_CACHE_BIN = google_$(BASENAME)_nss_cache +NSS_CACHE_SRC = $(NSS_CACHE_DIR)/$(NSS_CACHE).cc + +# LIBNSS OSLOGIN CACHE +LIBNSS_CACHE_OSLOGIN_DIR = $(LIBNSS_CACHE_OSLOGIN) +LIBNSS_CACHE = nss_cache_$(BASENAME) +LIBNSS_CACHE_SRC = $(LIBNSS_CACHE_OSLOGIN_DIR)/$(LIBNSS_CACHE).c +LIBNSS_CACHE_OBJ = $(LIBNSS_CACHE_OSLOGIN_DIR)/$(LIBNSS_CACHE).o +LIBNSS_COMPAT = compat/getpwent_r +LIBNSS_COMPAT_SRC = $(LIBNSS_CACHE_OSLOGIN_DIR)/$(LIBNSS_COMPAT).c +LIBNSS_COMPAT_OBJ = $(LIBNSS_CACHE_OSLOGIN_DIR)/$(LIBNSS_COMPAT).o + +# PAM +PAM = pam_$(BASENAME) +PAM_DIR = pam_module +PAM_ADMIN = $(PAM)_admin +PAM_ADMIN_SRC = $(PAM_DIR)/$(PAM_ADMIN).cc +PAM_ADMIN_OBJ = $(PAM_DIR)/$(PAM_ADMIN).o +PAM_ADMIN_MOD = $(PAM_ADMIN).so +PAM_LOGIN = $(PAM)_login +PAM_LOGIN_SRC = $(PAM_DIR)/$(PAM_LOGIN).cc +PAM_LOGIN_OBJ = $(PAM_DIR)/$(PAM_LOGIN).o +PAM_LOGIN_MOD = $(PAM_LOGIN).so + +# HELPER SCRIPTS +BIN_DIR = bin +OSLOGIN_HELPER = $(BIN_DIR)/google_oslogin_control +BIN_INSTALL_PATH = /usr/bin + +# SELINUX POLICY +INSTALL_SELINUX = +POLICY_DIR = policy +SELINUX_INSTALL_NAME = oslogin.pp +SELINUX_MODULE = $(POLICY_DIR)/$(SELINUX_INSTALL_NAME) +SELINUX_INSTALL_PATH = /usr/share/selinux/packages + +LIBS = -lcurl -ljson-c +PAM_LIBS = -lpam $(LIBS) + +ifdef INSTALL_SELINUX +all: $(NSS) $(NSS_CACHE_BIN) $(LIBNSS_CACHE_OSLOGIN_NAME) $(PAM) $(AUTHKEYS_BIN) +else +all: $(NSS) $(NSS_CACHE_BIN) $(LIBNSS_CACHE_OSLOGIN_NAME) $(PAM) $(AUTHKEYS_BIN) +endif + +$(NSS): $(NSS_LIBRARY_SOURCE) $(UTILS) + $(CXX) $(CXXFLAGS) $(NSSFLAGS) -o $(NSS_LIBRARY_NAME) \ + $(NSS_SRC) $(UTILS) $(LIBS) + +$(NSS_CACHE_BIN): $(NSS_CACHE_SRC) $(UTILS_SRC) + $(CXX) $(CXXFLAGS) $(LDFLAGS) $(INCLUDE_FLAGS) -o $(NSS_CACHE_BIN) $(NSS_CACHE_SRC) $(UTILS_SRC) $(LIBS) + +$(LIBNSS_CACHE_OSLOGIN_NAME): $(LIBNSS_CACHE_OBJ) $(LIBNSS_COMPAT_OBJ) + $(CXX) $(LIBNSS_SO_FLAGS) -o $(LIBNSS_CACHE_OSLOGIN_NAME) $(LIBNSS_CACHE_OBJ) $(LIBNSS_COMPAT_OBJ) + +$(LIBNSS_CACHE_OBJ): $(LIBNSS_CACHE_SRC) + $(CC) $(LIBNSSFLAGS) -c -o $(LIBNSS_CACHE_OBJ) $(LIBNSS_CACHE_SRC) + +$(LIBNSS_COMPAT_OBJ): $(LIBNSS_COMPAT_SRC) + $(CC) $(LIBNSSFLAGS) -c -o $(LIBNSS_COMPAT_OBJ) $(LIBNSS_COMPAT_SRC) + +$(PAM): $(PAM_ADMIN_MOD) $(PAM_LOGIN_MOD) + +$(PAM_LOGIN_MOD): $(PAM_LOGIN_OBJ) $(UTILS) + $(CXX) $(PAMFLAGS) -o $(PAM_LOGIN_MOD) $(PAM_LOGIN_OBJ) $(UTILS) $(PAM_LIBS) + +$(PAM_ADMIN_MOD): $(PAM_ADMIN_OBJ) $(UTILS) + $(CXX) $(PAMFLAGS) -o $(PAM_ADMIN_MOD) $(PAM_ADMIN_OBJ) $(UTILS) $(PAM_LIBS) + +$(PAM_LOGIN_OBJ): $(PAM_LOGIN_SRC) + $(CXX) $(CXXFLAGS) -c $(PAM_LOGIN_SRC) -o $(PAM_LOGIN_OBJ) + +$(PAM_ADMIN_OBJ): $(PAM_ADMIN_SRC) + $(CXX) $(CXXFLAGS) -c $(PAM_ADMIN_SRC) -o $(PAM_ADMIN_OBJ) + +$(AUTHKEYS_BIN): $(AUTHKEYS_SRC) $(UTILS_SRC) + $(CXX) $(CXXFLAGS) $(LDFLAGS) $(INCLUDE_FLAGS) -o $(AUTHKEYS_BIN) $(AUTHKEYS_SRC) $(UTILS_SRC) $(LIBS) + +$(UTILS): $(UTILS_SRC) + $(CXX) $(CXXFLAGS) $(INCLUDE_FLAGS) -c $(UTILS_SRC) -o $(UTILS) + +install: $(NSS_LIBRARY_NAME) $(LIBNSS_OSLOGIN_CACHE_NAME) $(PAM_ADMIN_MOD) $(PAM_LOGIN_MOD) $(AUTHKEYS_BIN) $(NSS_CACHE_BIN) + mkdir -p $(DESTDIR)$(PREFIX)/$(NSS_INSTALL_PATH) + mkdir -p $(DESTDIR)$(PREFIX)/$(PAM_INSTALL_PATH) + mkdir -p $(DESTDIR)$(PREFIX)/$(AUTHKEYS_INSTALL_PATH) + mkdir -p $(DESTDIR)$(PREFIX)/$(BIN_INSTALL_PATH) + install -m 0644 $(LIBNSS_CACHE_OSLOGIN_NAME) $(DESTDIR)$(PREFIX)/$(NSS_INSTALL_PATH) + install -m 0644 $(NSS_LIBRARY_NAME) $(DESTDIR)$(PREFIX)/$(NSS_INSTALL_PATH) + install -m 0644 $(PAM_ADMIN_MOD) $(PAM_LOGIN_MOD) $(DESTDIR)$(PREFIX)/$(PAM_INSTALL_PATH) + install -m 0755 $(AUTHKEYS_BIN) $(DESTDIR)$(PREFIX)/$(AUTHKEYS_INSTALL_PATH) + install -m 0755 $(OSLOGIN_HELPER) $(DESTDIR)$(PREFIX)/$(BIN_INSTALL_PATH) + install -m 0755 $(NSS_CACHE_BIN) $(DESTDIR)$(PREFIX)/$(BIN_INSTALL_PATH) +ifdef INSTALL_SELINUX + mkdir -p $(DESTDIR)$(PREFIX)/$(SELINUX_INSTALL_PATH) + install -T -m 0644 $(SELINUX_MODULE) $(DESTDIR)$(PREFIX)/$(SELINUX_INSTALL_PATH)/$(SELINUX_INSTALL_NAME) +endif + +uninstall: + rm -f $(DESTDIR)$(PREFIX)/$(NSS_INSTALL_PATH)/$(LIBNSS_CACHE_OSLOGIN_NAME) + rm -f $(DESTDIR)$(PREFIX)/$(NSS_INSTALL_PATH)/$(NSS_LIBRARY_NAME) + rm -f $(DESTDIR)$(PREFIX)/$(PAM_INSTALL_PATH)/$(PAM_ADMIN_MOD) + rm -f $(DESTDIR)$(PREFIX)/$(PAM_INSTALL_PATH)/$(PAM_LOGIN_MOD) + rm -f $(DESTDIR)$(PREFIX)/$(AUTHKEYS_INSTALL_PATH)/$(AUTHKEYS_BIN) + rm -f $(DESTDIR)$(PREFIX)/$(BIN_INSTALL_PATH)/$(OSLOGIN_HELPER) + rm -f $(DESTDIR)$(PREFIX)/$(BIN_INSTALL_PATH)/$(NSS_CACHE_BIN) +clean: + rm -f $(UTILS) $(NSS_LIBRARY_NAME) $(LIBNSS_CACHE_OSLOGIN_NAME) $(LIBNSS_CACHE_OBJ) $(LIBNSS_COMPAT_OBJ) $(PAM_ADMIN_OBJ) $(PAM_ADMIN_MOD) $(PAM_LOGIN_OBJ) $(PAM_LOGIN_MOD) $(AUTHKEYS_BIN) $(NSS_CACHE_BIN) + diff --git a/packages/google-compute-engine-oslogin/README.md b/packages/google-compute-engine-oslogin/README.md new file mode 100644 index 0000000..ce15018 --- /dev/null +++ b/packages/google-compute-engine-oslogin/README.md @@ -0,0 +1,245 @@ +## OS Login Guest Environment for Google Compute Engine + +This package enables Google Cloud OS Login features on Google Compute Engine +instances. + +**Table of Contents** + +* [Overview](#overview) +* [Components](#components) + * [Authorized Keys Command](#authorized-keys-command) + * [NSS Module](#nss-module) + * [PAM Module](#pam-module) + * [Utils](#utils) +* [Utility Directories](#utility-directories) + * [bin](#bin) + * [packaging](#packaging) + * [policy](#policy) +* [Source Packages](#source-packages) + * [DEB](#deb) + * [RPM](#rpm) +* [Version Updates](#version-updates) + +## Overview + +The OS Login package has the following components: + +* **Authorized Keys Command** to fetch SSH keys from the user's OS Login + profile and make them available to sshd. +* **NSS Module** provides support for making OS Login user and group + information available to the system, using NSS (Name Service Switch) + functionality. +* **PAM Module** provides authorization and authentication support allowing + the system to use data stored in Google Cloud IAM permissions to control + both, the ability to log into an instance, and to perform operations as root + (sudo). +* **Utils** provides common code to support the components listed above. + +In addition to the main components, there are also utilities for packaging and +installing these components: + +* **bin** contains a shell script for activating/deactivating the package + components. +* **packaging** contains files used to generate `.deb` and `.rpm` packages for + the OS Login components. +* **policy** contains SELinux "type enforcement" files for configuring SELinux + on CentOS/RHEL systems. + +## Components + +#### Authorized Keys Command + +The `google_authorized_keys` binary is designed to be used with the sshd +[AuthorizedKeysCommand](https://linux.die.net/man/5/sshd_config) option in +`sshd_config`. It does the following: + +* Reads the user's profile information from the metadata server. + ``` + http://metadata.google.internal/computeMetadata/v1/oslogin/users?username=<username> + ``` +* Checks to make sure that the user is authorized to log in. + ``` + http://metadata.google.internal/computeMetadata/v1/oslogin/authorize?email=<user_email>&policy=login + ``` +* If the check is successful, returns the SSH keys associated with the user + for use by sshd. + +#### NSS Module + +The `nss_oslogin` module is built and installed in the appropriate `lib` +directory as a shared object with the name `libnss_oslogin.so.2`. The module is +then activated by an `oslogin` entry in `/etc/nsswitch.conf`. The NSS module +supports looking up `passwd` entries from the metadata server via +`getent passwd`. + +* To return a list of all users, the NSS module queries: + ``` + http://metadata.google.internal/computeMetadata/v1/oslogin/users?pagesize=<pagesize> + ``` +* To look up a user by username, the NSS module queries: + ``` + http://metadata.google.internal/computeMetadata/v1/oslogin/users?username=<username + ``` +* To look up a user by UID, the NSS module queries: + ``` + http://metadata.google.internal/computeMetadata/v1/oslogin/users?uid=<uid> + ``` + +#### PAM Module + +The `pam_module` directory contains two modules used by Linux PAM (Pluggable +Authentication Modules). + +The first module, `pam_oslogin_login.so`, determines whether a given user is +allowed to SSH into an instance. It is activated by adding an +`account requisite` line to the PAM sshd config file and does the following: + +* Retrieves the user's profile information from the metadata server. + ``` + http://metadata.google.internal/computeMetadata/v1/oslogin/users?username=<username> + ``` +* If the user has OS Login profile information (as opposed to a local user + account), confirms whether the user has permissions to SSH into the + instance. + ``` + http://metadata.google.internal/computeMetadata/v1/oslogin/authorize?email=<user_email>&policy=login + ``` +* If the user is a local user account or is authorized, PAM returns a success + message and SSH can proceed. Otherwise, PAM returns a denied message and the + SSH check will fail. + +The second module, `pam_oslogin_admin.so`, determines whether a given user +should have admin (sudo) permissions on the instance. It is activated by adding +an `account optional` line to the PAM sshd config file and does the following: + +* Retrieves the user's profile information from the metadata server. + ``` + http://metadata.google.internal/computeMetadata/v1/oslogin/users?username=<username> + ``` +* If the user is a local user account, the module exits with success. +* If the user is an OS Login user, the module perform an authorization check + to determine if the user has admin permissions. + ``` + http://metadata.google.internal/computeMetadata/v1/oslogin/authorize?email=<user_email>&policy=adminLogin + ``` +* If the user is authorized as an admin, a file with the username is added to + `/var/google-sudoers.d/`. The file gives the user sudo privileges. +* If the authorization check fails for admin permissions, the file is removed + from `/var/google-sudoers.d/` if it exists. + +#### Utils + +`oslogin_utils` contains common functions for making HTTP calls, +interacting with the metadata server, and for parsing JSON objects. + +## Utility Directories + +#### bin + +The `bin` directory contains a shell script called `google_oslogin_control` that +activates or deactivates the OS Login features. It is called in the pre and post +install scripts in the `.deb` and `.rpm` packages. The control file performs the +following tasks: + +* Adds (or removes) AuthorizedKeysCommand and AuthorizedKeysCommandUser lines + to (from) `sshd_config` and restarts sshd. +* Adds (or removes) `oslogin` to (from) `nsswitch.conf`. +* Adds (or removes) the `account` entries to (from) the PAM sshd config. Also + adds (or removes) the `pam_mkhomedir.so` module to automatically create the + home directory for an OS Login user. +* Creates (or deletes) the `/var/google-sudoers.d/` directory, and a file + called `google-oslogin` in `/etc/sudoers.d/` that includes the directory. + +#### packaging + +The `packaging` directory contains files for creating `.deb` and `.rpm` +packages. See [Source Packages](#source-packages) for details. + +#### policy + +The `policy` directory contains `.te` (type enforcement) files used by SELinux +to give the OS Login features the appropriate SELinux permissions. These are +compiled using `checkmodule` and `semodule_package` to create an `oslogin.pp` +that is intstalled in the appropriate SELinux directory. + +## Source Packages + +There is currently support for creating packages for the following distros: +* Debian 8 +* Debian 9 +* CentOS/RHEL 6 +* CentOS/RHEL 7 + +#### DEB + +_Note: the `packaging/setup_deb.sh` script performs these steps, but is not +production quality._ + +1. Install build dependencies: + ``` + sudo apt-get -y install make g++ libcurl4-openssl-dev libjson-c-dev libpam-dev + ``` +1. Install deb creation tools: + ``` + sudo apt-get -y install debhelper devscripts build-essential + ``` +1. Create a compressed tar file named + `google-compute-engine-oslogin_M.M.R.orig.tar.gz` using the files in this + directory, excluding the `packaging` directory (where M.M.R is the version + number). +1. In a separate directory, extract the `.orig.tar.gz` file and copy the + appropriate `debian` directory into the top level. (e.g. When working on + Debian 8, copy the `debian8` directory to a directory named `debian` within + the code directory.) +1. To build the package, run the command + ``` + debuild -us -uc + ``` + +#### RPM + +_Note: the `packaging/setup_rpm.sh` script performs these steps, but is not +production quality._ + +1. Install build dependencies: + ``` + sudo yum -y install make gcc-c++ libcurl-devel json-c json-c-devel pam-devel policycoreutils-python + ``` +1. Install rpm creation tools: + ``` + sudo yum -y install rpmdevtools + ``` +1. Create a compressed tar file named + `google-compute-engine-oslogin_M.M.R.orig.tar.gz` using the files in this + directory, excluding the `packaging` directory (where M.M.R is the version + number). +1. In a separate location, create a directory called `rpmbuild` and a + subdirectory called `SOURCES`. Copy the `.orig.tar.gz` file into the + `SOURCES` directory. +1. Copy the `SPECS` directory from the `rpmbuild` directory here into the + `rpmbuild` directory you created. +1. To build the package, run the command: + ``` + rpmbuild --define "_topdir /path/to/rpmbuild" -ba /path/to/rpmbuild/SPECS/google-compute-engine-oslogin.spec + ``` + + +## Version Updates + +When updating version numbers, changes need to be made in a few different +places: + +* `Makefile` Update the MAJOR, MINOR, and REVISION variables. +* `packaging/debian8/changelog` Add a new entry with the new version. +* `packaging/debian9/changelog` Add a new entry with the new version. +* `packaging/debian10/changelog` Add a new entry with the new version. +* `packaging/debian8/google-compute-engine-oslogin.links` Update the libnss + version string. +* `packaging/debian9/google-compute-engine-oslogin.links` Update the libnss + version string. +* `packaging/debian10/google-compute-engine-oslogin.links` Update the libnss + version string. +* `packaging/rpmbuild/SPECS/google-compute-engine-oslogin.spec` Update the + Version field. +* `packaging/setup_deb.sh` Update VERSION variable. +* `packaging/setup_rpm.sh` Update VERSION variable. diff --git a/packages/google-compute-engine-oslogin/authorized_keys/authorized_keys.cc b/packages/google-compute-engine-oslogin/authorized_keys/authorized_keys.cc new file mode 100644 index 0000000..24d1b26 --- /dev/null +++ b/packages/google-compute-engine-oslogin/authorized_keys/authorized_keys.cc @@ -0,0 +1,78 @@ +// Copyright 2017 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include <iostream> +#include <sstream> +#include <string> + +#include "../utils/oslogin_utils.h" + +using std::cout; +using std::endl; +using std::string; + +using oslogin_utils::HttpGet; +using oslogin_utils::ParseJsonToSuccess; +using oslogin_utils::ParseJsonToKey; +using oslogin_utils::ParseJsonToEmail; +using oslogin_utils::ParseJsonToSshKeys; +using oslogin_utils::UrlEncode; +using oslogin_utils::kMetadataServerUrl; + +int main(int argc, char* argv[]) { + if (argc != 2) { + cout << "usage: authorized_keys [username]" << endl; + return 1; + } + std::stringstream url; + url << kMetadataServerUrl << "users?username=" << UrlEncode(argv[1]); + string user_response; + long http_code = 0; + if (!HttpGet(url.str(), &user_response, &http_code) || + user_response.empty() || http_code != 200) { + if (http_code == 404) { + // Return 0 if the user is not an oslogin user. If we returned a failure + // code, we would populate auth.log with useless error messages. + return 0; + } + return 1; + } + string email; + if (!ParseJsonToEmail(user_response, &email) || email.empty()) { + return 1; + } + // Redundantly verify that this user has permission to log in to this VM. + // Normally the PAM module determines this, but in the off chance a transient + // error causes the PAM module to permit a user without login permissions, + // perform the same check here. If this fails, we can guarantee that we won't + // accidentally allow a user to log in without permissions. + url.str(""); + url << kMetadataServerUrl << "authorize?email=" << UrlEncode(email) + << "&policy=login"; + string auth_response; + if (!HttpGet(url.str(), &auth_response, &http_code) || http_code != 200 || + auth_response.empty()) { + return 1; + } + if (!ParseJsonToSuccess(auth_response)) { + return 1; + } + // At this point, we've verified the user can log in. Grab the ssh keys from + // the user response. + std::vector<string> ssh_keys = ParseJsonToSshKeys(user_response); + for (int i = 0; i < ssh_keys.size(); i++) { + cout << ssh_keys[i] << endl; + } + return 0; +} diff --git a/packages/google-compute-engine-oslogin/bin/google_oslogin_control b/packages/google-compute-engine-oslogin/bin/google_oslogin_control new file mode 100644 index 0000000..65c0f15 --- /dev/null +++ b/packages/google-compute-engine-oslogin/bin/google_oslogin_control @@ -0,0 +1,378 @@ +#!/bin/sh +# Copyright 2017 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +nss_config="/etc/nsswitch.conf" +pam_config="/etc/pam.d/sshd" +sshd_config="/etc/ssh/sshd_config" +sudoers_dir="/var/google-sudoers.d" +users_dir="/var/google-users.d" +sudoers_file="/etc/sudoers.d/google-oslogin" +added_comment="# Added by Google Compute Engine OS Login." +sshd_block="#### Google OS Login control. Do not edit this section. ####" +sshd_end_block="#### End Google OS Login control section. ####" + +is_freebsd() { + [ "$(uname)" = "FreeBSD" ] + return $? +} + +# Update nsswitch.conf to include OS Login NSS module for passwd. +modify_nsswitch_conf() { + local nss_config="${1:-${nss_config}}" + + if ! grep -q '^passwd:.*oslogin' ${nss_config}; then + $sed -i"" '/^passwd:/ s/$/ cache_oslogin oslogin/' ${nss_config} + fi + + if is_freebsd && grep -q '^passwd:.*compat'; then + $sed -i"" '/^passwd:/ s/compat/files/' ${nss_config} + fi +} + +restore_nsswitch_conf() { + local nss_config="${1:-${nss_config}}" + + $sed -i"" '/^passwd:/ s/ cache_oslogin oslogin//' ${nss_config} + if is_freebsd; then + $sed -i"" '/^passwd:/ s/files/compat/' ${nss_config} + fi +} + +modify_sshd_conf() ( + set -e + + local sshd_config="${1:-${sshd_config}}" + + local sshd_auth_keys_command="AuthorizedKeysCommand /usr/bin/google_authorized_keys" + local sshd_auth_keys_command_user="AuthorizedKeysCommandUser root" + local sshd_auth_methods="AuthenticationMethods publickey,keyboard-interactive" + local sshd_challenge="ChallengeResponseAuthentication yes" + + # Update directives for EL 6. + if grep -qs "release 6" /etc/redhat-release; then + sshd_auth_keys_command_user="AuthorizedKeysCommandRunAs root" + sshd_auth_methods="RequiredAuthentications2 publickey,keyboard-interactive" + fi + + add_or_update_sshd() { + local entry="$1" + local sshd_config="$2" + local directive="$(echo "$entry" | cut -d' ' -f1)" + local value="$(echo "$entry" | cut -d' ' -f2-)" + + # Check if directive is present. + if grep -Eq "^\s*${directive}" "$sshd_config"; then + # Check if value is incorrect. + if ! grep -Eq "^\s*${directive}(\s|=)+${value}" "$sshd_config"; then + # Comment out the line (because sshd_config is first-directive-found) + # and add to end section. + $sed -i"" -E "/^\s*${directive}/ s/^/${added_comment}\n#/" "$sshd_config" + $sed -i"" "/$sshd_end_block/ i${entry}" "$sshd_config" + fi + else + $sed -i"" "/$sshd_end_block/ i${entry}" "$sshd_config" + fi + } + + # Setup Google config block. + if ! grep -q "$sshd_block" "$sshd_config"; then + # Remove old-style additions. + $sed -i"" "/${added_comment}/,+1d" "$sshd_config" + /bin/echo -e "\n\n${sshd_block}\n${sshd_end_block}" >> "$sshd_config" + fi + + for entry in "$sshd_auth_keys_command" "$sshd_auth_keys_command_user"; do + add_or_update_sshd "$entry" "$sshd_config" + done + + if [ -n "$two_factor" ]; then + for entry in "$sshd_auth_methods" "$sshd_challenge"; do + add_or_update_sshd "$entry" "$sshd_config" + done + fi +) + +restore_sshd_conf() { + local sshd_config="${1:-${sshd_config}}" + + if ! grep -q "$sshd_block" "$sshd_config"; then + # Remove old-style additions. + $sed -i"" "/${added_comment}/,+1d" "$sshd_config" + else + # Uncomment commented-out fields and remove Google config block. + $sed -i"" "/${added_comment}/{n;s/^#//}" "$sshd_config" + $sed -i"" "/${added_comment}/d" "$sshd_config" + $sed -i"" "/${sshd_block}/,/${sshd_end_block}/d" "$sshd_config" + fi +} + +# Inserts pam modules to relevant pam stacks if missing. +modify_pam_sshd() ( + set -e + + local pam_config="${1:-${pam_config}}" + + local pam_auth_oslogin="auth [success=done perm_denied=bad default=ignore] pam_oslogin_login.so" + local pam_account_oslogin="account [success=ok default=ignore] pam_oslogin_admin.so" + local pam_account_admin="account [success=ok ignore=ignore default=die] pam_oslogin_login.so" + local pam_session_homedir="session [success=ok default=ignore] pam_mkhomedir.so" + + # In FreeBSD, the used flags are not supported, replacing them with the + # previous ones (requisite and optional). + if is_freebsd; then + pam_auth_oslogin="auth optional pam_oslogin_login.so" + pam_account_oslogin="account optional pam_oslogin_admin.so" + pam_account_admin="account requisite pam_oslogin_login.so" + pam_session_homedir="session optional pam_mkhomedir.so" + fi + + local added_config="" + + # For COS this file is solely includes, so simply prepend the new config, + # making each entry the top of its stack. + if [ -e /etc/os-release ] && grep -q "ID=cos" /etc/os-release; then + added_config="${added_comment}\n" + for cfg in "$pam_account_admin" "$pam_account_oslogin" \ + "$pam_session_homedir"; do + grep -qE "^${cfg%% *}.*${cfg##* }" ${pam_config} || added_config+="${cfg}\n" + done + + if [ -n "$two_factor" ]; then + grep -q "$pam_auth_oslogin" "$pam_config" || added_config+="${pam_auth_oslogin}\n" + fi + + $sed -i"" "1i ${added_config}\n\n" "$pam_config" + + return 0 + fi + + # Find the distro-specific insertion point for auth and insert OS Login + # two-factor auth module if requested. + if [ -n "$two_factor" ] && ! grep -q "$pam_auth_oslogin" ${pam_config}; then + if [ -e /etc/debian_version ]; then + # Get location of common-auth and check if preceding line is a comment. + insert=$($sed -rn "/^@include\s+common-auth/=" "$pam_config") + $sed -n "$((insert-1))p" "$pam_config" | grep -q '^#' && insert=$((insert-1)) + elif [ -e /etc/redhat-release ]; then + # Get location of password-auth. + insert=$($sed -rn "/^auth\s+(substack|include)\s+password-auth/=" "$pam_config") + elif [ -e /etc/os-release ] && grep -q 'ID="sles"' /etc/os-release; then + # Get location of common-auth. + insert=$($sed -rn "/^auth\s+include\s+common-auth/=" "$pam_config") + fi + + # Insert pam_auth_oslogin at insertion point detected above. + if [ -n "$insert" ]; then + added_config="${added_comment}\n${pam_auth_oslogin}" + $sed -i"" "${insert}i ${added_config}" "$pam_config" + fi + fi + + # Append account modules at end of `account` stack. + if ! grep -qE '^account.*oslogin' ${pam_config}; then + added_config="\\\n${added_comment}\n${pam_account_admin}\n${pam_account_oslogin}" + account_end=$($sed -n '/^account/=' "$pam_config" | tail -1) + $sed -i"" "${account_end}a ${added_config}" "$pam_config" + fi + + # Append mkhomedir module at end of `session` stack. + if ! grep -qE '^session.*mkhomedir' ${pam_config}; then + added_config="\\\n${added_comment}\n${pam_session_homedir}" + session_end=$($sed -n '/^session/=' "$pam_config" | tail -1) + $sed -i"" "${session_end}a ${added_config}" "$pam_config" + fi + + # TODO: SLES, others? +) + +restore_pam_sshd() { + local pam_config="${1:-${pam_config}}" + + $sed -i"" "/${added_comment}/d" "$pam_config" + $sed -i"" "/pam_oslogin/d" "$pam_config" + $sed -i"" "/^session.*mkhomedir/d" "$pam_config" +} + +restart_service() { + local service="$1" + + # The other options will be wrappers to systemctl on + # systemd-enabled systems, so stop if found. + if readlink -f /sbin/init|grep -q systemd; then + if systemctl is-active --quiet "$service"; then + systemctl restart "$service" + return $? + else + return 0 + fi + fi + + # Use the service helper if it exists. + if command -v service > /dev/null; then + if ! service "$service" status 2>&1 | grep -q unrecognized; then + service "$service" restart + return $? + else + return 0 + fi + fi + + # Fallback to trying sysvinit script of the same name. + if command -v /etc/init.d/"$service" > /dev/null; then + if /etc/init.d/"$service" status > /dev/null 2>&1; then + /etc/init.d/"$service" restart + return $? + else + return 0 + fi + fi + + # We didn't find any way to restart this service. + return 1 +} + +# Restart sshd unless --norestartsshd flag is set. +restart_sshd() { + if [ -n "$no_restart_sshd" ]; then + return 0 + fi + echo "Restarting SSHD" + for svc in "ssh" "sshd"; do + restart_service "$svc" + done +} + +restart_svcs() { + echo "Restarting optional services." + for svc in "nscd" "unscd" "systemd-logind"; do + restart_service "$svc" + done +} + +setup_google_dirs() { + for dir in "$sudoers_dir" "$users_dir"; do + [ -d "$dir" ] && continue + mkdir -p "$dir" + chmod 750 "$dir" + if fixfiles=$(command -v fixfiles); then + $fixfiles restore "$dir" + fi + done + echo "#includedir ${sudoers_dir}" > "$sudoers_file" +} + +remove_google_dirs() { + for dir in "$sudoers_dir" "$users_dir"; do + rm -rf "$dir" + done + rm -f "$sudoers_file" +} + +activate() { + for func in modify_sshd_conf modify_nsswitch_conf \ + modify_pam_sshd setup_google_dirs restart_svcs restart_sshd; do + $func + [ $? -eq 0 ] || return 1 + done +} + +deactivate() { + for func in remove_google_dirs restore_nsswitch_conf \ + restore_sshd_conf restore_pam_sshd restart_svcs restart_sshd; do + $func + done +} + +# get_status checks each file for appropriate updates and exits on first +# failure. Checks for two factor config changes only if requested. +get_status() ( + set -e + + grep -Eq '^account.*oslogin' "$pam_config" + grep -Eq 'google_authorized_keys' "$sshd_config" + grep -Eq 'passwd:.*oslogin' "$nss_config" + if [ -n "$two_factor" ]; then + grep -Eq '^auth.*oslogin' "$pam_config" + grep -Eq '^(AuthenticationMethods|RequiredAuthentications2).*publickey,keyboard-interactive' "$sshd_config" + fi +) + +usage() { + echo "Usage: $(basename "$0") {activate|deactivate|status} [--norestartsshd] [--twofactor]" + echo "This script will activate or deactivate the features for" + echo "Google Compute Engine OS Login and (optionally) two-factor authentication." + echo "This script must be run as root." + exit 1 +} + + +# Main +if [ $(id -u) -ne 0 ] || [ $# -lt 1 ]; then + usage +fi + +sed="sed" +is_freebsd && sed="gsed" + +while [ $# -gt 0 ]; do + case "$1" in + --norestartsshd) + no_restart_sshd="true" + shift + ;; + --twofactor) + two_factor="true" + shift + ;; + activate) + action="activate" + shift + ;; + deactivate) + action="deactivate" + shift + ;; + status) + action="status" + shift + ;; + *) + shift + ;; + esac +done + +case "$action" in + activate) + echo "Activating Google Compute Engine OS Login." + activate + if [ $? -ne 0 ]; then + echo "Failed to apply changes, rolling back" + deactivate + exit 1 + fi + ;; + deactivate) + echo "Deactivating Google Compute Engine OS Login." + deactivate + ;; + status) + get_status + exit $? + ;; + *) + usage + ;; +esac diff --git a/packages/google-compute-engine-oslogin/libnss_cache_oslogin/compat/getpwent_r.c b/packages/google-compute-engine-oslogin/libnss_cache_oslogin/compat/getpwent_r.c new file mode 100644 index 0000000..b1be6fc --- /dev/null +++ b/packages/google-compute-engine-oslogin/libnss_cache_oslogin/compat/getpwent_r.c @@ -0,0 +1,87 @@ +/* + * ---------------------------------------------------------------------- + * Copyright © 2005-2014 Rich Felker, et al. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + * ---------------------------------------------------------------------- + * + * Adapted from http://www.musl-libc.org/ for libnss-cache + * Copyright © 2015 Kevin Bowling <k@kev009.com> + */ + +#include <sys/param.h> + +#ifdef BSD + +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <errno.h> +#include <string.h> + +static unsigned atou(char **s) +{ + unsigned x; + for (x=0; **s-'0'<10U; ++*s) x=10*x+(**s-'0'); + return x; +} + +int fgetpwent_r(FILE *f, struct passwd *pw, char *line, size_t size, struct passwd **res) +{ + char *s; + int rv = 0; + for (;;) { + line[size-1] = '\xff'; + if ( (fgets(line, size, f) == NULL) || ferror(f) || line[size-1] != '\xff' ) { + rv = (line[size-1] != '\xff') ? ERANGE : ENOENT; + line = 0; + pw = 0; + break; + } + line[strcspn(line, "\n")] = 0; + + s = line; + pw->pw_name = s++; + if (!(s = strchr(s, ':'))) continue; + + *s++ = 0; pw->pw_passwd = s; + if (!(s = strchr(s, ':'))) continue; + + *s++ = 0; pw->pw_uid = atou(&s); + if (*s != ':') continue; + + *s++ = 0; pw->pw_gid = atou(&s); + if (*s != ':') continue; + + *s++ = 0; pw->pw_gecos = s; + if (!(s = strchr(s, ':'))) continue; + + *s++ = 0; pw->pw_dir = s; + if (!(s = strchr(s, ':'))) continue; + + *s++ = 0; pw->pw_shell = s; + break; + } + *res = pw; + if (rv) errno = rv; + return rv; +} + +#endif // ifdef BSD diff --git a/packages/google-compute-engine-oslogin/libnss_cache_oslogin/nss_cache_oslogin.c b/packages/google-compute-engine-oslogin/libnss_cache_oslogin/nss_cache_oslogin.c new file mode 100644 index 0000000..56fe8a0 --- /dev/null +++ b/packages/google-compute-engine-oslogin/libnss_cache_oslogin/nss_cache_oslogin.c @@ -0,0 +1,437 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// An NSS module which adds supports for file /etc/oslogin_passwd.cache + +#include "nss_cache_oslogin.h" + +#include <sys/mman.h> + +// Locking implementation: use pthreads. +#include <pthread.h> +static pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; +#define NSS_CACHE_OSLOGIN_LOCK() \ + do { \ + pthread_mutex_lock(&mutex); \ + } while (0) +#define NSS_CACHE_OSLOGIN_UNLOCK() \ + do { \ + pthread_mutex_unlock(&mutex); \ + } while (0) + +static FILE *p_file = NULL; +static char p_filename[NSS_CACHE_OSLOGIN_PATH_LENGTH] = + "/etc/oslogin_passwd.cache"; +#ifdef BSD +extern int fgetpwent_r(FILE *, struct passwd *, char *, size_t, + struct passwd **); +#endif // ifdef BSD + +/* Common return code routine for all *ent_r_locked functions. + * We need to return TRYAGAIN if the underlying files guy raises ERANGE, + * so that our caller knows to try again with a bigger buffer. + */ + +static inline enum nss_status _nss_cache_oslogin_ent_bad_return_code( + int errnoval) { + enum nss_status ret; + + switch (errnoval) { + case ERANGE: + DEBUG("ERANGE: Try again with a bigger buffer\n"); + ret = NSS_STATUS_TRYAGAIN; + break; + case ENOENT: + default: + DEBUG("ENOENT or default case: Not found\n"); + ret = NSS_STATUS_NOTFOUND; + }; + return ret; +} + +// +// Binary search routines below here +// + +int _nss_cache_oslogin_bsearch2_compare(const void *key, const void *value) { + struct nss_cache_oslogin_args *args = (struct nss_cache_oslogin_args *)key; + const char *value_text = (const char *)value; + + // Using strcmp as the generation of the index sorts without + // locale awareness. + return strcmp(args->lookup_key, value_text); +} + +enum nss_status _nss_cache_oslogin_bsearch2(struct nss_cache_oslogin_args *args, + int *errnop) { + enum nss_cache_oslogin_match (*lookup)( + FILE *, struct nss_cache_oslogin_args *) = args->lookup_function; + FILE *file = NULL; + FILE *system_file_stream = NULL; + struct stat system_file; + struct stat sorted_file; + enum nss_status ret = 100; + long offset = 0; + void *mapped_data = NULL; + + file = fopen(args->sorted_filename, "r"); + if (file == NULL) { + DEBUG("error opening %s\n", args->sorted_filename); + return NSS_STATUS_UNAVAIL; + } + + // if the sorted file is older than the system file, do not risk stale + // data and abort + // TODO(vasilios): should be a compile or runtime option + if (stat(args->system_filename, &system_file) != 0) { + DEBUG("failed to stat %s\n", args->system_filename); + fclose(file); + return NSS_STATUS_UNAVAIL; + } + if (fstat(fileno(file), &sorted_file) != 0) { + DEBUG("failed to stat %s\n", args->sorted_filename); + fclose(file); + return NSS_STATUS_UNAVAIL; + } + if (difftime(system_file.st_mtime, sorted_file.st_mtime) > 0) { + DEBUG("%s may be stale, aborting lookup\n", args->sorted_filename); + fclose(file); + return NSS_STATUS_UNAVAIL; + } + + mapped_data = + mmap(NULL, sorted_file.st_size, PROT_READ, MAP_PRIVATE, fileno(file), 0); + if (mapped_data == MAP_FAILED) { + DEBUG("mmap failed\n"); + fclose(file); + return NSS_STATUS_UNAVAIL; + } + + const char *data = (const char *)mapped_data; + while (*data != '\n') { + ++data; + } + long entry_size = data - (const char *)mapped_data + 1; + long entry_count = sorted_file.st_size / entry_size; + + void *entry = bsearch(args, mapped_data, entry_count, entry_size, + &_nss_cache_oslogin_bsearch2_compare); + if (entry != NULL) { + const char *entry_text = entry; + sscanf(entry_text + strlen(entry_text) + 1, "%ld", &offset); + } + + if (munmap(mapped_data, sorted_file.st_size) == -1) { + DEBUG("munmap failed\n"); + } + fclose(file); + + if (entry == NULL) { + return NSS_STATUS_NOTFOUND; + } + + system_file_stream = fopen(args->system_filename, "r"); + if (system_file_stream == NULL) { + DEBUG("error opening %s\n", args->system_filename); + return NSS_STATUS_UNAVAIL; + } + + if (fseek(system_file_stream, offset, SEEK_SET) != 0) { + DEBUG("fseek fail\n"); + return NSS_STATUS_UNAVAIL; + } + + switch (lookup(system_file_stream, args)) { + case NSS_CACHE_OSLOGIN_EXACT: + ret = NSS_STATUS_SUCCESS; + break; + case NSS_CACHE_OSLOGIN_ERROR: + if (errno == ERANGE) { + // let the caller retry + *errnop = errno; + ret = _nss_cache_oslogin_ent_bad_return_code(*errnop); + } + break; + default: + ret = NSS_STATUS_UNAVAIL; + break; + } + + fclose(system_file_stream); + return ret; +} + +// +// Routines for passwd map defined below here +// + +// _nss_cache_oslogin_setpwent_path() +// Helper function for testing + +extern char *_nss_cache_oslogin_setpwent_path(const char *path) { + DEBUG("%s %s\n", "Setting p_filename to", path); + return strncpy(p_filename, path, NSS_CACHE_OSLOGIN_PATH_LENGTH - 1); +} + +// _nss_cache_oslogin_pwuid_wrap() +// Internal wrapper for binary searches, using uid-specific calls. + +static enum nss_cache_oslogin_match _nss_cache_oslogin_pwuid_wrap( + FILE *file, struct nss_cache_oslogin_args *args) { + struct passwd *result = args->lookup_result; + uid_t *uid = args->lookup_value; + + if (fgetpwent_r(file, result, args->buffer, args->buflen, &result) == 0) { + if (result->pw_uid == *uid) { + DEBUG("SUCCESS: found user %d:%s\n", result->pw_uid, result->pw_name); + return NSS_CACHE_OSLOGIN_EXACT; + } + DEBUG("Failed match at uid %d\n", result->pw_uid); + if (result->pw_uid > *uid) { + return NSS_CACHE_OSLOGIN_HIGH; + } else { + return NSS_CACHE_OSLOGIN_LOW; + } + } + + return NSS_CACHE_OSLOGIN_ERROR; +} + +// _nss_cache_oslogin_pwnam_wrap() +// Internal wrapper for binary searches, using username-specific calls. + +static enum nss_cache_oslogin_match _nss_cache_oslogin_pwnam_wrap( + FILE *file, struct nss_cache_oslogin_args *args) { + struct passwd *result = args->lookup_result; + char *name = args->lookup_value; + int ret; + + if (fgetpwent_r(file, result, args->buffer, args->buflen, &result) == 0) { + ret = strcoll(result->pw_name, name); + if (ret == 0) { + DEBUG("SUCCESS: found user %s\n", result->pw_name); + return NSS_CACHE_OSLOGIN_EXACT; + } + DEBUG("Failed match at name %s\n", result->pw_name); + if (ret > 0) { + return NSS_CACHE_OSLOGIN_HIGH; + } else { + return NSS_CACHE_OSLOGIN_LOW; + } + } + + return NSS_CACHE_OSLOGIN_ERROR; +} + +// _nss_cache_oslogin_setpwent_locked() +// Internal setup routine + +static enum nss_status _nss_cache_oslogin_setpwent_locked(void) { + DEBUG("%s %s\n", "Opening", p_filename); + p_file = fopen(p_filename, "r"); + + if (p_file) { + return NSS_STATUS_SUCCESS; + } else { + return NSS_STATUS_UNAVAIL; + } +} + +// _nss_cache_oslogin_setpwent() +// Called by NSS to open the passwd file +// 'stayopen' parameter is ignored. + +enum nss_status _nss_cache_oslogin_setpwent(int stayopen) { + enum nss_status ret; + NSS_CACHE_OSLOGIN_LOCK(); + ret = _nss_cache_oslogin_setpwent_locked(); + NSS_CACHE_OSLOGIN_UNLOCK(); + return ret; +} + +// _nss_cache_oslogin_endpwent_locked() +// Internal close routine + +static enum nss_status _nss_cache_oslogin_endpwent_locked(void) { + DEBUG("Closing passwd.cache\n"); + if (p_file) { + fclose(p_file); + p_file = NULL; + } + return NSS_STATUS_SUCCESS; +} + +// _nss_cache_oslogin_endpwent() +// Called by NSS to close the passwd file + +enum nss_status _nss_cache_oslogin_endpwent(void) { + enum nss_status ret; + NSS_CACHE_OSLOGIN_LOCK(); + ret = _nss_cache_oslogin_endpwent_locked(); + NSS_CACHE_OSLOGIN_UNLOCK(); + return ret; +} + +// _nss_cache_oslogin_getpwent_r_locked() +// Called internally to return the next entry from the passwd file + +static enum nss_status _nss_cache_oslogin_getpwent_r_locked( + struct passwd *result, char *buffer, size_t buflen, int *errnop) { + enum nss_status ret = NSS_STATUS_SUCCESS; + + if (p_file == NULL) { + DEBUG("p_file == NULL, going to setpwent\n"); + ret = _nss_cache_oslogin_setpwent_locked(); + } + + if (ret == NSS_STATUS_SUCCESS) { + if (fgetpwent_r(p_file, result, buffer, buflen, &result) == 0) { + DEBUG("Returning user %d:%s\n", result->pw_uid, result->pw_name); + } else { + if (errno == ENOENT) { + errno = 0; + } + *errnop = errno; + ret = _nss_cache_oslogin_ent_bad_return_code(*errnop); + } + } + + return ret; +} + +// _nss_cache_oslogin_getpwent_r() +// Called by NSS to look up next entry in passwd file + +enum nss_status _nss_cache_oslogin_getpwent_r(struct passwd *result, + char *buffer, size_t buflen, + int *errnop) { + enum nss_status ret; + NSS_CACHE_OSLOGIN_LOCK(); + ret = _nss_cache_oslogin_getpwent_r_locked(result, buffer, buflen, errnop); + NSS_CACHE_OSLOGIN_UNLOCK(); + return ret; +} + +// _nss_cache_oslogin_getpwuid_r() +// Find a user account by uid + +enum nss_status _nss_cache_oslogin_getpwuid_r(uid_t uid, struct passwd *result, + char *buffer, size_t buflen, + int *errnop) { + char filename[NSS_CACHE_OSLOGIN_PATH_LENGTH]; + struct nss_cache_oslogin_args args; + enum nss_status ret; + + strncpy(filename, p_filename, NSS_CACHE_OSLOGIN_PATH_LENGTH - 1); + if (strlen(filename) > NSS_CACHE_OSLOGIN_PATH_LENGTH - 7) { + DEBUG("filename too long\n"); + return NSS_STATUS_UNAVAIL; + } + strncat(filename, ".ixuid", 6); + + args.sorted_filename = filename; + args.system_filename = p_filename; + args.lookup_function = _nss_cache_oslogin_pwuid_wrap; + args.lookup_value = &uid; + args.lookup_result = result; + args.buffer = buffer; + args.buflen = buflen; + char uid_text[11]; + snprintf(uid_text, sizeof(uid_text), "%d", uid); + args.lookup_key = uid_text; + args.lookup_key_length = strlen(uid_text); + + DEBUG("Binary search for uid %d\n", uid); + NSS_CACHE_OSLOGIN_LOCK(); + ret = _nss_cache_oslogin_bsearch2(&args, errnop); + + if (ret == NSS_STATUS_UNAVAIL) { + DEBUG("Binary search failed, falling back to full linear search\n"); + ret = _nss_cache_oslogin_setpwent_locked(); + + if (ret == NSS_STATUS_SUCCESS) { + while ((ret = _nss_cache_oslogin_getpwent_r_locked( + result, buffer, buflen, errnop)) == NSS_STATUS_SUCCESS) { + if (result->pw_uid == uid) break; + } + } + } + + _nss_cache_oslogin_endpwent_locked(); + NSS_CACHE_OSLOGIN_UNLOCK(); + + return ret; +} + +// _nss_cache_oslogin_getpwnam_r() +// Find a user account by name + +enum nss_status _nss_cache_oslogin_getpwnam_r(const char *name, + struct passwd *result, + char *buffer, size_t buflen, + int *errnop) { + char *pw_name; + char filename[NSS_CACHE_OSLOGIN_PATH_LENGTH]; + struct nss_cache_oslogin_args args; + enum nss_status ret; + + NSS_CACHE_OSLOGIN_LOCK(); + + // name is a const char, we need a non-const copy + pw_name = malloc(strlen(name) + 1); + if (pw_name == NULL) { + DEBUG("malloc error\n"); + return NSS_STATUS_UNAVAIL; + } + strncpy(pw_name, name, strlen(name) + 1); + + strncpy(filename, p_filename, NSS_CACHE_OSLOGIN_PATH_LENGTH - 1); + if (strlen(filename) > NSS_CACHE_OSLOGIN_PATH_LENGTH - 8) { + DEBUG("filename too long\n"); + free(pw_name); + return NSS_STATUS_UNAVAIL; + } + strncat(filename, ".ixname", 7); + + args.sorted_filename = filename; + args.system_filename = p_filename; + args.lookup_function = _nss_cache_oslogin_pwnam_wrap; + args.lookup_value = pw_name; + args.lookup_result = result; + args.buffer = buffer; + args.buflen = buflen; + args.lookup_key = pw_name; + args.lookup_key_length = strlen(pw_name); + + DEBUG("Binary search for user %s\n", pw_name); + ret = _nss_cache_oslogin_bsearch2(&args, errnop); + + if (ret == NSS_STATUS_UNAVAIL) { + DEBUG("Binary search failed, falling back to full linear search\n"); + ret = _nss_cache_oslogin_setpwent_locked(); + + if (ret == NSS_STATUS_SUCCESS) { + while ((ret = _nss_cache_oslogin_getpwent_r_locked( + result, buffer, buflen, errnop)) == NSS_STATUS_SUCCESS) { + if (!strcmp(result->pw_name, name)) break; + } + } + } + + free(pw_name); + _nss_cache_oslogin_endpwent_locked(); + NSS_CACHE_OSLOGIN_UNLOCK(); + + return ret; +} diff --git a/packages/google-compute-engine-oslogin/libnss_cache_oslogin/nss_cache_oslogin.h b/packages/google-compute-engine-oslogin/libnss_cache_oslogin/nss_cache_oslogin.h new file mode 100644 index 0000000..25c7274 --- /dev/null +++ b/packages/google-compute-engine-oslogin/libnss_cache_oslogin/nss_cache_oslogin.h @@ -0,0 +1,65 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include <errno.h> +#include <nss.h> +#include <stdlib.h> +#include <pwd.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <sys/param.h> +#include <time.h> +#include <unistd.h> + +#ifndef NSS_CACHE_OSLOGIN_H +#define NSS_CACHE_OSLOGIN_H + +#ifdef DEBUG +#undef DEBUG +#define DEBUG(fmt, args...) \ + do { \ + fprintf(stderr, fmt, ##args); \ + } while (0) +#else +#define DEBUG(fmt, ...) \ + do { \ + } while (0) +#endif /* DEBUG */ + +#define NSS_CACHE_OSLOGIN_PATH_LENGTH 255 +extern char *_nss_cache_oslogin_setpwent_path(const char *path); + +enum nss_cache_oslogin_match { + NSS_CACHE_OSLOGIN_EXACT = 0, + NSS_CACHE_OSLOGIN_HIGH = 1, + NSS_CACHE_OSLOGIN_LOW = 2, + NSS_CACHE_OSLOGIN_ERROR = 3, +}; + +struct nss_cache_oslogin_args { + char *system_filename; + char *sorted_filename; + void *lookup_function; + void *lookup_value; + void *lookup_result; + char *buffer; + size_t buflen; + char *lookup_key; + size_t lookup_key_length; +}; + +#endif /* NSS_CACHE_OSLOGIN_H */ diff --git a/packages/google-compute-engine-oslogin/nss_cache/nss_cache.cc b/packages/google-compute-engine-oslogin/nss_cache/nss_cache.cc new file mode 100644 index 0000000..a720859 --- /dev/null +++ b/packages/google-compute-engine-oslogin/nss_cache/nss_cache.cc @@ -0,0 +1,118 @@ +// Copyright 2018 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include <errno.h> +#include <nss.h> +#include <pthread.h> +#include <pwd.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <syslog.h> +#include <unistd.h> + +#include <fstream> + +#include "../utils/oslogin_utils.h" + + +using oslogin_utils::BufferManager; +using oslogin_utils::MutexLock; +using oslogin_utils::NssCache; + +// File paths for the nss cache file. +static const char kDefaultFilePath[] = "/etc/oslogin_passwd.cache"; +static const char kDefaultBackupFilePath[] = "/etc/oslogin_passwd.cache.bak"; + +// Local NSS Cache size. This affects the maximum number of passwd entries per +// http request. +static const uint64_t kNssCacheSize = 2048; + +// Passwd buffer size. We are guaranteed that a single OS Login user will not +// exceed 32k. +static const uint64_t kPasswdBufferSize = 32768; + +static NssCache nss_cache(kNssCacheSize); + +static pthread_mutex_t cache_mutex = PTHREAD_MUTEX_INITIALIZER; + +int main(int argc, char* argv[]) { + int error_code = 0; + // Temporary buffer to hold passwd entries before writing. + char buffer[kPasswdBufferSize]; + struct passwd pwd; + + // Perform the writes under a global lock. + MutexLock ml(&cache_mutex); + nss_cache.Reset(); + + // Check if there is a cache already. + struct stat stat_buf; + bool backup = !stat(kDefaultFilePath, &stat_buf); + if (backup) { + // Write a backup file first, in case lookup fails. + error_code = rename(kDefaultFilePath, kDefaultBackupFilePath); + if (error_code) { + openlog("nss_cache_oslogin", LOG_PID, LOG_USER); + syslog(LOG_ERR, "Could not create backup file."); + closelog(); + return error_code; + } + } + + std::ofstream cache_file(kDefaultFilePath); + if (cache_file.fail()) { + openlog("nss_cache_oslogin", LOG_PID, LOG_USER); + syslog(LOG_ERR, "Failed to open file %s.", kDefaultFilePath); + closelog(); + return -1; + } + chown(kDefaultFilePath, 0, 0); + chmod(kDefaultFilePath, S_IRUSR | S_IWUSR | S_IROTH); + + while (!nss_cache.OnLastPage() || nss_cache.HasNextPasswd()) { + BufferManager buffer_manager(buffer, kPasswdBufferSize); + if (!nss_cache.NssGetpwentHelper(&buffer_manager, &pwd, &error_code)) { + break; + } + cache_file << pwd.pw_name << ":" << pwd.pw_passwd << ":" << pwd.pw_uid + << ":" << pwd.pw_gid << ":" << pwd.pw_gecos << ":" << pwd.pw_dir + << ":" << pwd.pw_shell << "\n"; + } + cache_file.close(); + + // Check for errors. + if (error_code) { + openlog("nss_cache_oslogin", LOG_PID, LOG_USER); + if (error_code == ERANGE) { + syslog(LOG_ERR, "Received unusually large passwd entry."); + } else if (error_code == EINVAL) { + syslog(LOG_ERR, "Encountered malformed passwd entry."); + } else { + syslog(LOG_ERR, "Unknown error while retrieving passwd entry."); + } + // Restore the backup. + if (backup) { + if (rename(kDefaultBackupFilePath, kDefaultFilePath)) { + syslog(LOG_ERR, "Could not restore data from backup file."); + } + } + closelog(); + } + + // Remove the backup file on success. + if (!error_code && backup) { + remove(kDefaultBackupFilePath); + } + return error_code; +} diff --git a/packages/google-compute-engine-oslogin/nss_module/nss_oslogin.cc b/packages/google-compute-engine-oslogin/nss_module/nss_oslogin.cc new file mode 100644 index 0000000..d808a6f --- /dev/null +++ b/packages/google-compute-engine-oslogin/nss_module/nss_oslogin.cc @@ -0,0 +1,106 @@ +// Copyright 2017 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include <curl/curl.h> +#include <errno.h> +#include <grp.h> +#include <nss.h> +#include <pthread.h> +#include <pwd.h> +#include <sys/types.h> +#include <syslog.h> +#include <unistd.h> + +#include <iostream> +#include <sstream> +#include <string> + +#include "../utils/oslogin_utils.h" + +using std::string; + +using oslogin_utils::BufferManager; +using oslogin_utils::HttpGet; +using oslogin_utils::MutexLock; +using oslogin_utils::NssCache; +using oslogin_utils::ParseJsonToPasswd; +using oslogin_utils::UrlEncode; +using oslogin_utils::kMetadataServerUrl; + +// Size of the NssCache. This also determines how many users will be requested +// per HTTP call. +static const uint64_t kNssCacheSize = 2048; + +// NssCache for storing passwd entries. +static NssCache nss_cache(kNssCacheSize); + +// Protects access to nss_cache. +static pthread_mutex_t cache_mutex = PTHREAD_MUTEX_INITIALIZER; + +extern "C" { + +// Get a passwd entry by id. +int _nss_oslogin_getpwuid_r(uid_t uid, struct passwd *result, char *buffer, + size_t buflen, int *errnop) { + BufferManager buffer_manager(buffer, buflen); + std::stringstream url; + url << kMetadataServerUrl << "users?uid=" << uid; + string response; + long http_code = 0; + if (!HttpGet(url.str(), &response, &http_code) || http_code != 200 || + response.empty()) { + *errnop = ENOENT; + return NSS_STATUS_NOTFOUND; + } + if (!ParseJsonToPasswd(response, result, &buffer_manager, errnop)) { + if (*errnop == EINVAL) { + openlog("nss_oslogin", LOG_PID, LOG_USER); + syslog(LOG_ERR, "Received malformed response from server: %s", + response.c_str()); + closelog(); + } + return *errnop == ERANGE ? NSS_STATUS_TRYAGAIN : NSS_STATUS_NOTFOUND; + } + return NSS_STATUS_SUCCESS; +} + +// Get a passwd entry by name. +int _nss_oslogin_getpwnam_r(const char *name, struct passwd *result, + char *buffer, size_t buflen, int *errnop) { + BufferManager buffer_manager(buffer, buflen); + std::stringstream url; + url << kMetadataServerUrl << "users?username=" << UrlEncode(name); + string response; + long http_code = 0; + if (!HttpGet(url.str(), &response, &http_code) || http_code != 200 || + response.empty()) { + *errnop = ENOENT; + return NSS_STATUS_NOTFOUND; + } + if (!ParseJsonToPasswd(response, result, &buffer_manager, errnop)) { + if (*errnop == EINVAL) { + openlog("nss_oslogin", LOG_PID, LOG_USER); + syslog(LOG_ERR, "Received malformed response from server: %s", + response.c_str()); + closelog(); + } + return *errnop == ERANGE ? NSS_STATUS_TRYAGAIN : NSS_STATUS_NOTFOUND; + } + return NSS_STATUS_SUCCESS; +} + +// nss_getpwent_r() is intentionally left unimplemented. This functionality is +// now covered by the nss_cache binary and nss_cache module. + +} // extern "C" diff --git a/packages/google-compute-engine-oslogin/packaging/debian/changelog b/packages/google-compute-engine-oslogin/packaging/debian/changelog new file mode 100644 index 0000000..45e2b3e --- /dev/null +++ b/packages/google-compute-engine-oslogin/packaging/debian/changelog @@ -0,0 +1,119 @@ +google-compute-engine-oslogin (1.4.3-1) unstable; urgency=low + + * Improve OS Login control file for BSD support. + * Improve SELinux support. + + -- Google Cloud Team <gc-team@google.com> Wed, 05 Dec 2018 12:00:00 -0700 + +google-compute-engine-oslogin (1.4.2-1+deb9) unstable; urgency=low + + * Improve OS Login control file. + * Restart systemd-logind on OS Login enable. + + -- Google Cloud Team <gc-team@google.com> Tue, 04 Dec 2018 12:00:00 -0700 + +google-compute-engine-oslogin (1.4.1-1+deb9) unstable; urgency=low + + * Improve SELinux support. + * Improve OS Login control file. + + -- Google Cloud Team <gc-team@google.com> Fri, 30 Nov 2018 12:00:00 -0700 + +google-compute-engine-oslogin (1.4.0-1+deb9) unstable; urgency=low + + * Support OS Login two factor authentication. + + -- Google Cloud Team <gc-team@google.com> Wed, 28 Nov 2018 12:00:00 -0700 + +google-compute-engine-oslogin (1.3.1-1+deb9) unstable; urgency=low + + * Add user name validation to pam modules. + * Return false on failed final load. + + -- Google Cloud Team <gc-team@google.com> Wed, 05 Sep 2018 12:00:00 -0700 + +google-compute-engine-oslogin (1.3.0-1+deb9) unstable; urgency=low + + * Include libnss cache as part of the OS Login package. + + -- Google Cloud Team <gc-team@google.com> Tue, 01 May 2018 12:00:00 -0700 + +google-compute-engine-oslogin (1.2.0-1+deb9) unstable; urgency=low + + * Add support for NSS cache. + + -- Google Cloud Team <gc-team@google.com> Thu, 08 Mar 2018 12:00:00 -0700 + +google-compute-engine-oslogin (1.1.5-1+deb9) unstable; urgency=low + + * Clear the CURL_GLOBAL_SSL bit on curl initialization. + + -- Google Cloud Team <gc-team@google.com> Mon, 26 Feb 2018 12:00:00 -0700 + +google-compute-engine-oslogin (1.1.4-1+deb9) unstable; urgency=low + + * Close socket connections when requesting metadata. + + -- Google Cloud Team <gc-team@google.com> Mon, 29 Jan 2018 12:00:00 -0700 + +google-compute-engine-oslogin (1.1.3-1+deb9) unstable; urgency=low + + * Change the OS Login uid restriction to allow uid 1000. + + -- Google Cloud Team <gc-team@google.com> Thu, 25 Jan 2018 12:00:00 -0700 + +google-compute-engine-oslogin (1.1.2-1+deb9) unstable; urgency=low + + * Fix parsing logic for expiration time on SSH public keys. + * Fix home directory creation PAM config. + + -- MAINTAINER <gc-team@google.com> Wed, 29 Nov 2017 12:00:00 -0700 + +google-compute-engine-oslogin (1.1.1-1+deb9) unstable; urgency=low + + * Remove logging when checking OS Login status. + + -- MAINTAINER <gc-team@google.com> Wed, 25 Oct 2017 12:00:00 -0700 + +google-compute-engine-oslogin (1.1.0-1+deb9) unstable; urgency=low + + * OS Login is enabled via the google-compute-engine package. + + -- MAINTAINER <gc-team@google.com> Tue, 17 Oct 2017 12:00:00 -0700 + +google-compute-engine-oslogin (1.0.5-1+deb9) unstable; urgency=low + + * JSON parser accepts string types for int64 values. + + -- MAINTAINER <gc-team@google.com> Fri, 06 Oct 2017 12:00:00 -0700 + +google-compute-engine-oslogin (1.0.4-1+deb9) unstable; urgency=low + + * JSON parser casts uid and gid to unsigned integers. + + -- MAINTAINER <gc-team@google.com> Tue, 20 Sep 2017 12:00:00 -0700 + +google-compute-engine-oslogin (1.0.3-1+deb9) unstable; urgency=low + + * Strictly check for HTTP code 200. + + -- MAINTAINER <gc-team@google.com> Tue, 25 Aug 2017 12:00:00 -0700 + +google-compute-engine-oslogin (1.0.2-1+deb9) unstable; urgency=low + + * Improve security in case of transient errors. + + -- MAINTAINER <gc-team@google.com> Tue, 15 Aug 2017 12:00:00 -0700 + +google-compute-engine-oslogin (1.0.1-1+deb9) unstable; urgency=low + + * Fix for restarting sshd and nscd. + + -- MAINTAINER <gc-team@google.com> Mon, 17 Jul 2017 12:00:00 -0700 + +google-compute-engine-oslogin (1.0.0-1+deb9) unstable; urgency=low + + * Team Upload. + * Initial release. + + -- MAINTAINER <gc-team@google.com> Thu, 22 Jun 2017 12:00:00 -0700 diff --git a/packages/google-compute-engine-oslogin/packaging/debian/compat b/packages/google-compute-engine-oslogin/packaging/debian/compat new file mode 100644 index 0000000..ec63514 --- /dev/null +++ b/packages/google-compute-engine-oslogin/packaging/debian/compat @@ -0,0 +1 @@ +9 diff --git a/packages/google-compute-engine-oslogin/packaging/debian/control b/packages/google-compute-engine-oslogin/packaging/debian/control new file mode 100644 index 0000000..a8d0064 --- /dev/null +++ b/packages/google-compute-engine-oslogin/packaging/debian/control @@ -0,0 +1,13 @@ +Source: google-compute-engine-oslogin +Maintainer: Google Cloud Team <gc-team@google.com> +Section: misc +Priority: optional +Standards-Version: 3.9.6.1 +Build-Depends: debhelper (>= 9), libcurl4-openssl-dev, libjson-c-dev | libjson0-dev, libpam-dev + +Package: google-compute-engine-oslogin +Architecture: any +Depends: ${shlibs:Depends}, ${misc:Depends} +Description: Google Compute Engine OS Login + Contains libraries, applications and configurations for using OS Login + on Google Compute Engine Virtual Machine Instances. diff --git a/packages/google-compute-engine-oslogin/packaging/debian/copyright b/packages/google-compute-engine-oslogin/packaging/debian/copyright new file mode 100644 index 0000000..f1c5775 --- /dev/null +++ b/packages/google-compute-engine-oslogin/packaging/debian/copyright @@ -0,0 +1,27 @@ +Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: google-compute-engine-oslogin +Upstream-Contact: gc-team@google.com + +Files: * +Copyright: Copyright 2017 Google Inc. +License: Apache-2.0 + +Files: debian/* +Copyright: Copyright 2017 Google Inc. +License: Apache-2.0 + +License: Apache-2.0 + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + . + http://www.apache.org/licenses/LICENSE-2.0 + . + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + . + On Debian systems, the complete text of the Apache version 2.0 license + can be found in "/usr/share/common-licenses/Apache-2.0". diff --git a/packages/google-compute-engine-oslogin/packaging/debian/google-compute-engine-oslogin.links b/packages/google-compute-engine-oslogin/packaging/debian/google-compute-engine-oslogin.links new file mode 100644 index 0000000..fd75bc5 --- /dev/null +++ b/packages/google-compute-engine-oslogin/packaging/debian/google-compute-engine-oslogin.links @@ -0,0 +1,2 @@ +/lib/libnss_google-compute-engine-oslogin-1.3.2.so /lib/libnss_oslogin.so.2 +/lib/libnss_cache_google-compute-engine-oslogin-1.3.2.so /lib/libnss_cache_oslogin.so.2 diff --git a/packages/google-compute-engine-oslogin/packaging/debian/rules b/packages/google-compute-engine-oslogin/packaging/debian/rules new file mode 100755 index 0000000..65b1f4b --- /dev/null +++ b/packages/google-compute-engine-oslogin/packaging/debian/rules @@ -0,0 +1,6 @@ +#!/usr/bin/make -f +%: + dh $@ + +override_dh_auto_install: + dh_auto_install -- PAM_INSTALL_PATH=/lib/x86_64-linux-gnu/security BIN_INSTALL_PATH=/usr/bin diff --git a/packages/google-compute-engine-oslogin/packaging/debian/source/format b/packages/google-compute-engine-oslogin/packaging/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/packages/google-compute-engine-oslogin/packaging/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/packages/google-compute-engine-oslogin/packaging/google-compute-engine-oslogin.spec b/packages/google-compute-engine-oslogin/packaging/google-compute-engine-oslogin.spec new file mode 100644 index 0000000..f3a12e6 --- /dev/null +++ b/packages/google-compute-engine-oslogin/packaging/google-compute-engine-oslogin.spec @@ -0,0 +1,88 @@ +# Copyright 2017 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Force the dist to be el7 to avoid el7.centos. +%if 0%{?rhel} == 7 + %define dist .el7 +%endif + +Name: google-compute-engine-oslogin +Version: 1.4.3 +Release: 1%{?dist} +Summary: OS Login Functionality for Google Compute Engine + +License: ASL 2.0 +Source0: %{name}_%{version}.orig.tar.gz + +BuildRequires: boost-devel +BuildRequires: gcc-c++ +BuildRequires: make +BuildRequires: libcurl +BuildRequires: json-c +BuildRequires: pam-devel +BuildRequires: policycoreutils-python +Requires: boost-regex +Requires: policycoreutils-python + +%define pam_install_path /%{_lib}/security + +%description +This package contains several libraries and changes to enable OS Login functionality +for Google Compute Engine. + +%prep +%setup + +%build +make %{?_smp_mflags} LIBS="-lcurl -ljson-c -lboost_regex" + +%install +rm -rf %{buildroot} +make install DESTDIR=%{buildroot} NSS_INSTALL_PATH=/%{_lib} PAM_INSTALL_PATH=%{pam_install_path} INSTALL_SELINUX=true + +%files +%doc +/%{_lib}/libnss_%{name}-%{version}.so +/%{_lib}/libnss_cache_%{name}-%{version}.so +%{pam_install_path}/pam_oslogin_admin.so +%{pam_install_path}/pam_oslogin_login.so +/usr/bin/google_authorized_keys +/usr/bin/google_oslogin_control +/usr/bin/google_oslogin_nss_cache +/usr/share/selinux/packages/oslogin.pp + +%post +/sbin/ldconfig +if [ $1 -gt 1 ]; then # This is an upgrade. + if semodule -l | grep -qi oslogin.el6; then + echo "Removing old SELinux module for OS Login." + semodule -r oslogin.el6 + fi +fi +echo "Installing SELinux module for OS Login." +semodule -i /usr/share/selinux/packages/oslogin.pp +if [ -e /var/google-sudoers.d ]; then + fixfiles restore /var/google-sudoers.d +fi + +%postun +/sbin/ldconfig +if [ $1 = 0 ]; then # This is an uninstall. + if semodule -l|grep -qi oslogin; then + echo "Removing SELinux module for OS Login." + semodule -r oslogin + fi +fi + +%changelog diff --git a/packages/google-compute-engine-oslogin/packaging/setup_deb.sh b/packages/google-compute-engine-oslogin/packaging/setup_deb.sh new file mode 100755 index 0000000..3c41f20 --- /dev/null +++ b/packages/google-compute-engine-oslogin/packaging/setup_deb.sh @@ -0,0 +1,44 @@ +#!/bin/bash +# Copyright 2018 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +NAME="google-compute-engine-oslogin" +VERSION="1.4.3" + +working_dir=${PWD} +if [[ $(basename "$working_dir") != $NAME ]]; then + echo "Packaging scripts must be run from top of package dir." + exit 1 +fi + +# Build dependencies. +sudo apt-get -y install make g++ libcurl4-openssl-dev libjson-c-dev libpam-dev + +# DEB creation tools. +sudo apt-get -y install debhelper devscripts build-essential + +rm -rf /tmp/debpackage +mkdir /tmp/debpackage +tar czvf /tmp/debpackage/${NAME}_${VERSION}.orig.tar.gz --exclude .git --exclude packaging --transform "s/^\./${NAME}-${VERSION}/" . + +pushd /tmp/debpackage +tar xzvf ${NAME}_${VERSION}.orig.tar.gz + +cd ${NAME}-${VERSION} + +cp -r ${working_dir}/packaging/debian ./ + +debuild -us -uc + +popd diff --git a/packages/google-compute-engine-oslogin/packaging/setup_rpm.sh b/packages/google-compute-engine-oslogin/packaging/setup_rpm.sh new file mode 100755 index 0000000..640b9e2 --- /dev/null +++ b/packages/google-compute-engine-oslogin/packaging/setup_rpm.sh @@ -0,0 +1,39 @@ +#!/bin/bash +# Copyright 2018 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +NAME="google-compute-engine-oslogin" +VERSION="1.4.3" + +rpm_working_dir=/tmp/rpmpackage/${NAME}-${VERSION} +working_dir=${PWD} +if [[ $(basename "$working_dir") != $NAME ]]; then + echo "Packaging scripts must be run from top of package dir." + exit 1 +fi + +# Build dependencies. +sudo yum -y install make gcc-c++ libcurl-devel json-c json-c-devel pam-devel policycoreutils-python boost-devel + +# RPM creation tools. +sudo yum -y install rpmdevtools + +rm -rf /tmp/rpmpackage +mkdir -p ${rpm_working_dir}/{SOURCES,SPECS} +cp packaging/${NAME}.spec ${rpm_working_dir}/SPECS/ + +tar czvf ${rpm_working_dir}/SOURCES/${NAME}_${VERSION}.orig.tar.gz --exclude .git --exclude packaging --transform "s/^\./${NAME}-${VERSION}/" . + +rpmbuild --define "_topdir ${rpm_working_dir}/" -ba ${rpm_working_dir}/SPECS/${NAME}.spec diff --git a/packages/google-compute-engine-oslogin/pam_module/pam_oslogin_admin.cc b/packages/google-compute-engine-oslogin/pam_module/pam_oslogin_admin.cc new file mode 100644 index 0000000..0b5f3c0 --- /dev/null +++ b/packages/google-compute-engine-oslogin/pam_module/pam_oslogin_admin.cc @@ -0,0 +1,100 @@ +// Copyright 2017 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#define PAM_SM_ACCOUNT +#include <security/pam_appl.h> +#include <security/pam_ext.h> +#include <security/pam_modules.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <syslog.h> +#include <unistd.h> + +#include <iostream> +#include <fstream> +#include <sstream> +#include <string> + +#include "../utils/oslogin_utils.h" + +using std::string; + +using oslogin_utils::HttpGet; +using oslogin_utils::GetUser; +using oslogin_utils::kMetadataServerUrl; +using oslogin_utils::ParseJsonToKey; +using oslogin_utils::ParseJsonToEmail; +using oslogin_utils::ParseJsonToSuccess; +using oslogin_utils::UrlEncode; +using oslogin_utils::ValidateUserName; + +static const char kSudoersDir[] = "/var/google-sudoers.d/"; + +extern "C" { + +PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, + const char **argv) { + // The return value for this module should generally be ignored. By default we + // will return PAM_SUCCESS. + int pam_result = PAM_SUCCESS; + const char *user_name; + if ((pam_result = pam_get_user(pamh, &user_name, NULL)) != PAM_SUCCESS) { + pam_syslog(pamh, LOG_INFO, "Could not get pam user."); + return pam_result; + } + + if (!ValidateUserName(user_name)) { + // If the user name is not a valid oslogin user, don't bother continuing. + return PAM_SUCCESS; + } + + string response; + if (!GetUser(user_name, &response)) { + return PAM_SUCCESS; + } + + string email; + if (!ParseJsonToEmail(response, &email) || email.empty()) { + return PAM_SUCCESS; + } + + std::stringstream url; + url << kMetadataServerUrl << "authorize?email=" << UrlEncode(email) + << "&policy=adminLogin"; + + string filename = kSudoersDir; + filename.append(user_name); + struct stat buffer; + bool file_exists = !stat(filename.c_str(), &buffer); + long http_code; + if (HttpGet(url.str(), &response, &http_code) && http_code == 200 && + ParseJsonToSuccess(response)) { + if (!file_exists) { + pam_syslog(pamh, LOG_INFO, + "Granting sudo permissions to organization user %s.", + user_name); + std::ofstream sudoers_file; + sudoers_file.open(filename.c_str()); + sudoers_file << user_name << " ALL=(ALL) NOPASSWD: ALL" + << "\n"; + sudoers_file.close(); + chown(filename.c_str(), 0, 0); + chmod(filename.c_str(), S_IRUSR | S_IWUSR | S_IRGRP); + } + } else if (file_exists) { + remove(filename.c_str()); + } + return pam_result; +} +} diff --git a/packages/google-compute-engine-oslogin/pam_module/pam_oslogin_login.cc b/packages/google-compute-engine-oslogin/pam_module/pam_oslogin_login.cc new file mode 100644 index 0000000..fa1b151 --- /dev/null +++ b/packages/google-compute-engine-oslogin/pam_module/pam_oslogin_login.cc @@ -0,0 +1,264 @@ +// Copyright 2017 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#define PAM_SM_ACCOUNT +#include <security/pam_appl.h> +#include <security/pam_ext.h> +#include <security/pam_modules.h> +#include <sys/stat.h> +#include <sys/types.h> +#include <syslog.h> +#include <unistd.h> + +#include <iostream> +#include <fstream> +#include <sstream> +#include <string> +#include <map> + +#include "../utils/oslogin_utils.h" + +using std::string; + +using oslogin_utils::ContinueSession; +using oslogin_utils::GetUser; +using oslogin_utils::HttpGet; +using oslogin_utils::HttpPost; +using oslogin_utils::kMetadataServerUrl; +using oslogin_utils::ParseJsonToChallenges; +using oslogin_utils::ParseJsonToKey; +using oslogin_utils::ParseJsonToEmail; +using oslogin_utils::ParseJsonToSuccess; +using oslogin_utils::StartSession; +using oslogin_utils::UrlEncode; +using oslogin_utils::ValidateUserName; + +static const char kUsersDir[] = "/var/google-users.d/"; + +extern "C" { + +PAM_EXTERN int pam_sm_acct_mgmt(pam_handle_t *pamh, int flags, int argc, + const char **argv) { + int pam_result = PAM_PERM_DENIED; + const char *user_name; + if ((pam_result = pam_get_user(pamh, &user_name, NULL)) != PAM_SUCCESS) { + pam_syslog(pamh, LOG_INFO, "Could not get pam user."); + return pam_result; + } + string str_user_name(user_name); + if (!ValidateUserName(user_name)) { + // If the user name is not a valid oslogin user, don't bother continuing. + return PAM_SUCCESS; + } + string users_filename = kUsersDir; + users_filename.append(user_name); + struct stat buffer; + bool file_exists = !stat(users_filename.c_str(), &buffer); + + std::stringstream url; + url << kMetadataServerUrl << "users?username=" << UrlEncode(str_user_name); + string response; + long http_code = 0; + if (!HttpGet(url.str(), &response, &http_code) || response.empty() || + http_code != 200) { + if (http_code == 404) { + // Return success on non-oslogin users. + return PAM_SUCCESS; + } + // If we can't reliably tell if this is an oslogin user, check if there is + // a local file for that user as a last resort. + if (file_exists) { + return PAM_PERM_DENIED; + } + // Otherwise, fall back on success to allow local users to log in. + return PAM_SUCCESS; + } + + string email; + if (!ParseJsonToEmail(response, &email) || email.empty()) { + return PAM_PERM_DENIED; + } + + url.str(""); + url << kMetadataServerUrl << "authorize?email=" << UrlEncode(email) + << "&policy=login"; + if (HttpGet(url.str(), &response, &http_code) && http_code == 200 && + ParseJsonToSuccess(response)) { + if (!file_exists) { + std::ofstream users_file(users_filename.c_str()); + chown(users_filename.c_str(), 0, 0); + chmod(users_filename.c_str(), S_IRUSR | S_IWUSR | S_IRGRP); + } + pam_syslog(pamh, LOG_INFO, + "Granting login permission for organization user %s.", + user_name); + pam_result = PAM_SUCCESS; + } else { + if (file_exists) { + remove(users_filename.c_str()); + } + pam_syslog(pamh, LOG_INFO, + "Denying login permission for organization user %s.", + user_name); + + pam_result = PAM_PERM_DENIED; + } + return pam_result; +} +PAM_EXTERN int pam_sm_setcred(pam_handle_t * pamh, int flags, int argc, + const char **argv) +{ + return PAM_SUCCESS; +} + + +PAM_EXTERN int pam_sm_authenticate(pam_handle_t * pamh, int flags, + int argc, const char **argv) +{ + const char* user_name; + if (pam_get_user(pamh, &user_name, NULL) != PAM_SUCCESS) { + pam_syslog(pamh, LOG_INFO, "Could not get pam user."); + return PAM_PERM_DENIED; + } + + string str_user_name(user_name); + if (!ValidateUserName(user_name)) { + return PAM_PERM_DENIED; + } + + string response; + if (!(GetUser(str_user_name, &response))) { + return PAM_PERM_DENIED; + } + + // System accounts begin with the prefix `sa_`. + string sa_prefix = "sa_"; + if (str_user_name.compare(0, sa_prefix.size(), sa_prefix) == 0) { + return PAM_SUCCESS; + } + + string email; + if (!ParseJsonToEmail(response, &email) || email.empty()) { + return PAM_PERM_DENIED; + } + + response = ""; + if (!StartSession(email, &response)) { + pam_syslog(pamh, LOG_ERR, + "Bad response from the two-factor start session request: %s", + response.empty() ? "empty response" : response.c_str()); + return PAM_PERM_DENIED; + } + + string status; + if (!ParseJsonToKey(response, "status", &status)) { + pam_syslog(pamh, LOG_ERR, + "Failed to parse status from start session response"); + return PAM_PERM_DENIED; + } + + if (status == "NO_AVAILABLE_CHALLENGES") { + return PAM_SUCCESS; // User is not two-factor enabled. + } + + string session_id; + if (!ParseJsonToKey(response, "sessionId", &session_id)) { + return PAM_PERM_DENIED; + } + + std::vector<oslogin_utils::Challenge> challenges; + if (!ParseJsonToChallenges(response, &challenges)) { + pam_syslog(pamh, LOG_ERR, + "Failed to parse challenge values from JSON response"); + return PAM_PERM_DENIED; + } + + std::map<string,string> user_prompts; + user_prompts[AUTHZEN] = "Google phone prompt"; + user_prompts[TOTP] = "Security code from Google Authenticator application"; + user_prompts[INTERNAL_TWO_FACTOR] = "Security code from security key"; + user_prompts[IDV_PREREGISTERED_PHONE] = + "Voice or text message verification code"; + + oslogin_utils::Challenge challenge; + if (challenges.size() > 1) { + std::stringstream prompt; + prompt << "Available authentication methods: "; + for(vector<oslogin_utils::Challenge>::size_type i = 0; + i != challenges.size(); ++i) + prompt << "\n" << i+1 << ": " << user_prompts[challenges[i].type]; + prompt << "\n\nEnter a number: "; + + char *choice = NULL; + if (pam_prompt(pamh, PAM_PROMPT_ECHO_ON, &choice, "%s", + prompt.str().c_str()) != PAM_SUCCESS) { + pam_error(pamh, "Unable to get user input"); + } + + int choicei; + if (sscanf(choice, "%d", &choicei) == EOF) { + pam_error(pamh, "Unable to get user input"); + } + if (choicei > challenges.size()) { + pam_error(pamh, "Invalid option"); + } + challenge = challenges[choicei - 1]; + } else { + challenge = challenges[0]; + } + + char* user_token = NULL; + if (challenge.type == INTERNAL_TWO_FACTOR) { + if (pam_prompt(pamh, PAM_PROMPT_ECHO_ON, &user_token, + "Enter your security code: ") != PAM_SUCCESS) { + pam_error(pamh, "Unable to get user input"); + } + } else if (challenge.type == TOTP) { + if (pam_prompt(pamh, PAM_PROMPT_ECHO_ON, &user_token, + "Enter your one-time password: ") != PAM_SUCCESS) { + pam_error(pamh, "Unable to get user input"); + } + } else if (challenge.type == AUTHZEN) { + if (pam_prompt(pamh, PAM_PROMPT_ECHO_ON, &user_token, + "A login prompt has been sent to your enrolled device. " + "Press enter to continue") != PAM_SUCCESS) { + pam_error(pamh, "Unable to get user input"); + } + } else if (challenge.type == IDV_PREREGISTERED_PHONE) { + if (pam_prompt(pamh, PAM_PROMPT_ECHO_ON, &user_token, + "A security code has been sent to your phone. " + "Enter code to continue: ") != PAM_SUCCESS) { + pam_error(pamh, "Unable to get user input"); + } + } else { + pam_syslog(pamh, LOG_ERR, "Unsupported challenge type %s", + challenge.type.c_str()); + return PAM_PERM_DENIED; + } + + if (!ContinueSession(email, user_token, session_id, challenge, &response)) { + pam_syslog(pamh, LOG_ERR, + "Bad response from two-factor continue session request: %s", + response.empty() ? "empty response" : response.c_str()); + return PAM_PERM_DENIED; + } + + if (!ParseJsonToKey(response, "status", &status) + || status != "AUTHENTICATED") { + return PAM_PERM_DENIED; + } + + return PAM_SUCCESS; +} +} diff --git a/packages/google-compute-engine-oslogin/policy/Makefile b/packages/google-compute-engine-oslogin/policy/Makefile new file mode 100644 index 0000000..c858c9d --- /dev/null +++ b/packages/google-compute-engine-oslogin/policy/Makefile @@ -0,0 +1,17 @@ +# SELINUX POLICY +MOD_BASE = oslogin +SELINUX_MODULE_SRC = $(MOD_BASE).te +SELINUX_MOD_FILE = $(MOD_BASE).mod +SELINUX_FC_FILE = $(MOD_BASE).fc +SELINUX_MODULE = $(MOD_BASE).pp + +all: $(SELINUX_MODULE) + +$(SELINUX_MOD_FILE): $(SELINUX_MODULE_SRC) + checkmodule -M -m -o $(SELINUX_MOD_FILE) $(SELINUX_MODULE_SRC) + +$(SELINUX_MODULE): $(SELINUX_MOD_FILE) + semodule_package -o $(SELINUX_MODULE) -m $(SELINUX_MOD_FILE) -f $(SELINUX_FC_FILE) + +clean: + rm -f $(SELINUX_MODULE) $(SELINUX_MOD_FILE) diff --git a/packages/google-compute-engine-oslogin/policy/README.md b/packages/google-compute-engine-oslogin/policy/README.md new file mode 100644 index 0000000..b2cc5a9 --- /dev/null +++ b/packages/google-compute-engine-oslogin/policy/README.md @@ -0,0 +1,17 @@ +## SELinux policy module for OS Login + +This module adds specific policy updates which enable OS Login features to +function on SELinux-enabled systems (currently default on GCE RHEL6/7 images). + +It primarily enables `SSHD(8)` to make network calls to the metadata server to +verify OS Login users, and to create per-user `SUDOERS(5)` files in +`/var/google-sudoers.d` + +### Building the module + +The provided Makefile compiles type enforcement and file context files into a +binary SELinux policy module. It must be compiled on the oldest version of the +destination OS you intend to support, as binary module versions are not +backwards compatible. Therefore, this Makefile is not run as part of the normal +packaging process but is done 'by hand', only when changes are made to the +policy. diff --git a/packages/google-compute-engine-oslogin/policy/oslogin.fc b/packages/google-compute-engine-oslogin/policy/oslogin.fc new file mode 100644 index 0000000..3e70358 --- /dev/null +++ b/packages/google-compute-engine-oslogin/policy/oslogin.fc @@ -0,0 +1,2 @@ +/var/google-sudoers.d(/.*)? system_u:object_r:google_t:s0 +/var/google-users.d(/.*)? system_u:object_r:google_t:s0 diff --git a/packages/google-compute-engine-oslogin/policy/oslogin.pp b/packages/google-compute-engine-oslogin/policy/oslogin.pp Binary files differnew file mode 100644 index 0000000..6ec6ed0 --- /dev/null +++ b/packages/google-compute-engine-oslogin/policy/oslogin.pp diff --git a/packages/google-compute-engine-oslogin/policy/oslogin.te b/packages/google-compute-engine-oslogin/policy/oslogin.te new file mode 100644 index 0000000..381f769 --- /dev/null +++ b/packages/google-compute-engine-oslogin/policy/oslogin.te @@ -0,0 +1,24 @@ + +module oslogin 1.0; + + +require { + attribute file_type; + attribute non_security_file_type; + type http_port_t; + type sshd_t; + class tcp_socket name_connect; + class file { create getattr setattr write open unlink }; + class dir { search write remove_name add_name }; +} + +#============= types ============== + +type google_t; # defined in oslogin.fc +typeattribute google_t file_type, non_security_file_type; + +#============= sshd_t ============== + +allow sshd_t google_t:file { create getattr setattr write open unlink }; +allow sshd_t google_t:dir { search write remove_name add_name }; +allow sshd_t http_port_t:tcp_socket name_connect; diff --git a/packages/google-compute-engine-oslogin/utils/oslogin_utils.cc b/packages/google-compute-engine-oslogin/utils/oslogin_utils.cc new file mode 100644 index 0000000..f035adb --- /dev/null +++ b/packages/google-compute-engine-oslogin/utils/oslogin_utils.cc @@ -0,0 +1,657 @@ +// Copyright 2017 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Requires libcurl4-openssl-dev libjson0 and libjson0-dev +#include <curl/curl.h> +#include <errno.h> +#include <json.h> +#include <nss.h> +#include <stdio.h> +#include <time.h> +#include <cstring> +#include <iostream> +#include <sstream> + +#ifdef __GNUC__ +#if __GNUC__ > 4 || \ + (__GNUC__ == 4 && (__GNUC_MINOR__ > 9 || \ + (__GNUC_MINOR__ == 9 && \ + __GNUC_PATCHLEVEL__ > 0))) +#include <regex> +#define Regex std +#else +#include <boost/regex.hpp> +#define Regex boost +#endif +#endif + +#include "oslogin_utils.h" + +using std::string; + +// Maximum number of retries for HTTP requests. +const int kMaxRetries = 1; + +// Regex for validating user names. +const char kUserNameRegex[] = "^[a-zA-Z0-9._][a-zA-Z0-9._-]{0,31}$"; + +namespace oslogin_utils { + +BufferManager::BufferManager(char* buf, size_t buflen) + : buf_(buf), buflen_(buflen) {} + +bool BufferManager::AppendString(const string& value, char** buffer, + int* errnop) { + size_t bytes_to_write = value.length() + 1; + if (!CheckSpaceAvailable(bytes_to_write)) { + *errnop = ERANGE; + return false; + } + *buffer = static_cast<char*>(Reserve(bytes_to_write)); + strncpy(*buffer, value.c_str(), bytes_to_write); + return true; +} + +bool BufferManager::CheckSpaceAvailable(size_t bytes_to_write) const { + if (bytes_to_write > buflen_) { + return false; + } + return true; +} + +void* BufferManager::Reserve(size_t bytes) { + if (buflen_ < bytes) { + std::cerr << "Attempted to reserve more bytes than the buffer can hold!" + << "\n"; + abort(); + } + void* result = buf_; + buf_ += bytes; + buflen_ -= bytes; + return result; +} + +NssCache::NssCache(int cache_size) + : cache_size_(cache_size), + passwd_cache_(cache_size), + page_token_(""), + on_last_page_(false) {} + +void NssCache::Reset() { + page_token_ = ""; + index_ = 0; + passwd_cache_.clear(); + on_last_page_ = false; +} + +bool NssCache::HasNextPasswd() { + return index_ < passwd_cache_.size() && !passwd_cache_[index_].empty(); +} + +bool NssCache::GetNextPasswd(BufferManager* buf, passwd* result, int* errnop) { + if (!HasNextPasswd()) { + *errnop = ENOENT; + return false; + } + string cached_passwd = passwd_cache_[index_]; + bool success = ParseJsonToPasswd(cached_passwd, result, buf, errnop); + if (success) { + index_++; + } + return success; +} + +bool NssCache::LoadJsonArrayToCache(string response) { + Reset(); + json_object* root = NULL; + root = json_tokener_parse(response.c_str()); + if (root == NULL) { + return false; + } + // First grab the page token. + json_object* page_token_object; + if (json_object_object_get_ex(root, "nextPageToken", &page_token_object)) { + page_token_ = json_object_get_string(page_token_object); + } else { + // If the page token is not found, assume something went wrong. + page_token_ = ""; + on_last_page_ = true; + return false; + } + // A page_token of 0 means we are done. This response will not contain any + // login profiles. + if (page_token_ == "0") { + page_token_ = ""; + on_last_page_ = true; + return false; + } + // Now grab all of the loginProfiles. + json_object* login_profiles = NULL; + if (!json_object_object_get_ex(root, "loginProfiles", &login_profiles)) { + page_token_ = ""; + return false; + } + if (json_object_get_type(login_profiles) != json_type_array) { + return false; + } + int arraylen = json_object_array_length(login_profiles); + if (arraylen == 0 || arraylen > cache_size_) { + page_token_ = ""; + return false; + } + for (int i = 0; i < arraylen; i++) { + json_object* profile = json_object_array_get_idx(login_profiles, i); + passwd_cache_.push_back( + json_object_to_json_string_ext(profile, JSON_C_TO_STRING_PLAIN)); + } + return true; +} + +bool NssCache::NssGetpwentHelper(BufferManager* buf, struct passwd* result, + int* errnop) { + if (!HasNextPasswd() && !OnLastPage()) { + std::stringstream url; + url << kMetadataServerUrl << "users?pagesize=" << cache_size_; + string page_token = GetPageToken(); + if (!page_token.empty()) { + url << "&pagetoken=" << page_token; + } + string response; + long http_code = 0; + if (!HttpGet(url.str(), &response, &http_code) || http_code != 200 || + response.empty() || !LoadJsonArrayToCache(response)) { + // It is possible this to be true after LoadJsonArrayToCache(), so we + // must check it again. + if(!OnLastPage()) { + *errnop = ENOENT; + } + return false; + } + } + if (HasNextPasswd() && !GetNextPasswd(buf, result, errnop)) { + return false; + } + return true; +} + +size_t OnCurlWrite(void* buf, size_t size, size_t nmemb, void* userp) { + if (userp) { + std::ostream& os = *static_cast<std::ostream*>(userp); + std::streamsize len = size * nmemb; + if (os.write(static_cast<char*>(buf), len)) { + return len; + } + } + return 0; +} + +bool HttpDo(const string& url, const string& data, string* response, + long* http_code) { + if (response == NULL || http_code == NULL) { + return false; + } + CURLcode code(CURLE_FAILED_INIT); + curl_global_init(CURL_GLOBAL_ALL & ~CURL_GLOBAL_SSL); + CURL* curl = curl_easy_init(); + std::ostringstream response_stream; + int retry_count = 0; + if (curl) { + struct curl_slist* header_list = NULL; + header_list = curl_slist_append(header_list, "Metadata-Flavor: Google"); + if (header_list == NULL) { + curl_easy_cleanup(curl); + curl_global_cleanup(); + return false; + } + do { + response_stream.str(""); + response_stream.clear(); + curl_easy_setopt(curl, CURLOPT_HTTPHEADER, header_list); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &OnCurlWrite); + curl_easy_setopt(curl, CURLOPT_FILE, &response_stream); + curl_easy_setopt(curl, CURLOPT_TIMEOUT, 5); + curl_easy_setopt(curl, CURLOPT_URL, url.c_str()); + if (data != "") { + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, data.c_str()); + } + + code = curl_easy_perform(curl); + if (code != CURLE_OK) { + curl_easy_cleanup(curl); + curl_global_cleanup(); + return false; + } + curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, http_code); + } while (retry_count++ < kMaxRetries && *http_code == 500); + curl_slist_free_all(header_list); + } + *response = response_stream.str(); + curl_easy_cleanup(curl); + curl_global_cleanup(); + return true; +} + +bool HttpGet(const string& url, string* response, long* http_code) { + return HttpDo(url, "", response, http_code); +} + +bool HttpPost(const string& url, const string& data, string* response, + long* http_code) { + return HttpDo(url, data, response, http_code); +} + +bool ValidateUserName(const string& user_name) { + Regex::regex r(kUserNameRegex); + return Regex::regex_match(user_name, r); +} + +string UrlEncode(const string& param) { + CURL* curl = curl_easy_init(); + char* encoded = curl_easy_escape(curl, param.c_str(), param.length()); + if (encoded == NULL) { + curl_easy_cleanup(curl); + return ""; + } + string encoded_param = encoded; + curl_free(encoded); + curl_easy_cleanup(curl); + return encoded_param; +} + +bool ValidatePasswd(struct passwd* result, BufferManager* buf, + int* errnop) { + // OS Login disallows uids less than 1000. + if (result->pw_uid < 1000) { + *errnop = EINVAL; + return false; + } + if (result->pw_gid == 0) { + *errnop = EINVAL; + return false; + } + if (strlen(result->pw_name) == 0) { + *errnop = EINVAL; + return false; + } + if (strlen(result->pw_dir) == 0) { + string home_dir = "/home/"; + home_dir.append(result->pw_name); + if (!buf->AppendString(home_dir, &result->pw_dir, errnop)) { + return false; + } + } + if (strlen(result->pw_shell) == 0) { + if (!buf->AppendString("/bin/bash", &result->pw_shell, errnop)) { + return false; + } + } + + // OS Login does not utilize the passwd field and reserves the gecos field. + // Set these to be empty. + if (!buf->AppendString("", &result->pw_gecos, errnop)) { + return false; + } + if (!buf->AppendString("", &result->pw_passwd, errnop)) { + return false; + } + return true; +} + +std::vector<string> ParseJsonToSshKeys(const string& json) { + std::vector<string> result; + json_object* root = NULL; + root = json_tokener_parse(json.c_str()); + if (root == NULL) { + return result; + } + // Locate the sshPublicKeys object. + json_object* login_profiles = NULL; + if (!json_object_object_get_ex(root, "loginProfiles", &login_profiles)) { + return result; + } + if (json_object_get_type(login_profiles) != json_type_array) { + return result; + } + login_profiles = json_object_array_get_idx(login_profiles, 0); + + json_object* ssh_public_keys = NULL; + if (!json_object_object_get_ex(login_profiles, "sshPublicKeys", + &ssh_public_keys)) { + return result; + } + + if (json_object_get_type(ssh_public_keys) != json_type_object) { + return result; + } + json_object_object_foreach(ssh_public_keys, key, val) { + json_object* iter; + if (!json_object_object_get_ex(ssh_public_keys, key, &iter)) { + return result; + } + if (json_object_get_type(iter) != json_type_object) { + continue; + } + string key_to_add = ""; + bool expired = false; + json_object_object_foreach(iter, key, val) { + string string_key(key); + int val_type = json_object_get_type(val); + if (string_key == "key") { + if (val_type != json_type_string) { + continue; + } + key_to_add = (char*)json_object_get_string(val); + } + if (string_key == "expirationTimeUsec") { + if (val_type == json_type_int || val_type == json_type_string) { + uint64_t expiry_usec = (uint64_t)json_object_get_int64(val); + struct timeval tp; + gettimeofday(&tp, NULL); + uint64_t cur_usec = tp.tv_sec * 1000000 + tp.tv_usec; + expired = cur_usec > expiry_usec; + } else { + continue; + } + } + } + if (!key_to_add.empty() && !expired) { + result.push_back(key_to_add); + } + } + return result; +} + +bool ParseJsonToPasswd(const string& json, struct passwd* result, + BufferManager* buf, int* errnop) { + json_object* root = NULL; + root = json_tokener_parse(json.c_str()); + if (root == NULL) { + *errnop = ENOENT; + return false; + } + json_object* login_profiles = NULL; + // If this is called from getpwent_r, loginProfiles won't be in the response. + if (json_object_object_get_ex(root, "loginProfiles", &login_profiles)) { + if (json_object_get_type(login_profiles) != json_type_array) { + return false; + } + root = login_profiles; + root = json_object_array_get_idx(root, 0); + } + // Locate the posixAccounts object. + json_object* posix_accounts = NULL; + if (!json_object_object_get_ex(root, "posixAccounts", &posix_accounts)) { + *errnop = ENOENT; + return false; + } + if (json_object_get_type(posix_accounts) != json_type_array) { + return false; + } + posix_accounts = json_object_array_get_idx(posix_accounts, 0); + + // Populate with some default values that ValidatePasswd can detect if they + // are not set. + result->pw_uid = 0; + result->pw_shell = (char*)""; + result->pw_name = (char*)""; + result->pw_dir = (char*)""; + + // Iterate through the json response and populate the passwd struct. + if (json_object_get_type(posix_accounts) != json_type_object) { + return false; + } + json_object_object_foreach(posix_accounts, key, val) { + int val_type = json_object_get_type(val); + // Convert char* to c++ string for easier comparison. + string string_key(key); + + if (string_key == "uid") { + if (val_type == json_type_int || val_type == json_type_string) { + result->pw_uid = (uint32_t)json_object_get_int64(val); + if (result->pw_uid == 0) { + *errnop = EINVAL; + return false; + } + } else { + *errnop = EINVAL; + return false; + } + } else if (string_key == "gid") { + if (val_type == json_type_int || val_type == json_type_string) { + result->pw_gid = (uint32_t)json_object_get_int64(val); + // Use the uid as the default group when gid is not set or is zero. + if (result->pw_gid == 0) { + result->pw_gid = result->pw_uid; + } + } else { + *errnop = EINVAL; + return false; + } + } else if (string_key == "username") { + if (val_type != json_type_string) { + *errnop = EINVAL; + return false; + } + if (!buf->AppendString((char*)json_object_get_string(val), + &result->pw_name, errnop)) { + return false; + } + } else if (string_key == "homeDirectory") { + if (val_type != json_type_string) { + *errnop = EINVAL; + return false; + } + if (!buf->AppendString((char*)json_object_get_string(val), + &result->pw_dir, errnop)) { + return false; + } + } else if (string_key == "shell") { + if (val_type != json_type_string) { + *errnop = EINVAL; + return false; + } + if (!buf->AppendString((char*)json_object_get_string(val), + &result->pw_shell, errnop)) { + return false; + } + } + } + + return ValidatePasswd(result, buf, errnop); +} + +bool ParseJsonToEmail(const string& json, string* email) { + json_object* root = NULL; + root = json_tokener_parse(json.c_str()); + if (root == NULL) { + return false; + } + // Locate the email object. + json_object* login_profiles = NULL; + if (!json_object_object_get_ex(root, "loginProfiles", &login_profiles)) { + return false; + } + if (json_object_get_type(login_profiles) != json_type_array) { + return false; + } + login_profiles = json_object_array_get_idx(login_profiles, 0); + json_object* json_email = NULL; + if (!json_object_object_get_ex(login_profiles, "name", &json_email)) { + return false; + } + + *email = json_object_get_string(json_email); + return true; +} + +bool ParseJsonToSuccess(const string& json) { + json_object* root = NULL; + root = json_tokener_parse(json.c_str()); + if (root == NULL) { + return false; + } + json_object* success = NULL; + if (!json_object_object_get_ex(root, "success", &success)) { + return false; + } + return (bool)json_object_get_boolean(success); +} + +bool ParseJsonToKey(const string& json, const string& key, string* response) { + json_object* root = NULL; + json_object* json_response = NULL; + const char* c_response; + + root = json_tokener_parse(json.c_str()); + if (root==NULL) { + return false; + } + + if (!json_object_object_get_ex(root, key.c_str(), &json_response)) { + return false; + } + + if (!(c_response = json_object_get_string(json_response))) { + return false; + } + + *response = c_response; + return true; +} + +bool ParseJsonToChallenges(const string& json, + std::vector<Challenge> *challenges) { + json_object* root = NULL; + + root = json_tokener_parse(json.c_str()); + if (root == NULL) { + return false; + } + + json_object *jsonChallenges = NULL; + if (!json_object_object_get_ex(root, "challenges", &jsonChallenges)) { + return false; + } + + json_object *challengeId, *challengeType, *challengeStatus = NULL; + for (int i = 0; i < json_object_array_length(jsonChallenges); ++i) { + if (!json_object_object_get_ex(json_object_array_get_idx(jsonChallenges, i), + "challengeId", &challengeId)) { + return false; + } + if (!json_object_object_get_ex(json_object_array_get_idx(jsonChallenges, i), + "challengeType", &challengeType)) { + return false; + } + if (!json_object_object_get_ex(json_object_array_get_idx(jsonChallenges, i), + "status", &challengeStatus)) { + return false; + } + Challenge challenge; + challenge.id = json_object_get_int(challengeId); + challenge.type = json_object_get_string(challengeType); + challenge.status = json_object_get_string(challengeStatus); + + challenges->push_back(challenge); + } + + return true; +} + +bool GetUser(const string& username, string* response) { + std::stringstream url; + url << kMetadataServerUrl << "users?username=" << UrlEncode(username); + + long http_code = 0; + if (!HttpGet(url.str(), response, &http_code) || response->empty() + || http_code != 200) { + return false; + } + return true; +} + +bool StartSession(const string& email, string* response) { + bool ret = true; + struct json_object *jobj, *jarr; + + jarr = json_object_new_array(); + json_object_array_add(jarr, json_object_new_string(INTERNAL_TWO_FACTOR)); + json_object_array_add(jarr, json_object_new_string(TOTP)); + json_object_array_add(jarr, json_object_new_string(IDV_PREREGISTERED_PHONE)); + + jobj = json_object_new_object(); + json_object_object_add(jobj, "email", json_object_new_string(email.c_str())); + json_object_object_add(jobj, "supportedChallengeTypes", jarr); + + const char* data; + data = json_object_to_json_string_ext(jobj, JSON_C_TO_STRING_PLAIN); + + std::stringstream url; + url << kMetadataServerUrl << "authenticate/sessions/start"; + + long http_code = 0; + if (!HttpPost(url.str(), data, response, &http_code) || response->empty() + || http_code != 200) { + ret = false; + } + + json_object_put(jarr); + json_object_put(jobj); + + return ret; +} + +bool ContinueSession(const string& email, const string& user_token, + const string& session_id, const Challenge& challenge, + string* response) { + bool ret = true; + struct json_object *jobj, *jresp; + + jobj = json_object_new_object(); + json_object_object_add(jobj, "email", json_object_new_string(email.c_str())); + json_object_object_add(jobj, "challengeId", + json_object_new_int(challenge.id)); + + if (challenge.type != AUTHZEN) { + jresp = json_object_new_object(); + json_object_object_add(jresp, "credential", + json_object_new_string(user_token.c_str())); + json_object_object_add(jobj, "proposalResponse", jresp); + } + + if (challenge.status != "READY") { + json_object_object_add(jobj, "action", + json_object_new_string("startAlternate")); + } + + const char* data = NULL; + data = json_object_to_json_string_ext(jobj, JSON_C_TO_STRING_PLAIN); + + std::stringstream url; + url << kMetadataServerUrl << "authenticate/sessions/" + << session_id << "/continue"; + long http_code = 0; + if (!HttpPost(url.str(), data, response, &http_code) || response->empty() + || http_code != 200) { + ret = false; + } + + json_object_put(jobj); + if (challenge.type != AUTHZEN) { + json_object_put(jresp); + } + + return ret; +} +} // namespace oslogin_utils diff --git a/packages/google-compute-engine-oslogin/utils/oslogin_utils.h b/packages/google-compute-engine-oslogin/utils/oslogin_utils.h new file mode 100644 index 0000000..3788521 --- /dev/null +++ b/packages/google-compute-engine-oslogin/utils/oslogin_utils.h @@ -0,0 +1,202 @@ +// Copyright 2017 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include <pthread.h> +#include <pwd.h> +#include <string> +#include <vector> + +#define TOTP "TOTP" +#define AUTHZEN "AUTHZEN" +#define INTERNAL_TWO_FACTOR "INTERNAL_TWO_FACTOR" +#define IDV_PREREGISTERED_PHONE "IDV_PREREGISTERED_PHONE" + +using std::string; +using std::vector; + +namespace oslogin_utils { + +// Metadata server URL. +static const char kMetadataServerUrl[] = + "http://metadata.google.internal/computeMetadata/v1/oslogin/"; + +// BufferManager encapsulates and manages a buffer and length. This class is not +// thread safe. +class BufferManager { + public: + // Create a BufferManager that will dole out chunks of buf as requested. + BufferManager(char* buf, size_t buflen); + + // Copies a string to the buffer and sets the buffer to point to that + // string. Copied string is guaranteed to be null-terminated. + // Returns false and sets errnop if there is not enough space left in the + // buffer for the string. + bool AppendString(const string& value, char** buffer, int* errnop); + + private: + // Return a pointer to a buffer of size bytes. + void* Reserve(size_t bytes); + // Whether there is space available in the buffer. + bool CheckSpaceAvailable(size_t bytes_to_write) const; + + char* buf_; + size_t buflen_; + + // Not copyable or assignable. + BufferManager& operator=(const BufferManager&); + BufferManager(const BufferManager&); +}; + +// Challenge represents a security challenge available to the user. +class Challenge { + public: + int id; + string type; + string status; +}; + +// NssCache caches passwd entries for getpwent_r. This is used to prevent making +// an HTTP call on every getpwent_r invocation. Stores up to cache_size entries +// at a time. This class is not thread safe. +class NssCache { + public: + explicit NssCache(int cache_size); + + // Clears and resets the NssCache. + void Reset(); + + // Whether the cache has a next passwd entry. + bool HasNextPasswd(); + + // Whether the cache has reached the last page of the database. + bool OnLastPage() { return on_last_page_; } + + // Grabs the next passwd entry. Returns true on success. Sets errnop on + // failure. + bool GetNextPasswd(BufferManager* buf, passwd* result, int* errnop); + + // Loads a json array of passwd entries in the cache, starting at the + // beginning of the cache. This will remove all previous entries in the cache. + // response is expected to be a JSON array of passwd entries. Returns + // true on success. + bool LoadJsonArrayToCache(string response); + + // Helper method that effectively implements the getpwent_r nss method. Each + // call will iterate through the OsLogin database and return the next entry. + // Internally, the cache will keep track of pages of passwd entries, and will + // make an http call to the server if necessary to retrieve additional + // entries. Returns whether passwd retrieval was successful. If true, the + // passwd result will contain valid data. + bool NssGetpwentHelper(BufferManager* buf, struct passwd* result, int* errnop); + + // Returns the page token for requesting the next page of passwd entries. + string GetPageToken() { return page_token_; } + + private: + // The maximum size of the cache. + int cache_size_; + + // Vector of passwds. These are represented as stringified json object. + std::vector<std::string> passwd_cache_; + + // The page token for requesting the next page of passwds. + std::string page_token_; + + // Index for requesting the next passwd from the cache. + int index_; + + // Whether the NssCache has reached the last page of the database. + bool on_last_page_; + + // Not copyable or assignable. + NssCache& operator=(const NssCache&); + NssCache(const NssCache&); +}; + +// Auto locks and unlocks a given mutex on construction/destruction. Does NOT +// take ownership of the mutex. +class MutexLock { + public: + explicit MutexLock(pthread_mutex_t* mutex) : mutex_(mutex) { + pthread_mutex_lock(mutex_); + } + + ~MutexLock() { pthread_mutex_unlock(mutex_); } + + private: + // The mutex to lock/unlock + pthread_mutex_t* const mutex_; + + // Not copyable or assignable. + MutexLock& operator=(const MutexLock); + MutexLock(const MutexLock&); +}; + +// Callback invoked when Curl completes a request. +size_t +OnCurlWrite(void* buf, size_t size, size_t nmemb, void* userp); + +// Uses Curl to issue a GET request to the given url. Returns whether the +// request was successful. If successful, the result from the server will be +// stored in response, and the HTTP response code will be stored in http_code. +bool HttpGet(const string& url, string* response, long* http_code); +bool HttpPost(const string& url, const string& data, string* response, + long* http_code); + +// Returns whether user_name is a valid OsLogin user name. +bool ValidateUserName(const string& user_name); + +// URL encodes the given parameter. Returns the encoded parameter. +std::string UrlEncode(const string& param); + +// Returns true if the given passwd contains valid fields. If pw_dir, pw_shell, +// or pw_passwd are not set, this will populate these entries with default +// values. +bool ValidatePasswd(struct passwd* result, BufferManager* buf, + int* errnop); + +// Parses a JSON LoginProfiles response for SSH keys. Returns a vector of valid +// ssh_keys. A key is considered valid if it's expiration date is greater than +// current unix time. +std::vector<string> ParseJsonToSshKeys(const string& json); + +// Parses a JSON LoginProfiles response and returns the email under the "name" +// field. +bool ParseJsonToKey(const string& json, const string& key, string* email); +bool ParseJsonToEmail(const string& json, string* email); + +// Parses a JSON LoginProfiles response and populates the passwd struct with the +// corresponding values set in the JSON object. Returns whether the parse was +// successful or not. If unsuccessful, errnop will also be set. +bool ParseJsonToPasswd(const string& response, struct passwd* result, + BufferManager* buf, int* errnop); + +// Parses a JSON adminLogin or login response and returns whether the user has +// the requested privilege. +bool ParseJsonToSuccess(const string& json); + +// Parses a JSON startSession response into a vector of Challenge objects. +bool ParseJsonToChallenges(const string& json, vector<Challenge> *challenges); + +// Calls the startSession API. +bool StartSession(const string& email, string* response); + +// Calls the continueSession API. +bool ContinueSession(const string& email, const string& user_token, + const string& session_id, const Challenge& challenge, + string* response); + +// Returns user information from the metadata server. +bool GetUser(const string& username, string* response); +} // namespace oslogin_utils diff --git a/packages/google-compute-engine-oslogin/utils/oslogin_utils_test.cc b/packages/google-compute-engine-oslogin/utils/oslogin_utils_test.cc new file mode 100644 index 0000000..ecc4c11 --- /dev/null +++ b/packages/google-compute-engine-oslogin/utils/oslogin_utils_test.cc @@ -0,0 +1,510 @@ +// Copyright 2017 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Requires libgtest-dev and gtest compiled and installed. +#include "oslogin_utils.h" + +#include <errno.h> +#include <gtest/gtest.h> +#include <stdio.h> +#include <stdlib.h> + +using std::string; +using std::vector; + +namespace oslogin_utils { + + +// Test that the buffer can successfully append multiple strings. +TEST(BufferManagerTest, TestAppendString) { + size_t buflen = 20; + char* buffer = (char*)malloc(buflen *sizeof(char)); + ASSERT_STRNE(buffer, NULL); + + char* first_string; + char* second_string; + int test_errno = 0; + oslogin_utils::BufferManager buffer_manager(buffer, buflen); + buffer_manager.AppendString("test1", &first_string, &test_errno); + buffer_manager.AppendString("test2", &second_string, &test_errno); + EXPECT_EQ(test_errno, 0); + ASSERT_STREQ(first_string, "test1"); + ASSERT_STREQ(second_string, "test2"); + ASSERT_STREQ(buffer, "test1"); + ASSERT_STREQ((buffer + 6), "test2"); +} + +// Test that attempting to append a string larger than the buffer can handle +// fails with ERANGE. +TEST(BufferManagerTest, TestAppendStringTooLarge) { + size_t buflen = 1; + char* buffer = (char*)malloc(buflen * sizeof(char)); + ASSERT_STRNE(buffer, NULL); + + char* first_string; + int test_errno = 0; + oslogin_utils::BufferManager buffer_manager(buffer, buflen); + ASSERT_FALSE( + buffer_manager.AppendString("test1", &first_string, &test_errno)); + EXPECT_EQ(test_errno, ERANGE); +} + +// Test successfully loading and retrieving an array of JSON posix accounts. +TEST(NssCacheTest, TestLoadJsonArray) { + NssCache nss_cache(2); + string test_user1 = + "{\"name\":\"foo@example.com\",\"posixAccounts\":[" + "{\"primary\":true,\"username\":\"foo\",\"uid\":1337,\"gid\":1337," + "\"homeDirectory\":\"/home/foo\",\"shell\":\"/bin/bash\"}]}"; + string test_user2 = + "{\"name\":\"bar@example.com\"," + "\"posixAccounts\":[" + "{\"primary\":true,\"username\":\"bar\",\"uid\":1338,\"gid\":1338," + "\"homeDirectory\":\"/home/bar\",\"shell\":\"/bin/bash\"}]}"; + string response = "{\"loginProfiles\": [" + test_user1 + ", " + test_user2 + + "], \"nextPageToken\": \"token\"}"; + + ASSERT_TRUE(nss_cache.LoadJsonArrayToCache(response)); + + size_t buflen = 500; + char* buffer = (char*)malloc(buflen * sizeof(char)); + ASSERT_STRNE(buffer, NULL); + BufferManager buf(buffer, buflen); + struct passwd result; + int test_errno = 0; + + // Verify that the first user was stored. + ASSERT_TRUE(nss_cache.HasNextPasswd()); + ASSERT_TRUE(nss_cache.GetNextPasswd(&buf, &result, &test_errno)); + EXPECT_EQ(test_errno, 0); + EXPECT_EQ(result.pw_uid, 1337); + EXPECT_EQ(result.pw_gid, 1337); + ASSERT_STREQ(result.pw_name, "foo"); + ASSERT_STREQ(result.pw_shell, "/bin/bash"); + ASSERT_STREQ(result.pw_dir, "/home/foo"); + + // Verify that the second user was stored. + ASSERT_TRUE(nss_cache.HasNextPasswd()); + ASSERT_TRUE(nss_cache.GetNextPasswd(&buf, &result, &test_errno)); + EXPECT_EQ(test_errno, 0); + EXPECT_EQ(result.pw_uid, 1338); + EXPECT_EQ(result.pw_gid, 1338); + ASSERT_STREQ(result.pw_name, "bar"); + ASSERT_STREQ(result.pw_shell, "/bin/bash"); + ASSERT_STREQ(result.pw_dir, "/home/bar"); + + // Verify that there are no more users stored. + ASSERT_FALSE(nss_cache.HasNextPasswd()); + ASSERT_FALSE(nss_cache.GetNextPasswd(&buf, &result, &test_errno)); + EXPECT_EQ(test_errno, ENOENT); +} + +// Test successfully loading and retrieving a partial array. +TEST(NssCacheTest, TestLoadJsonPartialArray) { + NssCache nss_cache(2); + string test_user1 = + "{\"name\":\"foo@example.com\",\"posixAccounts\":[" + "{\"primary\":true,\"username\":\"foo\",\"uid\":1337,\"gid\":1337," + "\"homeDirectory\":\"/home/foo\",\"shell\":\"/bin/bash\"}]}"; + string response = + "{\"loginProfiles\": [" + test_user1 + "], \"nextPageToken\": \"token\"}"; + + ASSERT_TRUE(nss_cache.LoadJsonArrayToCache(response)); + + size_t buflen = 500; + char* buffer = (char*)malloc(buflen * sizeof(char)); + ASSERT_STRNE(buffer, NULL); + BufferManager buf(buffer, buflen); + struct passwd result; + int test_errno = 0; + + // Verify that the first user was stored. + ASSERT_TRUE(nss_cache.HasNextPasswd()); + ASSERT_TRUE(nss_cache.GetNextPasswd(&buf, &result, &test_errno)); + EXPECT_EQ(test_errno, 0); + EXPECT_EQ(result.pw_uid, 1337); + EXPECT_EQ(result.pw_gid, 1337); + ASSERT_STREQ(result.pw_name, "foo"); + ASSERT_STREQ(result.pw_shell, "/bin/bash"); + ASSERT_STREQ(result.pw_dir, "/home/foo"); + + ASSERT_EQ(nss_cache.GetPageToken(), "token"); + + // Verify that there are no more users stored. + ASSERT_FALSE(nss_cache.HasNextPasswd()); + ASSERT_FALSE(nss_cache.GetNextPasswd(&buf, &result, &test_errno)); + EXPECT_EQ(test_errno, ENOENT); +} + +// Test successfully loading and retrieving the final response. +TEST(NssCacheTest, TestLoadJsonFinalResponse) { + NssCache nss_cache(2); + string response = + "{\"nextPageToken\": \"0\"}"; + + ASSERT_FALSE(nss_cache.LoadJsonArrayToCache(response)); + ASSERT_EQ(nss_cache.GetPageToken(), ""); + + size_t buflen = 500; + char* buffer = (char*)malloc(buflen * sizeof(char)); + ASSERT_STRNE(buffer, NULL); + BufferManager buf(buffer, buflen); + struct passwd result; + int test_errno = 0; + + // Verify that there are no more users stored. + ASSERT_FALSE(nss_cache.HasNextPasswd()); + ASSERT_TRUE(nss_cache.OnLastPage()); + ASSERT_FALSE(nss_cache.GetNextPasswd(&buf, &result, &test_errno)); + EXPECT_EQ(test_errno, ENOENT); +} + + +// Tests that resetting, and checking HasNextPasswd does not crash. +TEST(NssCacheTest, ResetNullPtrTest) { + NssCache nss_cache(2); + nss_cache.Reset(); + ASSERT_FALSE(nss_cache.HasNextPasswd()); +} + +// Test parsing a valid JSON response from the metadata server. +TEST(ParseJsonPasswdTest, ParseJsonToPasswdSucceeds) { + string test_user = + "{\"loginProfiles\":[{\"name\":\"foo@example.com\",\"posixAccounts\":[" + "{\"primary\":true,\"username\":\"foo\",\"uid\":1337,\"gid\":1338," + "\"homeDirectory\":\"/home/foo\",\"shell\":\"/bin/bash\"}]}]}"; + + size_t buflen = 200; + char* buffer = (char*)malloc(buflen * sizeof(char)); + ASSERT_STRNE(buffer, NULL); + BufferManager buf(buffer, buflen); + struct passwd result; + int test_errno = 0; + ASSERT_TRUE(ParseJsonToPasswd(test_user, &result, &buf, &test_errno)); + EXPECT_EQ(result.pw_uid, 1337); + EXPECT_EQ(result.pw_gid, 1338); + ASSERT_STREQ(result.pw_name, "foo"); + ASSERT_STREQ(result.pw_shell, "/bin/bash"); + ASSERT_STREQ(result.pw_dir, "/home/foo"); +} + +// Test parsing a valid JSON response from the metadata server with uid > 2^31. +TEST(ParseJsonPasswdTest, ParseJsonToPasswdSucceedsWithHighUid) { + string test_user = + "{\"loginProfiles\":[{\"name\":\"foo@example.com\",\"posixAccounts\":[" + "{\"primary\":true,\"username\":\"foo\",\"uid\":4294967295,\"gid\":" + "4294967295,\"homeDirectory\":\"/home/foo\",\"shell\":\"/bin/bash\"}]}]}"; + + size_t buflen = 200; + char* buffer = (char*)malloc(buflen * sizeof(char)); + ASSERT_STRNE(buffer, NULL); + BufferManager buf(buffer, buflen); + struct passwd result; + int test_errno = 0; + ASSERT_TRUE(ParseJsonToPasswd(test_user, &result, &buf, &test_errno)); + EXPECT_EQ(result.pw_uid, 4294967295); + EXPECT_EQ(result.pw_gid, 4294967295); + ASSERT_STREQ(result.pw_name, "foo"); + ASSERT_STREQ(result.pw_shell, "/bin/bash"); + ASSERT_STREQ(result.pw_dir, "/home/foo"); +} + +TEST(ParseJsonPasswdTest, ParseJsonToPasswdSucceedsWithStringUid) { + string test_user = + "{\"loginProfiles\":[{\"name\":\"foo@example.com\",\"posixAccounts\":[" + "{\"primary\":true,\"username\":\"foo\",\"uid\":\"1337\",\"gid\":" + "\"1338\",\"homeDirectory\":\"/home/foo\",\"shell\":\"/bin/bash\"}]}]}"; + + size_t buflen = 200; + char* buffer = (char*)malloc(buflen * sizeof(char)); + ASSERT_STRNE(buffer, NULL); + BufferManager buf(buffer, buflen); + struct passwd result; + int test_errno = 0; + ASSERT_TRUE(ParseJsonToPasswd(test_user, &result, &buf, &test_errno)); + EXPECT_EQ(result.pw_uid, 1337); + EXPECT_EQ(result.pw_gid, 1338); + ASSERT_STREQ(result.pw_name, "foo"); + ASSERT_STREQ(result.pw_shell, "/bin/bash"); + ASSERT_STREQ(result.pw_dir, "/home/foo"); +} + +TEST(ParseJsonPasswdTest, ParseJsonToPasswdNoLoginProfilesSucceeds) { + string test_user = + "{\"name\":\"foo@example.com\",\"posixAccounts\":[" + "{\"primary\":true,\"username\":\"foo\",\"uid\":1337,\"gid\":1337," + "\"homeDirectory\":\"/home/foo\",\"shell\":\"/bin/bash\"}]}"; + + size_t buflen = 200; + char* buffer = (char*)malloc(buflen * sizeof(char)); + ASSERT_STRNE(buffer, NULL); + BufferManager buf(buffer, buflen); + struct passwd result; + int test_errno = 0; + ASSERT_TRUE(ParseJsonToPasswd(test_user, &result, &buf, &test_errno)); + EXPECT_EQ(result.pw_uid, 1337); + EXPECT_EQ(result.pw_gid, 1337); + ASSERT_STREQ(result.pw_name, "foo"); + ASSERT_STREQ(result.pw_shell, "/bin/bash"); + ASSERT_STREQ(result.pw_dir, "/home/foo"); +} + +// Test parsing a JSON response without enough space in the buffer. +TEST(ParseJsonPasswdTest, ParseJsonToPasswdFailsWithERANGE) { + string test_user = + "{\"loginProfiles\":[{\"name\":\"foo@example.com\",\"posixAccounts\":[" + "{\"primary\":true,\"username\":\"foo\",\"uid\":1337,\"gid\":1337," + "\"homeDirectory\":\"/home/foo\",\"shell\":\"/bin/bash\"}]}]}"; + + size_t buflen = 1; + char* buffer = (char*)malloc(buflen * sizeof(char)); + ASSERT_STRNE(buffer, NULL); + BufferManager buf(buffer, buflen); + struct passwd result; + int test_errno = 0; + ASSERT_FALSE(ParseJsonToPasswd(test_user, &result, &buf, &test_errno)); + EXPECT_EQ(test_errno, ERANGE); +} + +// Test parsing malformed JSON responses. +TEST(ParseJsonPasswdTest, ParseJsonToPasswdFailsWithEINVAL) { + string test_user = + "{\"loginProfiles\":[{\"name\":\"foo@example.com\",\"posixAccounts\":[" + "{\"primary\":true,\"username\":\"foo\",\"uid\": \"bad_stuff\"" + ",\"gid\":1337,\"homeDirectory\":\"/home/foo\"," + "\"shell\":\"/bin/bash\"}]}]}"; + string test_user2 = + "{\"loginProfiles\":[{\"name\":\"foo@example.com\",\"posixAccounts\":[" + "{\"primary\":true,\"username\":\"foo\",\"uid\": 1337," + "\"gid\":\"bad_stuff\",\"homeDirectory\":\"/home/foo\"," + "\"shell\":\"/bin/bash\"}]}]}"; + + size_t buflen = 200; + char* buffer = (char*)malloc(buflen * sizeof(char)); + ASSERT_STRNE(buffer, NULL); + BufferManager buf(buffer, buflen); + struct passwd result; + int test_errno = 0; + ASSERT_FALSE(ParseJsonToPasswd(test_user, &result, &buf, &test_errno)); + EXPECT_EQ(test_errno, EINVAL); + // Reset errno. + test_errno = 0; + ASSERT_TRUE(ParseJsonToPasswd(test_user2, &result, &buf, &test_errno)); + EXPECT_EQ(test_errno, 0); + EXPECT_EQ(result.pw_uid, 1337); + EXPECT_EQ(result.pw_gid, 1337); +} + +// Test parsing a partially filled response. Validate should fill empty fields +// with default values. +TEST(ParseJsonPasswdTest, ValidatePartialJsonResponse) { + string test_user = + "{\"loginProfiles\":[{\"name\":\"foo@example.com\",\"posixAccounts\":[" + "{\"primary\":true,\"username\":\"foo\",\"uid\":1337,\"gid\":1337}]" + "}]}"; + + size_t buflen = 200; + char* buffer = (char*)malloc(buflen * sizeof(char)); + ASSERT_STRNE(buffer, NULL); + BufferManager buf(buffer, buflen); + struct passwd result; + int test_errno = 0; + ASSERT_TRUE(ParseJsonToPasswd(test_user, &result, &buf, &test_errno)); + EXPECT_EQ(result.pw_uid, 1337); + EXPECT_EQ(result.pw_gid, 1337); + ASSERT_STREQ(result.pw_name, "foo"); + ASSERT_STREQ(result.pw_shell, "/bin/bash"); + ASSERT_STREQ(result.pw_dir, "/home/foo"); +} + +// Test parsing an invalid response. Validate should cause the parse to fail if +// there is no uid. +TEST(ParseJsonPasswdTest, ValidateInvalidJsonResponse) { + string test_user = + "{\"loginProfiles\":[{\"name\":\"foo@example.com\",\"posixAccounts\":[" + "{\"primary\":true,\"username\":\"foo\",\"gid\":1337}]" + "}]}"; + + size_t buflen = 200; + char* buffer = (char*)malloc(buflen * sizeof(char)); + ASSERT_STRNE(buffer, NULL); + BufferManager buf(buffer, buflen); + struct passwd result; + int test_errno = 0; + ASSERT_FALSE(ParseJsonToPasswd(test_user, &result, &buf, &test_errno)); + EXPECT_EQ(test_errno, EINVAL); +} + +TEST(ParseJsonEmailTest, SuccessfullyParsesEmail) { + string test_user = + "{\"loginProfiles\":[{\"name\":\"foo@example.com\",\"posixAccounts\":[" + "{\"primary\":true,\"username\":\"foo\",\"gid\":1337}]" + "}]}"; + + string email; + ASSERT_TRUE(ParseJsonToEmail(test_user, &email)); + ASSERT_EQ(email, "foo@example.com"); +} + +TEST(ParseJsonEmailTest, FailsParseEmail) { + string email; + ASSERT_FALSE(ParseJsonToEmail("random_junk", &email)); + ASSERT_EQ(email, ""); +} + +TEST(ParseJsonSshKeyTest, ParseJsonToSshKeysSucceeds) { + string test_user = + "{\"loginProfiles\":[{\"name\":\"foo@example.com\",\"sshPublicKeys\":" + "{\"fingerprint\": {\"key\": \"test_key\"}}}]}"; + + size_t buflen = 200; + char* buffer = (char*)malloc(buflen * sizeof(char)); + ASSERT_STRNE(buffer, NULL); + BufferManager buf(buffer, buflen); + int test_errno = 0; + std::vector<string> result = ParseJsonToSshKeys(test_user); + EXPECT_EQ(result.size(), 1); + EXPECT_EQ(result[0], "test_key"); +} + +TEST(ParseJsonSshKeyTest, ParseJsonToSshKeysMultipleKeys) { + string test_user = + "{\"loginProfiles\":[{\"name\":\"foo@example.com\",\"sshPublicKeys\":" + "{\"fingerprint\": {\"key\": \"test_key\"}, \"fingerprint2\": {\"key\": " + "\"test_key2\"}}}]}"; + + size_t buflen = 200; + char* buffer = (char*)malloc(buflen * sizeof(char)); + ASSERT_STRNE(buffer, NULL); + BufferManager buf(buffer, buflen); + int test_errno = 0; + std::vector<string> result = ParseJsonToSshKeys(test_user); + EXPECT_EQ(result.size(), 2); + EXPECT_EQ(result[0], "test_key"); + EXPECT_EQ(result[1], "test_key2"); +} + +TEST(ParseJsonSshKeyTest, ParseJsonToSshKeysFiltersExpiredKeys) { + string test_user = + "{\"loginProfiles\":[{\"name\":\"foo@example.com\",\"sshPublicKeys\":" + "{\"fingerprint\": {\"key\": \"test_key\"}, \"fingerprint2\": {\"key\": " + "\"test_key2\", \"expirationTimeUsec\": 0}}}]}"; + + size_t buflen = 200; + char* buffer = (char*)malloc(buflen * sizeof(char)); + ASSERT_STRNE(buffer, NULL); + BufferManager buf(buffer, buflen); + int test_errno = 0; + std::vector<string> result = ParseJsonToSshKeys(test_user); + EXPECT_EQ(result.size(), 1); + EXPECT_EQ(result[0], "test_key"); +} + +TEST(ParseJsonSshKeyTest, ParseJsonToSshKeysFiltersMalformedExpiration) { + string test_user = + "{\"loginProfiles\":[{\"name\":\"foo@example.com\",\"sshPublicKeys\":" + "{\"fingerprint\": {\"key\": \"test_key\"}, \"fingerprint2\": {\"key\": " + "\"test_key2\", \"expirationTimeUsec\": \"bad_stuff\"}}}]}"; + + size_t buflen = 200; + char* buffer = (char*)malloc(buflen * sizeof(char)); + ASSERT_STRNE(buffer, NULL); + BufferManager buf(buffer, buflen); + int test_errno = 0; + std::vector<string> result = ParseJsonToSshKeys(test_user); + EXPECT_EQ(result.size(), 1); + EXPECT_EQ(result[0], "test_key"); +} + +TEST(ParseJsonAuthorizeSuccess, SuccessfullyAuthorized) { + string response = "{\"success\": true}"; + ASSERT_TRUE(ParseJsonToSuccess(response)); +} + +TEST(ValidateUserNameTest, ValidateValidUserNames) { + string cases[] = { + "user", + "_", + ".", + ".abc_", + "_abc-", + "ABC", + "A_.-", + "ausernamethirtytwocharacterslong" + }; + for (auto test_user : cases) { + ASSERT_TRUE(ValidateUserName(test_user)); + } +} + +TEST(ValidateUserNameTest, ValidateInvalidUserNames) { + string cases[] = { + "", + "!#$%^", + "-abc", + "#abc", + "^abc", + "abc*xyz", + "abc xyz", + "xyz*", + "xyz$", + "usernamethirtythreecharacterslong", + "../../etc/shadow", + }; + for (auto test_user : cases) { + ASSERT_FALSE(ValidateUserName(test_user)); + } +} + +TEST(ParseJsonKeyTest, TestKey) { + string test_json = "{\"some_key\":\"some_value\"}"; + string value; + ASSERT_TRUE(ParseJsonToKey(test_json, "some_key", &value)); + ASSERT_EQ(value, "some_value"); +} + +TEST(ParseJsonKeyTest, TestMissingKey) { + string test_json = "{\"some_key\":\"some_value\"}"; + string value; + ASSERT_FALSE(ParseJsonToKey(test_json, "some_other_key", &value)); + ASSERT_EQ(value, ""); +} + +TEST(ParseJsonChallengesTest, TestChallenges) { + string challenges_json = "{\"status\":\"CHALLENGE_REQUIRED\",\"sessionId\":" + "\"testSessionId\",\"challenges\":[{\"challengeId\":1,\"challengeType\":" + "\"TOTP\",\"status\":\"READY\"}, {\"challengeId\":2,\"challengeType\":" + "\"AUTHZEN\",\"status\":\"PROPOSED\"}]}"; + vector<Challenge> challenges; + ASSERT_TRUE(ParseJsonToChallenges(challenges_json, &challenges)); + EXPECT_EQ(challenges.size(), 2); + EXPECT_EQ(challenges[0].id, 1); + EXPECT_EQ(challenges[0].type, "TOTP"); +} + +TEST(ParseJsonChallengesTest, TestMalformedChallenges) { + string challenges_json = "{\"status\":\"CHALLENGE_REQUIRED\",\"sessionId\":" + "\"testSessionId\",\"challenges\":[{\"challengeId\":1,\"challengeType\":" + "\"TOTP\",\"status\":\"READY\"}, {\"challengeId\":2,\"challengeType\":" + "\"AUTHZEN\"}]}"; + vector<Challenge> challenges; + ASSERT_FALSE(ParseJsonToChallenges(challenges_json, &challenges)); + EXPECT_EQ(challenges.size(), 1); +} +} // namespace oslogin_utils + +int main(int argc, char **argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} diff --git a/packages/google-compute-engine-oslogin/utils/run_tests.sh b/packages/google-compute-engine-oslogin/utils/run_tests.sh new file mode 100755 index 0000000..83adcdc --- /dev/null +++ b/packages/google-compute-engine-oslogin/utils/run_tests.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Copyright 2017 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Unit tests require gtest to be installed. +g++ -o test_runner oslogin_utils_test.cc oslogin_utils.cc -I/usr/include/json-c -lcurl -ljson-c -lgtest -lpthread +./test_runner +rm ./test_runner |