diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..6240779 --- /dev/null +++ b/.clang-format @@ -0,0 +1,12 @@ +# See more @ https://clang.llvm.org/docs/ClangFormatStyleOptions.html +BasedOnStyle: LLVM +UseTab: Never +IndentWidth: 2 +TabWidth: 2 +BreakBeforeBraces: Allman +AllowShortIfStatementsOnASingleLine: false +IndentCaseLabels: false +ColumnLimit: 0 +AccessModifierOffset: -2 +PointerAlignment: Left +NamespaceIndentation: None diff --git a/.devcontainer/alpine/Dockerfile b/.devcontainer/alpine/Dockerfile new file mode 100644 index 0000000..af5ad95 --- /dev/null +++ b/.devcontainer/alpine/Dockerfile @@ -0,0 +1,58 @@ +FROM alpine:3.13 + +RUN sed -i 's|dl-cdn.alpinelinux.org/alpine|ftp.acc.umu.se/mirror/alpinelinux.org|g' /etc/apk/repositories + +# base-tools for C and C++ +RUN apk update \ + && apk add --no-cache \ + autoconf \ + automake \ + cmake \ + g++ \ + gcc \ + gdb \ + libtool \ + make \ + pkgconf \ + && rm -r /var/cache/apk/* + +# Other useful tools for C and C++ +RUN apk update \ + && apk add --no-cache \ + clang-extra-tools \ + valgrind \ + && rm -r /var/cache/apk/* + +# Other useful tools. +RUN apk update \ + && apk add --no-cache \ + bash \ + ca-certificates \ + curl \ + git \ + sudo \ + && rm -r /var/cache/apk/* + +# project-specific dependencies +RUN apk update \ + && apk add --no-cache \ + openssl-dev \ + musl-nscd-dev \ + && rm -r /var/cache/apk/* + +RUN adduser \ + -s /bin/bash \ + -D \ + build \ + && addgroup sudo \ + && adduser build sudo \ + && echo -e "\n# Allow sudo without password\n%sudo ALL=(ALL) NOPASSWD:ALL\n" >> /etc/sudoers + +# Don't run as root inside container, +# see https://github.com/microsoft/vscode-remote-release/issues/22 +ENV HOME /home/build +ENV THIRD_PARTY_ROOT /home/build/third_party +ENV DIST alpine +USER build + +CMD ["/bin/bash", "-l"] diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..734c81e --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,17 @@ +// Ref: https://code.visualstudio.com/docs/remote/containers#_devcontainerjson-reference +{ + "name": "nss-http", + "image": "1nfiniteloop/nss-http-builder", + "extensions": [ + "llvm-vs-code-extensions.vscode-clangd", + "ms-vscode.cmake-tools", + "twxs.cmake" + ], + "runArgs": [ + "--name=nss-http.vscode", + "--volume=vscode.cache:/home/build", + "--cap-add=SYS_PTRACE", + "--security-opt", + "seccomp=unconfined" + ] +} diff --git a/.devcontainer/ubuntu/Dockerfile b/.devcontainer/ubuntu/Dockerfile new file mode 100644 index 0000000..a8961c6 --- /dev/null +++ b/.devcontainer/ubuntu/Dockerfile @@ -0,0 +1,73 @@ +FROM ubuntu:focal + +ENV DEBIAN_FRONTEND=noninteractive + +RUN sed --in-place=~ 's|archive.ubuntu.com|ftp.acc.umu.se|g' /etc/apt/sources.list + +# base-tools for C and C++ +RUN apt-get update \ + && apt-get install \ + --no-install-recommends \ + --assume-yes \ + --quiet \ + autoconf \ + automake \ + cmake \ + g++ \ + gcc \ + gdb \ + libtool \ + make \ + pkg-config \ + && rm -r /var/lib/apt/lists/* + +# Other useful tools for C and C++ +RUN apt-get update \ + && apt-get install \ + --no-install-recommends \ + --assume-yes \ + --quiet \ + clangd \ + valgrind \ + && rm -r /var/lib/apt/lists/* + +# Other useful tools. +RUN apt-get update \ + && apt-get install \ + --no-install-recommends \ + --assume-yes \ + --quiet \ + ca-certificates \ + curl \ + git \ + less \ + nano \ + sudo \ + xz-utils \ + && rm -r /var/lib/apt/lists/* + +# Project-specific dependencies +RUN apt-get update \ + && apt-get install \ + --no-install-recommends \ + --assume-yes \ + --quiet \ + libssl-dev \ + && rm -r /var/lib/apt/lists/* + +RUN useradd \ + --uid 1000 \ + --shell /bin/bash \ + --create-home \ + build \ + && adduser build sudo \ + && echo "\n# Allow sudo without password\n%sudo ALL=(ALL) NOPASSWD:ALL\n" >> /etc/sudoers + +# Don't run as root inside container, +# see https://github.com/microsoft/vscode-remote-release/issues/22 +ENV HOME /home/build +ENV THIRD_PARTY_ROOT /home/build/third_party +ENV DIST ubuntu +USER build + +CMD ["/bin/bash", "-l"] diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 0000000..4f156bf --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,81 @@ +name: Build app + +on: + push: + branches: main + tags: + - 'v*' + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to Docker Hub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USER }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + # ref: https://github.com/docker/build-push-action + - name: Create builder container + uses: docker/build-push-action@v2 + with: + context: . + file: .devcontainer/ubuntu/Dockerfile + platforms: linux/amd64 + push: true + tags: | + 1nfiniteloop/nss-http-builder:latest + 1nfiniteloop/nss-http-builder:1.0.0 + # ref: https://github.com/docker/buildx/blob/master/docs/reference/buildx_build.md#-use-an-external-cache-source-for-a-build---cache-from + cache-from: type=registry,ref=1nfiniteloop/nss-http-builder:latest + cache-to: type=inline + + # ref: https://github.com/actions/cache + - name: Get cache for third party dependencies + id: cache + uses: actions/cache@v2 + with: + path: third_party_root/install + key: third-party-${{ hashFiles('third_party') }} + + - name: Build third-party dependencies + if: steps.cache.outputs.cache-hit != 'true' + run: | + docker run \ + --rm \ + --user root \ + --workdir /home/build \ + --env THIRD_PARTY_ROOT=/home/build/nss-http/third_party_root \ + --volume $(pwd):/home/build/nss-http \ + 1nfiniteloop/nss-http-builder /bin/bash -c ' \ + mkdir --parents nss-http/third_party_root/source \ + && for dep in nss-http/third_party/{boost,cpr,yaml_cpp}; \ + do ${dep} || break; done' + + - name: Build nss-http + run: | + docker run \ + --rm \ + --user root \ + --workdir /home/build \ + --env THIRD_PARTY_ROOT=/home/build/nss-http/third_party_root \ + --volume $(pwd):/home/build/nss-http \ + 1nfiniteloop/nss-http-builder /bin/bash -c ' \ + mkdir --parents nss-http/build/Release + cmake \ + -D VERSION=1.0.0 \ + -D BUILD=Release \ + -D UNITTEST=OFF \ + -S nss-http \ + -B nss-http/build/Release \ + && make --directory=nss-http/build/Release package' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c604ac4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +# Prerequisites +*.d + +# Compiled Object files +*.slo +*.lo +*.o +*.obj + +# Precompiled Headers +*.gch +*.pch + +# Compiled Dynamic libraries +*.so +*.dylib +*.dll + +# Fortran module files +*.mod +*.smod + +# Compiled Static libraries +*.lai +*.la +*.a +*.lib + +# Executables +*.exe +*.out +*.app + +# Project +build/ +.vscode/ +.clangd/ +compile_commands.json diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..0c0d0d6 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,42 @@ +cmake_minimum_required(VERSION 3.5) +project(nss-http + VERSION ${VERSION} + DESCRIPTION "Name service switch implementation for query unix accounts over http" + HOMEPAGE_URL "https://github.com/1nfiniteloop/nss-http" +) + +set(CMAKE_MODULE_PATH ${PROJECT_SOURCE_DIR}/cmake-module) +set(CMAKE_BUILD_TYPE ${BUILD}) +option(UNITTEST "Build unit tests and its dependencies" ON) + +# Language-specific settings +add_compile_options(-Wall -Wextra -pedantic -Werror) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_FLAGS_DEBUG "-ggdb3") +set(CMAKE_CXX_FLAGS_RELEASE "-O3") + +# THIRD_PARTY_ROOT and DIST provided from Dockerfile +set(CMAKE_PREFIX_PATH + $ENV{THIRD_PARTY_ROOT}/install/$ENV{DIST}/boost_1_75_0 + $ENV{THIRD_PARTY_ROOT}/install/$ENV{DIST}/cpr-1.6.2 + $ENV{THIRD_PARTY_ROOT}/install/$ENV{DIST}/yaml-cpp-0.6.3 + $ENV{THIRD_PARTY_ROOT}/install/$ENV{DIST}/googletest-release-1.10.0 +) + +if (UNITTEST) + include(ImportGTest) + enable_testing() + add_custom_target(test_all COMMAND ${CMAKE_CTEST_COMMAND}) +endif() + +# Third party dependencies: +include(ImportBoost) +include(ImportCpr) +include(ImportYamlCpp) + +# Project components +add_subdirectory(debian) +add_subdirectory(source) + +# Package +include(PackageDebian) \ No newline at end of file diff --git a/README.md b/README.md index 2007a4e..cc54802 100644 --- a/README.md +++ b/README.md @@ -1 +1,136 @@ -# nss-http \ No newline at end of file +# nss http + +nss-http is a plugin to query unix accounts over http(s). It is intended to be +used for storing unix accounts globally in one common database, instead of +manually keeping them synchronized locally on each computers. + +See server implementation @ . + +## Install + +Get debian package from latest release below, or build local debian package, see +[Build](Build). + + curl \ + --silent \ + --remote-name \ + https://github.com/1nfiniteloop/nss-http/releases/download/v1.0.0/nss-http-1.0.0-Linux.deb + +Install package + + sudo dpkg -i nss-http-1.0.0-Linux.deb + +## Configure + +Add http into `/etc/nsswitch.conf`. Make sure http is placed at the end, local +databases shall be queried first. + + passwd: files http + group: files http + shadow: files http + +Configuration files for nss-http are located in `/etc/nss-http`. + +* `shadow.auth-token.base64`: Add your token here generated from + `unix-accounts-server --generate-token` __with an additional new line__ after + the token. Permissions on this file controls who can query the password + database, they shall be equal to permissions on `/etc/shadow`. +* `configuration.yaml`: contains configuration for server url, endpoints and tls + client certificates, adjust accordingly. + +Errors are written to syslog tagged as "nss-http". + +## Additional configurations + +It's also convenient to get a home folder created for new users, configure +`/etc/pam.d/login` according to below. See more @ + + + ... + # Create home for new users + session required pam_mkhomedir.so skel=/etc/skel/ umask=0022 + + # Standard Un*x authentication. + @include common-auth + +It is recommended to use nss-http plugin with name service cache daemon +`nscd`. Install with: + + sudo apt-get install nscd + +Configure `/etc/nscd.conf` and keep only records for "group" and "passwd". + +## Develop + +### Overview + +The build-environment is based on a portable docker-container including all +basic tools needed, see more in `.devcontainer/ubuntu/Dockerfile`. No further +package installations is required, the only prerequisite is that Docker is +installed. + +Visual Studio Code (VS Code) together with the plugin +_"ms-vscode-remote.remote-containers"_ makes it very convenient to have the +development environment inside a container, see more @ +. To setup the development +environment simply open this project in VS Code. A notification with an option +to open this project inside the container will appear. Recommended VS Code +extensions will also be installed and set-up accordingly, see more in +`.devcontainer/devcontainer.json`. + +### Install dependencies + +__Note:__ This shall be run from inside the container. The dependencies is +cached on a docker volume under `~/third_party`. + + third-party/gtest \ + && third-party/boost \ + && third-party/cpr \ + && third-party/yaml-cpp + +### Build + +1. Create build tree with `mkdir --parents build/{Debug,Release}` + +2. Build _Debug_ and run tests: + + cd build/Debug \ + && cmake \ + -D CMAKE_EXPORT_COMPILE_COMMANDS=1 \ + -D VERSION=1.0.0 \ + -D BUILD=Debug \ + ../../ \ + && make test_all + +3. Build _Release_ and make package: + + cd build/Release \ + && cmake \ + -D CMAKE_EXPORT_COMPILE_COMMANDS=1 \ + -D VERSION=1.0.0 \ + -D BUILD=Release \ + -D UNITTEST=OFF \ + ../../ \ + && make package + +### Test + +Login with nss-http can be tested from a container with: + + docker exec -it -u root nss-http.vscode /bin/login foo + +nscd can be started in a container with + + sudo /etc/init.d/nscd start + +## Reference + +* glibc nss source: +* glibc unofficial github mirror: + +* Name service switch: + +* libc user and groups: + +* Another nss-http implementation served as inspiration: + diff --git a/cmake-module/ImportBoost.cmake b/cmake-module/ImportBoost.cmake new file mode 100644 index 0000000..d46d482 --- /dev/null +++ b/cmake-module/ImportBoost.cmake @@ -0,0 +1,8 @@ +set(Boost_USE_STATIC_LIBS ON) +find_package(Boost 1.75 REQUIRED + COMPONENTS json + PATH_SUFFIXES lib/cmake/Boost-1.75.0 +) + +# target library names can be found in file: +# ${THIRD_PARTY_ROOT}/install/${DIST}/boost_*/lib/cmake/Boost-*/BoostConfig.cmake diff --git a/cmake-module/ImportCpr.cmake b/cmake-module/ImportCpr.cmake new file mode 100644 index 0000000..b5dc021 --- /dev/null +++ b/cmake-module/ImportCpr.cmake @@ -0,0 +1,31 @@ +find_package(CURL REQUIRED + PATH_SUFFIXES + /usr/local/lib/cmake/CURL + /usr/local/lib64/cmake/CURL +) + +# target library names can be found in file: +# ${THIRD_PARTY_ROOT}/install/${DIST}/cpr-*/usr/local/lib/cmake/CURL/CURLTargets.cmake + + +find_library(CPR_LIBRARY NAMES cpr + PATH_SUFFIXES /usr/local/lib +) +find_path(CPR_INCLUDE_DIR + NAMES cpr/cpr.h + PATH_SUFFIXES /usr/local/include +) +if(CPR_LIBRARY AND CPR_INCLUDE_DIR) + set(CPR_FOUND "YES") +endif() + +if(CPR_FOUND) + add_library(Cpr::cpr STATIC IMPORTED) + set_target_properties(Cpr::cpr PROPERTIES + IMPORTED_LOCATION ${CPR_LIBRARY} + INTERFACE_INCLUDE_DIRECTORIES ${CPR_INCLUDE_DIR} + IMPORTED_LINK_INTERFACE_LIBRARIES CURL::libcurl + ) +else() + message(FATAL_ERROR "Could not find cpr library") +endif() diff --git a/cmake-module/ImportGTest.cmake b/cmake-module/ImportGTest.cmake new file mode 100644 index 0000000..e198ea5 --- /dev/null +++ b/cmake-module/ImportGTest.cmake @@ -0,0 +1,8 @@ +find_package(GTest REQUIRED + PATH_SUFFIXES + /usr/local/lib/cmake/GTest + /usr/local/lib64/cmake/GTest +) + +# target library names can be found in file: +# ${THIRD_PARTY_ROOT}/install/${DIST}/googletest-release-*/usr/local/lib/cmake/GTest/GTestTargets.cmake diff --git a/cmake-module/ImportYamlCpp.cmake b/cmake-module/ImportYamlCpp.cmake new file mode 100644 index 0000000..c659831 --- /dev/null +++ b/cmake-module/ImportYamlCpp.cmake @@ -0,0 +1,8 @@ +find_package(yaml-cpp REQUIRED + PATH_SUFFIXES + /usr/local/lib/cmake/yaml-cpp + /usr/local/lib64/cmake/yaml-cpp +) + +# target library names can be found in file: +# ${THIRD_PARTY_ROOT}/install/${DIST}/yaml-cpp-*/usr/local/lib/cmake/yaml-cpp/yaml-cpp-targets.cmake diff --git a/cmake-module/PackageDebian.cmake b/cmake-module/PackageDebian.cmake new file mode 100644 index 0000000..3ab034b --- /dev/null +++ b/cmake-module/PackageDebian.cmake @@ -0,0 +1,10 @@ +set(CPACK_GENERATOR "DEB") +set(CPACK_PACKAGE_CONTACT "Lars Gunnarsson ") +set(CPACK_DEBIAN_PACKAGE_HOMEPAGE "${PROJECT_HOMEPAGE_URL}") +set(CPACK_DEBIAN_PACKAGE_DEPENDS "libssl | libssl-dev") +set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA + "${PROJECT_SOURCE_DIR}/debian/script/postinst" + "${PROJECT_SOURCE_DIR}/debian/script/conffiles" +) + +include(CPack) \ No newline at end of file diff --git a/debian/CMakeLists.txt b/debian/CMakeLists.txt new file mode 100644 index 0000000..b5bd0b3 --- /dev/null +++ b/debian/CMakeLists.txt @@ -0,0 +1,10 @@ +install( + FILES config/configuration.yaml + DESTINATION /etc/nss-http +) + +install( + FILES config/shadow.auth-token.base64 + DESTINATION /etc/nss-http + PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ +) diff --git a/debian/config/configuration.yaml b/debian/config/configuration.yaml new file mode 100644 index 0000000..1ef9941 --- /dev/null +++ b/debian/config/configuration.yaml @@ -0,0 +1,11 @@ +endpoints: + url: http://unix-account.lan:8025 + group: /api/group + user: /api/user + password: /api/password + +tls: + dir: /etc/nss-http/certs + cert: unix-account.lan.cert.pem + key: unix-account.lan.key.pem + ca_cert: unix-account.lan.ca_chain.pem diff --git a/debian/config/shadow.auth-token.base64 b/debian/config/shadow.auth-token.base64 new file mode 100644 index 0000000..d45aa2a --- /dev/null +++ b/debian/config/shadow.auth-token.base64 @@ -0,0 +1 @@ +YSB2ZXJ5IHNlY3JldCB0b2tlbgo= diff --git a/debian/script/conffiles b/debian/script/conffiles new file mode 100644 index 0000000..a90d4ad --- /dev/null +++ b/debian/script/conffiles @@ -0,0 +1,2 @@ +/etc/nss-http/shadow.auth-token.base64 +/etc/nss-http/configuration.yaml diff --git a/debian/script/postinst b/debian/script/postinst new file mode 100755 index 0000000..da087b8 --- /dev/null +++ b/debian/script/postinst @@ -0,0 +1,3 @@ +#!/bin/bash + +chown :shadow /etc/nss-http/shadow.auth-token.base64 diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt new file mode 100644 index 0000000..e2d7cb1 --- /dev/null +++ b/source/CMakeLists.txt @@ -0,0 +1,4 @@ +add_subdirectory(libnss_http) +add_subdirectory(nss_api) +add_subdirectory(unix_account) +add_subdirectory(unix_account_http) diff --git a/source/libnss_http/CMakeLists.txt b/source/libnss_http/CMakeLists.txt new file mode 100644 index 0000000..b6b9e76 --- /dev/null +++ b/source/libnss_http/CMakeLists.txt @@ -0,0 +1,19 @@ +set(LIBRARY_NAME nss_http) +set(SOURCES + src/NssHttp.cc +) +add_library(${LIBRARY_NAME} SHARED ${SOURCES}) +set_target_properties(${LIBRARY_NAME} PROPERTIES + VERSION 2 + CXX_VISIBILITY_PRESET hidden +) +target_link_libraries(${LIBRARY_NAME} + PRIVATE + nss_api + unix_account_http +) +install( + TARGETS ${LIBRARY_NAME} + DESTINATION /lib/x86_64-linux-gnu + COMPONENT ${LIBRARY_NAME} +) diff --git a/source/libnss_http/src/NssHttp.cc b/source/libnss_http/src/NssHttp.cc new file mode 100644 index 0000000..7f7d57e --- /dev/null +++ b/source/libnss_http/src/NssHttp.cc @@ -0,0 +1,17 @@ +#include +#include + +#if 0 +void __attribute__((constructor)) _nss_http_init(); +void __attribute__((destructor)) _nss_http_exit(); + +void _nss_http_init() +{ + std::cout << "nss-http: init" << std::endl; +} + +void _nss_http_exit() +{ + std::cout << "nss-http: exit" << std::endl; +} +#endif \ No newline at end of file diff --git a/source/nss_api/CMakeLists.txt b/source/nss_api/CMakeLists.txt new file mode 100644 index 0000000..3d678e2 --- /dev/null +++ b/source/nss_api/CMakeLists.txt @@ -0,0 +1,27 @@ +set(LIBRARY_NAME nss_api) +set(SOURCES + src/BufferWriter.cc + src/GroupSerializer.cc + src/LogError.cc + src/PasswdSerializer.cc + src/ShadowSerializer.cc + src/NssApiGroup.cc + src/NssApiPasswd.cc + src/NssApiShadow.cc +) +add_library(${LIBRARY_NAME} OBJECT ${SOURCES}) +set_target_properties(${LIBRARY_NAME} PROPERTIES + POSITION_INDEPENDENT_CODE ON + CXX_VISIBILITY_PRESET hidden +) +target_include_directories(${LIBRARY_NAME} + PUBLIC inc + PRIVATE src +) +target_link_libraries(${LIBRARY_NAME} + PUBLIC unix_account +) + +if (UNITTEST) + add_subdirectory(test) +endif() \ No newline at end of file diff --git a/source/nss_api/src/BufferWriter.cc b/source/nss_api/src/BufferWriter.cc new file mode 100644 index 0000000..0782682 --- /dev/null +++ b/source/nss_api/src/BufferWriter.cc @@ -0,0 +1,76 @@ +#include + +#include "BufferWriter.h" + +namespace nss_api +{ +BufferWriter::BufferWriter(byte_t* begin, std::size_t size) + : _cursor{begin}, + _end{begin + size} +{ + std::fill(_cursor, _end, 0); +} + +BufferWriter::byte_t** BufferWriter::writeArray(const std::vector& textFields) +{ + byte_t** arr = reinterpret_cast(_cursor); + _cursor += sizeof(byte_t*) * (textFields.size() + 1); // array null termination + if (_cursor < _end) + { + for (std::size_t n{}; n < textFields.size(); ++n) + { + arr[n] = _write(textFields[n]); + } + } + return arr; +} + +BufferWriter::byte_t* BufferWriter::_write(const std::string& text) +{ + byte_t* begin = _cursor; + if (_cursor < _end) + { + begin = _cursor; + _try_write(text); + } + else + { + // point to last byte in buffer (which is a null termination) on recurrent + // writes after overflow. + begin = _end - 1; + } + return begin; +} + +void BufferWriter::_try_write(const std::string& text) +{ + const std::size_t available = getAvailable(); + const std::size_t size = text.size(); + if (available) + { + if (size) + { + // text might get truncated: + const std::size_t bytesToCopy = std::min( + available, + size); + std::copy_n( + text.begin(), + bytesToCopy, + _cursor); + } + _cursor += size + 1; // insert null termination after text (implicitly, since buffer contains all zeroes) + } +} + +std::size_t BufferWriter::getAvailable() const +{ + std::size_t available{}; + const byte_t* last = _end - 1; // always reserve space for last null termination in buffer + if (_cursor < last) + { + available = last - _cursor; + } + return available; +} +} // namespace nss_api \ No newline at end of file diff --git a/source/nss_api/src/BufferWriter.h b/source/nss_api/src/BufferWriter.h new file mode 100644 index 0000000..ae7f151 --- /dev/null +++ b/source/nss_api/src/BufferWriter.h @@ -0,0 +1,42 @@ +#pragma once + +#include +#include +#include + +namespace nss_api +{ +class BufferWriter final +{ +private: + using byte_t = char; + byte_t* _cursor{}; + byte_t* const _end{}; + +public: + BufferWriter(byte_t* begin, std::size_t size); + ~BufferWriter() = default; + + BufferWriter(const BufferWriter& cls) = delete; // Copy-constructor + BufferWriter& operator=(const BufferWriter& cls) = delete; // Copy-assignment constructor + BufferWriter(BufferWriter&& cls) = delete; // Move-constructor + BufferWriter& operator=(BufferWriter&& cls) = delete; // Move-assignment constructor + + byte_t** writeArray(const std::vector& textFields); + + byte_t* write(const std::string& text) + { + return _write(text); + } + + bool isTruncated() const + { + return _cursor >= _end; + } + +private: + byte_t* _write(const std::string& text); + void _try_write(const std::string& text); + std::size_t getAvailable() const; +}; +} // namespace nss_api diff --git a/source/nss_api/src/GroupSerializer.cc b/source/nss_api/src/GroupSerializer.cc new file mode 100644 index 0000000..8b5de73 --- /dev/null +++ b/source/nss_api/src/GroupSerializer.cc @@ -0,0 +1,32 @@ +#include + +#include "GroupSerializer.h" +#include "unix_account/Group.h" + +using unix_account::Group; + +namespace nss_api +{ +GroupSerializer::GroupSerializer( + group& group, + char* buffer, + std::size_t size) + : _group{group}, + _writer{buffer, size} +{ + char* const last = buffer + size - 1; + char* const lastIndex = buffer + size - sizeof(char*); + _group.gr_gid = 1000; + _group.gr_name = last; + _group.gr_passwd = last; + _group.gr_mem = reinterpret_cast(lastIndex); +} + +void GroupSerializer::append(const Group& group) +{ + _group.gr_gid = group.gid; + _group.gr_name = _writer.write(group.name); + _group.gr_passwd = _writer.write(group.password); + _group.gr_mem = _writer.writeArray(group.members); +} +} // namespace nss_api \ No newline at end of file diff --git a/source/nss_api/src/GroupSerializer.h b/source/nss_api/src/GroupSerializer.h new file mode 100644 index 0000000..e008da5 --- /dev/null +++ b/source/nss_api/src/GroupSerializer.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include "BufferWriter.h" +#include "unix_account/Group.h" + +struct group; + +namespace nss_api +{ +class GroupSerializer final +{ +private: + group& _group; + BufferWriter _writer; + +public: + GroupSerializer( + group& group, + char* buffer, + std::size_t size); + ~GroupSerializer() = default; + + GroupSerializer(const GroupSerializer& cls) = delete; // Copy-constructor + GroupSerializer& operator=(const GroupSerializer& cls) = delete; // Copy-assignment constructor + GroupSerializer(GroupSerializer&& cls) = delete; // Move-constructor + GroupSerializer& operator=(GroupSerializer&& cls) = delete; // Move-assignment constructor + + GroupSerializer& operator<<(const unix_account::Group& group) + { + append(group); + return *this; + } + + bool isTruncated() const { return _writer.isTruncated(); } + +private: + void append(const unix_account::Group& group); +}; +} // namespace nss_api \ No newline at end of file diff --git a/source/nss_api/src/LogError.cc b/source/nss_api/src/LogError.cc new file mode 100644 index 0000000..6687e88 --- /dev/null +++ b/source/nss_api/src/LogError.cc @@ -0,0 +1,14 @@ +#include +#include + +#include "LogError.h" + +namespace nss_api +{ +void LogError(const std::string& message) +{ + openlog("nss-http", LOG_CONS | LOG_NDELAY, LOG_LOCAL1); + syslog(LOG_ERR, "nss-http error: %s", message.c_str()); + closelog(); +} +} // namespace nss_api \ No newline at end of file diff --git a/source/nss_api/src/LogError.h b/source/nss_api/src/LogError.h new file mode 100644 index 0000000..805d0a8 --- /dev/null +++ b/source/nss_api/src/LogError.h @@ -0,0 +1,8 @@ +#pragma once + +#include + +namespace nss_api +{ +void LogError(const std::string& message); +} \ No newline at end of file diff --git a/source/nss_api/src/NssApiGroup.cc b/source/nss_api/src/NssApiGroup.cc new file mode 100644 index 0000000..b9320ba --- /dev/null +++ b/source/nss_api/src/NssApiGroup.cc @@ -0,0 +1,165 @@ +#include +#include +#include +#include +#include +#include + +#include "unix_account/Exception.h" +#include "unix_account/Factory.h" +#include "unix_account/Group.h" +#include "unix_account/GroupReader.h" + +#include "GroupSerializer.h" +#include "LogError.h" +#include "NssApiStatus.h" + +#define NSS_API __attribute__((visibility("default"))) + +using namespace nss_api; +using namespace unix_account; + +namespace +{ +thread_local std::forward_list groups{}; // stream for reading all +} // namespace + + +extern "C" +{ +/** + * Open stream + */ +NSS_API enum nss_status _nss_http_setgrent(int stayopen) +{ + (void)stayopen; + nss_status response{NSS_STATUS_SUCCESS}; + try + { + auto reader = unix_account::create(); + auto all = reader->getAll(); + groups = { + std::make_move_iterator(all.begin()), + std::make_move_iterator(all.end())}; + } + catch (const UnixAccountLookupFailed& err) + { + LogError(err.what()); + response = NSS_STATUS_UNAVAIL; + } + return response; +} + +/** + * Get next entry in stream + */ +NSS_API enum nss_status _nss_http_getgrent_r( + struct group* grp, + char* buffer, + size_t buflen, + int* errnop) +{ + NssApiStatus status{*errnop}; + GroupSerializer out{*grp, buffer, buflen}; + if (groups.empty()) + { + status.SetNoResultFound(); + } + else + { + out << groups.front(); + if (out.isTruncated()) + { + status.SetBufferTooSmall(); + } + else + { + groups.pop_front(); + } + } + return status; +} + +/** + * Close stream + */ +NSS_API enum nss_status _nss_http_endgrent() +{ + groups.clear(); + return NSS_STATUS_SUCCESS; +} + +/** + * Get group by name + */ +NSS_API enum nss_status _nss_http_getgrnam_r( + const char* name, + struct group* grp, + char* buffer, + size_t buflen, + int* errnop) +{ + NssApiStatus status{*errnop}; + GroupSerializer out{*grp, buffer, buflen}; + try + { + auto reader = unix_account::create(); + auto res = reader->GetByName(name); + if (res) + { + out << *res; + if (out.isTruncated()) + { + status.SetBufferTooSmall(); + } + } + else + { + status.SetNoResultFound(); + } + } + catch (UnixAccountLookupFailed& err) + { + LogError(err.what()); + status.SetLookupFailed(); + } + return status; +} + +/** + * Get group by gid + */ +NSS_API enum nss_status _nss_http_getgrgid_r( + gid_t gid, + struct group* grp, + char* buffer, + size_t buflen, + int* errnop) +{ + NssApiStatus status{*errnop}; + GroupSerializer out{*grp, buffer, buflen}; + try + { + auto reader = unix_account::create(); + auto res = reader->GetById(gid); + if (res) + { + out << *res; + if (out.isTruncated()) + { + status.SetBufferTooSmall(); + } + } + else + { + status.SetNoResultFound(); + } + } + catch (UnixAccountLookupFailed& err) + { + LogError(err.what()); + status.SetLookupFailed(); + } + return status; +} +} \ No newline at end of file diff --git a/source/nss_api/src/NssApiPasswd.cc b/source/nss_api/src/NssApiPasswd.cc new file mode 100644 index 0000000..f079517 --- /dev/null +++ b/source/nss_api/src/NssApiPasswd.cc @@ -0,0 +1,164 @@ +#include +#include +#include +#include +#include + +#include "unix_account/Exception.h" +#include "unix_account/Factory.h" +#include "unix_account/Passwd.h" +#include "unix_account/PasswdReader.h" + +#include "PasswdSerializer.h" +#include "LogError.h" +#include "NssApiStatus.h" + +#define NSS_API __attribute__((visibility("default"))) + +using namespace nss_api; +using namespace unix_account; + + +namespace +{ +thread_local std::forward_list passwds{}; // stream for reading all +} // namespace + +extern "C" +{ +/** + * Open stream + */ +NSS_API enum nss_status _nss_http_setpwent(int stayopen) +{ + (void)stayopen; + nss_status response{NSS_STATUS_SUCCESS}; + try + { + auto reader = unix_account::create(); + auto all = reader->getAll(); + passwds = { + std::make_move_iterator(all.begin()), + std::make_move_iterator(all.end())}; + } + catch (const UnixAccountLookupFailed& err) + { + LogError(err.what()); + response = NSS_STATUS_UNAVAIL; + } + return response; +} + +/** + * Get next entry in stream + */ +NSS_API enum nss_status _nss_http_getpwent_r( + struct passwd* passwd, + char* buffer, + size_t buflen, + int* errnop) +{ + NssApiStatus status{*errnop}; + PasswdSerializer out{*passwd, buffer, buflen}; + if (passwds.empty()) + { + status.SetNoResultFound(); + } + else + { + out << passwds.front(); + if (out.isTruncated()) + { + status.SetBufferTooSmall(); + } + else + { + passwds.pop_front(); + } + } + return status; +} + +/** + * Close stream + */ +NSS_API enum nss_status _nss_http_endpwent() +{ + passwds.clear(); + return NSS_STATUS_SUCCESS; +} + +/** + * Get passwd by name + */ +NSS_API enum nss_status _nss_http_getpwnam_r( + const char* name, + struct passwd* passwd, + char* buffer, + size_t buflen, + int* errnop) +{ + NssApiStatus status{*errnop}; + PasswdSerializer out{*passwd, buffer, buflen}; + try + { + auto reader = unix_account::create(); + auto res = reader->GetByName(name); + if (res) + { + out << *res; + if (out.isTruncated()) + { + status.SetBufferTooSmall(); + } + } + else + { + status.SetNoResultFound(); + } + } + catch (UnixAccountLookupFailed& err) + { + LogError(err.what()); + status.SetLookupFailed(); + } + return status; +} + +/** + * Get passwd by gid + */ +NSS_API enum nss_status _nss_http_getpwuid_r( + gid_t gid, + struct passwd* passwd, + char* buffer, + size_t buflen, + int* errnop) +{ + NssApiStatus status{*errnop}; + PasswdSerializer out{*passwd, buffer, buflen}; + try + { + auto reader = unix_account::create(); + auto res = reader->GetById(gid); + if (res) + { + out << *res; + if (out.isTruncated()) + { + status.SetBufferTooSmall(); + } + } + else + { + status.SetNoResultFound(); + } + } + catch (UnixAccountLookupFailed& err) + { + LogError(err.what()); + status.SetLookupFailed(); + } + return status; +} +} \ No newline at end of file diff --git a/source/nss_api/src/NssApiShadow.cc b/source/nss_api/src/NssApiShadow.cc new file mode 100644 index 0000000..2b8b566 --- /dev/null +++ b/source/nss_api/src/NssApiShadow.cc @@ -0,0 +1,126 @@ +#include +#include +#include +#include +#include + +#include "unix_account/Exception.h" +#include "unix_account/Factory.h" +#include "unix_account/Shadow.h" +#include "unix_account/ShadowReader.h" + +#include "ShadowSerializer.h" +#include "LogError.h" +#include "NssApiStatus.h" + +#define NSS_API __attribute__((visibility("default"))) + +using namespace nss_api; +using namespace unix_account; + +namespace +{ +thread_local std::forward_list shadows{}; // stream for reading all +} // namespace + +extern "C" +{ +/** + * Open stream + */ +NSS_API enum nss_status _nss_http_setspent(int stayopen) +{ + (void)stayopen; + nss_status response{NSS_STATUS_SUCCESS}; + try + { + auto reader = unix_account::create(); + auto all = reader->getAll(); + shadows = { + std::make_move_iterator(all.begin()), + std::make_move_iterator(all.end())}; + } + catch (const UnixAccountLookupFailed& err) + { + LogError(err.what()); + response = NSS_STATUS_UNAVAIL; + } + return response; +} + +/** + * Get next entry in stream + */ +NSS_API enum nss_status _nss_http_getspent_r( + struct spwd* shadow, + char* buffer, + size_t buflen, + int* errnop) +{ + NssApiStatus status{*errnop}; + ShadowSerializer out{*shadow, buffer, buflen}; + if (shadows.empty()) + { + status.SetNoResultFound(); + } + else + { + out << shadows.front(); + if (out.isTruncated()) + { + status.SetBufferTooSmall(); + } + else + { + shadows.pop_front(); + } + } + return status; +} + +/** + * Close stream + */ +NSS_API enum nss_status _nss_http_endspent() +{ + shadows.clear(); + return NSS_STATUS_SUCCESS; +} + +/** + * Get shadow by name + */ +NSS_API enum nss_status _nss_http_getspnam_r( + const char* name, + struct spwd* shadow, + char* buffer, + size_t buflen, + int* errnop) +{ + NssApiStatus status{*errnop}; + ShadowSerializer out{*shadow, buffer, buflen}; + try + { + auto reader = unix_account::create(); + auto res = reader->GetByName(name); + if (res) + { + out << *res; + if (out.isTruncated()) + { + status.SetBufferTooSmall(); + } + } + else + { + status.SetNoResultFound(); + } + } + catch (UnixAccountLookupFailed& err) + { + LogError(err.what()); + status.SetLookupFailed(); + } + return status; +} +} \ No newline at end of file diff --git a/source/nss_api/src/NssApiStatus.h b/source/nss_api/src/NssApiStatus.h new file mode 100644 index 0000000..649eca2 --- /dev/null +++ b/source/nss_api/src/NssApiStatus.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include + +namespace nss_api +{ +class NssApiStatus final +{ +private: + int& _errnop; + nss_status _returnStatus; + +public: + NssApiStatus(int& errnop) + : _errnop{errnop}, + _returnStatus{NSS_STATUS_SUCCESS} + { + } + ~NssApiStatus() = default; + + NssApiStatus(const NssApiStatus& cls) = delete; // Copy-constructor + NssApiStatus& operator=(const NssApiStatus& cls) = delete; // Copy-assignment constructor + NssApiStatus(NssApiStatus&& cls) = delete; // Move-constructor + NssApiStatus& operator=(NssApiStatus&& cls) = delete; // Move-assignment constructor + + void SetBufferTooSmall() + { + _returnStatus = NSS_STATUS_TRYAGAIN; + _errnop = ERANGE; + } + + void SetLookupFailed() + { + _returnStatus = NSS_STATUS_UNAVAIL; // is this correct? set error code also?? + } + + void SetNoResultFound() + { + _returnStatus = NSS_STATUS_NOTFOUND; + } + + operator nss_status() const + { + return _returnStatus; + } +}; +} // namespace nss_api diff --git a/source/nss_api/src/PasswdSerializer.cc b/source/nss_api/src/PasswdSerializer.cc new file mode 100644 index 0000000..0986759 --- /dev/null +++ b/source/nss_api/src/PasswdSerializer.cc @@ -0,0 +1,36 @@ +#include + +#include "PasswdSerializer.h" + +using unix_account::Passwd; + +namespace nss_api +{ +PasswdSerializer::PasswdSerializer( + passwd& passwd, + char* buffer, + std::size_t size) + : _passwd{passwd}, + _writer{buffer, size} +{ + char* const last = buffer + size - 1; + _passwd.pw_name = last; + _passwd.pw_passwd = last; + _passwd.pw_uid = 1000; + _passwd.pw_gid = 1000; + _passwd.pw_gecos = last; + _passwd.pw_dir = last; + _passwd.pw_shell = last; +} + +void PasswdSerializer::append(const Passwd& passwd) +{ + _passwd.pw_name = _writer.write(passwd.name); + _passwd.pw_passwd = _writer.write(passwd.password); + _passwd.pw_uid = passwd.uid; + _passwd.pw_gid = passwd.gid; + _passwd.pw_gecos = _writer.write(passwd.gecos); + _passwd.pw_dir = _writer.write(passwd.dir); + _passwd.pw_shell = _writer.write(passwd.shell); +} +} // namespace nss_api \ No newline at end of file diff --git a/source/nss_api/src/PasswdSerializer.h b/source/nss_api/src/PasswdSerializer.h new file mode 100644 index 0000000..ea93370 --- /dev/null +++ b/source/nss_api/src/PasswdSerializer.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include "BufferWriter.h" +#include "unix_account/Passwd.h" + +struct passwd; + +namespace nss_api +{ +class PasswdSerializer final +{ +private: + passwd& _passwd; + BufferWriter _writer; + +public: + PasswdSerializer( + passwd& passwd, + char* buffer, + std::size_t size); + ~PasswdSerializer() = default; + + PasswdSerializer(const PasswdSerializer& cls) = delete; // Copy-constructor + PasswdSerializer& operator=(const PasswdSerializer& cls) = delete; // Copy-assignment constructor + PasswdSerializer(PasswdSerializer&& cls) = delete; // Move-constructor + PasswdSerializer& operator=(PasswdSerializer&& cls) = delete; // Move-assignment constructor + + PasswdSerializer& operator<<(const unix_account::Passwd& passwd) + { + append(passwd); + return *this; + } + + bool isTruncated() const { return _writer.isTruncated(); } + +private: + void append(const unix_account::Passwd& passwd); +}; +} // namespace nss_api \ No newline at end of file diff --git a/source/nss_api/src/ShadowSerializer.cc b/source/nss_api/src/ShadowSerializer.cc new file mode 100644 index 0000000..aa0729d --- /dev/null +++ b/source/nss_api/src/ShadowSerializer.cc @@ -0,0 +1,51 @@ +#include + +#include "ShadowSerializer.h" + +constexpr std::uint32_t DEFAULT_DAYS_MIN{}; +constexpr std::uint32_t DEFAULT_DAYS_MAX{9999}; +constexpr std::uint32_t DEFAULT_DAYS_WARN{7}; +constexpr std::int8_t DEFAULT_DAYS_INACTIVE{-1}; +constexpr std::int8_t DEFAULT_TIME_EXPIRES{-1}; + +using unix_account::Shadow; + +namespace nss_api +{ +ShadowSerializer::ShadowSerializer( + spwd& shadow, + char* buffer, + std::size_t size) + : _shadow{shadow}, + _writer{buffer, size} +{ + char* const last = buffer + size - 1; + _shadow.sp_namp = last; + _shadow.sp_pwdp = last; + _shadow.sp_lstchg = 0; + _shadow.sp_min = DEFAULT_DAYS_MIN; + _shadow.sp_max = DEFAULT_DAYS_MAX; + _shadow.sp_warn = DEFAULT_DAYS_WARN; + _shadow.sp_inact = DEFAULT_DAYS_INACTIVE; + _shadow.sp_expire = DEFAULT_TIME_EXPIRES; +} + +void ShadowSerializer::append(const Shadow& shadow) +{ + _shadow.sp_namp = _writer.write(shadow.name); + _shadow.sp_pwdp = _writer.write(shadow.password); + _shadow.sp_lstchg = shadow.timeLastChange; + // TODO: check if set, to not overwrite defaults? + _shadow.sp_min = shadow.daysMin; + _shadow.sp_max = shadow.daysMax; + _shadow.sp_warn = shadow.daysWarn; + if (shadow.daysInactive) + { + _shadow.sp_inact = shadow.daysInactive; + } + if (shadow.timeExpires) + { + _shadow.sp_expire = shadow.timeExpires; + } +} +} // namespace nss_api \ No newline at end of file diff --git a/source/nss_api/src/ShadowSerializer.h b/source/nss_api/src/ShadowSerializer.h new file mode 100644 index 0000000..5f8c086 --- /dev/null +++ b/source/nss_api/src/ShadowSerializer.h @@ -0,0 +1,41 @@ +#pragma once + +#include + +#include "BufferWriter.h" +#include "unix_account/Shadow.h" + +struct spwd; + +namespace nss_api +{ +class ShadowSerializer final +{ +private: + spwd& _shadow; + BufferWriter _writer; + +public: + ShadowSerializer( + spwd& shadow, + char* buffer, + std::size_t size); + ~ShadowSerializer() = default; + + ShadowSerializer(const ShadowSerializer& cls) = delete; // Copy-constructor + ShadowSerializer& operator=(const ShadowSerializer& cls) = delete; // Copy-assignment constructor + ShadowSerializer(ShadowSerializer&& cls) = delete; // Move-constructor + ShadowSerializer& operator=(ShadowSerializer&& cls) = delete; // Move-assignment constructor + + ShadowSerializer& operator<<(const unix_account::Shadow& shadow) + { + append(shadow); + return *this; + } + + bool isTruncated() const { return _writer.isTruncated(); } + +private: + void append(const unix_account::Shadow& shadow); +}; +} // namespace nss_api \ No newline at end of file diff --git a/source/nss_api/test/CMakeLists.txt b/source/nss_api/test/CMakeLists.txt new file mode 100644 index 0000000..6cfadca --- /dev/null +++ b/source/nss_api/test/CMakeLists.txt @@ -0,0 +1,15 @@ +set(UNITTEST_NAME "${LIBRARY_NAME}_test") +set(SOURCES + src/BufferWriterTest.cc + src/StorageFactoryStub.cc +) +add_executable(${UNITTEST_NAME} ${SOURCES}) +add_test(NAME ${UNITTEST_NAME} COMMAND ${UNITTEST_NAME}) +add_dependencies(test_all ${UNITTEST_NAME}) +target_include_directories(${UNITTEST_NAME} + PRIVATE ../src +) +target_link_libraries(${UNITTEST_NAME} PRIVATE + ${LIBRARY_NAME} + GTest::gmock_main +) diff --git a/source/nss_api/test/src/BufferWriterTest.cc b/source/nss_api/test/src/BufferWriterTest.cc new file mode 100644 index 0000000..b385682 --- /dev/null +++ b/source/nss_api/test/src/BufferWriterTest.cc @@ -0,0 +1,152 @@ +#include +#include +#include +#include +#include + +#include "BufferWriter.h" + +using namespace nss_api; +using namespace testing; + +TEST(BufferWriter, CheckOffset) +{ + constexpr std::size_t size{64}; + char buffer[size]; + BufferWriter writer{buffer, size}; + const std::string text{"Hello, world!"}; + char* first = writer.write(text); + char* second = writer.write(text); + EXPECT_EQ(buffer, first); + EXPECT_EQ(buffer + text.size() + /*null-termination=*/1, second); +} + +TEST(BufferWriter, CheckNullTermination) +{ + constexpr std::size_t size{64}; + char buffer[size]; + BufferWriter writer{buffer, size}; + const std::string text{"Hello, world!"}; + char* first = writer.write(text); + char* second = writer.write(text); + EXPECT_EQ(0, *(first + text.size())); + EXPECT_EQ(0, *(second + text.size())); +} + +TEST(BufferWriter, CheckArrayNullTermination) +{ + constexpr std::size_t size{64}; + char buffer[size]; + BufferWriter writer{buffer, size}; + const std::string text{"Hello, world!"}; + char** arr = writer.writeArray({text, text, text}); + EXPECT_EQ(0, arr[3]); +} + +TEST(BufferWriter, WriteManyTexts) +{ + constexpr std::size_t size{64}; + char buffer[size]; + BufferWriter writer{buffer, size}; + const std::string text{"Hello, world!"}; + char* first = writer.write(text); + char* second = writer.write(text); + char* third = writer.write(text); + EXPECT_EQ(text, first); + EXPECT_EQ(text, second); + EXPECT_EQ(text, third); +} + +TEST(BufferWriter, WriteEmpty) +{ + constexpr std::size_t size{64}; + char buffer[size]; + BufferWriter writer{buffer, size}; + const std::string text{}; + char* first = writer.write(text); + char* second = writer.write(text); + char* third = writer.write(text); + // expects to insert null termination when writing empty text: + EXPECT_EQ(&buffer[0], first); + EXPECT_EQ(&buffer[1], second); + EXPECT_EQ(&buffer[2], third); +} + +TEST(BufferWriter, TruncateTextWhenArrayIndexOverflow) +{ + constexpr std::size_t size{20}; // index takes 8 * (3+1) = 32 bytes + char buffer[size]; + BufferWriter writer{buffer, size}; + const std::string text{"Hello, world!"}; + writer.writeArray({text, text, text}); + EXPECT_TRUE(writer.isTruncated()); +} + +TEST(BufferWriter, TruncateTextWhenOverflow) +{ + constexpr std::size_t size{20}; + char buffer[size]; + BufferWriter writer{buffer, size}; + const std::string text{"Hello, world!"}; + writer.write(text); + char* truncatedText = writer.write(text); + EXPECT_TRUE(writer.isTruncated()); + EXPECT_THAT(text, StartsWith(truncatedText)); +} + +TEST(BufferWriter, TruncateTextWhenArrayOverflow) +{ + constexpr std::size_t size{64}; + char buffer[size]; + BufferWriter writer{buffer, size}; + const std::string text{"Hello, world!"}; + char** arr = writer.writeArray({text, text, text}); + EXPECT_EQ(text, arr[0]); + EXPECT_EQ(text, arr[1]); + EXPECT_THAT(text, StartsWith(arr[2])); // text gets truncated. +} + +TEST(BufferWriter, PointToBufferEndWhenOverflow) +{ + constexpr std::size_t size{16}; + char buffer[size]; + BufferWriter writer{buffer, size}; + const std::string text{"Hello, world!"}; + writer.write(text); + writer.write(text); + char* pos = writer.write(text); + char* last = buffer + size - 1; + EXPECT_EQ(last, pos); + EXPECT_EQ(*last, 0); +} + +TEST(BufferWriter, PointToBufferEndWhenArrayOverflow) +{ + constexpr std::size_t size{40}; // index takes 8 * (3+1) = 32 bytes + char buffer[size]; + BufferWriter writer{buffer, size}; + const std::string text{"Hello, world!"}; + char** arr = writer.writeArray({text, text, text}); + char* last = buffer + size - 1; + // arr[0] contains truncated text. + EXPECT_EQ(last, arr[1]); + EXPECT_EQ(last, arr[2]); + EXPECT_EQ(*last, 0); +} + +TEST(BufferWriter, TextFitsExcactly) +{ + constexpr std::size_t size{12}; + char buffer[size]; + BufferWriter writer{buffer, size}; + const std::string text{"Hey"}; + char* first = writer.write(text); // +4 bytes + char* second = writer.write(text); // +4 bytes + char* third = writer.write(text); // +4 bytes + EXPECT_EQ(text, first); + EXPECT_EQ(text, second); + EXPECT_EQ(text, third); + // all text fits, but it is flagged as truncated anyway to avoid + // complicated corner cases in implementation for recurrent writes. + EXPECT_TRUE(writer.isTruncated()); +} diff --git a/source/nss_api/test/src/StorageFactoryStub.cc b/source/nss_api/test/src/StorageFactoryStub.cc new file mode 100644 index 0000000..61d21ce --- /dev/null +++ b/source/nss_api/test/src/StorageFactoryStub.cc @@ -0,0 +1,30 @@ +#include + +#include "unix_account/Factory.h" +#include "unix_account/Group.h" +#include "unix_account/GroupReader.h" +#include "unix_account/Passwd.h" +#include "unix_account/PasswdReader.h" +#include "unix_account/Shadow.h" +#include "unix_account/ShadowReader.h" + +namespace unix_account +{ +template <> +std::unique_ptr create() +{ + return {}; +} + +template <> +std::unique_ptr create() +{ + return {}; +} + +template <> +std::unique_ptr create() +{ + return {}; +} +} // namespace unix_account \ No newline at end of file diff --git a/source/unix_account/CMakeLists.txt b/source/unix_account/CMakeLists.txt new file mode 100644 index 0000000..ec198a3 --- /dev/null +++ b/source/unix_account/CMakeLists.txt @@ -0,0 +1,5 @@ +set(LIBRARY_NAME unix_account) +add_library(${LIBRARY_NAME} INTERFACE) +target_include_directories(${LIBRARY_NAME} + INTERFACE inc +) diff --git a/source/unix_account/inc/unix_account/Exception.h b/source/unix_account/inc/unix_account/Exception.h new file mode 100644 index 0000000..b424fbe --- /dev/null +++ b/source/unix_account/inc/unix_account/Exception.h @@ -0,0 +1,17 @@ +#pragma once + +#include +#include + +namespace unix_account +{ +class UnixAccountLookupFailed : public std::runtime_error +{ +public: + UnixAccountLookupFailed(const std::string message) + : std::runtime_error{message} + { + } + ~UnixAccountLookupFailed() = default; +}; +} // namespace unix_account diff --git a/source/unix_account/inc/unix_account/Factory.h b/source/unix_account/inc/unix_account/Factory.h new file mode 100644 index 0000000..8a0361c --- /dev/null +++ b/source/unix_account/inc/unix_account/Factory.h @@ -0,0 +1,9 @@ +#pragma once + +#include + +namespace unix_account +{ +template +std::unique_ptr create(); +} // namespace unix_account \ No newline at end of file diff --git a/source/unix_account/inc/unix_account/Group.h b/source/unix_account/inc/unix_account/Group.h new file mode 100644 index 0000000..a1331e4 --- /dev/null +++ b/source/unix_account/inc/unix_account/Group.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include +#include + +namespace unix_account +{ +struct Group +{ + std::string name{}; + std::string password{"x"}; + std::uint32_t gid{}; + std::vector members{}; +}; +} // namespace unix_account \ No newline at end of file diff --git a/source/unix_account/inc/unix_account/GroupReader.h b/source/unix_account/inc/unix_account/GroupReader.h new file mode 100644 index 0000000..f13d52b --- /dev/null +++ b/source/unix_account/inc/unix_account/GroupReader.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +#include "Group.h" + +namespace unix_account +{ +class GroupReader +{ +public: + virtual ~GroupReader() = default; + + virtual std::optional GetById(std::uint32_t gid) = 0; + virtual std::optional GetByName(const std::string& name) = 0; + virtual std::vector getAll() = 0; +}; +} // namespace unix_account \ No newline at end of file diff --git a/source/unix_account/inc/unix_account/Passwd.h b/source/unix_account/inc/unix_account/Passwd.h new file mode 100644 index 0000000..daaa20c --- /dev/null +++ b/source/unix_account/inc/unix_account/Passwd.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include + +namespace unix_account +{ +struct Passwd +{ + std::string name{}; + std::string password{"x"}; + std::uint32_t uid{}; + std::uint32_t gid{}; + std::string gecos{}; + std::string dir{}; + std::string shell{}; +}; +} // namespace unix_account \ No newline at end of file diff --git a/source/unix_account/inc/unix_account/PasswdReader.h b/source/unix_account/inc/unix_account/PasswdReader.h new file mode 100644 index 0000000..d6512d3 --- /dev/null +++ b/source/unix_account/inc/unix_account/PasswdReader.h @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include + +#include "Passwd.h" + +namespace unix_account +{ +class PasswdReader +{ +public: + virtual ~PasswdReader() = default; + + virtual std::optional GetById(std::uint32_t uid) = 0; + virtual std::optional GetByName(const std::string& name) = 0; + virtual std::vector getAll() = 0; +}; +} // namespace unix_account \ No newline at end of file diff --git a/source/unix_account/inc/unix_account/Shadow.h b/source/unix_account/inc/unix_account/Shadow.h new file mode 100644 index 0000000..bedfb90 --- /dev/null +++ b/source/unix_account/inc/unix_account/Shadow.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include +#include + +// man 3 getspent +namespace unix_account +{ +struct Shadow +{ + std::string name{}; + std::string password{"!"}; + std::time_t timeLastChange{}; + std::uint32_t daysMin{}; + std::uint32_t daysMax{}; + std::uint32_t daysWarn{}; + std::uint32_t daysInactive{}; + std::time_t timeExpires{}; +}; +} // namespace unix_account \ No newline at end of file diff --git a/source/unix_account/inc/unix_account/ShadowReader.h b/source/unix_account/inc/unix_account/ShadowReader.h new file mode 100644 index 0000000..9371621 --- /dev/null +++ b/source/unix_account/inc/unix_account/ShadowReader.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include + +#include "Shadow.h" + +namespace unix_account +{ +class ShadowReader +{ +public: + virtual ~ShadowReader() = default; + + virtual std::optional GetByName(const std::string& name) = 0; + virtual std::vector getAll() = 0; +}; +} // namespace unix_account \ No newline at end of file diff --git a/source/unix_account_http/CMakeLists.txt b/source/unix_account_http/CMakeLists.txt new file mode 100644 index 0000000..9f4cca1 --- /dev/null +++ b/source/unix_account_http/CMakeLists.txt @@ -0,0 +1,32 @@ +set(LIBRARY_NAME unix_account_http) +set(SOURCES + src/ConfigurationReader.cc + src/GroupReaderFactory.cc + src/HttpRequest.cc + src/JsonValueToGroup.cc + src/JsonValueToPasswd.cc + src/JsonValueToShadow.cc + src/PasswdReaderFactory.cc + src/ShadowReaderFactory.cc +) +add_library(${LIBRARY_NAME} OBJECT ${SOURCES}) +set_target_properties(${LIBRARY_NAME} PROPERTIES + POSITION_INDEPENDENT_CODE ON + CXX_VISIBILITY_PRESET hidden +) +target_include_directories(${LIBRARY_NAME} + PUBLIC inc + PRIVATE src +) +target_link_libraries(${LIBRARY_NAME} +PUBLIC + unix_account + Cpr::cpr + yaml-cpp +PRIVATE + Boost::json +) + +if (UNITTEST) + add_subdirectory(test) +endif() diff --git a/source/unix_account_http/src/Configuration.h b/source/unix_account_http/src/Configuration.h new file mode 100644 index 0000000..25b4d90 --- /dev/null +++ b/source/unix_account_http/src/Configuration.h @@ -0,0 +1,30 @@ +#pragma once + +#include +#include +#include + +namespace unix_account +{ +struct Configuration +{ + struct Endpoints + { + std::string url{"http://localhost:8025"}; + std::string group{"/api/group"}; + std::string user{"/api/user"}; + std::string password{"/api/password"}; + }; + + struct Tls + { + std::filesystem::path dir{}; + std::filesystem::path certFile{}; + std::filesystem::path keyFile{}; + std::filesystem::path caFile{}; + }; + Endpoints endpoints{}; + std::optional tls{}; + std::string apiToken{}; // required to access shadow api. +}; +} // namespace unix_account \ No newline at end of file diff --git a/source/unix_account_http/src/ConfigurationFileName.h b/source/unix_account_http/src/ConfigurationFileName.h new file mode 100644 index 0000000..ba97eab --- /dev/null +++ b/source/unix_account_http/src/ConfigurationFileName.h @@ -0,0 +1,7 @@ +#pragma once + +namespace unix_account +{ +constexpr const char* CONFIGURATION_FILE_NAME{"/etc/nss-http/configuration.yaml"}; +constexpr const char* API_TOKEN_FILE_NAME{"/etc/nss-http/shadow.auth-token.base64"}; +} // namespace unix_account \ No newline at end of file diff --git a/source/unix_account_http/src/ConfigurationReader.cc b/source/unix_account_http/src/ConfigurationReader.cc new file mode 100644 index 0000000..e63dd37 --- /dev/null +++ b/source/unix_account_http/src/ConfigurationReader.cc @@ -0,0 +1,91 @@ +#include +#include +#include + +#include "Configuration.h" +#include "ConfigurationReader.h" +#include "unix_account/Exception.h" + +namespace unix_account +{ +ConfigurationReader::ConfigurationReader( + const std::string& configurationFile, + const std::string& apiTokenFile) + : _configurationFile{configurationFile}, + _apiTokenFile{apiTokenFile} +{ +} + +Configuration ConfigurationReader::read() const +{ + try + { + return tryRead(); + } + catch (YAML::Exception& err) + { + throw UnixAccountLookupFailed("Failed to read or parse configuration: " + err.msg); + } +} + +Configuration::Endpoints readEndpointsFrom(const YAML::Node& node) +{ + Configuration::Endpoints endpoints{}; + auto config = node["endpoints"]; + endpoints.url = config["url"].as(); + endpoints.group = config["group"].as(); + endpoints.user = config["user"].as(); + endpoints.password = config["password"].as(); + return endpoints; +} + +Configuration::Tls parseTlsConfiguration(const YAML::Node& config) +{ + Configuration::Tls tls{}; + tls.dir = config["dir"].as(); + tls.certFile = config["cert"].as(); + tls.keyFile = config["key"].as(); + if (config["ca_cert"]) + { + tls.caFile = config["ca_cert"].as(); + } + return tls; +} + +std::optional readTlsFrom(const YAML::Node& node) +{ + auto config = node["tls"]; + if (config) + { + return parseTlsConfiguration(config); + } + else + { + return {}; + } +} + +Configuration ConfigurationReader::tryRead() const +{ + Configuration config{}; + auto root = YAML::LoadFile(_configurationFile); + config.endpoints = readEndpointsFrom(root); + config.tls = readTlsFrom(root); + config.apiToken = readApiToken(); + return config; +} + +/** + * Api token has a separate file to restrict permissions on shadow api calls. + */ +std::string ConfigurationReader::readApiToken() const +{ + std::string apiToken{}; + std::ifstream stream{_apiTokenFile}; + if (stream) + { + std::getline(stream, apiToken); + } + return apiToken; +} +} // namespace unix_account \ No newline at end of file diff --git a/source/unix_account_http/src/ConfigurationReader.h b/source/unix_account_http/src/ConfigurationReader.h new file mode 100644 index 0000000..94f732e --- /dev/null +++ b/source/unix_account_http/src/ConfigurationReader.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include + +#include "Configuration.h" + +namespace unix_account +{ +class ConfigurationReader final +{ +private: + const std::string _configurationFile{}; + const std::string _apiTokenFile{}; + +public: + ConfigurationReader( + const std::string& configurationFile, + const std::string& apiTokenFile); + + ConfigurationReader(std::istream& stream); + + ~ConfigurationReader() = default; + + ConfigurationReader(const ConfigurationReader& cls) = delete; // Copy-constructor + ConfigurationReader& operator=(const ConfigurationReader& cls) = delete; // Copy-assignment constructor + ConfigurationReader(ConfigurationReader&& cls) = delete; // Move-constructor + ConfigurationReader& operator=(ConfigurationReader&& cls) = delete; // Move-assignment constructor + + Configuration read() const; + +private: + Configuration tryRead() const; + std::string readApiToken() const; +}; +} // namespace unix_account \ No newline at end of file diff --git a/source/unix_account_http/src/GroupReaderFactory.cc b/source/unix_account_http/src/GroupReaderFactory.cc new file mode 100644 index 0000000..b90d99a --- /dev/null +++ b/source/unix_account_http/src/GroupReaderFactory.cc @@ -0,0 +1,19 @@ +#include "unix_account/Factory.h" +#include "unix_account/GroupReader.h" + +#include "Configuration.h" +#include "ConfigurationFileName.h" +#include "ConfigurationReader.h" +#include "GroupReaderHttp.h" + +namespace unix_account +{ +template <> +std::unique_ptr create() +{ + ConfigurationReader configuration{ + CONFIGURATION_FILE_NAME, + API_TOKEN_FILE_NAME}; + return std::make_unique(configuration.read()); +} +} // namespace unix_account diff --git a/source/unix_account_http/src/GroupReaderHttp.h b/source/unix_account_http/src/GroupReaderHttp.h new file mode 100644 index 0000000..eeda7aa --- /dev/null +++ b/source/unix_account_http/src/GroupReaderHttp.h @@ -0,0 +1,55 @@ +#pragma once + +#include + +#include "unix_account/Group.h" +#include "unix_account/GroupReader.h" + +#include "Configuration.h" +#include "HttpRequest.h" + +namespace unix_account +{ +class GroupReaderHttp final + : public GroupReader +{ +public: + HttpRequest _http; + + GroupReaderHttp(Configuration config) + : _http{GetUrl(config.endpoints)} + { + if (config.tls) + { + _http.setTlsConfig(*config.tls); + } + } + + cpr::Url GetUrl(const Configuration::Endpoints endpoints) const + { + return {endpoints.url + endpoints.group}; + } + + ~GroupReaderHttp() = default; + + GroupReaderHttp(const GroupReaderHttp& cls) = delete; // Copy-constructor + GroupReaderHttp& operator=(const GroupReaderHttp& cls) = delete; // Copy-assignment constructor + GroupReaderHttp(GroupReaderHttp&& cls) = delete; // Move-constructor + GroupReaderHttp& operator=(GroupReaderHttp&& cls) = delete; // Move-assignment constructor + + std::optional GetById(std::uint32_t gid) override + { + return _http.Get({{"id", std::to_string(gid)}}); + } + + std::optional GetByName(const std::string& name) override + { + return _http.Get({{"name", name}}); + } + + std::vector getAll() override + { + return _http.Get(); + } +}; +} // namespace unix_account diff --git a/source/unix_account_http/src/HttpRequest.cc b/source/unix_account_http/src/HttpRequest.cc new file mode 100644 index 0000000..a397036 --- /dev/null +++ b/source/unix_account_http/src/HttpRequest.cc @@ -0,0 +1,56 @@ +#include +#include +#include + +#include "unix_account/Exception.h" + +#include "Configuration.h" +#include "HttpRequest.h" + +namespace +{ +cpr::SslOptions BuildSslOptions(const unix_account::Configuration::Tls& tls) +{ + return cpr::Ssl( + cpr::ssl::CaInfo{tls.dir / tls.caFile}, + cpr::ssl::CertFile{tls.dir / tls.certFile}, + cpr::ssl::KeyFile{tls.dir / tls.keyFile}); +} +} // namespace + +namespace unix_account +{ +HttpRequest::HttpRequest(cpr::Url url) + : _url{std::move(url)} +{ +} + +void HttpRequest::setTlsConfig(const Configuration::Tls& tls) +{ + _session.SetSslOptions(BuildSslOptions(tls)); +} + +void HttpRequest::setApiToken(const std::string& apiToken) +{ + _session.SetOption(cpr::Bearer{apiToken}); +} + +std::string HttpRequest::HttpGet(cpr::Parameters parameters) +{ + _session.SetUrl(_url); + _session.SetParameters(std::move(parameters)); + auto resp = _session.Get(); + if (resp.status_code == 200 or resp.status_code == 404) + { + return resp.text; + } + else if (resp.error) + { + throw UnixAccountLookupFailed(resp.error.message); + } + else + { + throw UnixAccountLookupFailed("Unexpected http response code: " + std::to_string(resp.status_code)); + } +} +} // namespace unix_account \ No newline at end of file diff --git a/source/unix_account_http/src/HttpRequest.h b/source/unix_account_http/src/HttpRequest.h new file mode 100644 index 0000000..837c49a --- /dev/null +++ b/source/unix_account_http/src/HttpRequest.h @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "Configuration.h" +#include "JsonValueTo.h" + +namespace unix_account +{ +class HttpRequest final +{ +private: + cpr::Session _session{}; + cpr::Url _url{}; + +public: + HttpRequest(cpr::Url url); + ~HttpRequest() = default; + + HttpRequest(const HttpRequest& cls) = delete; // Copy-constructor + HttpRequest& operator=(const HttpRequest& cls) = delete; // Copy-assignment constructor + HttpRequest(HttpRequest&& cls) = delete; // Move-constructor + HttpRequest& operator=(HttpRequest&& cls) = delete; // Move-assignment constructor + + void setTlsConfig(const Configuration::Tls& tls); + void setApiToken(const std::string& apiToken); + + /** + * Get one item + */ + template + std::optional Get(cpr::Parameters parameters) + { + return JsonValueTo(HttpGet(std::move(parameters))); + } + + /** + * Get all items + */ + template + std::vector Get() + { + return JsonValueToVector(HttpGet()); + } + +private: + std::string HttpGet(cpr::Parameters parameters = {}); +}; +} // namespace unix_account \ No newline at end of file diff --git a/source/unix_account_http/src/JsonValueExtract.h b/source/unix_account_http/src/JsonValueExtract.h new file mode 100644 index 0000000..4442dfe --- /dev/null +++ b/source/unix_account_http/src/JsonValueExtract.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include + +#include + +namespace boost::json +{ +template +void extract(const object& obj, string_view key, T& dst) +{ + auto val = obj.at(key); + if (val.is_null() or val.kind() == kind::null) + { + dst = T{}; + } + else + { + dst = value_to(val); + } +} +} // namespace boost::json \ No newline at end of file diff --git a/source/unix_account_http/src/JsonValueTo.h b/source/unix_account_http/src/JsonValueTo.h new file mode 100644 index 0000000..45a943d --- /dev/null +++ b/source/unix_account_http/src/JsonValueTo.h @@ -0,0 +1,68 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "unix_account/Exception.h" +#include "unix_account/Group.h" +#include "unix_account/Passwd.h" +#include "unix_account/Shadow.h" + +using namespace unix_account; + +namespace boost::json +{ +Group tag_invoke(const value_to_tag&, const value& jsonValue); +Passwd tag_invoke(const value_to_tag&, const value& jsonValue); +Shadow tag_invoke(const value_to_tag&, const value& jsonValue); +} // namespace boost::json + +namespace unix_account +{ +template +std::optional JsonValueTo(const std::string& text) +{ + if (text.size()) + { + try + { + return boost::json::value_to( + boost::json::parse(text)); + } + catch (std::exception& err) + { + throw UnixAccountLookupFailed(std::string{"Json conversion error: "} + err.what()); + } + } + else + { + return {}; + } +} + +template +std::vector JsonValueToVector(const std::string& text) +{ + if (text.size()) + { + try + { + auto jv = boost::json::parse(text); + auto groups = jv.as_object().at("all"); + return boost::json::value_to>( + groups); + } + catch (std::exception& err) + { + throw UnixAccountLookupFailed(std::string{"Json conversion error: "} + err.what()); + } + } + else + { + return {}; + } +} +} // namespace unix_account \ No newline at end of file diff --git a/source/unix_account_http/src/JsonValueToGroup.cc b/source/unix_account_http/src/JsonValueToGroup.cc new file mode 100644 index 0000000..0acf361 --- /dev/null +++ b/source/unix_account_http/src/JsonValueToGroup.cc @@ -0,0 +1,21 @@ +#include + +#include "JsonValueExtract.h" +#include "unix_account/Group.h" + +using namespace boost::json; +using unix_account::Group; + +namespace boost::json +{ +Group tag_invoke(const value_to_tag&, const value& jsonValue) +{ + Group group{}; + const object& obj = jsonValue.as_object(); + extract(obj, "gr_name", group.name); + extract(obj, "gr_passwd", group.password); + extract(obj, "gr_gid", group.gid); + extract(obj, "gr_mem", group.members); + return group; +} +} // namespace boost::json diff --git a/source/unix_account_http/src/JsonValueToPasswd.cc b/source/unix_account_http/src/JsonValueToPasswd.cc new file mode 100644 index 0000000..597f162 --- /dev/null +++ b/source/unix_account_http/src/JsonValueToPasswd.cc @@ -0,0 +1,24 @@ +#include + +#include "JsonValueExtract.h" +#include "unix_account/Passwd.h" + +using namespace boost::json; +using unix_account::Passwd; + +namespace boost::json +{ +Passwd tag_invoke(const value_to_tag&, const value& jsonValue) +{ + Passwd passwd{}; + const object& obj = jsonValue.as_object(); + extract(obj, "pw_name", passwd.name); + extract(obj, "pw_passwd", passwd.password); + extract(obj, "pw_uid", passwd.uid); + extract(obj, "pw_gid", passwd.gid); + extract(obj, "pw_gecos", passwd.gecos); + extract(obj, "pw_dir", passwd.dir); + extract(obj, "pw_shell", passwd.shell); + return passwd; +} +} // namespace boost::json diff --git a/source/unix_account_http/src/JsonValueToShadow.cc b/source/unix_account_http/src/JsonValueToShadow.cc new file mode 100644 index 0000000..253e518 --- /dev/null +++ b/source/unix_account_http/src/JsonValueToShadow.cc @@ -0,0 +1,25 @@ +#include + +#include "JsonValueExtract.h" +#include "unix_account/Shadow.h" + +using namespace boost::json; +using unix_account::Shadow; + +namespace boost::json +{ +Shadow tag_invoke(const value_to_tag&, const value& jsonValue) +{ + Shadow shadow{}; + const object& obj = jsonValue.as_object(); + extract(obj, "sp_namp", shadow.name); + extract(obj, "sp_pwdp", shadow.password); + extract(obj, "sp_lstchg", shadow.timeLastChange); + extract(obj, "sp_min", shadow.daysMin); + extract(obj, "sp_max", shadow.daysMax); + extract(obj, "sp_warn", shadow.daysWarn); + extract(obj, "sp_inact", shadow.daysInactive); + extract(obj, "sp_expire", shadow.timeExpires); + return shadow; +} +} // namespace boost::json diff --git a/source/unix_account_http/src/PasswdReaderFactory.cc b/source/unix_account_http/src/PasswdReaderFactory.cc new file mode 100644 index 0000000..7ceca6c --- /dev/null +++ b/source/unix_account_http/src/PasswdReaderFactory.cc @@ -0,0 +1,21 @@ + + +#include "unix_account/Factory.h" +#include "unix_account/PasswdReader.h" + +#include "Configuration.h" +#include "ConfigurationFileName.h" +#include "ConfigurationReader.h" +#include "PasswdReaderHttp.h" + +namespace unix_account +{ +template <> +std::unique_ptr create() +{ + ConfigurationReader configuration{ + CONFIGURATION_FILE_NAME, + API_TOKEN_FILE_NAME}; + return std::make_unique(configuration.read()); +} +} // namespace unix_account diff --git a/source/unix_account_http/src/PasswdReaderHttp.h b/source/unix_account_http/src/PasswdReaderHttp.h new file mode 100644 index 0000000..68fe4d1 --- /dev/null +++ b/source/unix_account_http/src/PasswdReaderHttp.h @@ -0,0 +1,55 @@ +#pragma once + +#include + +#include "unix_account/Passwd.h" +#include "unix_account/PasswdReader.h" + +#include "Configuration.h" +#include "HttpRequest.h" + +namespace unix_account +{ +class PasswdReaderHttp final + : public PasswdReader +{ +public: + HttpRequest _http; + + PasswdReaderHttp(Configuration config) + : _http{GetUrl(config.endpoints)} + { + if (config.tls) + { + _http.setTlsConfig(*config.tls); + } + } + + cpr::Url GetUrl(const Configuration::Endpoints endpoints) const + { + return {endpoints.url + endpoints.user}; + } + + ~PasswdReaderHttp() = default; + + PasswdReaderHttp(const PasswdReaderHttp& cls) = delete; // Copy-constructor + PasswdReaderHttp& operator=(const PasswdReaderHttp& cls) = delete; // Copy-assignment constructor + PasswdReaderHttp(PasswdReaderHttp&& cls) = delete; // Move-constructor + PasswdReaderHttp& operator=(PasswdReaderHttp&& cls) = delete; // Move-assignment constructor + + std::optional GetById(std::uint32_t gid) override + { + return _http.Get({{"id", std::to_string(gid)}}); + } + + std::optional GetByName(const std::string& name) override + { + return _http.Get({{"name", name}}); + } + + std::vector getAll() override + { + return _http.Get(); + } +}; +} // namespace unix_account diff --git a/source/unix_account_http/src/ShadowReaderFactory.cc b/source/unix_account_http/src/ShadowReaderFactory.cc new file mode 100644 index 0000000..b168a95 --- /dev/null +++ b/source/unix_account_http/src/ShadowReaderFactory.cc @@ -0,0 +1,19 @@ +#include "unix_account/Factory.h" +#include "unix_account/ShadowReader.h" + +#include "Configuration.h" +#include "ConfigurationFileName.h" +#include "ConfigurationReader.h" +#include "ShadowReaderHttp.h" + +namespace unix_account +{ +template <> +std::unique_ptr create() +{ + ConfigurationReader configuration{ + CONFIGURATION_FILE_NAME, + API_TOKEN_FILE_NAME}; + return std::make_unique(configuration.read()); +} +} // namespace unix_account diff --git a/source/unix_account_http/src/ShadowReaderHttp.h b/source/unix_account_http/src/ShadowReaderHttp.h new file mode 100644 index 0000000..4063f9a --- /dev/null +++ b/source/unix_account_http/src/ShadowReaderHttp.h @@ -0,0 +1,71 @@ +#pragma once + +#include + +#include "unix_account/Shadow.h" +#include "unix_account/ShadowReader.h" + +#include "Configuration.h" +#include "HttpRequest.h" + +namespace unix_account +{ +class ShadowReaderHttp final + : public ShadowReader +{ +public: + HttpRequest _http; + const std::optional _apiToken{}; + const bool _hasApiToken{}; + + ShadowReaderHttp(Configuration config) + : _http{GetUrl(config.endpoints)}, + _hasApiToken{not config.apiToken.empty()} + { + if (config.tls) + { + _http.setTlsConfig(*config.tls); + } + if (config.apiToken.size()) + { + _http.setApiToken(config.apiToken); + } + } + + cpr::Url GetUrl(const Configuration::Endpoints endpoints) const + { + return {endpoints.url + endpoints.password}; + } + + ~ShadowReaderHttp() = default; + + ShadowReaderHttp(const ShadowReaderHttp& cls) = delete; // Copy-constructor + ShadowReaderHttp& operator=(const ShadowReaderHttp& cls) = delete; // Copy-assignment constructor + ShadowReaderHttp(ShadowReaderHttp&& cls) = delete; // Move-constructor + ShadowReaderHttp& operator=(ShadowReaderHttp&& cls) = delete; // Move-assignment constructor + + std::optional GetByName(const std::string& name) override + { + if (_hasApiToken) + { + return _http.Get({{"name", name}}); + } + else + { + return {}; + } + } + + std::vector getAll() override + { + if (_hasApiToken) + { + return _http.Get(); + } + else + { + return {}; + } + } +}; +} // namespace unix_account diff --git a/source/unix_account_http/test/CMakeLists.txt b/source/unix_account_http/test/CMakeLists.txt new file mode 100644 index 0000000..a1d8036 --- /dev/null +++ b/source/unix_account_http/test/CMakeLists.txt @@ -0,0 +1,16 @@ +set(UNITTEST_NAME "${LIBRARY_NAME}_test") +set(SOURCES + src/ConfigurationReaderTest.cc + src/JsonParseTest.cc +) +add_executable(${UNITTEST_NAME} ${SOURCES}) +add_test(NAME ${UNITTEST_NAME} COMMAND ${UNITTEST_NAME}) +add_dependencies(test_all ${UNITTEST_NAME}) +target_include_directories(${UNITTEST_NAME} + PRIVATE ../src +) +target_link_libraries(${UNITTEST_NAME} PRIVATE + ${LIBRARY_NAME} + GTest::gmock_main + Boost::json +) diff --git a/source/unix_account_http/test/src/ConfigurationReaderTest.cc b/source/unix_account_http/test/src/ConfigurationReaderTest.cc new file mode 100644 index 0000000..cd34b29 --- /dev/null +++ b/source/unix_account_http/test/src/ConfigurationReaderTest.cc @@ -0,0 +1,229 @@ +#include +#include +#include +#include +#include +#include + +#include "unix_account/Exception.h" + +#include "Configuration.h" +#include "ConfigurationReader.h" + +using namespace unix_account; + +std::ostream& operator<<(std::ostream& out, const Configuration::Endpoints& endpoints) +{ + out << "endpoints:" << std::endl + << " url: " << endpoints.url << std::endl + << " group: " << endpoints.group << std::endl + << " user: " << endpoints.user << std::endl + << " password: " << endpoints.password << std::endl + << std::endl; + return out; +} + +std::ostream& operator<<(std::ostream& out, const Configuration::Tls& tls) +{ + out << "tls:" << std::endl + << " dir: " << tls.dir << std::endl + << " cert: " << tls.certFile << std::endl + << " key: " << tls.keyFile << std::endl + << " ca_cert: " << tls.caFile << std::endl + << std::endl; + return out; +} + +std::ostream& operator<<(std::ostream& out, const Configuration& config) +{ + out << config.endpoints; + if (config.tls) + { + out << *config.tls; + } + return out; +} + +class ConfigurationReaderTest : public ::testing::Test +{ +public: + struct Defaults + { + Configuration configuration{ + Configuration::Endpoints{"http://localhost:8025", "/api/group", "/api/user", "/api/password"}, + Configuration::Tls{"/etc/certs", "*.cert.pem", "*.key.pem", "*.ca-chain.pem"}}; + std::string configFileName{"nss-api-config.yaml"}; + std::string apiTokenFileName{"nss-api-token.base64"}; + std::string apiToken{"secret token"}; + }; + static const Defaults defaults; + + /** + * Helper for writing to default configuration file + */ + class ConfigurationFile : public std::ofstream + { + public: + ConfigurationFile() + : std::ofstream{defaults.configFileName, std::ios::out | std::ios::trunc} + { + } + ConfigurationFile(const Configuration& config) + : ConfigurationFile{} + { + *this << config; + } + + ConfigurationFile(const Configuration::Endpoints& endpoints) + : ConfigurationFile{} + { + *this << endpoints; + } + + ConfigurationFile(const Configuration::Tls& tls) + : ConfigurationFile{} + { + *this << tls; + } + + ~ConfigurationFile() = default; + }; + + /** + * Helper for writing to default api token file + */ + class ApiTokenFile : public std::ofstream + { + public: + ApiTokenFile(const std::string& apiToken) + : ApiTokenFile{} + { + *this << apiToken; + } + + ApiTokenFile() + : std::ofstream{defaults.apiTokenFileName, std::ios::out | std::ios::trunc} + { + } + ~ApiTokenFile() = default; + }; + + static void SetUpTestSuite() + { + // create files + ConfigurationFile{}; + ApiTokenFile{}; + } + + static void TearDownTestSuite() + { + std::filesystem::remove(defaults.configFileName); + std::filesystem::remove(defaults.apiTokenFileName); + } + +protected: + ConfigurationReader reader{defaults.configFileName, defaults.apiTokenFileName}; +}; + +const ConfigurationReaderTest::Defaults ConfigurationReaderTest::defaults{}; + +TEST_F(ConfigurationReaderTest, ReadNonexistingTlsConfiguration) +{ + ConfigurationFile{defaults.configuration.endpoints}; + + auto config = reader.read(); + ASSERT_FALSE(config.tls); +} + +TEST_F(ConfigurationReaderTest, ReadTlsConfiguration) +{ + ConfigurationFile{defaults.configuration}; + + auto config = reader.read(); + ASSERT_TRUE(config.tls); + EXPECT_EQ(defaults.configuration.tls->dir, config.tls->dir); + EXPECT_EQ(defaults.configuration.tls->certFile, config.tls->certFile); + EXPECT_EQ(defaults.configuration.tls->keyFile, config.tls->keyFile); + EXPECT_EQ(defaults.configuration.tls->caFile, config.tls->caFile); +} + +TEST_F(ConfigurationReaderTest, ReadInconsistentTlsConfiguration) +{ + ConfigurationFile out{defaults.configuration.endpoints}; + out << "tls:" << std::endl + // cert + key is missing + << " dir: " << defaults.configuration.tls->dir << std::endl + << " ca_cert: " << defaults.configuration.tls->caFile << std::endl + << std::endl; + ASSERT_THROW({ reader.read(); }, UnixAccountLookupFailed); +} + +TEST_F(ConfigurationReaderTest, ReadConfigurationFromNonexistingFile) +{ + ConfigurationReader reader{"nonexisting-file.yaml", defaults.apiTokenFileName}; + ASSERT_THROW({ reader.read(); }, UnixAccountLookupFailed); +} + +TEST_F(ConfigurationReaderTest, ReadApiTokenFromNonexistingFile) +{ + ConfigurationFile{defaults.configuration}; + ConfigurationReader reader{defaults.configFileName, "nonexisting-token.base64"}; + auto config = reader.read(); + ASSERT_TRUE(config.apiToken.empty()); +} + +TEST_F(ConfigurationReaderTest, ReadEndpointsConfiguration) +{ + ConfigurationFile{defaults.configuration}; + + auto config = reader.read(); + EXPECT_EQ(defaults.configuration.endpoints.url, config.endpoints.url); + EXPECT_EQ(defaults.configuration.endpoints.group, config.endpoints.group); + EXPECT_EQ(defaults.configuration.endpoints.user, config.endpoints.user); + EXPECT_EQ(defaults.configuration.endpoints.password, config.endpoints.password); +} + +TEST_F(ConfigurationReaderTest, ReadInconsistentEndpointsConfiguration) +{ + ConfigurationFile out{}; + out << "endpoints:" << std::endl + << " url: http://localhost:8025" << std::endl + // group, user, password is missing + << std::endl; + + ASSERT_THROW({ reader.read(); }, UnixAccountLookupFailed); +} + +TEST_F(ConfigurationReaderTest, ReadNonexistingEndpointsConfiguration) +{ + ConfigurationFile out{}; + out << "foo:" << std::endl + << " key: val" << std::endl + << std::endl; + + ASSERT_THROW({ reader.read(); }, UnixAccountLookupFailed); +} + +TEST_F(ConfigurationReaderTest, ReadApiToken) +{ + ConfigurationFile{defaults.configuration}; + ApiTokenFile{defaults.apiToken}; + + auto config = reader.read(); + EXPECT_EQ(defaults.apiToken, config.apiToken); +} + +TEST_F(ConfigurationReaderTest, ReadApiTokenWithoutPermission) +{ + ConfigurationFile{defaults.configuration}; + ApiTokenFile{defaults.apiToken}; + + using std::filesystem::perms; + std::filesystem::permissions( + defaults.apiTokenFileName, + perms::none | perms::owner_write); + auto config = reader.read(); + EXPECT_TRUE(config.apiToken.empty()); + + std::filesystem::permissions(defaults.apiTokenFileName, perms::all); +} \ No newline at end of file diff --git a/source/unix_account_http/test/src/JsonParseTest.cc b/source/unix_account_http/test/src/JsonParseTest.cc new file mode 100644 index 0000000..38a9885 --- /dev/null +++ b/source/unix_account_http/test/src/JsonParseTest.cc @@ -0,0 +1,21 @@ +#include + +#include "unix_account/Group.h" +#include "unix_account/Passwd.h" +#include "unix_account/Shadow.h" + +#include "JsonValueTo.h" + +using namespace unix_account; + +TEST(JsonParseTest, PasswdValueIsNull) +{ + const std::string jsonText{"{\"pw_name\": \"foo\", \"pw_passwd\": \"x\", \"pw_uid\": 10000, \"pw_gid\": 10000, \"pw_gecos\": null, \"pw_dir\": \"/home/foo\", \"pw_shell\": \"/bin/bash\"}"}; + ASSERT_NO_THROW({ JsonValueTo(jsonText); }); +} + +TEST(JsonParseTest, ShadowValueIsNull) +{ + const std::string jsonText{"{\"sp_namp\": \"foo\", \"sp_pwdp\": \"*\", \"sp_lstchg\": 18820, \"sp_min\": 0, \"sp_max\": 99999, \"sp_warn\": 7, \"sp_inact\": null, \"sp_expire\": null}"}; + ASSERT_NO_THROW({ JsonValueTo(jsonText); }); +} diff --git a/third_party/boost b/third_party/boost new file mode 100755 index 0000000..daeeb24 --- /dev/null +++ b/third_party/boost @@ -0,0 +1,36 @@ +#!/bin/bash + +readonly prefix_path=${0%/*} +source ${prefix_path}/paths.sh + +readonly boost_version="1.75.0" +readonly boost_dir="boost_${boost_version//./_}" + + +boost_fetch() +{ + curl \ + --silent \ + --location \ + https://boostorg.jfrog.io/artifactory/main/release/${boost_version}/source/${boost_dir}.tar.gz \ + |tar -xz --directory ${source_dir} +} + +boost_configure() +{ + pushd ${source_dir}/${boost_dir} + ./bootstrap.sh + popd +} + +boost_install() +{ + pushd ${source_dir}/${boost_dir} + ./b2 cxxflags=-fPIC link=static install \ + --build-dir=${build_dir}/${boost_dir} \ + --prefix=${install_dir}/${boost_dir} \ + --with-json + popd +} + +main_for boost "${@}" diff --git a/third_party/cpr b/third_party/cpr new file mode 100755 index 0000000..7a8d0ae --- /dev/null +++ b/third_party/cpr @@ -0,0 +1,38 @@ +#!/bin/bash + +readonly prefix_path=${0%/*} +source ${prefix_path}/paths.sh + +readonly cpr_version="1.6.2" # "${0##*-}" from filename +readonly cpr_dir="cpr-${cpr_version}" + + +cpr_fetch() +{ + curl \ + --silent \ + --location \ + https://github.com/whoshuu/cpr/archive/refs/tags/${cpr_version}.tar.gz \ + |tar -xz --directory ${source_dir} +} + +cpr_configure() +{ + cmake \ + -D CMAKE_C_FLAGS="-fPIC" \ + -D CMAKE_CXX_FLAGS="-fPIC" \ + -D BUILD_SHARED_LIBS=OFF \ + -D CPR_BUILD_TESTS=OFF \ + -D CPR_BUILD_TESTS_SSL=OFF \ + -S "${source_dir}/${cpr_dir}" \ + -B "${build_dir}/${cpr_dir}" +} + +cpr_install() +{ + make \ + --directory "${build_dir}/${cpr_dir}" \ + install DESTDIR="${install_dir}/${cpr_dir}" +} + +main_for cpr "${@}" diff --git a/third_party/gtest b/third_party/gtest new file mode 100755 index 0000000..087e68b --- /dev/null +++ b/third_party/gtest @@ -0,0 +1,33 @@ +#!/bin/bash + +readonly prefix_path=${0%/*} +source ${prefix_path}/paths.sh + +readonly gtest_version="1.10.0" # "${0##*-}" from filename +readonly gtest_dir="googletest-release-${gtest_version}" + + +gtest_fetch() +{ + curl \ + --silent \ + --location \ + https://github.com/google/googletest/archive/release-${gtest_version}.tar.gz \ + |tar -xz --directory ${source_dir} +} + +gtest_configure() +{ + cmake \ + -S "${source_dir}/${gtest_dir}" \ + -B "${build_dir}/${gtest_dir}" +} + +gtest_install() +{ + make \ + --directory "${build_dir}/${gtest_dir}" \ + install DESTDIR="${install_dir}/${gtest_dir}" +} + +main_for gtest "${@}" diff --git a/third_party/paths.sh b/third_party/paths.sh new file mode 100644 index 0000000..6ad24d8 --- /dev/null +++ b/third_party/paths.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +readonly source_dir="${THIRD_PARTY_ROOT}/source" +readonly install_dir="${THIRD_PARTY_ROOT}/install/${DIST}" +readonly build_dir="${THIRD_PARTY_ROOT}/build/${DIST}" + + +_execute_steps_on() +{ + local module_name="${1}" + shift + local steps="${@}" + for fcn in "${@}"; do + echo "${module_name}_${fcn}" + ${module_name}_${fcn} || break + done +} + + +main_for() +{ + local module_name="${1}" + shift + local positional="${@}" + + if [[ -z "${THIRD_PARTY_ROOT}" ]]; then + echo "Environment variable THIRD_PARTY_ROOT not set" + exit 1 + elif [[ -z "${DIST}" ]]; then + echo "Environment variable DIST not set" + return + elif [[ ${#} -eq 0 ]]; then + _execute_steps_on ${module_name} \ + fetch \ + configure \ + install + else + _execute_steps_on ${module_name} \ + "${@}" + fi +} diff --git a/third_party/yaml_cpp b/third_party/yaml_cpp new file mode 100755 index 0000000..74ea7bb --- /dev/null +++ b/third_party/yaml_cpp @@ -0,0 +1,36 @@ +#!/bin/bash + +readonly prefix_path=${0%/*} +source ${prefix_path}/paths.sh + +readonly yaml_cpp_version="0.6.3" +readonly yaml_cpp_dir="yaml-cpp-${yaml_cpp_version}" + + +yaml_cpp_fetch() +{ + curl \ + --silent \ + --location \ + https://github.com/jbeder/yaml-cpp/archive/yaml-cpp-${yaml_cpp_version}.tar.gz \ + |tar -xz --directory ${source_dir} +} + +yaml_cpp_configure() +{ + cmake \ + -D CMAKE_CXX_FLAGS="-fPIC" \ + -D CMAKE_BUILD_TYPE=Release \ + -D YAML_CPP_BUILD_TESTS=NO \ + -S "${source_dir}/yaml-cpp-${yaml_cpp_dir}" \ + -B "${build_dir}/${yaml_cpp_dir}" +} + +yaml_cpp_install() +{ + make \ + --directory "${build_dir}/${yaml_cpp_dir}" \ + install DESTDIR="${install_dir}/${yaml_cpp_dir}" +} + +main_for yaml_cpp "${@}"