├── .clippy.toml ├── .deepsource.toml ├── .github ├── dependabot.yml └── workflows │ ├── main.yml │ └── release.yml ├── .gitignore ├── .prettierrc.toml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE.md ├── Makefile ├── README.md ├── ansible ├── roles │ └── sectora │ │ ├── handlers │ │ └── main.yml │ │ └── tasks │ │ └── main.yml ├── sectora.service ├── sectora.sh ├── site.yml └── templates │ └── sectora.conf ├── assets ├── conf-files │ └── sectora.conf ├── scripts │ ├── config │ ├── postinst │ ├── postrm │ └── templates ├── sectora.service └── sectora.sh ├── build.rs ├── how-it-works.svg ├── rustfmt.toml ├── src ├── applog.rs ├── buffer.rs ├── connection.rs ├── cstructs.rs ├── daemon.rs ├── error.rs ├── ghclient.rs ├── lib.rs ├── main.rs ├── message.rs ├── statics.rs └── structs.rs └── test ├── .gitignore ├── Makefile ├── client ├── Dockerfile ├── hosts ├── localtest.yml └── sshconfig ├── json-server ├── Dockerfile ├── db.json └── routes.json └── keys ├── root ├── id_rsa └── id_rsa.pub └── user ├── id_rsa └── id_rsa.pub /.clippy.toml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yasuyuky/sectora/186e976d0f01be2e877d6c5f2409b314456f10a4/.clippy.toml -------------------------------------------------------------------------------- /.deepsource.toml: -------------------------------------------------------------------------------- 1 | version = 1 2 | 3 | [[analyzers]] 4 | name = "rust" 5 | 6 | [analyzers.meta] 7 | msrv = "stable" 8 | 9 | [[analyzers]] 10 | name = "shell" -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: cargo 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | time: '09:00' 8 | timezone: Asia/Tokyo 9 | open-pull-requests-limit: 10 10 | reviewers: 11 | - yasuyuky 12 | - package-ecosystem: docker 13 | directory: '/test/client' 14 | schedule: 15 | interval: daily 16 | time: '09:00' 17 | timezone: Asia/Tokyo 18 | open-pull-requests-limit: 10 19 | reviewers: 20 | - yasuyuky 21 | - package-ecosystem: docker 22 | directory: '/test/json-server' 23 | schedule: 24 | interval: daily 25 | time: '09:00' 26 | timezone: Asia/Tokyo 27 | open-pull-requests-limit: 10 28 | reviewers: 29 | - yasuyuky 30 | - package-ecosystem: github-actions 31 | directory: '/' 32 | schedule: 33 | interval: daily 34 | time: '09:00' 35 | timezone: Asia/Tokyo 36 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 30 15 | strategy: 16 | matrix: 17 | arch: [amd64, arm64, armhf] 18 | target: [lib, exe, daemon, deb] 19 | fail-fast: false 20 | steps: 21 | - uses: actions/checkout@v4 22 | - uses: actions/cache@v4 23 | with: 24 | path: ${{ github.workspace }}/.sccache-${{ matrix.arch }} 25 | key: ${{ runner.os }}-${{ matrix.arch }}-${{ matrix.target }}-sccache 26 | - name: Set up QEMU 27 | uses: docker/setup-qemu-action@v3 28 | - name: Build 29 | run: make TARGET=${{ matrix.arch }} ${{ matrix.target }} 30 | - name: Fix cache permission 31 | run: sudo chown -R $USER .sccache-${{ matrix.arch }} 32 | - uses: actions/upload-artifact@v4 33 | if: matrix.target == 'deb' 34 | with: 35 | name: sectora-${{ matrix.arch }} 36 | path: ${{ github.workspace }}/target/*/debian/*.deb 37 | 38 | set-dists: 39 | runs-on: ubuntu-latest 40 | outputs: 41 | dists: ${{ steps.set-matrix.outputs.matrix }} 42 | steps: 43 | - id: set-matrix 44 | run: | 45 | content=$(curl -L https://raw.githubusercontent.com/yasuyuky/docker-ssh-test/main/dist.json | jq -c .) 46 | echo matrix=${content} >>$GITHUB_OUTPUT 47 | echo ${content} 48 | 49 | test: 50 | needs: [build, set-dists] 51 | runs-on: ubuntu-latest 52 | strategy: 53 | matrix: ${{ fromJson(needs.set-dists.outputs.dists) }} 54 | fail-fast: false 55 | steps: 56 | - uses: actions/checkout@v4 57 | - uses: actions/download-artifact@v4 58 | with: 59 | name: sectora-amd64 60 | path: target 61 | - name: Run tests 62 | run: make test-deb-stub dist=${{ matrix.dist }} ver=${{ matrix.ver }} 63 | working-directory: test 64 | env: 65 | TERM: xterm-256color 66 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | outputs: 12 | version: ${{ steps.cargo_ver.outputs.version }} 13 | upload: ${{ steps.create_release.outputs.upload_url }} 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Get Date 18 | id: get_date 19 | run: | 20 | date +'%Y.%m.%d' 21 | echo date=$(date +'%Y.%m.%d') >>$GITHUB_OUTPUT 22 | 23 | - name: Get the version in cargo 24 | id: cargo_ver 25 | run: | 26 | VERSION=$(grep '^version' Cargo.toml | cut -d '"' -f 2) 27 | echo ${VERSION} 28 | echo version=${VERSION} >>$GITHUB_OUTPUT 29 | echo v=v${VERSION} >>$GITHUB_OUTPUT 30 | test ${GITHUB_REF/refs\/tags\//} = v${VERSION} 31 | 32 | - name: Create Release 33 | id: create_release 34 | uses: actions/create-release@v1 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | with: 38 | tag_name: ${{ steps.cargo_ver.outputs.v }} 39 | release_name: Release ${{ steps.cargo_ver.outputs.v }} (${{ steps.get_date.outputs.date }}) 40 | body: Automated release 41 | draft: false 42 | prerelease: false 43 | 44 | build-and-upload: 45 | needs: release 46 | strategy: 47 | matrix: 48 | arch: [amd64, arm64, armhf] 49 | runs-on: ubuntu-latest 50 | steps: 51 | - name: Checkout 52 | uses: actions/checkout@v4 53 | - name: Set up QEMU 54 | uses: docker/setup-qemu-action@v3 55 | - name: Build deb 56 | run: make TARGET=${{ matrix.arch }} deb 57 | - name: Get Path of Artifact 58 | id: getpath 59 | run: | 60 | ASSET_PATH=$(ls target/*/debian/sectora_${{ needs.release.outputs.version }}*_${{ matrix.arch }}.deb | head -n 1) 61 | echo "asset_path=$ASSET_PATH" >> $GITHUB_OUTPUT 62 | - name: Upload Release Asset 63 | uses: actions/upload-release-asset@v1 64 | env: 65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 66 | with: 67 | upload_url: ${{ needs.release.outputs.upload }} 68 | asset_path: ${{ steps.getpath.outputs.asset_path }} 69 | asset_name: basename(${{ steps.getpath.outputs.asset_path }}) 70 | asset_content_type: application/vnd.debian.binary-package 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .cargo* 3 | *.retry 4 | hosts-* 5 | *.local.yml 6 | .sccache* 7 | -------------------------------------------------------------------------------- /.prettierrc.toml: -------------------------------------------------------------------------------- 1 | singleQuote = true 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog][keep a changelog] and this project adheres to [Semantic Versioning][semantic versioning]. 6 | 7 | ## [Unreleased] 8 | 9 | --- 10 | 11 | ## [Released] 12 | 13 | ## [0.4.0] - 2021-01-22 14 | 15 | ### Changed 16 | 17 | - Update dependencies 18 | - Use tokio 1.0 19 | - Allow long messages 20 | 21 | ### Fixed 22 | 23 | - fix unknown key name issue #356 24 | 25 | ## [0.3.2] - 2020-09-12 26 | 27 | ### Changed 28 | 29 | - Update dependencies 30 | 31 | ## [0.3.1] - 2020-08-12 32 | 33 | ### Changed 34 | 35 | - Tweak PAM settings 36 | - Update depnedencies 37 | 38 | ### Fixed 39 | 40 | - Use the abusolute path for conffiles 41 | 42 | ## [0.3.0] - 2020-06-16 43 | 44 | ### Added 45 | 46 | - Notify Systemd 47 | 48 | ### Changed 49 | 50 | - Update the rust version 51 | - Handle the send-back error 52 | - Update default AuthorizedKeysCommand style 53 | - Update depnedencies 54 | 55 | ## [0.2.0] - 2020.05.30 56 | 57 | ### Added 58 | 59 | - Enhance rate limit information 60 | 61 | ### Fixed 62 | 63 | - Rewrite all hyper-related methods to asynchronous ones 64 | 65 | ### Changed 66 | 67 | - Make http client reusable 68 | - Update some depnedencies 69 | 70 | ## [0.1.0] - 2020.05.16 71 | 72 | ### Added 73 | 74 | - Initial release 75 | 76 | --- 77 | 78 | 79 | 80 | [keep a changelog]: https://keepachangelog.com/ 81 | [semantic versioning]: https://semver.org/ 82 | 83 | 84 | 85 | [unreleased]: https://github.com/yasuyuky/sectora/compare/v0.4.0...HEAD 86 | [released]: https://github.com/yasuyuky/sectora/releases 87 | [0.4.0]: https://github.com/yasuyuky/sectora/compare/v0.3.2...v0.4.0 88 | [0.3.2]: https://github.com/yasuyuky/sectora/compare/v0.3.1...v0.3.2 89 | [0.3.1]: https://github.com/yasuyuky/sectora/compare/v0.3.0...v0.3.1 90 | [0.3.0]: https://github.com/yasuyuky/sectora/compare/v0.2.0...v0.3.0 91 | [0.2.0]: https://github.com/yasuyuky/sectora/compare/v0.1.0...v0.2.0 92 | [0.1.0]: https://github.com/yasuyuky/sectora/releases/v0.1.0 93 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sectora" 3 | version = "0.5.0" 4 | authors = ["Yasuyuki YAMADA "] 5 | build = "build.rs" 6 | description = "SSH authentication with the GitHub team and repo" 7 | edition = "2024" 8 | license = "MIT" 9 | readme = "README.md" 10 | rust-version = "1.86.0" 11 | 12 | [dependencies] 13 | futures = "0.3" 14 | toml = "0.8" 15 | serde = { version = "1.0", features = ["derive"] } 16 | serde_json = "1.0" 17 | glob = "0.3" 18 | libc = "0.2" 19 | nix = "0.30" 20 | hyper = { version = "0.14.27", features = ["http1", "client", "tcp"] } 21 | hyper-tls = "0.6.0" 22 | log = "0.4.27" 23 | syslog = "7.0" 24 | tokio = { version = "1.45", features = ["macros", "rt", "rt-multi-thread"] } 25 | sd-notify = "0.4.5" 26 | clap = { version = "4.5.39", features = ["derive"] } 27 | clap_complete = "4.5.52" 28 | once_cell = "1.21.3" 29 | reqwest = { version = "0.12.19", features = ["json"] } 30 | 31 | [[bin]] 32 | name = "sectora" 33 | path = "src/main.rs" 34 | 35 | [[bin]] 36 | name = "sectorad" 37 | path = "src/daemon.rs" 38 | 39 | [lib] 40 | name = "nss_sectora" 41 | path = "src/lib.rs" 42 | crate-type = ["cdylib"] 43 | 44 | [package.metadata.deb] 45 | maintainer = "Yasuyuki YAMADA " 46 | copyright = "2017-2020 Yasuyuki YAMADA " 47 | depends = "$auto, systemd, openssh-server" 48 | extended-description = """\ 49 | **Sector A**uthentication 50 | (formerly named as **ghteam-auth**) 51 | Using this program, you can grant login privileges on your servers to GitHub team members or outside collaborators of your repository. 52 | Implemented with Rust.""" 53 | section = "admin" 54 | priority = "optional" 55 | assets = [ 56 | ["target/release/sectora", "usr/sbin/", "755"], 57 | ["target/release/sectorad", "usr/sbin/", "755"], 58 | ["target/release/libnss_sectora.so", "usr/lib/libnss_sectora.so", "644"], 59 | ["target/release/libnss_sectora.so", "usr/lib/libnss_sectora.so.2", "644"], 60 | ["assets/conf-files/sectora.conf", "etc/sectora.conf", "644"], 61 | ["assets/sectora.sh", "usr/sbin/", "755"], 62 | ["assets/sectora.service", "etc/systemd/system/", "644"], 63 | ] 64 | conf-files = ["/etc/sectora.conf"] 65 | maintainer-scripts = "assets/scripts" 66 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017-2020 Yasuyuki YAMADA 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | RUST_VER=$(shell grep "^rust-version" Cargo.toml | cut -f 2 -d '"') 2 | VERSION=$(shell grep "^version" Cargo.toml | cut -f 2 -d '"') 3 | TARGET:=amd64 4 | LOG_LEVEL:=OFF 5 | BUILD_IMG=ghcr.io/yasuyuky/rust-ubuntu:${RUST_VER} 6 | PLATFORM_OPT=--platform=linux/$(TARGET) 7 | ifeq ($(TARGET),amd64) 8 | RSTARGET=x86_64-unknown-linux-gnu 9 | else ifeq ($(TARGET),arm64) 10 | RSTARGET=aarch64-unknown-linux-gnu 11 | else ifeq ($(TARGET),armhf) 12 | RSTARGET=armv7-unknown-linux-gnueabihf 13 | LIBTARGET=arm-linux-gnueabihf 14 | endif 15 | LIBTARGET?=$(subst unknown-,,$(RSTARGET)) 16 | RELEASE_DIR=target/$(RSTARGET)/release 17 | DEBIAN_DIR=target/$(RSTARGET)/debian 18 | COMMON_BUILD_OPT= -v ${PWD}:/source -w /source -e RUSTC_WRAPPER=/usr/local/cargo/bin/sccache -e SCCACHE_DIR=/source/.sccache 19 | # BUILD_VOL= -v ${PWD}/.cargo-$(TARGET)/registry:/usr/local/cargo/registry -v ${PWD}/.cargo-$(TARGET)/bin:/source/.cargo/bin 20 | # BUILD_VOL= -v ${PWD}/.cargo-$(TARGET):/source/.cargo 21 | BUILD_VOL= -v ${PWD}/.sccache-$(TARGET):/source/.sccache 22 | OPENSSL_STATIC_OPT= -e OPENSSL_STATIC=yes -e OPENSSL_LIB_DIR=/usr/lib/$(LIBTARGET)/ -e OPENSSL_INCLUDE_DIR=/usr/include -e LOG_LEVEL=$(LOG_LEVEL) 23 | MEM_OPT= -m 4g --memory-swap 16g 24 | BUILD_OPT= $(MEM_OPT) $(BUILD_VOL) $(COMMON_BUILD_OPT) $(OPENSSL_STATIC_OPT) 25 | DEPLOY_TEST_IMG=yasuyuky/ubuntu-ssh 26 | ENTRIY_POINTS := src/main.rs src/daemon.rs src/lib.rs 27 | SRCS := $(filter-out $(ENTRIY_POINTS),$(wildcard src/*.rs)) 28 | ASSETS := $(wildcard assets/*) $(wildcard assets/*/*) 29 | CARGO_FILES := Cargo.toml Cargo.lock 30 | DOCKER_RUN=docker run --rm $(PLATFORM_OPT) 31 | 32 | all: 33 | 34 | deb: $(DEBIAN_DIR)/sectora_$(VERSION)_$(TARGET).deb 35 | 36 | exe: $(RELEASE_DIR)/sectora 37 | 38 | daemon: $(RELEASE_DIR)/sectorad 39 | 40 | lib: $(RELEASE_DIR)/libnss_sectora.so 41 | 42 | amd64: 43 | make TARGET=amd64 exe daemon lib deb 44 | 45 | armhf: 46 | make TARGET=armhf exe daemon lib deb 47 | 48 | arm64: 49 | make TARGET=arm64 exe daemon lib deb 50 | 51 | enter-build-image: 52 | $(DOCKER_RUN) -it $(BUILD_OPT) $(BUILD_IMG) bash 53 | 54 | $(RELEASE_DIR)/sectora: src/main.rs $(SRCS) $(CARGO_FILES) 55 | $(DOCKER_RUN) $(BUILD_OPT) $(BUILD_IMG) cargo build --bin sectora --release --target=$(RSTARGET) 56 | 57 | $(RELEASE_DIR)/sectorad: src/daemon.rs $(SRCS) $(CARGO_FILES) 58 | $(DOCKER_RUN) $(BUILD_OPT) $(BUILD_IMG) cargo build --bin sectorad --release --target=$(RSTARGET) 59 | 60 | $(RELEASE_DIR)/libnss_sectora.so: src/lib.rs $(SRCS) $(CARGO_FILES) 61 | $(DOCKER_RUN) $(BUILD_OPT) $(BUILD_IMG) cargo build --lib --release --target=$(RSTARGET) 62 | 63 | $(DEBIAN_DIR)/sectora_$(VERSION)_$(TARGET).deb: src/main.rs src/daemon.rs src/lib.rs $(SRCS) $(CARGO_FILES) $(ASSETS) 64 | $(DOCKER_RUN) $(BUILD_OPT) $(BUILD_IMG) sh -c "cargo deb --target=$(RSTARGET) && sccache -s" 65 | 66 | .PHONY: clean clean-cargo clean-exe clean-lib clean-deb 67 | 68 | clean-cargo: 69 | $(DOCKER_RUN) $(BUILD_OPT) $(BUILD_IMG) cargo clean 70 | 71 | clean-exe: 72 | rm -f target/$(RSTARGET)/release/sectora 73 | 74 | clean-daemon: 75 | rm -f target/$(RSTARGET)/release/sectorad 76 | 77 | clean-lib: 78 | rm -f target/$(RSTARGET)/release/libnss_sectora.so 79 | 80 | clean-deb: 81 | rm -f target/$(RSTARGET)/debian/sectora_$(VERSION)_*.deb 82 | 83 | clean: 84 | make clean-exe 85 | make clean-daemon 86 | make clean-lib 87 | make clean-deb 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sectora 2 | 3 | **Sector A**uthentication 4 | 5 | (formerly named as **ghteam-auth**) 6 | 7 | Using this program, you can grant login privileges on your servers to GitHub team members or outside collaborators of your repository. 8 | 9 | Implemented with Rust. 10 | 11 | [![Github Actions](https://github.com/yasuyuky/sectora/workflows/ci/badge.svg)](https://github.com/yasuyuky/sectora/actions) 12 | 13 | # How it works 14 | 15 | ![how it works](how-it-works.svg) 16 | 17 | # How to use 18 | 19 | 1. Generate the ssh key pair and [upload the public key to GitHub](https://help.github.com/en/github/authenticating-to-github/adding-a-new-ssh-key-to-your-github-account) 20 | 21 | 2. Use [the deb file](https://github.com/yasuyuky/sectora/releases) to setup sectora to the server 22 | 23 | - You need a developer token with the following scope 24 | - read:org 25 | - repo (optional) 26 | 27 | 3. Log in to the server with your private key 28 | 29 | # Manual Setup 30 | 31 | ## How to build manually 32 | 33 | See [Makefile](https://github.com/yasuyuky/sectora/blob/main/Makefile) for details 34 | 35 | ## How to install and setup manually 36 | 37 | 1. Copy executable and shared object to each path 38 | 2. Put config file for this program 39 | 3. Register sectora daemon to systemd and enable it 40 | 4. Configure name service switch 41 | 5. Configure sshd 42 | 6. Configure PAM (Optional) 43 | 44 | [A setting example of ansible is available](https://github.com/yasuyuky/sectora/blob/main/ansible/) 45 | 46 | ### Copy the executable file and shared object to each path 47 | 48 | #### Copy executable file 49 | 50 | Put `sectora` & `sectorad` to `/usr/sbin/`. 51 | 52 | #### Copy shared object 53 | 54 | Put `libnss_sectora.so` to `/usr/lib/`. 55 | 56 | #### Create link of shared object 57 | 58 | `ln -s /usr/lib/libnss_sectora.so /usr/lib/libnss_sectora.so.2` 59 | 60 | ### Put config file for this program 61 | 62 | The minimal setting is like as follows. 63 | 64 | ```toml 65 | token = "YOUR_PERSONAL_TOKEN_STRING" 66 | org = "YOUR_ORGANIZATION" 67 | 68 | [[team]] 69 | name = "YOUR_TEAM1" 70 | gid = 2019 # gid for YOUR_TEAM1 71 | 72 | [[team]] 73 | name = "YOUR_TEAM2" 74 | gid = 2020 # gid for YOUR_TEAM2 75 | group = "YOUR_GROUP_NAME" 76 | 77 | [[repo]] 78 | name = "YOUR_REPO_NAME" 79 | ``` 80 | 81 | See `struct Config` on `structs.rs` for details. 82 | 83 | ### Register sectora daemon to systemd 84 | 85 | Put `/etc/systemd/system/sectora.service` 86 | 87 | ``` 88 | [Unit] 89 | Description=Sectora Daemon 90 | After=network.target 91 | 92 | [Service] 93 | ExecStart=/usr/sbin/sectorad 94 | Restart=always 95 | StandardOutput=journal 96 | StandardError=journal 97 | 98 | [Install] 99 | WantedBy=multi-user.target 100 | ``` 101 | 102 | then execute `systemctl enable sectora && systemctl start sectora` 103 | 104 | ### Configure name service switch 105 | 106 | Add the following lines to `/etc/nsswitch.conf` 107 | 108 | ``` 109 | passwd: files sectora 110 | shadow: files sectora 111 | group: files sectora 112 | ``` 113 | 114 | ### Configure sshd 115 | 116 | Add the following lines to `/etc/ssh/sshd_config`. 117 | 118 | ``` 119 | AuthorizedKeysCommandUser root 120 | AuthorizedKeysCommand /usr/sbin/sectora key %u 121 | UsePAM yes 122 | ``` 123 | 124 | #### In the case of old sshd 125 | 126 | In the case of old sshd, you need to create the following shell script and put it in your PATH. 127 | 128 | ```sectora.sh 129 | #!/bin/bash 130 | /usr/sbin/sectora key $1 131 | ``` 132 | 133 | Also, sshd_config should look like this. 134 | 135 | ``` 136 | AuthorizedKeysCommandUser root 137 | AuthorizedKeysCommand /usr/sbin/sectora.sh 138 | UsePAM yes 139 | ``` 140 | 141 | ### Configure PAM (Optional) 142 | 143 | Add the following lines to `/etc/pam.d/sshd`. 144 | 145 | ``` 146 | account requisite pam_exec.so quiet /usr/sbin/sectora pam 147 | auth optional pam_unix.so not_set_pass use_first_pass nodelay 148 | session required pam_mkhomedir.so skel=/etc/skel/ umask=0022 149 | ``` 150 | 151 | Also, comment out the following line. 152 | 153 | ``` 154 | # @include common-auth 155 | ``` 156 | 157 | ## Personal settings 158 | 159 | To set personal settings, use `$HOME/.config/sectora.toml` like this. 160 | 161 | ```toml 162 | sh = "/path/to/login/shell" 163 | pass = "PASSWORD_HASH_STRING" 164 | ``` 165 | 166 | Use `mkpasswd` command to create your `PASSWORD_HASH_STRING` 167 | 168 | ``` 169 | mkpasswd -S $(head -c 4 /dev/urandom|xxd -p) -m sha-512 170 | ``` 171 | 172 | ## LICENSE 173 | 174 | MIT 175 | 176 | ## Special thanks 177 | 178 | This program is inspired by [Octopass](https://github.com/linyows/octopass). 179 | Thank you. 180 | -------------------------------------------------------------------------------- /ansible/roles/sectora/handlers/main.yml: -------------------------------------------------------------------------------- 1 | - name: restart sshd 2 | service: 3 | name: ssh 4 | state: restarted 5 | 6 | - name: start sectorad 7 | service: 8 | name: sectora 9 | state: started 10 | -------------------------------------------------------------------------------- /ansible/roles/sectora/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: copy sectora binary executable 3 | copy: 4 | src: "{{ target_dir }}/sectora" 5 | dest: /usr/sbin/sectora 6 | mode: 0755 7 | 8 | - name: copy sectora key script executable 9 | copy: 10 | src: ./sectora.sh 11 | dest: /usr/sbin/sectora.sh 12 | mode: 0700 13 | 14 | - name: copy sectorad binary executable 15 | copy: 16 | src: "{{ target_dir }}/sectorad" 17 | dest: /usr/sbin/sectorad 18 | mode: 0755 19 | 20 | - name: create sectorad config 21 | template: 22 | src: sectora.service 23 | dest: /etc/systemd/system/sectora.service 24 | notify: 25 | - start sectorad 26 | 27 | - name: Enable service sectora 28 | systemd: 29 | name: sectora 30 | enabled: yes 31 | 32 | - name: copy libnss_sectora shared object 33 | copy: 34 | src: "{{ target_dir }}/libnss_sectora.so" 35 | dest: /usr/lib/libnss_sectora.so 36 | 37 | - name: create link for shared object 38 | file: 39 | src: /usr/lib/libnss_sectora.so 40 | dest: /usr/lib/libnss_sectora.so.2 41 | state: link 42 | 43 | - name: create config 44 | template: 45 | src: sectora.conf 46 | dest: /etc/sectora.conf 47 | mode: 0600 48 | 49 | - name: configure sudoers 50 | lineinfile: 51 | path: /etc/sudoers 52 | state: present 53 | regexp: '^%{{ (item.group is defined and item.group) or item.name }} ALL=' 54 | line: '%{{ (item.group is defined and item.group) or item.name }} ALL=(ALL) NOPASSWD: ALL' 55 | validate: 'visudo -cf %s' 56 | when: (item.sudoers is defined) and (item.sudoers == true) 57 | with_items: 58 | '{{ gh_teams }}' 59 | 60 | - name: configure sshd_config 61 | lineinfile: 62 | path: /etc/ssh/sshd_config 63 | regexp: '{{ item.regexp }}' 64 | line: '{{ item.line }}' 65 | validate: '/usr/sbin/sshd -t -f %s' 66 | with_items: 67 | - regexp: '^AuthorizedKeysCommandUser\s' 68 | line: 'AuthorizedKeysCommandUser root' 69 | - regexp: '^AuthorizedKeysCommand\s' 70 | line: 'AuthorizedKeysCommand /usr/sbin/sectora.sh' 71 | # line: 'AuthorizedKeysCommand /usr/sbin/sectora key %u' 72 | - regexp: '^UsePAM\s' 73 | line: 'UsePAM yes' 74 | notify: 75 | - restart sshd 76 | 77 | - name: configure nss switch 78 | lineinfile: 79 | path: /etc/nsswitch.conf 80 | regexp: '{{ item.regexp }}' 81 | line: '{{ item.line }}' 82 | backrefs: yes 83 | with_items: 84 | - {regexp: '^passwd:\s+(.*)$', line: 'passwd: files sectora'} 85 | - {regexp: '^shadow:\s+(.*)$', line: 'shadow: files sectora'} 86 | - {regexp: '^group:\s+(.*)$', line: 'group: files sectora'} 87 | 88 | - name: pam configuration 89 | lineinfile: 90 | path: /etc/pam.d/sshd 91 | line: '{{ item.line }}' 92 | state: '{{ item.state }}' 93 | with_items: 94 | - {line: 'account sufficient pam_exec.so quiet /usr/sbin/sectora pam', state: 'present'} 95 | - {line: 'auth optional pam_unix.so not_set_pass use_first_pass nodelay', state: 'present'} 96 | - {line: 'session required pam_mkhomedir.so skel: /etc/skel/ umask: 0022', state: 'present'} 97 | -------------------------------------------------------------------------------- /ansible/sectora.service: -------------------------------------------------------------------------------- 1 | ../assets/sectora.service -------------------------------------------------------------------------------- /ansible/sectora.sh: -------------------------------------------------------------------------------- 1 | ../assets/sectora.sh -------------------------------------------------------------------------------- /ansible/site.yml: -------------------------------------------------------------------------------- 1 | - hosts: all 2 | become: yes 3 | become_method: sudo 4 | become_user: root 5 | vars: 6 | target_dir: "../target/YOUR_TARGET_TRIPLE/release" 7 | gh_token: "YOUR_TOKEN" 8 | gh_org: "YOUR_ORGANIZATION" 9 | # gh_home: "/path/to/home/{}" 10 | # gh_cache_duration: 7200 11 | # gh_user_conf_path: "path/to/relative/path/of/user/conf/from/home" 12 | gh_teams: 13 | - name: "YOUR_TEAM1" 14 | group: "YOUR_GROUP1" 15 | gid: YOUR_GID1 16 | sudoers: true # or false 17 | gh_repo: 18 | - name: "YOUR_REPO1" 19 | group: "YOUR_GROUP1" 20 | gid: YOUR_GID2 21 | sudoers: false # or true 22 | roles: 23 | - sectora 24 | -------------------------------------------------------------------------------- /ansible/templates/sectora.conf: -------------------------------------------------------------------------------- 1 | token = "{{ gh_token }}" 2 | org = "{{ gh_org }}" 3 | {% if gh_endpoint is defined %} 4 | endpoint = "{{ gh_endpoint }}" 5 | {% endif %} 6 | {% if gh_home is defined %} 7 | home = "{{ gh_home }}" 8 | {% endif %} 9 | {% if gh_sh is defined %} 10 | sh = "{{ gh_sh }}" 11 | {% endif %} 12 | {% if gh_cache_duration is defined %} 13 | cache_duration = {{ gh_cache_duration }} 14 | {% endif %} 15 | {% if gh_cert_path is defined %} 16 | cert_path = "{{ gh_cert_path }}" 17 | {% endif %} 18 | {% if gh_user_conf_path is defined %} 19 | user_conf_path = "{{ gh_user_conf_path }}" 20 | {% endif %} 21 | 22 | {% if gh_teams is defined %} 23 | {% for team in gh_teams %} 24 | 25 | [[team]] 26 | name = "{{ team.name }}" 27 | {% if team.gid is defined %} 28 | gid = {{ team.gid }} 29 | {% endif %} 30 | {% if team.group is defined %} 31 | group = "{{ team.group }}" 32 | {% endif %} 33 | {% endfor %} 34 | {% endif %} 35 | 36 | {% if gh_repo is defined %} 37 | {% for repo in gh_repos %} 38 | 39 | [[repo]] 40 | name = "{{ repo.name }}" 41 | {% if repo.gid is defined %} 42 | gid = {{ repo.gid }} 43 | {% endif %} 44 | {% if repo.group is defined %} 45 | group = "{{ repo.group }}" 46 | {% endif %} 47 | {% endfor %} 48 | {% endif %} 49 | -------------------------------------------------------------------------------- /assets/conf-files/sectora.conf: -------------------------------------------------------------------------------- 1 | # this config file is toml format 2 | 3 | token = "YOUR_PERSONAL_TOKEN_STRING" 4 | org = "YOUR_ORGANIZATION" 5 | 6 | [[team]] 7 | name = "YOUR_TEAM1" 8 | gid = YOUR_GID1 9 | 10 | [[team]] 11 | name = "YOUR_TEAM2" 12 | gid = YOUR_GID1 13 | group = "YOUR_GROUP_NAME" 14 | 15 | [[repo]] 16 | name = "YOUR_REPO_NAME" -------------------------------------------------------------------------------- /assets/scripts/config: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | # Source debconf library. 4 | . /usr/share/debconf/confmodule 5 | 6 | db_beginblock 7 | db_input high sectora/init_config || true 8 | db_endblock 9 | db_go 10 | -------------------------------------------------------------------------------- /assets/scripts/postinst: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | settings_start="### DO NOT REMOVE! SECTORA SETTINGS START ###" 4 | settings_end="### DO NOT REMOVE! SECTORA SETTINGS END ###" 5 | 6 | function check_config() { 7 | conf_file=$(echo $1) 8 | 9 | set +e 10 | start_line=$(grep -n "^${settings_start}" ${conf_file}) 11 | end_line=$(grep -n "^${settings_end}" ${conf_file}) 12 | set -e 13 | 14 | if [ "$start_line" = "" -a "$end_line" = "" ]; then 15 | # Success (No settings found) 16 | return 0 17 | fi 18 | echo "Found sectora settings in $conf_file" 19 | return 1 20 | } 21 | 22 | if [ "$1" = "configure" ] && [ -e /usr/share/debconf/confmodule ]; then 23 | # Source debconf library. 24 | . /usr/share/debconf/confmodule 25 | 26 | systemctl daemon-reload 27 | 28 | db_get sectora/init_config 29 | init_config=$RET 30 | db_stop 31 | if [ "$init_config" = "true" ]; then 32 | echo "Appending settings...(Please restore configs from *.backup files when you get some error)" 33 | 34 | # setup nssswitch 35 | if check_config /etc/nsswitch.conf; then 36 | cp /etc/nsswitch.conf /etc/nsswitch.conf.backup 37 | cat <>/etc/nsswitch.conf 38 | 39 | ${settings_start} 40 | passwd: files sectora 41 | shadow: files sectora 42 | group: files sectora 43 | ${settings_end} 44 | 45 | EOS 46 | fi 47 | 48 | # setup sshd_config 49 | if check_config /etc/ssh/sshd_config; then 50 | cp /etc/ssh/sshd_config /etc/ssh/sshd_config.backup 51 | sed -i -e "s/^UsePAM/#UsePAM/" /etc/ssh/sshd_config 52 | cat <>/etc/ssh/sshd_config 53 | 54 | ${settings_start} 55 | AuthorizedKeysCommandUser root 56 | AuthorizedKeysCommand /usr/sbin/sectora key %u 57 | UsePAM yes 58 | ${settings_end} 59 | 60 | EOS 61 | fi 62 | 63 | # setup pam 64 | if check_config /etc/pam.d/sshd; then 65 | if [ -e /etc/pam.d/sshd ]; then 66 | cp /etc/pam.d/sshd /etc/pam.d/sshd.backup 67 | fi 68 | cat <>/etc/pam.d/sshd 69 | 70 | ${settings_start} 71 | auth requisite pam_exec.so quiet /usr/sbin/sectora pam 72 | auth optional pam_unix.so not_set_pass use_first_pass nodelay 73 | session required pam_mkhomedir.so skel=/etc/skel/ umask=0022 74 | ${settings_end} 75 | 76 | EOS 77 | fi 78 | 79 | # restart sshd 80 | service ssh restart 81 | 82 | fi 83 | 84 | echo "Please setup github personal token to /etc/sectora.conf" 85 | fi 86 | -------------------------------------------------------------------------------- /assets/scripts/postrm: -------------------------------------------------------------------------------- 1 | #!/bin/bash -e 2 | 3 | settings_start="### DO NOT REMOVE! SECTORA SETTINGS START ###" 4 | settings_end="### DO NOT REMOVE! SECTORA SETTINGS END ###" 5 | 6 | function delete_sectora_setting() { 7 | conf_file=$(echo $1) 8 | 9 | start_line=$(grep -n "^${settings_start}" ${conf_file} | cut -d: -f1) 10 | end_line=$(grep -n "^${settings_end}" ${conf_file} | cut -d: -f1) 11 | if [ "$start_line" = "" -o "$end_line" = "" ]; then 12 | echo "Could not find sectora setting in ${conf_file}" 13 | return 0 14 | fi 15 | 16 | echo "Removed sectora settings in ${conf_file}" 17 | sed -i -e "${start_line},${end_line}d" $conf_file 18 | return 0 19 | } 20 | 21 | if [ "$1" = purge ] && [ -e /usr/share/debconf/confmodule ]; then 22 | # Source debconf library. 23 | . /usr/share/debconf/confmodule 24 | # Remove my changes to the db. 25 | db_purge 26 | 27 | # remove configures if available 28 | delete_sectora_setting /etc/nsswitch.conf 29 | delete_sectora_setting /etc/ssh/sshd_config 30 | delete_sectora_setting /etc/pam.d/sshd 31 | 32 | # remove configures 33 | rm -rf /etc/sectora.conf 34 | 35 | systemctl daemon-reload 36 | fi 37 | -------------------------------------------------------------------------------- /assets/scripts/templates: -------------------------------------------------------------------------------- 1 | Template: sectora/init_config 2 | Type: boolean 3 | Default: true 4 | Description: Do you want to append the sectora settings? 5 | -------------------------------------------------------------------------------- /assets/sectora.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Sectora Daemon 3 | After=network-online.target 4 | Requires=network-online.target 5 | 6 | [Service] 7 | Type=notify 8 | ExecStart=/usr/sbin/sectorad 9 | # Environment=LOG_LEVEL=DEBUG 10 | # Environment=RUST_BACKTRACE=1 11 | Restart=always 12 | StandardOutput=journal 13 | StandardError=journal 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /assets/sectora.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | sectora key $1 3 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::error::Error; 3 | use std::fs::File; 4 | use std::io::Write; 5 | use std::path::PathBuf; 6 | use std::process::Command; 7 | 8 | struct Ignore; 9 | 10 | impl From for Ignore where E: Error 11 | { 12 | fn from(_: E) -> Ignore { Ignore } 13 | } 14 | 15 | fn main() -> Result<(), std::io::Error> { 16 | let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap()); 17 | File::create(out_dir.join("commit-info.txt"))?.write_all(commit_info().as_bytes())?; 18 | let log_level = env::var("LOG_LEVEL").unwrap_or_else(|_| "OFF".to_string()); 19 | File::create(out_dir.join("log-level.txt"))?.write_all(log_level.as_bytes())?; 20 | println!("cargo:rerun-if-changed=build.rs"); 21 | Ok(()) 22 | } 23 | 24 | fn commit_info() -> String { 25 | match (commit_date(), commit_hash()) { 26 | (Ok(date), Ok(hash)) => format!(" {} {}", date.trim_end(), hash.trim_end(),), 27 | _ => String::default(), 28 | } 29 | } 30 | 31 | fn commit_hash() -> Result { 32 | let args = &["rev-parse", "--short=10", "HEAD"]; 33 | Ok(String::from_utf8(Command::new("git").args(args).output()?.stdout)?) 34 | } 35 | 36 | fn commit_date() -> Result { 37 | let args = &["log", "-1", "--date=short", "--pretty=format:%cd"]; 38 | Ok(String::from_utf8(Command::new("git").args(args).output()?.stdout)?) 39 | } 40 | -------------------------------------------------------------------------------- /how-it-works.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | Public Key 59 | 60 | 61 | Secret Key 62 | 63 | 64 | 65 | 66 | 67 | 68 | AuthorizedKeysCommand 69 | 70 | 71 | sectora key 72 | 73 | 74 | 75 | 76 | 77 | 78 | Name Service Switch 79 | 80 | 81 | libnss_sectora.so 82 | 83 | 84 | 85 | 86 | 87 | 88 | Get data via API 89 | 90 | 91 | Upload to GitHub 92 | 93 | 94 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 120 2 | indent_style = "Visual" 3 | reorder_imports = true 4 | match_block_trailing_comma = false 5 | trailing_comma = "Vertical" 6 | fn_single_line = true 7 | struct_lit_single_line = true 8 | fn_args_layout = "Compressed" 9 | -------------------------------------------------------------------------------- /src/applog.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::str::FromStr; 3 | 4 | pub fn init(appname: Option<&str>) { 5 | let log_level_text = include_str!(concat!(env!("OUT_DIR"), "/log-level.txt")); 6 | let log_level_env = env::var("LOG_LEVEL").unwrap_or(log_level_text.to_string()); 7 | let log_level = log::LevelFilter::from_str(&log_level_env).unwrap_or(log::LevelFilter::Off); 8 | syslog::init(syslog::Facility::LOG_AUTH, log_level, appname).unwrap_or_default(); 9 | } 10 | -------------------------------------------------------------------------------- /src/buffer.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::CString; 2 | use std::io::{Error, ErrorKind}; 3 | 4 | #[derive(Debug)] 5 | pub struct Buffer { 6 | buf: *mut libc::c_char, 7 | offset: isize, 8 | buflen: libc::size_t, 9 | } 10 | 11 | impl Buffer { 12 | pub fn new(buf: *mut libc::c_char, buflen: libc::size_t) -> Self { Self { buf, offset: 0, buflen } } 13 | 14 | fn write(&mut self, data: *const libc::c_char, len: usize) -> Result<*mut libc::c_char, Error> { 15 | if self.buflen < len + self.offset as libc::size_t { 16 | return Err(Error::new(ErrorKind::AddrNotAvailable, "ERANGE")); 17 | } 18 | unsafe { 19 | let pos = self.buf.offset(self.offset); 20 | #[allow(clippy::unnecessary_cast)] 21 | std::ptr::copy(data as *mut i8, pos as *mut i8, len); 22 | self.offset += len as isize; 23 | self.buflen -= len as libc::size_t; 24 | Ok(pos) 25 | } 26 | } 27 | 28 | #[allow(clippy::cast_ptr_alignment)] // NOTE: waiting for align_offset https://github.com/rust-lang/rust/issues/44488 29 | fn add_pointers(&mut self, ptrs: &[*mut libc::c_char]) -> Result<*mut *mut libc::c_char, Error> { 30 | use std::mem::size_of; 31 | let step = std::cmp::max(size_of::<*mut libc::c_char>() / size_of::(), 1); 32 | let align_offset = self.offset % step as isize; 33 | self.offset += align_offset; // NOTE: remove after stabilization of align_offset 34 | if self.buflen < (((ptrs.len() + 1) * step) as isize + self.offset) as libc::size_t { 35 | return Err(Error::new(ErrorKind::AddrNotAvailable, "ERANGE")); 36 | } 37 | unsafe { 38 | let mem = self.buf.offset(self.offset) as *mut *mut libc::c_char; 39 | for (i, p) in ptrs.iter().enumerate() { 40 | *(mem.add(i)) = *p; 41 | self.offset += step as isize; 42 | self.buflen -= step as libc::size_t; 43 | } 44 | *(mem.add(ptrs.len())) = std::ptr::null_mut::(); 45 | self.offset += step as isize; 46 | self.buflen -= step as libc::size_t; 47 | Ok(mem) 48 | } 49 | } 50 | 51 | pub fn write_string(&mut self, s: &str) -> Result<*mut libc::c_char, Error> { 52 | let cs = CString::new(s).unwrap(); 53 | self.write(cs.as_ptr(), s.len() + 1) 54 | } 55 | 56 | pub fn write_vecstr(&mut self, ss: &[&str]) -> Result<*mut *mut libc::c_char, Error> { 57 | let mut ptrs = Vec::<*mut libc::c_char>::new(); 58 | for s in ss { 59 | ptrs.push(self.write_string(s)?); 60 | } 61 | self.add_pointers(&ptrs) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/connection.rs: -------------------------------------------------------------------------------- 1 | use crate::applog; 2 | use crate::error; 3 | use crate::message::*; 4 | use crate::structs::SocketConfig as Config; 5 | use std::os::unix::net::UnixDatagram; 6 | use std::time::Duration; 7 | 8 | #[derive(Debug)] 9 | pub struct Connection { 10 | conf: Config, 11 | conn: UnixDatagram, 12 | } 13 | 14 | impl Connection { 15 | pub fn new(logid: &str) -> Result { 16 | applog::init(Some("sectora")); 17 | log::debug!("{}", logid); 18 | let conf = Config::new(); 19 | let conn = Self::connect_daemon(&conf)?; 20 | Ok(Self { conf, conn }) 21 | } 22 | 23 | fn socket_path(conf: &Config) -> String { format!("{}/{}", &conf.socket_dir, std::process::id()) } 24 | 25 | fn connect_daemon(conf: &Config) -> Result { 26 | let socket = UnixDatagram::bind(Self::socket_path(conf))?; 27 | log::debug!("{:?}", socket); 28 | socket.set_read_timeout(Some(Duration::from_secs(5)))?; 29 | socket.connect(&conf.socket_path)?; 30 | Ok(socket) 31 | } 32 | 33 | pub fn communicate(&self, msg: ClientMessage) -> Result { 34 | self.conn.send(msg.to_string().as_bytes())?; 35 | let mut msgstr = String::default(); 36 | let mut buf = [0u8; 4096]; 37 | while let Ok(cnt) = self.conn.recv(&mut buf) { 38 | log::debug!("msg cnt, {}", cnt); 39 | let msg = String::from_utf8(buf[..cnt].to_vec()).unwrap() 40 | .parse::()?; 41 | msgstr.push_str(&msg.message); 42 | if msg.cont { 43 | let _ = self.conn.send(ClientMessage::Cont.to_string().as_bytes())?; 44 | } else { 45 | break; 46 | } 47 | } 48 | log::debug!("recieved: {}", msgstr); 49 | Ok(msgstr.parse::()?) 50 | } 51 | } 52 | 53 | impl Drop for Connection { 54 | fn drop(&mut self) { let _ = std::fs::remove_file(Self::socket_path(&self.conf)); } 55 | } 56 | -------------------------------------------------------------------------------- /src/cstructs.rs: -------------------------------------------------------------------------------- 1 | use crate::buffer::Buffer; 2 | use std::io::Error; 3 | 4 | #[repr(C)] 5 | pub struct Passwd { 6 | name: *mut libc::c_char, 7 | passwd: *mut libc::c_char, 8 | uid: libc::uid_t, 9 | gid: libc::gid_t, 10 | gecos: *mut libc::c_char, 11 | dir: *mut libc::c_char, 12 | shell: *mut libc::c_char, 13 | } 14 | 15 | impl Passwd { 16 | #[allow(clippy::too_many_arguments)] 17 | fn pack(&mut self, buf: &mut Buffer, name: &str, passwd: &str, uid: libc::uid_t, gid: libc::gid_t, gecos: &str, 18 | dir: &str, shell: &str) 19 | -> Result<(), Error> { 20 | self.name = buf.write_string(name)?; 21 | self.passwd = buf.write_string(passwd)?; 22 | self.dir = buf.write_string(dir)?; 23 | self.shell = buf.write_string(shell)?; 24 | self.gecos = buf.write_string(gecos)?; 25 | self.uid = uid; 26 | self.gid = gid; 27 | Ok(()) 28 | } 29 | 30 | pub fn pack_args(&mut self, buf: &mut Buffer, name: &str, id: u64, gid: u64, home: &str, sh: &str) 31 | -> Result<(), Error> { 32 | self.pack(buf, name, "x", id as libc::uid_t, gid as libc::gid_t, "", home, sh) 33 | } 34 | } 35 | 36 | #[repr(C)] 37 | pub struct Spwd { 38 | namp: *mut libc::c_char, 39 | pwdp: *mut libc::c_char, 40 | lstchg: libc::c_long, 41 | min: libc::c_long, 42 | max: libc::c_long, 43 | warn: libc::c_long, 44 | inact: libc::c_long, 45 | expire: libc::c_long, 46 | flag: libc::c_ulong, 47 | } 48 | 49 | impl Spwd { 50 | #[allow(clippy::too_many_arguments)] 51 | fn pack(&mut self, buf: &mut Buffer, namp: &str, pwdp: &str, lstchg: libc::c_long, min: libc::c_long, 52 | max: libc::c_long, warn: libc::c_long, inact: libc::c_long, expire: libc::c_long, flag: libc::c_ulong) 53 | -> Result<(), Error> { 54 | self.namp = buf.write_string(namp)?; 55 | self.pwdp = buf.write_string(pwdp)?; 56 | self.lstchg = lstchg; 57 | self.min = min; 58 | self.max = max; 59 | self.warn = warn; 60 | self.inact = inact; 61 | self.expire = expire; 62 | self.flag = flag; 63 | Ok(()) 64 | } 65 | 66 | pub fn pack_args(&mut self, buf: &mut Buffer, name: &str, pass: &str) -> Result<(), Error> { 67 | self.pack(buf, name, pass, -1, -1, -1, -1, -1, -1, 0) 68 | } 69 | } 70 | 71 | #[repr(C)] 72 | pub struct Group { 73 | name: *mut libc::c_char, 74 | passwd: *mut libc::c_char, 75 | gid: libc::gid_t, 76 | mem: *mut *mut libc::c_char, 77 | } 78 | 79 | impl Group { 80 | fn pack(&mut self, buf: &mut Buffer, name: &str, passwd: &str, gid: libc::gid_t, mem: &[&str]) 81 | -> Result<(), Error> { 82 | self.name = buf.write_string(name)?; 83 | self.passwd = buf.write_string(passwd)?; 84 | self.gid = gid; 85 | self.mem = buf.write_vecstr(mem)?; 86 | Ok(()) 87 | } 88 | 89 | pub fn pack_args(&mut self, buf: &mut Buffer, name: &str, id: u64, members: &[&str]) -> Result<(), Error> { 90 | self.pack(buf, name, "x", id as libc::gid_t, members) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/daemon.rs: -------------------------------------------------------------------------------- 1 | mod applog; 2 | mod error; 3 | mod ghclient; 4 | mod message; 5 | mod statics; 6 | mod structs; 7 | 8 | use error::Error; 9 | use ghclient::GithubClient; 10 | use message::*; 11 | use statics::CONF_PATH; 12 | use std::collections::hash_map::Entry; 13 | use std::collections::{HashMap, VecDeque}; 14 | use std::fs; 15 | use std::os::unix; 16 | use std::path::Path; 17 | use structs::{Config, SocketConfig, UserConfig}; 18 | 19 | #[tokio::main] 20 | async fn main() { 21 | applog::init(Some("sectorad")); 22 | let mut d = Daemon::new(); 23 | d.run().await.expect("run"); 24 | log::debug!("Run stopped"); 25 | } 26 | 27 | struct Daemon { 28 | client: GithubClient, 29 | socket_conf: SocketConfig, 30 | socket: unix::net::UnixDatagram, 31 | msg_cache: HashMap>, 32 | } 33 | 34 | impl Drop for Daemon { 35 | fn drop(&mut self) { 36 | log::debug!("Drop daemon"); 37 | let _ = fs::remove_file(&self.socket_conf.socket_path); 38 | } 39 | } 40 | 41 | impl Daemon { 42 | fn new() -> Self { 43 | let config = Config::from_path(&CONF_PATH).expect("valid config"); 44 | let socket_conf = SocketConfig::new(); 45 | fs::create_dir_all(&socket_conf.socket_dir).expect("create socket dir"); 46 | fs::set_permissions(&socket_conf.socket_dir, unix::fs::PermissionsExt::from_mode(0o777)).unwrap_or_default(); 47 | if fs::metadata(&socket_conf.socket_path).is_ok() { 48 | std::fs::remove_file(&socket_conf.socket_path).expect("remove socket before bind"); 49 | } 50 | let socket = unix::net::UnixDatagram::bind(&socket_conf.socket_path).expect("bind socket"); 51 | fs::set_permissions(&socket_conf.socket_path, unix::fs::PermissionsExt::from_mode(0o666)).unwrap_or_default(); 52 | let client = GithubClient::new(&config); 53 | log::debug!("Initialised"); 54 | Daemon { client, 55 | socket_conf, 56 | socket, 57 | msg_cache: HashMap::new() } 58 | } 59 | 60 | async fn run(&mut self) -> Result<(), Error> { 61 | let rl = self.client.get_rate_limit().await.expect("get rate limit"); 62 | log::info!("Rate Limit: {:?}", rl); 63 | let sectors = self.client.get_sectors().await.expect("get sectors"); 64 | log::info!("{} sector[s] loaded", sectors.len()); 65 | let _ = sd_notify::notify(true, &[sd_notify::NotifyState::Ready]); 66 | log::info!("Start running @ {}", &self.socket_conf.socket_path); 67 | loop { 68 | let mut buf = [0u8; 4096]; 69 | let (recv_cnt, src) = self.socket.recv_from(&mut buf)?; 70 | let msgstr = String::from_utf8(buf[..recv_cnt].to_vec()).expect("decode msg str"); 71 | log::debug!("recv: {}, src:{:?}", msgstr, src); 72 | let response = self.handle(&msgstr.parse::().expect("parse ClientMessage")) 73 | .await; 74 | log::debug!("-> response: {}", response); 75 | 76 | let msg = response.to_string(); 77 | let msgs = message::DividedMessage::new(&msg, 1024); 78 | let src_path = src.as_pathname().expect("src"); 79 | for (i, dividedmsg) in msgs.iter().enumerate() { 80 | match self.socket.send_to(dividedmsg.to_string().as_bytes(), src_path) { 81 | Ok(sendsize) => log::debug!("send: {}", sendsize), 82 | Err(err) => log::warn!("failed to send back to the client {:?}:{}", src, err), 83 | } 84 | if i != msgs.len() - 1 { 85 | let mut buf = [0u8; 4096]; 86 | let (recv_cnt, _) = self.socket.recv_from(&mut buf)?; 87 | let msgstr = String::from_utf8(buf[..recv_cnt].to_vec()).expect("decode msg str"); 88 | match self.handle(&msgstr.parse::().expect("parse ClientMessage")) 89 | .await 90 | { 91 | DaemonMessage::Success => continue, 92 | _ => break, 93 | } 94 | } 95 | } 96 | } 97 | } 98 | 99 | async fn handle(&mut self, msg: &ClientMessage) -> DaemonMessage { 100 | match msg { 101 | ClientMessage::Key { user } => match self.client.get_user_public_keys(user).await { 102 | Ok(keys) => DaemonMessage::Key { keys: keys.join("\n") }, 103 | Err(_) => DaemonMessage::Error { message: String::from("get key failed") }, 104 | }, 105 | ClientMessage::Pam { user } => match self.client.check_pam(user).await { 106 | Ok(result) => DaemonMessage::Pam { result }, 107 | Err(_) => DaemonMessage::Error { message: String::from("check pam failed") }, 108 | }, 109 | ClientMessage::CleanUp => match self.client.clear_all_caches().await { 110 | Ok(_) => DaemonMessage::Success, 111 | Err(_) => DaemonMessage::Error { message: String::from("clean up failed") }, 112 | }, 113 | ClientMessage::RateLimit => match self.client.get_rate_limit().await { 114 | Ok(rl) => DaemonMessage::RateLimit { limit: rl.rate.limit, 115 | remaining: rl.rate.remaining, 116 | reset: rl.rate.reset }, 117 | Err(_) => DaemonMessage::Error { message: String::from("clean up failed") }, 118 | }, 119 | ClientMessage::SectorGroups => match self.client.get_sectors().await { 120 | Ok(sectors) => DaemonMessage::SectorGroups { sectors }, 121 | Err(_) => DaemonMessage::Error { message: String::from("get sectors failed") }, 122 | }, 123 | ClientMessage::Pw(pw) => self.handle_pw(pw).await, 124 | ClientMessage::Sp(sp) => self.handle_sp(sp).await, 125 | ClientMessage::Gr(gr) => self.handle_gr(gr).await, 126 | ClientMessage::Cont => DaemonMessage::Success, 127 | } 128 | } 129 | 130 | fn get_msg(&mut self, pid: u32) -> DaemonMessage { 131 | match self.msg_cache.entry(pid) { 132 | Entry::Occupied(mut o) => match o.get_mut().pop_front() { 133 | Some(msg) => msg, 134 | None => DaemonMessage::Error { message: String::from("not found") }, 135 | }, 136 | Entry::Vacant(_) => DaemonMessage::Error { message: String::from("not found") }, 137 | } 138 | } 139 | 140 | fn clear_cache(&mut self, pid: u32) -> DaemonMessage { 141 | self.msg_cache.remove(&pid).unwrap_or_default(); 142 | DaemonMessage::Success 143 | } 144 | 145 | fn get_home_sh(&self, login: &str) -> (String, String) { 146 | let conf = &self.client.conf; 147 | let home = conf.home.replace("{}", login); 148 | let sh: String = match UserConfig::from_path(&Path::new(&home).join(&conf.user_conf_path)) { 149 | Ok(personal) => match personal.sh { 150 | Some(sh) => { 151 | if Path::new(&sh).exists() { 152 | sh 153 | } else { 154 | conf.sh.clone() 155 | } 156 | } 157 | None => conf.sh.clone(), 158 | }, 159 | Err(_) => conf.sh.clone(), 160 | }; 161 | (home, sh) 162 | } 163 | 164 | fn get_pass(&self, login: &str) -> String { 165 | let home = self.client.conf.home.replace("{}", login); 166 | let pass: String = match UserConfig::from_path(&Path::new(&home).join(&self.client.conf.user_conf_path)) { 167 | Ok(personal) => match personal.pass { 168 | Some(pass) => pass, 169 | None => String::from("*"), 170 | }, 171 | Err(_) => String::from("*"), 172 | }; 173 | pass 174 | } 175 | 176 | async fn handle_pw(&mut self, pw: &Pw) -> DaemonMessage { 177 | match pw { 178 | Pw::Uid(uid) => { 179 | for sector in self.client.get_sectors().await.unwrap_or_default() { 180 | for member in sector.members.values() { 181 | if uid == &member.id { 182 | let (home, sh) = self.get_home_sh(&member.login); 183 | return DaemonMessage::Pw { login: member.login.clone(), 184 | uid: *uid, 185 | gid: sector.get_gid(), 186 | home, 187 | sh }; 188 | } 189 | } 190 | } 191 | } 192 | Pw::Nam(name) => { 193 | for sector in self.client.get_sectors().await.unwrap_or_default() { 194 | for member in sector.members.values() { 195 | if name == &member.login { 196 | let (home, sh) = self.get_home_sh(&member.login); 197 | return DaemonMessage::Pw { login: member.login.clone(), 198 | uid: member.id, 199 | gid: sector.get_gid(), 200 | home, 201 | sh }; 202 | } 203 | } 204 | } 205 | } 206 | Pw::Ent(Ent::Set(pid)) => { 207 | let mut ents = VecDeque::new(); 208 | for sector in self.client.get_sectors().await.unwrap_or_default() { 209 | for member in sector.members.values() { 210 | let (home, sh) = self.get_home_sh(&member.login); 211 | let pw = DaemonMessage::Pw { login: member.login.clone(), 212 | uid: member.id, 213 | gid: sector.get_gid(), 214 | home, 215 | sh }; 216 | ents.push_back(pw); 217 | } 218 | } 219 | self.msg_cache.insert(*pid, ents).unwrap_or_default(); 220 | return DaemonMessage::Success; 221 | } 222 | Pw::Ent(Ent::Get(pid)) => return self.get_msg(*pid), 223 | Pw::Ent(Ent::End(pid)) => return self.clear_cache(*pid), 224 | } 225 | DaemonMessage::Error { message: String::from("not found") } 226 | } 227 | 228 | async fn handle_sp(&mut self, sp: &Sp) -> DaemonMessage { 229 | match sp { 230 | Sp::Nam(name) => { 231 | for sector in self.client.get_sectors().await.unwrap_or_default() { 232 | if let Some(member) = sector.members.get(name) { 233 | let pass = self.get_pass(name); 234 | return DaemonMessage::Sp { login: member.login.clone(), 235 | pass }; 236 | } 237 | } 238 | } 239 | Sp::Ent(Ent::Set(pid)) => { 240 | let mut ents = VecDeque::new(); 241 | for sector in self.client.get_sectors().await.unwrap_or_default() { 242 | for member in sector.members.values() { 243 | let pass = self.get_pass(&member.login); 244 | let sp = DaemonMessage::Sp { login: member.login.clone(), 245 | pass }; 246 | ents.push_back(sp); 247 | } 248 | } 249 | self.msg_cache.insert(*pid, ents).unwrap_or_default(); 250 | return DaemonMessage::Success; 251 | } 252 | Sp::Ent(Ent::Get(pid)) => return self.get_msg(*pid), 253 | Sp::Ent(Ent::End(pid)) => return self.clear_cache(*pid), 254 | } 255 | DaemonMessage::Error { message: String::from("not found") } 256 | } 257 | 258 | async fn handle_gr(&mut self, gr: &Gr) -> DaemonMessage { 259 | match gr { 260 | Gr::Gid(gid) => { 261 | for sector in self.client.get_sectors().await.unwrap_or_default() { 262 | if gid == §or.get_gid() { 263 | return DaemonMessage::Gr { sector }; 264 | } 265 | } 266 | } 267 | Gr::Nam(name) => { 268 | for sector in self.client.get_sectors().await.unwrap_or_default() { 269 | if name == §or.get_group() { 270 | return DaemonMessage::Gr { sector }; 271 | } 272 | } 273 | } 274 | Gr::Ent(Ent::Set(pid)) => { 275 | let mut ents = VecDeque::new(); 276 | for sector in self.client.get_sectors().await.unwrap_or_default() { 277 | ents.push_back(DaemonMessage::Gr { sector }); 278 | } 279 | self.msg_cache.insert(*pid, ents).unwrap_or_default(); 280 | return DaemonMessage::Success; 281 | } 282 | Gr::Ent(Ent::Get(pid)) => return self.get_msg(*pid), 283 | Gr::Ent(Ent::End(pid)) => return self.clear_cache(*pid), 284 | } 285 | DaemonMessage::Error { message: String::from("not found") } 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | #[derive(Debug)] 2 | pub enum Error { 3 | Serde(serde_json::Error), 4 | Io(std::io::Error), 5 | Toml(toml::de::Error), 6 | ParseMsg(ParseMessageError), 7 | } 8 | 9 | impl From for Error { 10 | fn from(err: serde_json::Error) -> Error { Error::Serde(err) } 11 | } 12 | impl From for Error { 13 | fn from(err: std::io::Error) -> Error { Error::Io(err) } 14 | } 15 | impl From for Error { 16 | fn from(err: toml::de::Error) -> Error { Error::Toml(err) } 17 | } 18 | 19 | #[derive(Debug)] 20 | pub enum ParseSectorTypeError { 21 | UnknownType, 22 | } 23 | 24 | #[derive(Debug)] 25 | pub enum ParseSectorError { 26 | Id(std::num::ParseIntError), 27 | Type(ParseSectorTypeError), 28 | BadFormat, 29 | } 30 | 31 | #[derive(Debug)] 32 | pub enum ParseSectorGroupError { 33 | Sector(ParseSectorError), 34 | Gid(std::num::ParseIntError), 35 | Member(std::num::ParseIntError), 36 | } 37 | 38 | #[derive(Debug)] 39 | pub enum ParseMessageError { 40 | ParseClientMessageError, 41 | ParseDaemonMessageError, 42 | } 43 | 44 | impl From for Error { 45 | fn from(err: ParseMessageError) -> Error { Error::ParseMsg(err) } 46 | } 47 | -------------------------------------------------------------------------------- /src/ghclient.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use crate::structs::{Config, Member, PublicKey, RateLimit, Repo, Sector, SectorGroup, Team}; 3 | use glob::glob; 4 | use reqwest::{Client, Method, Request, Url, header}; 5 | use std::collections::HashMap; 6 | use std::fs::File; 7 | use std::io::prelude::*; 8 | 9 | pub struct GithubClient { 10 | client: Client, 11 | pub conf: Config, 12 | } 13 | 14 | impl GithubClient { 15 | pub fn new(config: &Config) -> GithubClient { 16 | if std::env::var("SSL_CERT_FILE").is_err() { 17 | unsafe { 18 | std::env::set_var("SSL_CERT_FILE", &config.cert_path); 19 | } 20 | } 21 | let client = Client::builder(); 22 | let token = String::from("token ") + &config.token; 23 | let mut hmap = header::HeaderMap::new(); 24 | hmap.insert(header::AUTHORIZATION, header::HeaderValue::from_str(&token).unwrap()); 25 | hmap.insert(header::USER_AGENT, header::HeaderValue::from_str("sectora").unwrap()); 26 | let client = client.default_headers(hmap).build().unwrap(); 27 | GithubClient { client, 28 | conf: config.clone() } 29 | } 30 | 31 | fn get_cache_path(&self, url: &str) -> std::path::PathBuf { 32 | let mut path = std::path::PathBuf::default(); 33 | path.push(&self.conf.cache_dir); 34 | path.push(url); 35 | path 36 | } 37 | 38 | fn load_contents_from_cache(&self, url: &str) -> Result<(std::fs::Metadata, String), Error> { 39 | let path = self.get_cache_path(url); 40 | let metadata = std::fs::metadata(path.to_str().unwrap())?; 41 | let mut f = File::open(path.to_str().unwrap())?; 42 | let mut contents = String::default(); 43 | f.read_to_string(&mut contents)?; 44 | Ok((metadata, contents)) 45 | } 46 | 47 | fn store_contents_to_cache(&self, url: &str, contents: &str) -> Result<(), Error> { 48 | let path = self.get_cache_path(url); 49 | std::fs::create_dir_all(path.parent().unwrap_or(std::path::Path::new("/")))?; 50 | let mut f = File::create(path.to_str().unwrap())?; 51 | f.write_all(contents.as_bytes())?; 52 | Ok(()) 53 | } 54 | 55 | async fn get_contents_from_url(&self, url: &str) -> Result { 56 | let mut all_contents: Vec = Vec::new(); 57 | let mut page = 1; 58 | loop { 59 | let mut new_array = self.get_contents_from_url_page(url, page).await?; 60 | if new_array.is_empty() { 61 | break; 62 | } 63 | all_contents.append(&mut new_array); 64 | page += 1; 65 | } 66 | let contents = serde_json::ser::to_string(&all_contents)?; 67 | self.store_contents_to_cache(url, &contents)?; 68 | Ok(contents) 69 | } 70 | 71 | fn build_request(&self, url: &str) -> Result { 72 | Ok(Request::new(Method::GET, Url::parse(url).unwrap())) 73 | } 74 | 75 | fn build_page_request(&self, url: &str, page: u64) -> Result { 76 | let sep = if url.contains('?') { '&' } else { '?' }; 77 | let url_p = format!("{}{}page={}", url, sep, page); 78 | self.build_request(&url_p) 79 | } 80 | 81 | async fn get_contents_from_url_page(&self, url: &str, page: u64) -> Result, Error> { 82 | let req = self.build_page_request(url, page)?; 83 | let resp = self.client.execute(req).await.unwrap(); 84 | let json = resp.json::>().await.unwrap(); 85 | Ok(json) 86 | } 87 | 88 | async fn get_contents(&self, url: &str) -> Result { 89 | match self.load_contents_from_cache(url) { 90 | Ok((metadata, cache_contents)) => match std::time::SystemTime::now().duration_since(metadata.modified()?) { 91 | Ok(caching_duration) => { 92 | if caching_duration.as_secs() > self.conf.cache_duration { 93 | match self.get_contents_from_url(url).await { 94 | Ok(contents_from_url) => Ok(contents_from_url), 95 | Err(_) => Ok(cache_contents), 96 | } 97 | } else { 98 | Ok(cache_contents) 99 | } 100 | } 101 | Err(_) => Ok(cache_contents), 102 | }, 103 | Err(_) => self.get_contents_from_url(url).await, 104 | } 105 | } 106 | 107 | pub async fn get_user_public_keys(&self, user: &str) -> Result, Error> { 108 | let url = format!("{}/users/{}/keys", self.conf.endpoint, user); 109 | let contents = self.get_contents(&url).await?; 110 | let keys = serde_json::from_str::>(&contents)?; 111 | Ok(keys.iter().map(|k| k.key.clone()).collect()) 112 | } 113 | 114 | pub async fn check_pam(&self, user: &str) -> Result { 115 | let sectors = self.get_sectors().await?; 116 | Ok(sectors.iter().any(|team| team.members.contains_key(user))) 117 | } 118 | 119 | pub async fn get_sectors(&self) -> Result, Error> { 120 | let mut sectors: Vec = self.get_teams_result().await?; 121 | sectors.append(&mut self.get_repos_result().await?); 122 | Ok(sectors) 123 | } 124 | 125 | async fn get_teams_result(&self) -> Result, Error> { 126 | let gh_teams = self.get_team_map(&self.conf.org).await?; 127 | let mut teams = Vec::new(); 128 | for team_conf in &self.conf.team { 129 | if let Some(gh_team) = gh_teams.get(&team_conf.name) { 130 | teams.push(SectorGroup { sector: Sector::from(gh_team.clone()), 131 | gid: team_conf.gid, 132 | group: team_conf.group.clone(), 133 | members: self.get_team_members(gh_team.id).await? }); 134 | } 135 | } 136 | Ok(teams) 137 | } 138 | 139 | async fn get_team_map(&self, org: &str) -> Result, Error> { 140 | let url = format!("{}/orgs/{}/teams", self.conf.endpoint, org); 141 | let contents = self.get_contents(&url).await?; 142 | let teams = serde_json::from_str::>(&contents)?; 143 | Ok(teams.iter().map(|t| (t.name.clone(), t.clone())).collect()) 144 | } 145 | 146 | async fn get_team_members(&self, mid: u64) -> Result, Error> { 147 | let url = format!("{}/teams/{}/members", self.conf.endpoint, mid); 148 | let contents = self.get_contents(&url).await?; 149 | let members = serde_json::from_str::>(&contents)?; 150 | Ok(members.iter().map(|m| (m.login.clone(), m.clone())).collect()) 151 | } 152 | 153 | async fn get_repos_result(&self) -> Result, Error> { 154 | let gh_repos = self.get_repo_map(&self.conf.org).await?; 155 | let mut repos = Vec::new(); 156 | for repo_conf in &self.conf.repo { 157 | if let Some(gh_repo) = gh_repos.get(&repo_conf.name) { 158 | repos.push(SectorGroup { sector: Sector::from(gh_repo.clone()), 159 | gid: repo_conf.gid, 160 | group: repo_conf.group.clone(), 161 | members: self.get_repo_collaborators(&self.conf.org, &gh_repo.name) 162 | .await? }); 163 | } 164 | } 165 | Ok(repos) 166 | } 167 | 168 | async fn get_repo_map(&self, org: &str) -> Result, Error> { 169 | let url = format!("{}/orgs/{}/repos", self.conf.endpoint, org); 170 | let contents = self.get_contents(&url).await?; 171 | let repos = serde_json::from_str::>(&contents)?; 172 | Ok(repos.iter().map(|t| (t.name.clone(), t.clone())).collect()) 173 | } 174 | 175 | async fn get_repo_collaborators(&self, org: &str, repo_name: &str) -> Result, Error> { 176 | let url = format!("{}/repos/{}/{}/collaborators?affiliation=outside", 177 | self.conf.endpoint, org, repo_name); 178 | let contents = self.get_contents(&url).await?; 179 | let members = serde_json::from_str::>(&contents)?; 180 | Ok(members.iter().map(|m| (m.login.clone(), m.clone())).collect()) 181 | } 182 | 183 | pub async fn get_rate_limit(&self) -> Result { 184 | let url = format!("{}/rate_limit", self.conf.endpoint); 185 | let req = self.build_request(&url)?; 186 | let resp = self.client.execute(req).await.unwrap(); 187 | Ok(resp.json().await.unwrap()) 188 | } 189 | 190 | pub async fn clear_all_caches(&self) -> Result<(), Error> { 191 | let mut path = self.get_cache_path(""); 192 | path.push("**/*"); 193 | for entry in glob(path.to_str().unwrap()).unwrap() { 194 | match entry { 195 | Ok(path) => { 196 | if path.is_file() { 197 | std::fs::remove_file(path)? 198 | } 199 | } 200 | Err(e) => println!("{:?}", e), 201 | } 202 | } 203 | Ok(()) 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod applog; 2 | mod buffer; 3 | mod connection; 4 | mod cstructs; 5 | mod error; 6 | mod message; 7 | mod structs; 8 | 9 | use buffer::Buffer; 10 | use connection::Connection; 11 | use cstructs::{Group, Passwd, Spwd}; 12 | use message::{ClientMessage as CMsg, DaemonMessage as DMsg, Ent, Gr, Pw, Sp}; 13 | use nix::errno::Errno; 14 | use std::ffi::CStr; 15 | use std::process; 16 | use std::string::String; 17 | 18 | #[allow(dead_code)] 19 | enum NssStatus { 20 | TryAgain, 21 | Unavail, 22 | NotFound, 23 | Success, 24 | } 25 | 26 | impl From for libc::c_int { 27 | fn from(status: NssStatus) -> libc::c_int { 28 | match status { 29 | NssStatus::TryAgain => -2, 30 | NssStatus::Unavail => -1, 31 | NssStatus::NotFound => 0, 32 | NssStatus::Success => 1, 33 | } 34 | } 35 | } 36 | 37 | fn string_from(cstrptr: *const libc::c_char) -> String { 38 | let cstr: &CStr = unsafe { CStr::from_ptr(cstrptr) }; 39 | String::from(cstr.to_str().unwrap_or("")) 40 | } 41 | 42 | macro_rules! succeed { 43 | () => {{ 44 | log::debug!("Success!"); 45 | return libc::c_int::from(NssStatus::Success); 46 | }}; 47 | } 48 | 49 | macro_rules! fail { 50 | ($err_no_p:ident, $err_no:expr, $return_val:expr) => {{ 51 | *$err_no_p = $err_no as libc::c_int; 52 | log::debug!("Faill!"); 53 | return libc::c_int::from($return_val); 54 | }}; 55 | } 56 | 57 | macro_rules! try_unwrap { 58 | ($getter:expr) => {{ 59 | match $getter { 60 | Ok(ret) => { 61 | log::debug!("Ok: {:?}", ret); 62 | ret 63 | } 64 | Err(e) => { 65 | log::debug!("failed (will retry): {:?}", e); 66 | return libc::c_int::from(NssStatus::TryAgain); 67 | } 68 | } 69 | }}; 70 | ($getter:expr, $err_no_p:ident) => {{ 71 | match $getter { 72 | Ok(ret) => ret, 73 | Err(e) => { 74 | log::debug!("failed (will retry): {:?}", e); 75 | *$err_no_p = Errno::EAGAIN as libc::c_int; 76 | return libc::c_int::from(NssStatus::TryAgain); 77 | } 78 | } 79 | }}; 80 | } 81 | 82 | /// # Safety 83 | /// 84 | /// This function intended to be called from nss 85 | #[unsafe(no_mangle)] 86 | pub unsafe extern "C" fn _nss_sectora_getpwnam_r(cnameptr: *const libc::c_char, pwptr: *mut Passwd, 87 | buf: *mut libc::c_char, buflen: libc::size_t, 88 | errnop: *mut libc::c_int) 89 | -> libc::c_int { 90 | let mut buffer = Buffer::new(buf, buflen); 91 | let conn = try_unwrap!(Connection::new("_nss_sectora_getpwnam_r"), errnop); 92 | let msg = try_unwrap!(conn.communicate(CMsg::Pw(Pw::Nam(string_from(cnameptr)))), errnop); 93 | if let DMsg::Pw { login, 94 | uid, 95 | gid, 96 | home, 97 | sh, } = msg 98 | { 99 | match { (*pwptr).pack_args(&mut buffer, &login, uid, gid, &home, &sh) } { 100 | Ok(_) => succeed!(), 101 | Err(_) => fail!(errnop, Errno::ERANGE, NssStatus::TryAgain), 102 | } 103 | } 104 | fail!(errnop, Errno::ENOENT, NssStatus::NotFound) 105 | } 106 | 107 | /// # Safety 108 | /// 109 | /// This function intended to be called from nss 110 | #[unsafe(no_mangle)] 111 | pub unsafe extern "C" fn _nss_sectora_getpwuid_r(uid: libc::uid_t, pwptr: *mut Passwd, buf: *mut libc::c_char, 112 | buflen: libc::size_t, errnop: *mut libc::c_int) 113 | -> libc::c_int { 114 | let mut buffer = Buffer::new(buf, buflen); 115 | let conn = try_unwrap!(Connection::new("_nss_sectora_getpwuid_r"), errnop); 116 | let msg = try_unwrap!(conn.communicate(CMsg::Pw(Pw::Uid(uid as u64))), errnop); 117 | if let DMsg::Pw { login, 118 | uid, 119 | gid, 120 | home, 121 | sh, } = msg 122 | { 123 | match { (*pwptr).pack_args(&mut buffer, &login, uid, gid, &home, &sh) } { 124 | Ok(_) => succeed!(), 125 | Err(_) => fail!(errnop, Errno::ERANGE, NssStatus::TryAgain), 126 | } 127 | } 128 | fail!(errnop, Errno::ENOENT, NssStatus::NotFound) 129 | } 130 | 131 | /// # Safety 132 | /// 133 | /// This function intended to be called from nss 134 | #[unsafe(no_mangle)] 135 | pub unsafe extern "C" fn _nss_sectora_setpwent() -> libc::c_int { 136 | let conn = try_unwrap!(Connection::new("_nss_sectora_setpwent")); 137 | let msg = try_unwrap!(conn.communicate(CMsg::Pw(Pw::Ent(Ent::Set(process::id()))))); 138 | if let DMsg::Success = msg { 139 | return libc::c_int::from(NssStatus::Success); 140 | } 141 | libc::c_int::from(NssStatus::TryAgain) 142 | } 143 | 144 | /// # Safety 145 | /// 146 | /// This function intended to be called from nss 147 | #[unsafe(no_mangle)] 148 | pub unsafe extern "C" fn _nss_sectora_getpwent_r(pwptr: *mut Passwd, buf: *mut libc::c_char, buflen: libc::size_t, 149 | errnop: *mut libc::c_int) 150 | -> libc::c_int { 151 | let mut buffer = Buffer::new(buf, buflen); 152 | let conn = try_unwrap!(Connection::new("_nss_sectora_getpwent_r"), errnop); 153 | let msg = try_unwrap!(conn.communicate(CMsg::Pw(Pw::Ent(Ent::Get(process::id())))), errnop); 154 | if let DMsg::Pw { login, 155 | uid, 156 | gid, 157 | home, 158 | sh, } = msg 159 | { 160 | match { (*pwptr).pack_args(&mut buffer, &login, uid, gid, &home, &sh) } { 161 | Ok(_) => succeed!(), 162 | Err(_) => fail!(errnop, Errno::ERANGE, NssStatus::TryAgain), 163 | } 164 | } 165 | fail!(errnop, Errno::ENOENT, NssStatus::NotFound) 166 | } 167 | 168 | /// # Safety 169 | /// 170 | /// This function intended to be called from nss 171 | #[unsafe(no_mangle)] 172 | pub unsafe extern "C" fn _nss_sectora_endpwent() -> libc::c_int { 173 | let conn = try_unwrap!(Connection::new("_nss_sectora_endpwent")); 174 | let msg = try_unwrap!(conn.communicate(CMsg::Pw(Pw::Ent(Ent::End(process::id()))))); 175 | if let DMsg::Success = msg { 176 | return libc::c_int::from(NssStatus::Success); 177 | } 178 | libc::c_int::from(NssStatus::TryAgain) 179 | } 180 | 181 | /// # Safety 182 | /// 183 | /// This function intended to be called from nss 184 | #[unsafe(no_mangle)] 185 | pub unsafe extern "C" fn _nss_sectora_getspnam_r(cnameptr: *const libc::c_char, spptr: *mut Spwd, 186 | buf: *mut libc::c_char, buflen: libc::size_t, 187 | errnop: *mut libc::c_int) 188 | -> libc::c_int { 189 | let mut buffer = Buffer::new(buf, buflen); 190 | let conn = try_unwrap!(Connection::new("_nss_sectora_getspnam_r"), errnop); 191 | let msg = try_unwrap!(conn.communicate(CMsg::Sp(Sp::Nam(string_from(cnameptr)))), errnop); 192 | if let DMsg::Sp { login, pass } = msg { 193 | match { (*spptr).pack_args(&mut buffer, &login, &pass) } { 194 | Ok(_) => succeed!(), 195 | Err(_) => fail!(errnop, Errno::ERANGE, NssStatus::TryAgain), 196 | } 197 | } 198 | fail!(errnop, Errno::ENOENT, NssStatus::NotFound) 199 | } 200 | 201 | /// # Safety 202 | /// 203 | /// This function intended to be called from nss 204 | #[unsafe(no_mangle)] 205 | pub unsafe extern "C" fn _nss_sectora_setspent() -> libc::c_int { 206 | let conn = try_unwrap!(Connection::new("_nss_sectora_setspent")); 207 | let msg = try_unwrap!(conn.communicate(CMsg::Sp(Sp::Ent(Ent::Set(process::id()))))); 208 | if let DMsg::Success = msg { 209 | return libc::c_int::from(NssStatus::Success); 210 | } 211 | libc::c_int::from(NssStatus::Success) 212 | } 213 | 214 | /// # Safety 215 | /// 216 | /// This function intended to be called from nss 217 | #[unsafe(no_mangle)] 218 | pub unsafe extern "C" fn _nss_sectora_getspent_r(spptr: *mut Spwd, buf: *mut libc::c_char, buflen: libc::size_t, 219 | errnop: *mut libc::c_int) 220 | -> libc::c_int { 221 | let mut buffer = Buffer::new(buf, buflen); 222 | let conn = try_unwrap!(Connection::new("_nss_sectora_getspent_r"), errnop); 223 | let msg = try_unwrap!(conn.communicate(CMsg::Sp(Sp::Ent(Ent::Get(process::id())))), errnop); 224 | if let DMsg::Sp { login, pass } = msg { 225 | match { (*spptr).pack_args(&mut buffer, &login, &pass) } { 226 | Ok(_) => succeed!(), 227 | Err(_) => fail!(errnop, Errno::ERANGE, NssStatus::TryAgain), 228 | } 229 | } 230 | fail!(errnop, Errno::ENOENT, NssStatus::NotFound) 231 | } 232 | 233 | /// # Safety 234 | /// 235 | /// This function intended to be called from nss 236 | #[unsafe(no_mangle)] 237 | pub unsafe extern "C" fn _nss_sectora_endspent() -> libc::c_int { 238 | let conn = try_unwrap!(Connection::new("_nss_sectora_endspent")); 239 | let msg = try_unwrap!(conn.communicate(CMsg::Sp(Sp::Ent(Ent::End(process::id()))))); 240 | if let DMsg::Success = msg { 241 | return libc::c_int::from(NssStatus::Success); 242 | } 243 | libc::c_int::from(NssStatus::TryAgain) 244 | } 245 | 246 | /// # Safety 247 | /// 248 | /// This function intended to be called from nss 249 | #[unsafe(no_mangle)] 250 | pub unsafe extern "C" fn _nss_sectora_getgrgid_r(gid: libc::gid_t, grptr: *mut Group, buf: *mut libc::c_char, 251 | buflen: libc::size_t, errnop: *mut libc::c_int) 252 | -> libc::c_int { 253 | let mut buffer = Buffer::new(buf, buflen); 254 | let conn = try_unwrap!(Connection::new("_nss_sectora_getgrgid_r"), errnop); 255 | let msg = try_unwrap!(conn.communicate(CMsg::Gr(Gr::Gid(gid as u64))), errnop); 256 | if let DMsg::Gr { sector } = msg { 257 | let members: Vec<&str> = sector.members.values().map(|m| m.login.as_str()).collect(); 258 | match { (*grptr).pack_args(&mut buffer, §or.get_group(), u64::from(gid), &members) } { 259 | Ok(_) => succeed!(), 260 | Err(_) => fail!(errnop, Errno::ERANGE, NssStatus::TryAgain), 261 | } 262 | } 263 | fail!(errnop, Errno::ENOENT, NssStatus::NotFound) 264 | } 265 | 266 | /// # Safety 267 | /// 268 | /// This function intended to be called from nss 269 | #[unsafe(no_mangle)] 270 | pub unsafe extern "C" fn _nss_sectora_getgrnam_r(cnameptr: *const libc::c_char, grptr: *mut Group, 271 | buf: *mut libc::c_char, buflen: libc::size_t, 272 | errnop: *mut libc::c_int) 273 | -> libc::c_int { 274 | let mut buffer = Buffer::new(buf, buflen); 275 | let conn = try_unwrap!(Connection::new("_nss_sectora_getgrnam_r"), errnop); 276 | let msg = try_unwrap!(conn.communicate(CMsg::Gr(Gr::Nam(string_from(cnameptr)))), errnop); 277 | if let DMsg::Gr { sector } = msg { 278 | let members: Vec<&str> = sector.members.values().map(|m| m.login.as_str()).collect(); 279 | match { (*grptr).pack_args(&mut buffer, §or.get_group(), sector.get_gid(), &members) } { 280 | Ok(_) => succeed!(), 281 | Err(_) => fail!(errnop, Errno::ERANGE, NssStatus::TryAgain), 282 | } 283 | } 284 | fail!(errnop, Errno::ENOENT, NssStatus::NotFound) 285 | } 286 | 287 | /// # Safety 288 | /// 289 | /// This function intended to be called from nss 290 | #[unsafe(no_mangle)] 291 | pub unsafe extern "C" fn _nss_sectora_setgrent() -> libc::c_int { 292 | let conn = try_unwrap!(Connection::new("_nss_sectora_setgrent")); 293 | let msg = try_unwrap!(conn.communicate(CMsg::Gr(Gr::Ent(Ent::Set(process::id()))))); 294 | if let DMsg::Success = msg { 295 | return libc::c_int::from(NssStatus::Success); 296 | } 297 | libc::c_int::from(NssStatus::Success) 298 | } 299 | 300 | /// # Safety 301 | /// 302 | /// This function intended to be called from nss 303 | #[unsafe(no_mangle)] 304 | pub unsafe extern "C" fn _nss_sectora_getgrent_r(grptr: *mut Group, buf: *mut libc::c_char, buflen: libc::size_t, 305 | errnop: *mut libc::c_int) 306 | -> libc::c_int { 307 | let mut buffer = Buffer::new(buf, buflen); 308 | let conn = try_unwrap!(Connection::new("_nss_sectora_getgrent_r"), errnop); 309 | let msg = try_unwrap!(conn.communicate(CMsg::Gr(Gr::Ent(Ent::Get(process::id())))), errnop); 310 | if let DMsg::Gr { sector } = msg { 311 | let members: Vec<&str> = sector.members.values().map(|m| m.login.as_str()).collect(); 312 | match { (*grptr).pack_args(&mut buffer, §or.get_group(), sector.get_gid(), &members) } { 313 | Ok(_) => succeed!(), 314 | Err(_) => fail!(errnop, Errno::ERANGE, NssStatus::TryAgain), 315 | } 316 | } 317 | fail!(errnop, Errno::ENOENT, NssStatus::NotFound) 318 | } 319 | 320 | /// # Safety 321 | /// 322 | /// This function intended to be called from nss 323 | #[unsafe(no_mangle)] 324 | pub unsafe extern "C" fn _nss_sectora_endgrent() -> libc::c_int { 325 | let conn = try_unwrap!(Connection::new("_nss_sectora_endgrent")); 326 | let msg = try_unwrap!(conn.communicate(CMsg::Gr(Gr::Ent(Ent::End(process::id()))))); 327 | if let DMsg::Success = msg { 328 | return libc::c_int::from(NssStatus::Success); 329 | } 330 | libc::c_int::from(NssStatus::TryAgain) 331 | } 332 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod applog; 2 | mod connection; 3 | mod error; 4 | mod message; 5 | mod structs; 6 | 7 | use clap::{CommandFactory, Parser}; 8 | use clap_complete::{generate, shells}; 9 | use log::debug; 10 | use message::*; 11 | use std::env; 12 | use std::io::{Error, ErrorKind}; 13 | use structs::Config; 14 | 15 | #[derive(Debug, Parser)] 16 | #[clap(rename_all = "kebab-case")] 17 | enum Command { 18 | /// Gets user public key 19 | Key { user: String }, 20 | /// Executes pam check 21 | Pam, 22 | /// Check configuration 23 | Check { confpath: std::path::PathBuf }, 24 | /// Cleans caches up 25 | #[clap(alias = "cleanup")] 26 | CleanUp, 27 | /// Get rate limit for github api 28 | #[clap(alias = "ratelimit")] 29 | RateLimit, 30 | /// Displays version details 31 | Version, 32 | /// Displays completion 33 | Completion { 34 | #[clap(subcommand)] 35 | shell: Shell, 36 | }, 37 | } 38 | 39 | #[allow(clippy::enum_variant_names)] 40 | #[derive(Debug, Parser)] 41 | #[clap(rename_all = "kebab-case")] 42 | enum Shell { 43 | Bash, 44 | Fish, 45 | Zsh, 46 | PowerShell, 47 | Elvish, 48 | } 49 | 50 | fn show_keys(conn: &connection::Connection, user: &str) -> Result<(), Error> { 51 | match conn.communicate(ClientMessage::Key { user: user.to_owned() }) { 52 | Ok(DaemonMessage::Key { keys }) => { 53 | println!("{}", keys); 54 | Ok(()) 55 | } 56 | _ => Err(Error::new(ErrorKind::PermissionDenied, "key check failed")), 57 | } 58 | } 59 | 60 | fn main() -> Result<(), Error> { 61 | let command = Command::parse(); 62 | let conn = match connection::Connection::new(&format!("{:?}", command)) { 63 | Ok(conn) => conn, 64 | Err(err) => return Err(Error::new(ErrorKind::Other, format!("{:?}", err))), 65 | }; 66 | debug!("connected to socket: {:?}", conn); 67 | 68 | match command { 69 | Command::Check { confpath } => match Config::from_path(&confpath) { 70 | Ok(_) => return Ok(()), 71 | Err(_) => return Err(Error::new(ErrorKind::Other, "check failed")), 72 | }, 73 | Command::Key { user } => show_keys(&conn, &user)?, 74 | Command::Pam => match env::var("PAM_USER") { 75 | Ok(user) => match conn.communicate(ClientMessage::Pam { user }) { 76 | Ok(DaemonMessage::Pam { result }) => { 77 | if result { 78 | return Ok(()); 79 | } else { 80 | return Err(Error::new(ErrorKind::NotFound, "user not found")); 81 | } 82 | } 83 | _ => return Err(Error::new(ErrorKind::Other, "faild")), 84 | }, 85 | Err(_) => return Err(Error::new(ErrorKind::ConnectionRefused, "failed")), 86 | }, 87 | Command::CleanUp => match conn.communicate(ClientMessage::CleanUp) { 88 | Ok(_) => return Ok(()), 89 | Err(_) => return Err(Error::new(ErrorKind::Other, "failed")), 90 | }, 91 | Command::RateLimit => match conn.communicate(ClientMessage::RateLimit) { 92 | Ok(DaemonMessage::RateLimit { limit, 93 | remaining, 94 | reset, }) => { 95 | println!("remaining: {}/{}, reset:{}", remaining, limit, reset); 96 | } 97 | _ => return Err(Error::new(ErrorKind::Other, "failed")), 98 | }, 99 | Command::Version => { 100 | println!("{}", 101 | concat!(env!("CARGO_PKG_VERSION"), 102 | include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt")))); 103 | } 104 | Command::Completion { shell } => { 105 | let shell = match shell { 106 | Shell::Bash => shells::Shell::Bash, 107 | Shell::Fish => shells::Shell::Fish, 108 | Shell::Zsh => shells::Shell::Zsh, 109 | Shell::PowerShell => shells::Shell::PowerShell, 110 | Shell::Elvish => shells::Shell::Elvish, 111 | }; 112 | let mut cmd = Command::command(); 113 | generate(shell, &mut cmd, "wagon", &mut std::io::stdout()); 114 | } 115 | }; 116 | Ok(()) 117 | } 118 | -------------------------------------------------------------------------------- /src/message.rs: -------------------------------------------------------------------------------- 1 | use crate::error::ParseMessageError; 2 | use crate::structs; 3 | use std::fmt; 4 | use std::str::FromStr; 5 | 6 | #[derive(Debug)] 7 | pub enum Pw { 8 | Uid(u64), 9 | Nam(String), 10 | Ent(Ent), 11 | } 12 | 13 | #[derive(Debug)] 14 | pub enum Sp { 15 | Nam(String), 16 | Ent(Ent), 17 | } 18 | 19 | #[derive(Debug)] 20 | pub enum Gr { 21 | Gid(u64), 22 | Nam(String), 23 | Ent(Ent), 24 | } 25 | 26 | #[derive(Debug)] 27 | pub enum Ent { 28 | Set(u32), 29 | Get(u32), 30 | End(u32), 31 | } 32 | 33 | pub struct DividedMessage { 34 | pub cont: bool, 35 | pub message: String, 36 | } 37 | 38 | impl DividedMessage { 39 | #[allow(dead_code)] 40 | pub fn new(msg: &str, size: usize) -> Vec { 41 | let mut msgs = vec![]; 42 | let mut idx = 0; 43 | while idx + size < msg.len() { 44 | msgs.push(Self { cont: true, 45 | message: msg[idx..idx + size].to_owned() }); 46 | idx += size 47 | } 48 | msgs.push(Self { cont: false, 49 | message: msg[idx..msg.len()].to_owned() }); 50 | msgs 51 | } 52 | } 53 | 54 | impl fmt::Display for DividedMessage { 55 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}:{}", i32::from(self.cont), self.message) } 56 | } 57 | 58 | impl FromStr for DividedMessage { 59 | type Err = ParseMessageError; 60 | fn from_str(s: &str) -> Result { 61 | if let Some(msg) = s.strip_prefix("0:") { 62 | Ok(Self { cont: false, 63 | message: msg.to_owned() }) 64 | } else if let Some(msg) = s.strip_prefix("1:") { 65 | Ok(Self { cont: true, 66 | message: msg.to_owned() }) 67 | } else { 68 | Err(ParseMessageError::ParseClientMessageError) 69 | } 70 | } 71 | } 72 | 73 | #[derive(Debug)] 74 | pub enum ClientMessage { 75 | Cont, 76 | Key { user: String }, 77 | Pam { user: String }, 78 | CleanUp, 79 | RateLimit, 80 | SectorGroups, 81 | Pw(Pw), 82 | Sp(Sp), 83 | Gr(Gr), 84 | } 85 | 86 | #[allow(dead_code)] 87 | #[derive(Debug)] 88 | pub enum DaemonMessage { 89 | Success, 90 | Error { 91 | message: String, 92 | }, 93 | Key { 94 | keys: String, 95 | }, 96 | Pam { 97 | result: bool, 98 | }, 99 | RateLimit { 100 | limit: usize, 101 | remaining: usize, 102 | reset: usize, 103 | }, 104 | SectorGroups { 105 | sectors: Vec, 106 | }, 107 | Pw { 108 | login: String, 109 | uid: u64, 110 | gid: u64, 111 | home: String, 112 | sh: String, 113 | }, 114 | Sp { 115 | login: String, 116 | pass: String, 117 | }, 118 | Gr { 119 | sector: structs::SectorGroup, 120 | }, 121 | } 122 | 123 | impl fmt::Display for Ent { 124 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 125 | match self { 126 | Ent::Set(pid) => write!(f, "set|{}", pid), 127 | Ent::Get(pid) => write!(f, "get|{}", pid), 128 | Ent::End(pid) => write!(f, "end|{}", pid), 129 | } 130 | } 131 | } 132 | 133 | impl fmt::Display for Pw { 134 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 135 | match self { 136 | Pw::Uid(uid) => write!(f, "uid={}", uid), 137 | Pw::Nam(name) => write!(f, "name={}", name), 138 | Pw::Ent(ent) => write!(f, "ent={}", ent), 139 | } 140 | } 141 | } 142 | 143 | impl fmt::Display for Sp { 144 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 145 | match self { 146 | Sp::Nam(name) => write!(f, "name={}", name), 147 | Sp::Ent(ent) => write!(f, "ent={}", ent), 148 | } 149 | } 150 | } 151 | 152 | impl fmt::Display for Gr { 153 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 154 | match self { 155 | Gr::Gid(gid) => write!(f, "gid={}", gid), 156 | Gr::Nam(name) => write!(f, "name={}", name), 157 | Gr::Ent(ent) => write!(f, "ent={}", ent), 158 | } 159 | } 160 | } 161 | 162 | impl fmt::Display for ClientMessage { 163 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 164 | match self { 165 | ClientMessage::Cont => write!(f, "c:cont"), 166 | ClientMessage::Key { user } => write!(f, "c:key:{}", user), 167 | ClientMessage::Pam { user } => write!(f, "c:pam:{}", user), 168 | ClientMessage::CleanUp => write!(f, "c:cleanup"), 169 | ClientMessage::RateLimit => write!(f, "c:ratelimit"), 170 | ClientMessage::SectorGroups => write!(f, "c:sectors"), 171 | ClientMessage::Pw(pw) => write!(f, "c:pw:{}", pw), 172 | ClientMessage::Sp(sp) => write!(f, "c:sp:{}", sp), 173 | ClientMessage::Gr(gr) => write!(f, "c:gr:{}", gr), 174 | } 175 | } 176 | } 177 | 178 | impl fmt::Display for DaemonMessage { 179 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 180 | match self { 181 | DaemonMessage::Error { message } => write!(f, "d:error:{}", message), 182 | DaemonMessage::Success => write!(f, "d:success"), 183 | DaemonMessage::Key { keys } => write!(f, "d:key:{}", keys), 184 | DaemonMessage::Pam { result } => write!(f, "d:pam:{}", result), 185 | DaemonMessage::RateLimit { limit, 186 | remaining, 187 | reset, } => write!(f, "d:ratelimit:{}:{}:{}", limit, remaining, reset), 188 | DaemonMessage::SectorGroups { sectors } => { 189 | let ss: Vec = sectors.iter().map(|s| s.to_string()).collect(); 190 | write!(f, "d:sectors:{}", ss.join("\n")) 191 | } 192 | DaemonMessage::Pw { login, 193 | uid, 194 | gid, 195 | home, 196 | sh, } => write!(f, "d:pw:{}:{}:{}:{}:{}", login, uid, gid, home, sh), 197 | DaemonMessage::Sp { login, pass } => write!(f, "d:sp:{}:{}", login, pass), 198 | DaemonMessage::Gr { sector } => write!(f, "d:gr:{}", sector), 199 | } 200 | } 201 | } 202 | 203 | impl FromStr for Ent { 204 | type Err = ParseMessageError; 205 | fn from_str(s: &str) -> Result { 206 | if let Some(msg) = s.strip_prefix("set|") { 207 | Ok(Ent::Set(msg.parse::().unwrap())) 208 | } else if let Some(msg) = s.strip_prefix("get|") { 209 | Ok(Ent::Get(msg.parse::().unwrap())) 210 | } else if let Some(msg) = s.strip_prefix("end|") { 211 | Ok(Ent::End(msg.parse::().unwrap())) 212 | } else { 213 | Err(ParseMessageError::ParseClientMessageError) 214 | } 215 | } 216 | } 217 | 218 | impl FromStr for Pw { 219 | type Err = ParseMessageError; 220 | fn from_str(s: &str) -> Result { 221 | if let Some(msg) = s.strip_prefix("uid=") { 222 | Ok(Pw::Uid(msg.parse::().unwrap())) 223 | } else if let Some(msg) = s.strip_prefix("name=") { 224 | Ok(Pw::Nam(String::from(msg))) 225 | } else if let Some(msg) = s.strip_prefix("ent=") { 226 | Ok(Pw::Ent(msg.parse::().unwrap())) 227 | } else { 228 | Err(ParseMessageError::ParseClientMessageError) 229 | } 230 | } 231 | } 232 | 233 | impl FromStr for Sp { 234 | type Err = ParseMessageError; 235 | fn from_str(s: &str) -> Result { 236 | if let Some(msg) = s.strip_prefix("name=") { 237 | Ok(Sp::Nam(String::from(msg))) 238 | } else if let Some(msg) = s.strip_prefix("ent=") { 239 | Ok(Sp::Ent(msg.parse::().unwrap())) 240 | } else { 241 | Err(ParseMessageError::ParseClientMessageError) 242 | } 243 | } 244 | } 245 | 246 | impl FromStr for Gr { 247 | type Err = ParseMessageError; 248 | fn from_str(s: &str) -> Result { 249 | if let Some(msg) = s.strip_prefix("gid=") { 250 | Ok(Gr::Gid(msg.parse::().unwrap())) 251 | } else if let Some(msg) = s.strip_prefix("name=") { 252 | Ok(Gr::Nam(String::from(msg))) 253 | } else if let Some(msg) = s.strip_prefix("ent=") { 254 | Ok(Gr::Ent(msg.parse::().unwrap())) 255 | } else { 256 | Err(ParseMessageError::ParseClientMessageError) 257 | } 258 | } 259 | } 260 | 261 | impl FromStr for ClientMessage { 262 | type Err = ParseMessageError; 263 | fn from_str(s: &str) -> Result { 264 | if s == "c:cont" { 265 | Ok(ClientMessage::Cont) 266 | } else if let Some(msg) = s.strip_prefix("c:key:") { 267 | Ok(ClientMessage::Key { user: String::from(msg) }) 268 | } else if let Some(msg) = s.strip_prefix("c:pam:") { 269 | Ok(ClientMessage::Pam { user: String::from(msg) }) 270 | } else if s == "c:cleanup" { 271 | Ok(ClientMessage::CleanUp) 272 | } else if s == "c:ratelimit" { 273 | Ok(ClientMessage::RateLimit) 274 | } else if s == "c:sectors" { 275 | Ok(ClientMessage::SectorGroups) 276 | } else if let Some(msg) = s.strip_prefix("c:pw:") { 277 | Ok(ClientMessage::Pw(msg.parse::()?)) 278 | } else if let Some(msg) = s.strip_prefix("c:sp:") { 279 | Ok(ClientMessage::Sp(msg.parse::()?)) 280 | } else if let Some(msg) = s.strip_prefix("c:gr:") { 281 | Ok(ClientMessage::Gr(msg.parse::()?)) 282 | } else { 283 | Err(ParseMessageError::ParseClientMessageError) 284 | } 285 | } 286 | } 287 | 288 | impl FromStr for DaemonMessage { 289 | type Err = ParseMessageError; 290 | fn from_str(s: &str) -> Result { 291 | if let Some(msg) = s.strip_prefix("d:key:") { 292 | Ok(DaemonMessage::Key { keys: String::from(msg) }) 293 | } else if let Some(msg) = s.strip_prefix("d:pam:") { 294 | Ok(DaemonMessage::Pam { result: FromStr::from_str(msg).unwrap_or(false) }) 295 | } else if s == "d:success" { 296 | Ok(DaemonMessage::Success) 297 | } else if let Some(msg) = s.strip_prefix("d:ratelimit:") { 298 | let fields: Vec = msg.split(':').map(|s| s.to_string()).collect(); 299 | if fields.len() < 3 { 300 | return Err(ParseMessageError::ParseDaemonMessageError); 301 | } 302 | let limit = fields[0].clone().parse().unwrap_or(0); 303 | let remaining = fields[1].clone().parse().unwrap_or(0); 304 | let reset = fields[2].clone().parse().unwrap_or(0); 305 | Ok(DaemonMessage::RateLimit { limit, 306 | remaining, 307 | reset }) 308 | } else if let Some(msg) = s.strip_prefix("d:sectors:") { 309 | let sectors = msg.lines() 310 | .filter_map(|l| l.parse::().ok()) 311 | .collect(); 312 | Ok(DaemonMessage::SectorGroups { sectors }) 313 | } else if let Some(msg) = s.strip_prefix("d:pw:") { 314 | let fields: Vec = msg.split(':').map(|s| s.to_string()).collect(); 315 | if fields.len() < 5 { 316 | return Err(ParseMessageError::ParseDaemonMessageError); 317 | } 318 | let login: String = fields[0].clone(); 319 | let home: String = fields[3].clone(); 320 | let sh: String = fields[4].clone(); 321 | match (fields[1].parse::(), fields[2].parse::()) { 322 | (Ok(uid), Ok(gid)) => Ok(DaemonMessage::Pw { login, 323 | uid, 324 | gid, 325 | home, 326 | sh }), 327 | _ => Err(ParseMessageError::ParseDaemonMessageError), 328 | } 329 | } else if let Some(msg) = s.strip_prefix("d:sp:") { 330 | let fields: Vec = msg.split(':').map(|s| s.to_string()).collect(); 331 | if fields.len() < 2 { 332 | return Err(ParseMessageError::ParseDaemonMessageError); 333 | } 334 | Ok(DaemonMessage::Sp { login: fields[0].clone(), 335 | pass: fields[1].clone() }) 336 | } else if let Some(msg) = s.strip_prefix("d:gr:") { 337 | match msg.parse::() { 338 | Ok(sector) => Ok(DaemonMessage::Gr { sector }), 339 | _ => Err(ParseMessageError::ParseDaemonMessageError), 340 | } 341 | } else { 342 | Err(ParseMessageError::ParseDaemonMessageError) 343 | } 344 | } 345 | } 346 | -------------------------------------------------------------------------------- /src/statics.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use std::env; 3 | use std::path::PathBuf; 4 | use std::string::String; 5 | 6 | const DEFAULT_CONF_PATH_STR: &str = "/etc/sectora.conf"; 7 | 8 | static CONF_PATH_STR: Lazy = 9 | Lazy::new(|| env::var("SECTORA_CONFIG").unwrap_or(String::from(DEFAULT_CONF_PATH_STR))); 10 | pub static CONF_PATH: Lazy = Lazy::new(|| PathBuf::from((*CONF_PATH_STR).clone())); 11 | -------------------------------------------------------------------------------- /src/structs.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Error, ParseSectorError, ParseSectorGroupError, ParseSectorTypeError}; 2 | use serde::{Deserialize, Serialize}; 3 | use std::collections::HashMap; 4 | use std::fmt; 5 | use std::fs::File; 6 | use std::io::Read; 7 | use std::str::FromStr; 8 | use std::string::ToString; 9 | 10 | #[derive(Deserialize, Debug, Clone)] 11 | pub struct Config { 12 | pub token: String, 13 | pub org: String, 14 | #[serde(default = "default_team")] 15 | pub team: Vec, 16 | #[serde(default = "default_repo")] 17 | pub repo: Vec, 18 | #[serde(default = "default_endpoint")] 19 | pub endpoint: String, 20 | #[serde(default = "default_home")] 21 | pub home: String, 22 | #[serde(default = "default_sh")] 23 | pub sh: String, 24 | #[serde(default = "default_cache_duration")] 25 | pub cache_duration: u64, 26 | #[serde(default = "default_cert_path")] 27 | pub cert_path: String, 28 | #[serde(default = "default_user_conf_path")] 29 | pub user_conf_path: String, 30 | #[serde(default = "default_cache_dir")] 31 | pub cache_dir: String, 32 | pub proxy_url: Option, 33 | } 34 | 35 | fn default_team() -> Vec { Vec::new() } 36 | fn default_repo() -> Vec { Vec::new() } 37 | fn default_endpoint() -> String { String::from("https://api.github.com") } 38 | fn default_home() -> String { String::from("/home/{}") } 39 | fn default_sh() -> String { String::from("/bin/bash") } 40 | fn default_cache_duration() -> u64 { 3600 } 41 | fn default_cert_path() -> String { String::from("/etc/ssl/certs/ca-certificates.crt") } 42 | fn default_user_conf_path() -> String { String::from(".config/sectora.toml") } 43 | fn default_cache_dir() -> String { 44 | let mut path = std::env::temp_dir(); 45 | path.push("sectora/cache"); 46 | String::from(path.as_os_str().to_str().unwrap_or_default()) 47 | } 48 | 49 | fn get_socket_path() -> String { 50 | let mut path = std::env::temp_dir(); 51 | path.push("sectorad"); 52 | String::from(path.as_os_str().to_str().unwrap_or_default()) 53 | } 54 | 55 | fn get_socket_dir() -> String { 56 | let mut path = std::env::temp_dir(); 57 | path.push("sectora"); 58 | String::from(path.as_os_str().to_str().unwrap_or_default()) 59 | } 60 | 61 | impl Config { 62 | #[allow(dead_code)] 63 | pub fn from_path(configpath: &std::path::Path) -> Result { 64 | let mut file = File::open(configpath)?; 65 | let mut contents = String::default(); 66 | file.read_to_string(&mut contents)?; 67 | Ok(toml::from_str::(&contents)?) 68 | } 69 | } 70 | 71 | #[derive(Debug, Clone)] 72 | pub struct SocketConfig { 73 | pub socket_path: String, 74 | pub socket_dir: String, 75 | } 76 | 77 | impl SocketConfig { 78 | pub fn new() -> Self { 79 | SocketConfig { socket_path: get_socket_path(), 80 | socket_dir: get_socket_dir() } 81 | } 82 | } 83 | 84 | #[derive(Serialize, Deserialize, Debug, Clone)] 85 | pub struct UserConfig { 86 | pub sh: Option, 87 | pub pass: Option, 88 | } 89 | 90 | impl UserConfig { 91 | #[allow(dead_code)] 92 | pub fn from_path(configpath: &std::path::Path) -> Result { 93 | let mut file = File::open(configpath)?; 94 | let mut contents = String::default(); 95 | file.read_to_string(&mut contents)?; 96 | Ok(toml::from_str::(&contents)?) 97 | } 98 | } 99 | 100 | #[derive(Serialize, Deserialize, Debug, Clone)] 101 | pub struct Team { 102 | pub id: u64, 103 | pub name: String, 104 | } 105 | 106 | #[derive(Serialize, Deserialize, Debug, Clone)] 107 | pub struct TeamConfig { 108 | pub name: String, 109 | pub gid: Option, 110 | pub group: Option, 111 | } 112 | 113 | #[derive(Serialize, Deserialize, Debug, Clone)] 114 | pub struct Repo { 115 | pub id: u64, 116 | pub name: String, 117 | } 118 | 119 | #[derive(Serialize, Deserialize, Debug, Clone)] 120 | pub struct RepoConfig { 121 | pub name: String, 122 | pub gid: Option, 123 | pub group: Option, 124 | } 125 | 126 | #[derive(Serialize, Deserialize, Debug, Clone)] 127 | pub enum SectorType { 128 | Team, 129 | Repo, 130 | } 131 | 132 | impl fmt::Display for SectorType { 133 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 134 | match self { 135 | SectorType::Team => write!(f, "T"), 136 | SectorType::Repo => write!(f, "R"), 137 | } 138 | } 139 | } 140 | 141 | impl FromStr for SectorType { 142 | type Err = ParseSectorTypeError; 143 | fn from_str(s: &str) -> Result { 144 | match s { 145 | "T" => Ok(SectorType::Team), 146 | "R" => Ok(SectorType::Repo), 147 | _ => Err(ParseSectorTypeError::UnknownType), 148 | } 149 | } 150 | } 151 | 152 | #[derive(Serialize, Deserialize, Debug, Clone)] 153 | pub struct Sector { 154 | pub id: u64, 155 | pub name: String, 156 | pub sector_type: SectorType, 157 | } 158 | 159 | impl From for Sector { 160 | fn from(team: Team) -> Self { 161 | Self { id: team.id, 162 | name: team.name, 163 | sector_type: SectorType::Team } 164 | } 165 | } 166 | 167 | impl From for Sector { 168 | fn from(repo: Repo) -> Self { 169 | Self { id: repo.id, 170 | name: repo.name, 171 | sector_type: SectorType::Repo } 172 | } 173 | } 174 | 175 | impl fmt::Display for Sector { 176 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}:{}:{}", self.id, self.name, self.sector_type) } 177 | } 178 | 179 | impl FromStr for Sector { 180 | type Err = ParseSectorError; 181 | fn from_str(s: &str) -> Result { 182 | let parts = s.split(':').collect::>(); 183 | if parts.len() == 3 { 184 | Ok(Self { id: parts[0].parse().map_err(ParseSectorError::Id)?, 185 | name: String::from(parts[1]), 186 | sector_type: parts[2].parse().map_err(ParseSectorError::Type)? }) 187 | } else { 188 | Err(ParseSectorError::BadFormat) 189 | } 190 | } 191 | } 192 | 193 | #[derive(Debug, Clone)] 194 | pub struct SectorGroup { 195 | pub sector: Sector, 196 | pub gid: Option, 197 | pub group: Option, 198 | pub members: HashMap, 199 | } 200 | 201 | impl SectorGroup { 202 | #[allow(dead_code)] 203 | pub fn get_gid(&self) -> u64 { self.gid.unwrap_or(self.sector.id) } 204 | #[allow(dead_code)] 205 | pub fn get_group(&self) -> String { self.group.clone().unwrap_or(self.sector.name.clone()) } 206 | } 207 | 208 | impl fmt::Display for SectorGroup { 209 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 210 | let members_str = self.members 211 | .values() 212 | .map(ToString::to_string) 213 | .collect::>() 214 | .join(" "); 215 | writeln!(f, 216 | "{}\t{}\t{}\t{}", 217 | self.sector, 218 | self.gid.map(|i| i.to_string()).unwrap_or_default(), 219 | self.group.clone().unwrap_or_default(), 220 | members_str) 221 | } 222 | } 223 | 224 | impl FromStr for SectorGroup { 225 | type Err = ParseSectorGroupError; 226 | fn from_str(s: &str) -> Result { 227 | let parts = s.split('\t').collect::>(); 228 | let sector = parts[0].parse().map_err(ParseSectorGroupError::Sector)?; 229 | let gid: Option = match parts[1] { 230 | "" => None, 231 | s => Some(s.parse().map_err(ParseSectorGroupError::Gid)?), 232 | }; 233 | let group: Option = match parts[2] { 234 | "" => None, 235 | s => Some(String::from(s)), 236 | }; 237 | let members = parts[3].split(' ') 238 | .map(|s| s.parse::().map_err(ParseSectorGroupError::Member)) 239 | .collect::, _>>()? 240 | .into_iter() 241 | .map(|m| (m.login.clone(), m)) 242 | .collect::>(); 243 | Ok(Self { sector, 244 | gid, 245 | group, 246 | members }) 247 | } 248 | } 249 | 250 | #[derive(Serialize, Deserialize, Debug, Clone)] 251 | pub struct Member { 252 | pub id: u64, 253 | pub login: String, 254 | } 255 | 256 | impl fmt::Display for Member { 257 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}:{}", self.id, self.login) } 258 | } 259 | 260 | impl FromStr for Member { 261 | type Err = std::num::ParseIntError; 262 | fn from_str(s: &str) -> Result { 263 | let parts = s.split(':').collect::>(); 264 | Ok(Self { id: parts[0].parse()?, 265 | login: String::from(parts[1]) }) 266 | } 267 | } 268 | 269 | #[allow(dead_code)] 270 | pub struct MemberGid { 271 | pub member: Member, 272 | pub gid: u64, 273 | } 274 | 275 | impl fmt::Display for MemberGid { 276 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}|{}", self.member, self.gid) } 277 | } 278 | 279 | impl FromStr for MemberGid { 280 | type Err = std::num::ParseIntError; 281 | fn from_str(s: &str) -> Result { 282 | let parts = s.split('|').collect::>(); 283 | Ok(Self { member: parts[0].parse()?, 284 | gid: parts[1].parse()? }) 285 | } 286 | } 287 | 288 | #[derive(Serialize, Deserialize, Debug)] 289 | pub struct PublicKey { 290 | pub id: u64, 291 | pub key: String, 292 | } 293 | 294 | #[derive(Serialize, Deserialize, Debug)] 295 | pub struct Rate { 296 | pub limit: usize, 297 | pub remaining: usize, 298 | pub reset: usize, 299 | } 300 | 301 | #[derive(Serialize, Deserialize, Debug)] 302 | pub struct RateLimit { 303 | pub rate: Rate, 304 | } 305 | -------------------------------------------------------------------------------- /test/.gitignore: -------------------------------------------------------------------------------- 1 | testconf.toml 2 | -------------------------------------------------------------------------------- /test/Makefile: -------------------------------------------------------------------------------- 1 | VERSION=$(shell grep "^version" ../Cargo.toml | cut -f 2 -d '"') 2 | dist:=ubuntu 3 | ver:=bionic 4 | user:=hunter 5 | keyfile:=id_rsa 6 | target:=x86_64-unknown-linux-gnu 7 | 8 | .PHONY: test-ansible test-deb up down reset 9 | 10 | test-ansible-stub: 11 | make up 12 | make setup-ansible 13 | make exec-login 14 | make down 15 | 16 | test-deb-stub: 17 | make up 18 | make create-test-conf-stub 19 | make setup-deb 20 | make exec-login 21 | make down 22 | 23 | test-deb: 24 | make up 25 | make create-test-conf-env 26 | make setup-deb 27 | make setup-client-key-env 28 | make user=${TEST_USER} exec-login 29 | make down 30 | 31 | setup-deb: 32 | @echo '::group::Installing deb packages' 33 | make -j 2 setup-host-deb setup-client-key 34 | @echo '::endgroup::' 35 | 36 | setup-ansible: 37 | @echo '::group::Installing ansible packages' 38 | make -j 2 setup-host-root setup-client-ansible 39 | docker exec client ansible-playbook -i hosts localtest.yml 40 | sleep 1 41 | @echo '::endgroup::' 42 | 43 | setup-host-root: 44 | docker cp ./keys/root/id_rsa.pub host:/root/.ssh/authorized_keys 45 | docker exec host chown -R root:root /root/.ssh/ 46 | docker exec host chmod -R 600 /root/.ssh/ 47 | 48 | enter-host: 49 | docker exec -it host bash 50 | 51 | setup-client-key: 52 | docker cp ./keys client:/work/keys 53 | 54 | setup-client-key-env: 55 | @echo "$${TEST_PRIVATE_KEY}" > $(keyfile) 56 | chmod 600 $(keyfile) 57 | docker exec client rm -rf /work/keys 58 | docker exec client mkdir -p /work/keys/user/ 59 | docker cp $(keyfile) client:/work/keys/user/$(keyfile) 60 | rm $(keyfile) 61 | 62 | create-test-conf-stub: 63 | @echo 'token = "TESTTOKEN"' > testconf.toml 64 | @echo 'org = "soundtribe"' >> testconf.toml 65 | @echo 'endpoint = "http://json-server:3000"' >> testconf.toml 66 | @echo >> testconf.toml 67 | @echo '[[team]]' >> testconf.toml 68 | @echo 'name = "sector9"' >> testconf.toml 69 | @echo 'gid = 2019' >> testconf.toml 70 | 71 | create-test-conf-env: 72 | @echo "token = \"${TEST_GITHUB_TOKEN}\"" > testconf.toml 73 | @echo "org = \"${TEST_GITHUB_ORG}\"" >> testconf.toml 74 | @echo >> testconf.toml 75 | @echo '[[team]]' >> testconf.toml 76 | @echo "name = \"${TEST_GITHUB_TEAM}\"" >> testconf.toml 77 | @echo "gid = 2022" >> testconf.toml 78 | 79 | setup-host-deb: 80 | docker cp ../target/$(target)/debian/sectora_$(VERSION)*.deb host:/tmp/ 81 | docker exec host sh -c "DEBIAN_FRONTEND=noninteractive apt-get install -y /tmp/sectora_$(VERSION)*.deb" 82 | docker cp ./testconf.toml host:/etc/sectora.conf 83 | time docker exec host systemctl start sectora 84 | time docker exec host systemctl restart ssh 85 | 86 | setup-client-ansible: 87 | make setup-client-key 88 | docker cp ./client/hosts client:/work/ 89 | docker cp ./client/localtest.yml client:/work/ 90 | docker cp ../ansible/roles client:/work/roles 91 | docker cp ../ansible/templates client:/work/templates 92 | docker cp ../assets/sectora.service client:/work/ 93 | docker cp ../assets/sectora.sh client:/work/ 94 | docker cp ../target/$(target)/release client:/work/release 95 | 96 | up: 97 | @echo '::group::Starting containers' 98 | @echo '$(shell tput setaf 6)$(dist) $(shell tput setaf 3)$(ver)$(shell tput sgr 0)' 99 | docker network create -d bridge testnw 100 | docker build -t json-server ./json-server 101 | docker run -d --network testnw --name json-server json-server json-server --watch db.json --host 0.0.0.0 --routes routes.json 102 | docker run -d --network testnw --name host --privileged --cgroupns=host -v /sys/fs/cgroup:/sys/fs/cgroup:rw yasuyuky/ssh-test:$(dist).$(ver) 103 | docker build -t client client 104 | docker run -d --network testnw --name client -w /work client sh -c 'while true; do sleep 1; done' 105 | @echo '::endgroup::' 106 | 107 | exec-login: 108 | @echo '$(shell tput setaf 6)LOGIN TEST START$(shell tput sgr 0)' 109 | @echo '$(shell tput setab 7)$(shell tput setaf 0) SSH $(shell tput sgr 0)' 110 | @docker exec client ssh $(user)@host -i keys/user/$(keyfile) \ 111 | echo '"$(shell tput setaf 2)SUCCESS$(shell tput sgr 0)"' 112 | @docker exec client ssh $(user)@host -i keys/user/$(keyfile) \ 113 | echo '::notice title=$(dist)/$(ver)::success' 114 | @echo '$(shell tput setab 7)$(shell tput setaf 0) ID $(shell tput sgr 0)' 115 | @docker exec client ssh $(user)@host -i keys/user/$(keyfile) id 116 | @echo '$(shell tput setab 7)$(shell tput setaf 0) VERSION $(shell tput sgr 0)' 117 | @docker exec client ssh $(user)@host -i keys/user/$(keyfile) /usr/sbin/sectora version 118 | @echo '$(shell tput setaf 6)LOGIN TEST END$(shell tput sgr 0)' 119 | 120 | down: 121 | @echo '::group::Stopping containers' 122 | docker rm -f json-server 123 | docker rm -f host 124 | docker rm -f client 125 | docker network rm testnw 126 | @echo '::endgroup::' 127 | 128 | reset: 129 | cd .. && make TARGET=$(target) deb LOG_LEVEL=DEBUG 130 | make down 131 | make up 132 | make setup 133 | 134 | restart: 135 | docker restart host 136 | -------------------------------------------------------------------------------- /test/client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.22.0 2 | 3 | RUN apk add openssh ansible 4 | COPY sshconfig /root/.ssh/config 5 | RUN chmod -R 600 /root/.ssh 6 | -------------------------------------------------------------------------------- /test/client/hosts: -------------------------------------------------------------------------------- 1 | [localhost] 2 | host ansible_user=root ansible_port=22 ansible_ssh_private_key_file=keys/root/id_rsa 3 | -------------------------------------------------------------------------------- /test/client/localtest.yml: -------------------------------------------------------------------------------- 1 | - hosts: localhost 2 | become: yes 3 | become_method: sudo 4 | become_user: root 5 | vars: 6 | target_dir: ../../release 7 | gh_endpoint: http://json-server:3000 8 | gh_token: TESTTOKEN 9 | gh_org: soundtribe 10 | gh_teams: 11 | - name: sector9 12 | gid: 2019 13 | sudoers: true 14 | roles: 15 | - sectora 16 | -------------------------------------------------------------------------------- /test/client/sshconfig: -------------------------------------------------------------------------------- 1 | Host host 2 | StrictHostKeyChecking no 3 | UserKnownHostsFile=/dev/null 4 | LogLevel ERROR 5 | -------------------------------------------------------------------------------- /test/json-server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:24-alpine 2 | 3 | RUN npm install -g json-server@0.17.4 4 | 5 | WORKDIR /data 6 | 7 | COPY db.json /data/db.json 8 | COPY routes.json /data/routes.json 9 | 10 | EXPOSE 3000 11 | -------------------------------------------------------------------------------- /test/json-server/db.json: -------------------------------------------------------------------------------- 1 | { 2 | "rate_limit": { 3 | "resources": { 4 | "core": { 5 | "limit": 5000, 6 | "remaining": 4999, 7 | "reset": 1372700873 8 | }, 9 | "search": { 10 | "limit": 30, 11 | "remaining": 18, 12 | "reset": 1372697452 13 | }, 14 | "graphql": { 15 | "limit": 5000, 16 | "remaining": 4993, 17 | "reset": 1372700389 18 | }, 19 | "integration_manifest": { 20 | "limit": 5000, 21 | "remaining": 4999, 22 | "reset": 1551806725 23 | } 24 | }, 25 | "rate": { 26 | "limit": 5000, 27 | "remaining": 4999, 28 | "reset": 1372700873 29 | } 30 | }, 31 | "orgs.teams.soundtribe": [ 32 | { 33 | "id": 9, 34 | "node_id": "MDQ6VGVhbTE=", 35 | "url": "https://api.github.com/teams/1", 36 | "name": "sector9", 37 | "slug": "sector9", 38 | "description": "A great team.", 39 | "privacy": "closed", 40 | "permission": "admin", 41 | "members_url": "https://api.github.com/teams/1/members{/member}", 42 | "repositories_url": "https://api.github.com/teams/1/repos", 43 | "parent": null 44 | } 45 | ], 46 | "orgs.repos.soundtribe": [], 47 | "teams.members.9": [ 48 | { 49 | "login": "hunter", 50 | "id": 2001, 51 | "node_id": "MDQ6VXNlcjE=", 52 | "avatar_url": "https://github.com/images/error/hunter_happy.gif", 53 | "gravatar_id": "", 54 | "url": "https://api.github.com/users/hunter", 55 | "html_url": "https://github.com/hunter", 56 | "followers_url": "https://api.github.com/users/hunter/followers", 57 | "following_url": "https://api.github.com/users/hunter/following{/other_user}", 58 | "gists_url": "https://api.github.com/users/hunter/gists{/gist_id}", 59 | "starred_url": "https://api.github.com/users/hunter/starred{/owner}{/repo}", 60 | "subscriptions_url": "https://api.github.com/users/hunter/subscriptions", 61 | "organizations_url": "https://api.github.com/users/hunter/orgs", 62 | "repos_url": "https://api.github.com/users/hunter/repos", 63 | "events_url": "https://api.github.com/users/hunter/events{/privacy}", 64 | "received_events_url": "https://api.github.com/users/hunter/received_events", 65 | "type": "User", 66 | "site_admin": false 67 | } 68 | ], 69 | "users.keys.hunter": [ 70 | { 71 | "id": 1, 72 | "key": "ssh-rsa AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA dummy" 73 | }, 74 | { 75 | "id": 9, 76 | "key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCzu+O14D9UPxq0BsE3iQuNu0z1z96JcqWAic91VPsz4FlFY+lZxih9O/tmuRxBgkKEf4WlpqlKosQYAqXaWLH+3IXH9NdS2EocSHSVBCSLsxd5TEox6cTRMd/mXXolW6PtcMpM/tQiHO3IVhHCX0N7G0MOolw3AdmIGop+mpTNhy+aBkeNLKn6hs/I9MhAr8xoTVJgiHfclGfPUIDzWtwErJD4tcvgY2RF2zFuAauRorz1tbrA5+nnVdTcb+wzV4bycyd+91kkBfhzzrybxlu/ZtVR92gVwAcjATvUtI8oW+wobRWmHPLMs2aZbtCdUbDmAOgjYCu3V/gnxZ3VbGAV hunter@local" 77 | } 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /test/json-server/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "/rate_limit?page=:page": "/rate_limit", 3 | "/orgs/:org/teams?page=:page": "/orgs.teams.:org?_page=:page", 4 | "/orgs/:org/repos?page=:page": "/orgs.repos.:org?_page=:page", 5 | "/teams/:id/members?page=:page": "/teams.members.:id?_page=:page", 6 | "/users/:login/keys?page=:page": "/users.keys.:login?_page=:page" 7 | } 8 | -------------------------------------------------------------------------------- /test/keys/root/id_rsa: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn 3 | NhAAAAAwEAAQAAAQEAwl6r0sjwYwcEXa8WhInNxGdYc+IXlA3swpyt8dFJ8vugT0ICuxVS 4 | ETgJXyj8xEh+hMD/wVuSvNbUt1VsKvx9FwHDwVkR/iRcgrwhx674gWssgR/83H8ATxnfqg 5 | Ajb7sb5Vt48EF7vVFPdiR3IlTMYLYbPidBpZd3V8RTsLSHZmm9wbQx9PxSgJBMYVbdr2zm 6 | xEuoIDpN0h04dS/uJdcvaA8LvS3496xfaUqmCRXE1prlAfeGN7qrY5Qetorv2Z/CvCefIJ 7 | uNPBDVLlGBzbD0FW1lby0UBvpkj/6LJt0tKvLwnrp/t8E6ITjo/qE8ei69ses4a8IgY8r7 8 | AFA1OdrEewAAA9AtDZtYLQ2bWAAAAAdzc2gtcnNhAAABAQDCXqvSyPBjBwRdrxaEic3EZ1 9 | hz4heUDezCnK3x0Uny+6BPQgK7FVIROAlfKPzESH6EwP/BW5K81tS3VWwq/H0XAcPBWRH+ 10 | JFyCvCHHrviBayyBH/zcfwBPGd+qACNvuxvlW3jwQXu9UU92JHciVMxgths+J0Gll3dXxF 11 | OwtIdmab3BtDH0/FKAkExhVt2vbObES6ggOk3SHTh1L+4l1y9oDwu9Lfj3rF9pSqYJFcTW 12 | muUB94Y3uqtjlB62iu/Zn8K8J58gm408ENUuUYHNsPQVbWVvLRQG+mSP/osm3S0q8vCeun 13 | +3wTohOOj+oTx6Lr2x6zhrwiBjyvsAUDU52sR7AAAAAwEAAQAAAQAI+jxvcO6BdGqENTkS 14 | CBdj8e4I9DFomjgMSRZTq/oBahPedUsQ/wwaVX9BUPBT1JFbalqlwKgHZtjOWvizB5Rzgp 15 | ZbENUe6ukG9M+OnItH1v5oPGT+fjMydBx7iqQYXgkMz+vHFQ81EFNePpLuGAKUmPSrKz8B 16 | +fv+JshCyiPS3AyqCwqTLKet82sxcdYDx1x2/8NWOEDEthbfQ5BQumDltLEl2bINKexfwi 17 | wnurgkUyRv0nOH9UV18lNy7ApwQLM1t55H6S9mH/olXKj9swwkB5vicX0tkC1DTdgjkl/s 18 | FV7cy5Bgi+6ygWjKh+m53tOaBytQLVNYbymAz3QfVZDpAAAAgC1CQlg82xW2xbv607mQpt 19 | wjbySJTyE1X1dDIZ+rms6ESs5WP6ZFKimg6rRyjR4ZSOqVF5xdAjI+nsn+CDRxJgscAnrx 20 | PptKxKRtZyUffTNmesSFwpUFxxBn/WYpJ7EWecxGeOoUrZT0YcZXEjcr+nJK3ITvobdwH/ 21 | lQysecCObCAAAAgQD+Vfc7R44C+ix5MKCltVg4XdzCzvpsH9QkLQhiVX6lzlbrvtHmuqUr 22 | GC98+ISRolXkIGQmeJujspyzXgRnJ9+3SG0rYGgcvM+2JEpKs5bYduiDY7yKui8oGZxE9I 23 | z66JfvE0VxfbWTVgtOmw5RocyCRIkrMLMN5F5gYrLiZZ3ORQAAAIEAw6RB29mO4h80HTPW 24 | p9Ba2wYWEQrrkGzyxCmAouCcDstl2T7RIKCoA+JFMzi/5Q5FyOW6q5K8zmrfykyk3Wks70 25 | EViVtEU+Kt8VdtmjCt9B4TOag0cAIr1RMWSnl6x2fhevn0/6vE2A/tIk0v8jiqKcOU2cgS 26 | PedSDbiDHKLf078AAAAXeWFzdXl1a3lAZmV2ZXJmZXcubG9jYWwBAgME 27 | -----END OPENSSH PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/keys/root/id_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDCXqvSyPBjBwRdrxaEic3EZ1hz4heUDezCnK3x0Uny+6BPQgK7FVIROAlfKPzESH6EwP/BW5K81tS3VWwq/H0XAcPBWRH+JFyCvCHHrviBayyBH/zcfwBPGd+qACNvuxvlW3jwQXu9UU92JHciVMxgths+J0Gll3dXxFOwtIdmab3BtDH0/FKAkExhVt2vbObES6ggOk3SHTh1L+4l1y9oDwu9Lfj3rF9pSqYJFcTWmuUB94Y3uqtjlB62iu/Zn8K8J58gm408ENUuUYHNsPQVbWVvLRQG+mSP/osm3S0q8vCeun+3wTohOOj+oTx6Lr2x6zhrwiBjyvsAUDU52sR7 yasuyuky@feverfew.local 2 | -------------------------------------------------------------------------------- /test/keys/user/id_rsa: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABFwAAAAdzc2gtcn 3 | NhAAAAAwEAAQAAAQEAs7vjteA/VD8atAbBN4kLjbtM9c/eiXKlgInPdVT7M+BZRWPpWcYo 4 | fTv7ZrkcQYJChH+FpaapSqLEGAKl2lix/tyFx/TXUthKHEh0lQQki7MXeUxKMenE0THf5l 5 | 16JVuj7XDKTP7UIhztyFYRwl9DextDDqJcNwHZiBqKfpqUzYcvmgZHjSyp+obPyPTIQK/M 6 | aE1SYIh33JRnz1CA81rcBKyQ+LXL4GNkRdsxbgGrkaK89bW6wOfp51XU3G/sM1eG8nMnfv 7 | dZJAX4c868m8Zbv2bVUfdoFcAHIwE71LSPKFvsKG0VphzyzLNmmW7QnVGw5gDoI2Art1f4 8 | J8Wd1WxgFQAAA9DQzZ460M2eOgAAAAdzc2gtcnNhAAABAQCzu+O14D9UPxq0BsE3iQuNu0 9 | z1z96JcqWAic91VPsz4FlFY+lZxih9O/tmuRxBgkKEf4WlpqlKosQYAqXaWLH+3IXH9NdS 10 | 2EocSHSVBCSLsxd5TEox6cTRMd/mXXolW6PtcMpM/tQiHO3IVhHCX0N7G0MOolw3AdmIGo 11 | p+mpTNhy+aBkeNLKn6hs/I9MhAr8xoTVJgiHfclGfPUIDzWtwErJD4tcvgY2RF2zFuAauR 12 | orz1tbrA5+nnVdTcb+wzV4bycyd+91kkBfhzzrybxlu/ZtVR92gVwAcjATvUtI8oW+wobR 13 | WmHPLMs2aZbtCdUbDmAOgjYCu3V/gnxZ3VbGAVAAAAAwEAAQAAAQAQUKTp9JIrFpNY9igB 14 | 34nR8seYpKbhuSt20Iupbe5jliDkYJ5lDMzOGWzHtVPwSl+5YU4DbG5/nOjJ+SuO93Ao32 15 | GxdfM5zPJlQNp8UGT03WvrEdbGUx8PkkRtx9x3mar2ub9TX+pnslKPVejEyRr6CM58fJZ8 16 | U8moRih+N4/8W/sU4DqEZsOY0CmJ80UVjmUdWN4ak0Qo8d2rYkdqk/JtHJDl/o2aCWVY/A 17 | lzDs4boPe/C9hPsm0DUJjCc++SSOV46IoVa0BqvezUDrov26vYf4kvdCiJhb8htNMUQTtN 18 | WCRsnNjSnMkR/cv4PUYmILWj6WaYjlTxQHkh6zdd/dTdAAAAgQDZxQoieivpUAXrrTu0Bl 19 | wOa4cK1VGh8MD85HtJ6IPfdDWAxF2LsatpvQmTQDPs49WwyWSCZ+B5sPFQWxFglRtcdpne 20 | ElcPSPD/pmTUW2uBIv1bHmLGEewXYGG5hjQvxLnjGRAJITcaiiTuq4NCPmD3TpX9xbUuRF 21 | dU3CsxNooudgAAAIEA5tu+prpwSKf3LVJoOpYyNcf+cPprLKo2VQj3ZSBJ2uRb6IUVOe3L 22 | XBJY72UkSuKzFXaDL1v5+yuQ870v+/LRqSlo3zhDwDGofk+WuZKFVTlkKf2coyWoNOT8Uo 23 | OaWt90UN6jOC3yNvlwiQdDZgiBfPoQRpr64pBHVTc62iIhuYcAAACBAMdOz/sqCV2CxIyk 24 | CuUojhV/N4mX+R1msZ2sOseHs3OiDudtUzcUrPjoh55Kx42XLdHtHrDwMZePl28Vfpe5jZ 25 | RhX9/LkkEeKE649UINsURPjtbr/HFL6X197Jt3u2w+xeEN6zJPQotWV/8BcOWjBSSIRmp0 26 | AFptMKLWMPGFXhCDAAAAF3lhc3V5dWt5QGZldmVyZmV3LmxvY2FsAQID 27 | -----END OPENSSH PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /test/keys/user/id_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCzu+O14D9UPxq0BsE3iQuNu0z1z96JcqWAic91VPsz4FlFY+lZxih9O/tmuRxBgkKEf4WlpqlKosQYAqXaWLH+3IXH9NdS2EocSHSVBCSLsxd5TEox6cTRMd/mXXolW6PtcMpM/tQiHO3IVhHCX0N7G0MOolw3AdmIGop+mpTNhy+aBkeNLKn6hs/I9MhAr8xoTVJgiHfclGfPUIDzWtwErJD4tcvgY2RF2zFuAauRorz1tbrA5+nnVdTcb+wzV4bycyd+91kkBfhzzrybxlu/ZtVR92gVwAcjATvUtI8oW+wobRWmHPLMs2aZbtCdUbDmAOgjYCu3V/gnxZ3VbGAV yasuyuky@feverfew.local 2 | --------------------------------------------------------------------------------