mirror of
https://github.com/uutils/shadow.git
synced 2026-06-10 16:14:57 -07:00
Merge pull request #115 from shadow-utils-rs/feat/102-e2e-deployment-tests
e2e: automated deployment tests in Docker (#102)
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
target/
|
||||
.git/
|
||||
*.swp
|
||||
*.swo
|
||||
@@ -110,6 +110,9 @@ feat_phase2 = ["feat_useradd", "feat_userdel", "feat_usermod", "feat_chpasswd",
|
||||
feat_phase3 = ["feat_groupadd", "feat_groupdel", "feat_groupmod", "feat_grpck", "feat_chfn", "feat_chsh", "feat_newgrp"]
|
||||
feat_common = ["feat_phase1", "feat_phase2"]
|
||||
|
||||
# PAM authentication (requires libpam-dev)
|
||||
pam = ["passwd/pam"]
|
||||
|
||||
# Shell completions generator
|
||||
completions = ["dep:clap_complete"]
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
# docker compose run --rm alpine cargo test
|
||||
# docker compose run --rm fedora cargo test
|
||||
# docker compose up --build # build all images
|
||||
# docker compose run --rm e2e # end-to-end deployment tests
|
||||
# docker compose run --rm e2e bash # debug e2e interactively
|
||||
#
|
||||
# The cargo registry cache is shared across Debian/Alpine (same CARGO_HOME path).
|
||||
# Fedora uses a separate cache (rustup installs to /root/.cargo).
|
||||
@@ -47,6 +49,20 @@ services:
|
||||
- cargo-cache-fedora:/root/.cargo/registry
|
||||
- target-fedora:/workspace/target
|
||||
|
||||
# End-to-end deployment test — self-contained, no shared volumes.
|
||||
# Builds shadow-rs from source, installs system-wide, runs ~100 assertions.
|
||||
e2e:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/Dockerfile.e2e
|
||||
security_opt:
|
||||
- seccomp:unconfined
|
||||
- "no-new-privileges:false"
|
||||
cap_add:
|
||||
- SYS_ADMIN
|
||||
tmpfs:
|
||||
- /tmp
|
||||
|
||||
volumes:
|
||||
cargo-cache:
|
||||
cargo-cache-fedora:
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
# shadow-rs end-to-end deployment test image
|
||||
#
|
||||
# Simulates a real deployment: build from source, install system-wide
|
||||
# (replacing GNU shadow-utils), then run ~100 assertions.
|
||||
#
|
||||
# Usage:
|
||||
# docker compose build e2e
|
||||
# docker compose run --rm e2e # run all tests
|
||||
# docker compose run --rm e2e bash # debug interactively
|
||||
|
||||
# ── Stage 1: build shadow-rs ────────────────────────────────────────
|
||||
FROM rust:latest AS builder
|
||||
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libpam0g-dev \
|
||||
libselinux1-dev \
|
||||
libaudit-dev \
|
||||
pkg-config \
|
||||
make \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /src
|
||||
COPY . .
|
||||
|
||||
RUN cargo build --release --features pam \
|
||||
&& make install DESTDIR=/install PREFIX=/usr
|
||||
|
||||
# ── Stage 2: runtime with tests ────────────────────────────────────
|
||||
FROM debian:trixie
|
||||
|
||||
# Install GNU shadow-utils first — this creates /etc/pam.d/passwd,
|
||||
# /etc/pam.d/su, and other PAM configs we need. We overwrite the
|
||||
# binaries below but keep the PAM configuration intact.
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
passwd \
|
||||
login \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Overwrite GNU binaries with shadow-rs
|
||||
COPY --from=builder /install/usr/sbin/ /usr/sbin/
|
||||
|
||||
# Setuid-root — chmod on symlink sets the bit on the target binary
|
||||
RUN chmod 4755 /usr/sbin/shadow-rs
|
||||
|
||||
# Install test dependencies
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
expect \
|
||||
nscd \
|
||||
ansible-core \
|
||||
python3-passlib \
|
||||
procps \
|
||||
openssl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create a non-root test runner using our own tools
|
||||
RUN /usr/sbin/useradd -m -s /bin/bash testrunner
|
||||
|
||||
# Copy test scripts
|
||||
COPY tests/e2e/ /tests/e2e/
|
||||
RUN chmod +x /tests/e2e/deploy-test.sh
|
||||
|
||||
WORKDIR /tests/e2e
|
||||
CMD ["./deploy-test.sh"]
|
||||
@@ -175,6 +175,7 @@ pub struct PamConv {
|
||||
// PAM FFI function declarations
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[link(name = "pam")]
|
||||
unsafe extern "C" {
|
||||
fn pam_start(
|
||||
service_name: *const libc::c_char,
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
---
|
||||
# Ansible integration test for shadow-rs
|
||||
#
|
||||
# Validates that shadow-rs tools work as drop-in replacements when
|
||||
# called by Ansible's user/group modules (which invoke useradd,
|
||||
# userdel, usermod, groupadd, groupdel directly as subprocesses).
|
||||
#
|
||||
# Usage:
|
||||
# ansible-playbook -c local -i "localhost," tests/e2e/ansible-test.yml
|
||||
|
||||
- name: shadow-rs integration test
|
||||
hosts: localhost
|
||||
connection: local
|
||||
become: true
|
||||
vars:
|
||||
test_user: ansibleuser
|
||||
test_group: ansiblegroup
|
||||
test_uid: 3000
|
||||
test_gid: 3000
|
||||
test_password: "AnsiblePass123"
|
||||
|
||||
tasks:
|
||||
# ── Create ──────────────────────────────────────────────────────
|
||||
|
||||
- name: Create test group
|
||||
ansible.builtin.group:
|
||||
name: "{{ test_group }}"
|
||||
gid: "{{ test_gid }}"
|
||||
state: present
|
||||
|
||||
- name: Create test user
|
||||
ansible.builtin.user:
|
||||
name: "{{ test_user }}"
|
||||
uid: "{{ test_uid }}"
|
||||
group: "{{ test_group }}"
|
||||
shell: /bin/bash
|
||||
create_home: true
|
||||
comment: "Ansible Test User"
|
||||
|
||||
- name: Verify user exists in passwd
|
||||
ansible.builtin.command: getent passwd {{ test_user }}
|
||||
register: getent_result
|
||||
changed_when: false
|
||||
|
||||
- name: Assert user fields
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "test_user in getent_result.stdout"
|
||||
- "'/bin/bash' in getent_result.stdout"
|
||||
- "test_uid | string in getent_result.stdout"
|
||||
fail_msg: "User {{ test_user }} not created correctly"
|
||||
|
||||
- name: Verify group exists
|
||||
ansible.builtin.command: getent group {{ test_group }}
|
||||
register: group_result
|
||||
changed_when: false
|
||||
|
||||
- name: Assert group fields
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "test_group in group_result.stdout"
|
||||
- "test_gid | string in group_result.stdout"
|
||||
fail_msg: "Group {{ test_group }} not created correctly"
|
||||
|
||||
# ── Modify ──────────────────────────────────────────────────────
|
||||
|
||||
# Set password via chpasswd -e (usermod -p not yet implemented)
|
||||
- name: Set user password via chpasswd
|
||||
ansible.builtin.shell: |
|
||||
echo "{{ test_user }}:$(openssl passwd -6 '{{ test_password }}')" | chpasswd -e
|
||||
changed_when: true
|
||||
no_log: true
|
||||
|
||||
- name: Modify user shell
|
||||
ansible.builtin.user:
|
||||
name: "{{ test_user }}"
|
||||
shell: /bin/sh
|
||||
|
||||
- name: Verify shell changed
|
||||
ansible.builtin.command: getent passwd {{ test_user }}
|
||||
register: shell_result
|
||||
changed_when: false
|
||||
|
||||
- name: Assert shell is /bin/sh
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- "'/bin/sh' in shell_result.stdout"
|
||||
fail_msg: "Shell not changed to /bin/sh"
|
||||
|
||||
- name: Add supplementary group
|
||||
ansible.builtin.user:
|
||||
name: "{{ test_user }}"
|
||||
groups: "{{ test_group }}"
|
||||
append: true
|
||||
|
||||
# ── Integrity checks ────────────────────────────────────────────
|
||||
|
||||
- name: Run pwck read-only check
|
||||
ansible.builtin.command: pwck -r
|
||||
register: pwck_result
|
||||
changed_when: false
|
||||
failed_when: pwck_result.rc not in [0, 2]
|
||||
|
||||
- name: Run grpck read-only check
|
||||
ansible.builtin.command: grpck -r
|
||||
register: grpck_result
|
||||
changed_when: false
|
||||
failed_when: grpck_result.rc not in [0, 2]
|
||||
|
||||
# ── Cleanup ─────────────────────────────────────────────────────
|
||||
|
||||
- name: Delete test user
|
||||
ansible.builtin.user:
|
||||
name: "{{ test_user }}"
|
||||
state: absent
|
||||
remove: true
|
||||
|
||||
- name: Delete test group
|
||||
ansible.builtin.group:
|
||||
name: "{{ test_group }}"
|
||||
state: absent
|
||||
|
||||
- name: Verify user deleted
|
||||
ansible.builtin.command: getent passwd {{ test_user }}
|
||||
register: deleted_user
|
||||
failed_when: deleted_user.rc == 0
|
||||
changed_when: false
|
||||
|
||||
- name: Verify group deleted
|
||||
ansible.builtin.command: getent group {{ test_group }}
|
||||
register: deleted_group
|
||||
failed_when: deleted_group.rc == 0
|
||||
changed_when: false
|
||||
Executable
+500
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user