├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── TODO.md ├── build ├── Vagrantfile ├── binaries │ └── .gitkeep ├── bootstrap_root.sh └── bootstrap_vagrant.sh ├── ci └── install.sh ├── examples ├── commands.md ├── commands.txt └── ssh-permit.json ├── rustfmt.toml ├── src ├── cli_flow.rs ├── database.rs ├── main.rs ├── ssh_config.rs ├── subcommand_group.rs ├── subcommand_host.rs ├── subcommand_howto.rs ├── subcommand_sync.rs └── subcommand_user.rs └── tests ├── fixtures └── ssh-permit.json ├── integration.rs └── tmp └── .gitkeep /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | tests/tmp/ 3 | .vagrant/ 4 | ssh-permit.json 5 | build/binaries/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # Based on the "trust" template v0.1.2 2 | # https://github.com/japaric/trust/tree/v0.1.2 3 | 4 | dist: trusty 5 | language: rust 6 | services: docker 7 | sudo: required 8 | 9 | env: 10 | global: 11 | - CRATE_NAME=ssh_permit_a38 12 | 13 | matrix: 14 | include: 15 | # Linux 16 | - env: TARGET=i686-unknown-linux-gnu 17 | - env: TARGET=x86_64-unknown-linux-gnu 18 | 19 | before_install: 20 | - set -e 21 | - rustup self update 22 | 23 | install: 24 | - sh ci/install.sh 25 | - source ~/.cargo/env || true 26 | 27 | script: 28 | - make test 29 | 30 | after_script: set +e 31 | 32 | cache: cargo 33 | before_cache: 34 | # Travis can't cache files that are not readable by "others" 35 | - chmod -R a+r $HOME/.cargo 36 | 37 | branches: 38 | only: 39 | # release tags 40 | - /^v\d+\.\d+\.\d+.*$/ 41 | - develop 42 | - master 43 | 44 | notifications: 45 | email: 46 | - boerni@gmail.com 47 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # SSH Permit A38 - Changelog 2 | 3 | ## v0.2.0 - 2018-08-18 4 | 5 | - Support for SSH config files [#5](https://github.com/ierror/ssh-permit-a38/issues/5) 6 | 7 | If a ssh-permit-a38 hostname or alias matches the ssh configs Host (or Hostname), User, Port and Host information are used for authorized_keys sync connection 8 | 9 | - sync command switch -y, --yes-authorized-keys-prompt: Automatic yes to authorized_keys location prompts 10 | 11 | 12 | ## v0.1.0 - 2018-04-01 13 | 14 | - Support for SSH agent authentication [#4](https://github.com/ierror/ssh-permit-a38/issues/4) - Thank you [@kdar:](https://github.com/kdar:) 15 | 16 | - Support for host aliases [#2](https://github.com/ierror/ssh-permit-a38/issues/2): 17 | 18 | - Set an alias "um" for hostname "urlsmash.403.io" 19 | ``` 20 | ssh-permit-a38 host urlsmash.403.io alias um 21 | ``` 22 | 23 | After this point you can use the alias or the hostname for all host related commands. 24 | 25 | - Remove an alias for hostname "urlsmash.403.io" 26 | ``` 27 | ssh-permit-a38 host urlsmash.403.io alias 28 | ``` 29 | 30 | - Vagrant files and Makefile targets to build linux releases 31 | 32 | - Fixed Typos [#1](https://github.com/ierror/ssh-permit-a38/issues/1) [#3](https://github.com/ierror/ssh-permit-a38/issues/3) - Thank you [@0xflotus](https://github.com/0xflotus) and [@robwetz](https://github.com/robwetz) 33 | 34 | 35 | ## v0.0.1 - 2018-03-18 36 | 37 | - initial release -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | [[package]] 2 | name = "ansi_term" 3 | version = "0.10.2" 4 | source = "registry+https://github.com/rust-lang/crates.io-index" 5 | 6 | [[package]] 7 | name = "assert_cli" 8 | version = "0.5.4" 9 | source = "registry+https://github.com/rust-lang/crates.io-index" 10 | dependencies = [ 11 | "colored 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", 12 | "difference 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 13 | "environment 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 14 | "error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", 15 | "serde_json 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", 16 | "skeptic 0.13.2 (registry+https://github.com/rust-lang/crates.io-index)", 17 | ] 18 | 19 | [[package]] 20 | name = "atty" 21 | version = "0.2.6" 22 | source = "registry+https://github.com/rust-lang/crates.io-index" 23 | dependencies = [ 24 | "libc 0.2.37 (registry+https://github.com/rust-lang/crates.io-index)", 25 | "termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)", 26 | "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 27 | ] 28 | 29 | [[package]] 30 | name = "backtrace" 31 | version = "0.3.5" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | dependencies = [ 34 | "backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", 35 | "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 36 | "libc 0.2.37 (registry+https://github.com/rust-lang/crates.io-index)", 37 | "rustc-demangle 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", 38 | "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 39 | ] 40 | 41 | [[package]] 42 | name = "backtrace-sys" 43 | version = "0.1.16" 44 | source = "registry+https://github.com/rust-lang/crates.io-index" 45 | dependencies = [ 46 | "cc 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 47 | "libc 0.2.37 (registry+https://github.com/rust-lang/crates.io-index)", 48 | ] 49 | 50 | [[package]] 51 | name = "bitflags" 52 | version = "0.7.0" 53 | source = "registry+https://github.com/rust-lang/crates.io-index" 54 | 55 | [[package]] 56 | name = "bitflags" 57 | version = "0.9.1" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | 60 | [[package]] 61 | name = "bitflags" 62 | version = "1.0.1" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | 65 | [[package]] 66 | name = "bytecount" 67 | version = "0.2.0" 68 | source = "registry+https://github.com/rust-lang/crates.io-index" 69 | 70 | [[package]] 71 | name = "cargo_metadata" 72 | version = "0.3.3" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | dependencies = [ 75 | "error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", 76 | "semver 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", 77 | "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", 78 | "serde_derive 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", 79 | "serde_json 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", 80 | ] 81 | 82 | [[package]] 83 | name = "cc" 84 | version = "1.0.4" 85 | source = "registry+https://github.com/rust-lang/crates.io-index" 86 | 87 | [[package]] 88 | name = "cfg-if" 89 | version = "0.1.2" 90 | source = "registry+https://github.com/rust-lang/crates.io-index" 91 | 92 | [[package]] 93 | name = "chrono" 94 | version = "0.4.0" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | dependencies = [ 97 | "num 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", 98 | "time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)", 99 | ] 100 | 101 | [[package]] 102 | name = "clap" 103 | version = "2.30.0" 104 | source = "registry+https://github.com/rust-lang/crates.io-index" 105 | dependencies = [ 106 | "ansi_term 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)", 107 | "atty 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", 108 | "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 109 | "strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", 110 | "textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", 111 | "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 112 | "vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)", 113 | ] 114 | 115 | [[package]] 116 | name = "cmake" 117 | version = "0.1.29" 118 | source = "registry+https://github.com/rust-lang/crates.io-index" 119 | dependencies = [ 120 | "cc 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 121 | ] 122 | 123 | [[package]] 124 | name = "colored" 125 | version = "1.6.0" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | dependencies = [ 128 | "lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 129 | ] 130 | 131 | [[package]] 132 | name = "difference" 133 | version = "1.0.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | 136 | [[package]] 137 | name = "difference" 138 | version = "2.0.0" 139 | source = "registry+https://github.com/rust-lang/crates.io-index" 140 | 141 | [[package]] 142 | name = "dtoa" 143 | version = "0.4.2" 144 | source = "registry+https://github.com/rust-lang/crates.io-index" 145 | 146 | [[package]] 147 | name = "environment" 148 | version = "0.1.1" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | 151 | [[package]] 152 | name = "error-chain" 153 | version = "0.11.0" 154 | source = "registry+https://github.com/rust-lang/crates.io-index" 155 | dependencies = [ 156 | "backtrace 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", 157 | ] 158 | 159 | [[package]] 160 | name = "fuchsia-zircon" 161 | version = "0.3.3" 162 | source = "registry+https://github.com/rust-lang/crates.io-index" 163 | dependencies = [ 164 | "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", 165 | "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 166 | ] 167 | 168 | [[package]] 169 | name = "fuchsia-zircon-sys" 170 | version = "0.3.3" 171 | source = "registry+https://github.com/rust-lang/crates.io-index" 172 | 173 | [[package]] 174 | name = "glob" 175 | version = "0.2.11" 176 | source = "registry+https://github.com/rust-lang/crates.io-index" 177 | 178 | [[package]] 179 | name = "itoa" 180 | version = "0.3.4" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | 183 | [[package]] 184 | name = "kernel32-sys" 185 | version = "0.2.2" 186 | source = "registry+https://github.com/rust-lang/crates.io-index" 187 | dependencies = [ 188 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 189 | "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 190 | ] 191 | 192 | [[package]] 193 | name = "lazy_static" 194 | version = "0.2.11" 195 | source = "registry+https://github.com/rust-lang/crates.io-index" 196 | 197 | [[package]] 198 | name = "libc" 199 | version = "0.2.37" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | 202 | [[package]] 203 | name = "libssh2-sys" 204 | version = "0.2.6" 205 | source = "registry+https://github.com/rust-lang/crates.io-index" 206 | dependencies = [ 207 | "cmake 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)", 208 | "libc 0.2.37 (registry+https://github.com/rust-lang/crates.io-index)", 209 | "libz-sys 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)", 210 | "openssl-sys 0.9.27 (registry+https://github.com/rust-lang/crates.io-index)", 211 | "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", 212 | ] 213 | 214 | [[package]] 215 | name = "libz-sys" 216 | version = "1.0.18" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | dependencies = [ 219 | "cc 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 220 | "libc 0.2.37 (registry+https://github.com/rust-lang/crates.io-index)", 221 | "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", 222 | "vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 223 | ] 224 | 225 | [[package]] 226 | name = "num" 227 | version = "0.1.42" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | dependencies = [ 230 | "num-integer 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", 231 | "num-iter 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)", 232 | "num-traits 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 233 | ] 234 | 235 | [[package]] 236 | name = "num-integer" 237 | version = "0.1.36" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | dependencies = [ 240 | "num-traits 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 241 | ] 242 | 243 | [[package]] 244 | name = "num-iter" 245 | version = "0.1.35" 246 | source = "registry+https://github.com/rust-lang/crates.io-index" 247 | dependencies = [ 248 | "num-integer 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)", 249 | "num-traits 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 250 | ] 251 | 252 | [[package]] 253 | name = "num-traits" 254 | version = "0.2.1" 255 | source = "registry+https://github.com/rust-lang/crates.io-index" 256 | 257 | [[package]] 258 | name = "openssl-sys" 259 | version = "0.9.27" 260 | source = "registry+https://github.com/rust-lang/crates.io-index" 261 | dependencies = [ 262 | "cc 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 263 | "libc 0.2.37 (registry+https://github.com/rust-lang/crates.io-index)", 264 | "pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)", 265 | "vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 266 | ] 267 | 268 | [[package]] 269 | name = "pkg-config" 270 | version = "0.3.9" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | 273 | [[package]] 274 | name = "pulldown-cmark" 275 | version = "0.1.2" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | dependencies = [ 278 | "bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)", 279 | ] 280 | 281 | [[package]] 282 | name = "quote" 283 | version = "0.3.15" 284 | source = "registry+https://github.com/rust-lang/crates.io-index" 285 | 286 | [[package]] 287 | name = "rand" 288 | version = "0.4.2" 289 | source = "registry+https://github.com/rust-lang/crates.io-index" 290 | dependencies = [ 291 | "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 292 | "libc 0.2.37 (registry+https://github.com/rust-lang/crates.io-index)", 293 | "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 294 | ] 295 | 296 | [[package]] 297 | name = "redox_syscall" 298 | version = "0.1.37" 299 | source = "registry+https://github.com/rust-lang/crates.io-index" 300 | 301 | [[package]] 302 | name = "redox_termios" 303 | version = "0.1.1" 304 | source = "registry+https://github.com/rust-lang/crates.io-index" 305 | dependencies = [ 306 | "redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", 307 | ] 308 | 309 | [[package]] 310 | name = "remove_dir_all" 311 | version = "0.3.0" 312 | source = "registry+https://github.com/rust-lang/crates.io-index" 313 | dependencies = [ 314 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 315 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 316 | ] 317 | 318 | [[package]] 319 | name = "rpassword" 320 | version = "1.0.2" 321 | source = "registry+https://github.com/rust-lang/crates.io-index" 322 | dependencies = [ 323 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 324 | "libc 0.2.37 (registry+https://github.com/rust-lang/crates.io-index)", 325 | "rprompt 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)", 326 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 327 | ] 328 | 329 | [[package]] 330 | name = "rprompt" 331 | version = "1.0.3" 332 | source = "registry+https://github.com/rust-lang/crates.io-index" 333 | 334 | [[package]] 335 | name = "rustc-demangle" 336 | version = "0.1.7" 337 | source = "registry+https://github.com/rust-lang/crates.io-index" 338 | 339 | [[package]] 340 | name = "same-file" 341 | version = "0.1.3" 342 | source = "registry+https://github.com/rust-lang/crates.io-index" 343 | dependencies = [ 344 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 345 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 346 | ] 347 | 348 | [[package]] 349 | name = "semver" 350 | version = "0.8.0" 351 | source = "registry+https://github.com/rust-lang/crates.io-index" 352 | dependencies = [ 353 | "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", 354 | "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", 355 | ] 356 | 357 | [[package]] 358 | name = "semver-parser" 359 | version = "0.7.0" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | 362 | [[package]] 363 | name = "serde" 364 | version = "1.0.27" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | 367 | [[package]] 368 | name = "serde_derive" 369 | version = "1.0.27" 370 | source = "registry+https://github.com/rust-lang/crates.io-index" 371 | dependencies = [ 372 | "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 373 | "serde_derive_internals 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)", 374 | "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", 375 | ] 376 | 377 | [[package]] 378 | name = "serde_derive_internals" 379 | version = "0.19.0" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | dependencies = [ 382 | "syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)", 383 | "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", 384 | ] 385 | 386 | [[package]] 387 | name = "serde_json" 388 | version = "1.0.10" 389 | source = "registry+https://github.com/rust-lang/crates.io-index" 390 | dependencies = [ 391 | "dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 392 | "itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 393 | "num-traits 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", 394 | "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", 395 | ] 396 | 397 | [[package]] 398 | name = "skeptic" 399 | version = "0.13.2" 400 | source = "registry+https://github.com/rust-lang/crates.io-index" 401 | dependencies = [ 402 | "bytecount 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", 403 | "cargo_metadata 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", 404 | "error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", 405 | "glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", 406 | "pulldown-cmark 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", 407 | "serde_json 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", 408 | "tempdir 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", 409 | "walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", 410 | ] 411 | 412 | [[package]] 413 | name = "ssh2" 414 | version = "0.3.2" 415 | source = "registry+https://github.com/rust-lang/crates.io-index" 416 | dependencies = [ 417 | "bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", 418 | "libc 0.2.37 (registry+https://github.com/rust-lang/crates.io-index)", 419 | "libssh2-sys 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", 420 | ] 421 | 422 | [[package]] 423 | name = "ssh_permit_a38" 424 | version = "0.2.0" 425 | dependencies = [ 426 | "assert_cli 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", 427 | "chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 428 | "clap 2.30.0 (registry+https://github.com/rust-lang/crates.io-index)", 429 | "colored 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)", 430 | "difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)", 431 | "rpassword 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", 432 | "serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", 433 | "serde_derive 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)", 434 | "serde_json 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)", 435 | "ssh2 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)", 436 | ] 437 | 438 | [[package]] 439 | name = "strsim" 440 | version = "0.7.0" 441 | source = "registry+https://github.com/rust-lang/crates.io-index" 442 | 443 | [[package]] 444 | name = "syn" 445 | version = "0.11.11" 446 | source = "registry+https://github.com/rust-lang/crates.io-index" 447 | dependencies = [ 448 | "quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)", 449 | "synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)", 450 | "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 451 | ] 452 | 453 | [[package]] 454 | name = "synom" 455 | version = "0.11.3" 456 | source = "registry+https://github.com/rust-lang/crates.io-index" 457 | dependencies = [ 458 | "unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", 459 | ] 460 | 461 | [[package]] 462 | name = "tempdir" 463 | version = "0.3.6" 464 | source = "registry+https://github.com/rust-lang/crates.io-index" 465 | dependencies = [ 466 | "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", 467 | "remove_dir_all 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)", 468 | ] 469 | 470 | [[package]] 471 | name = "termion" 472 | version = "1.5.1" 473 | source = "registry+https://github.com/rust-lang/crates.io-index" 474 | dependencies = [ 475 | "libc 0.2.37 (registry+https://github.com/rust-lang/crates.io-index)", 476 | "redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", 477 | "redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", 478 | ] 479 | 480 | [[package]] 481 | name = "textwrap" 482 | version = "0.9.0" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | dependencies = [ 485 | "unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)", 486 | ] 487 | 488 | [[package]] 489 | name = "time" 490 | version = "0.1.39" 491 | source = "registry+https://github.com/rust-lang/crates.io-index" 492 | dependencies = [ 493 | "libc 0.2.37 (registry+https://github.com/rust-lang/crates.io-index)", 494 | "redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)", 495 | "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", 496 | ] 497 | 498 | [[package]] 499 | name = "unicode-width" 500 | version = "0.1.4" 501 | source = "registry+https://github.com/rust-lang/crates.io-index" 502 | 503 | [[package]] 504 | name = "unicode-xid" 505 | version = "0.0.4" 506 | source = "registry+https://github.com/rust-lang/crates.io-index" 507 | 508 | [[package]] 509 | name = "vcpkg" 510 | version = "0.2.2" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | 513 | [[package]] 514 | name = "vec_map" 515 | version = "0.8.0" 516 | source = "registry+https://github.com/rust-lang/crates.io-index" 517 | 518 | [[package]] 519 | name = "walkdir" 520 | version = "1.0.7" 521 | source = "registry+https://github.com/rust-lang/crates.io-index" 522 | dependencies = [ 523 | "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", 524 | "same-file 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)", 525 | "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", 526 | ] 527 | 528 | [[package]] 529 | name = "winapi" 530 | version = "0.2.8" 531 | source = "registry+https://github.com/rust-lang/crates.io-index" 532 | 533 | [[package]] 534 | name = "winapi" 535 | version = "0.3.4" 536 | source = "registry+https://github.com/rust-lang/crates.io-index" 537 | dependencies = [ 538 | "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 539 | "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", 540 | ] 541 | 542 | [[package]] 543 | name = "winapi-build" 544 | version = "0.1.1" 545 | source = "registry+https://github.com/rust-lang/crates.io-index" 546 | 547 | [[package]] 548 | name = "winapi-i686-pc-windows-gnu" 549 | version = "0.4.0" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | 552 | [[package]] 553 | name = "winapi-x86_64-pc-windows-gnu" 554 | version = "0.4.0" 555 | source = "registry+https://github.com/rust-lang/crates.io-index" 556 | 557 | [metadata] 558 | "checksum ansi_term 0.10.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6b3568b48b7cefa6b8ce125f9bb4989e52fbcc29ebea88df04cc7c5f12f70455" 559 | "checksum assert_cli 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "72342c21057a3cb5f7c2d849bf7999a83795434dd36d74fa8c24680581bd1930" 560 | "checksum atty 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "8352656fd42c30a0c3c89d26dea01e3b77c0ab2af18230835c15e2e13cd51859" 561 | "checksum backtrace 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ebbbf59b1c43eefa8c3ede390fcc36820b4999f7914104015be25025e0d62af2" 562 | "checksum backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "44585761d6161b0f57afc49482ab6bd067e4edef48c12a152c237eb0203f7661" 563 | "checksum bitflags 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "aad18937a628ec6abcd26d1489012cc0e18c21798210f491af69ded9b881106d" 564 | "checksum bitflags 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "4efd02e230a02e18f92fc2735f44597385ed02ad8f831e7c1c1156ee5e1ab3a5" 565 | "checksum bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b3c30d3802dfb7281680d6285f2ccdaa8c2d8fee41f93805dba5c4cf50dc23cf" 566 | "checksum bytecount 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "af27422163679dea46a1a7239dffff64d3dcdc3ba5fe9c49c789fbfe0eb949de" 567 | "checksum cargo_metadata 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1f56ec3e469bca7c276f2eea015aa05c5e381356febdbb0683c2580189604537" 568 | "checksum cc 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "deaf9ec656256bb25b404c51ef50097207b9cbb29c933d31f92cae5a8a0ffee0" 569 | "checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de" 570 | "checksum chrono 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "7c20ebe0b2b08b0aeddba49c609fe7957ba2e33449882cb186a180bc60682fa9" 571 | "checksum clap 2.30.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1c07b9257a00f3fc93b7f3c417fc15607ec7a56823bc2c37ec744e266387de5b" 572 | "checksum cmake 0.1.29 (registry+https://github.com/rust-lang/crates.io-index)" = "56d741ea7a69e577f6d06b36b7dff4738f680593dc27a701ffa8506b73ce28bb" 573 | "checksum colored 1.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b0aa3473e85a3161b59845d6096b289bb577874cafeaf75ea1b1beaa6572c7fc" 574 | "checksum difference 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3304d19798a8e067e48d8e69b2c37f0b5e9b4e462504ad9e27e9f3fce02bba8" 575 | "checksum difference 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "524cbf6897b527295dff137cec09ecf3a05f4fddffd7dfcd1585403449e74198" 576 | "checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab" 577 | "checksum environment 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1f4b14e20978669064c33b4c1e0fb4083412e40fe56cbea2eae80fd7591503ee" 578 | "checksum error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3" 579 | "checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" 580 | "checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" 581 | "checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb" 582 | "checksum itoa 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8324a32baf01e2ae060e9de58ed0bc2320c9a2833491ee36cd3b4c414de4db8c" 583 | "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 584 | "checksum lazy_static 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" 585 | "checksum libc 0.2.37 (registry+https://github.com/rust-lang/crates.io-index)" = "56aebce561378d99a0bb578f8cb15b6114d2a1814a6c7949bbe646d968bb4fa9" 586 | "checksum libssh2-sys 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)" = "0db4ec23611747ef772db1c4d650f8bd762f07b461727ec998f953c614024b75" 587 | "checksum libz-sys 1.0.18 (registry+https://github.com/rust-lang/crates.io-index)" = "87f737ad6cc6fd6eefe3d9dc5412f1573865bded441300904d2f42269e140f16" 588 | "checksum num 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)" = "4703ad64153382334aa8db57c637364c322d3372e097840c72000dabdcf6156e" 589 | "checksum num-integer 0.1.36 (registry+https://github.com/rust-lang/crates.io-index)" = "f8d26da319fb45674985c78f1d1caf99aa4941f785d384a2ae36d0740bc3e2fe" 590 | "checksum num-iter 0.1.35 (registry+https://github.com/rust-lang/crates.io-index)" = "4b226df12c5a59b63569dd57fafb926d91b385dfce33d8074a412411b689d593" 591 | "checksum num-traits 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "0b3c2bd9b9d21e48e956b763c9f37134dc62d9e95da6edb3f672cacb6caf3cd3" 592 | "checksum openssl-sys 0.9.27 (registry+https://github.com/rust-lang/crates.io-index)" = "d6fdc5c4a02e69ce65046f1763a0181107038e02176233acb0b3351d7cc588f9" 593 | "checksum pkg-config 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "3a8b4c6b8165cd1a1cd4b9b120978131389f64bdaf456435caa41e630edba903" 594 | "checksum pulldown-cmark 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d6fdf85cda6cadfae5428a54661d431330b312bc767ddbc57adbedc24da66e32" 595 | "checksum quote 0.3.15 (registry+https://github.com/rust-lang/crates.io-index)" = "7a6e920b65c65f10b2ae65c831a81a073a89edd28c7cce89475bff467ab4167a" 596 | "checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5" 597 | "checksum redox_syscall 0.1.37 (registry+https://github.com/rust-lang/crates.io-index)" = "0d92eecebad22b767915e4d529f89f28ee96dbbf5a4810d2b844373f136417fd" 598 | "checksum redox_termios 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7e891cfe48e9100a70a3b6eb652fef28920c117d366339687bd5576160db0f76" 599 | "checksum remove_dir_all 0.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b5d2f806b0fcdabd98acd380dc8daef485e22bcb7cddc811d1337967f2528cf5" 600 | "checksum rpassword 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b273c91bd242ca03ad6d71c143b6f17a48790e61f21a6c78568fa2b6774a24a4" 601 | "checksum rprompt 1.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "1601f32bc5858aae3cbfa1c645c96c4d820cc5c16be0194f089560c00b6eb625" 602 | "checksum rustc-demangle 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11fb43a206a04116ffd7cfcf9bcb941f8eb6cc7ff667272246b0a1c74259a3cb" 603 | "checksum same-file 0.1.3 (registry+https://github.com/rust-lang/crates.io-index)" = "d931a44fdaa43b8637009e7632a02adc4f2b2e0733c08caa4cf00e8da4a117a7" 604 | "checksum semver 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bee2bc909ab2d8d60dab26e8cad85b25d795b14603a0dcb627b78b9d30b6454b" 605 | "checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" 606 | "checksum serde 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)" = "db99f3919e20faa51bb2996057f5031d8685019b5a06139b1ce761da671b8526" 607 | "checksum serde_derive 1.0.27 (registry+https://github.com/rust-lang/crates.io-index)" = "f4ba7591cfe93755e89eeecdbcc668885624829b020050e6aec99c2a03bd3fd0" 608 | "checksum serde_derive_internals 0.19.0 (registry+https://github.com/rust-lang/crates.io-index)" = "6e03f1c9530c3fb0a0a5c9b826bdd9246a5921ae995d75f512ac917fc4dd55b5" 609 | "checksum serde_json 1.0.10 (registry+https://github.com/rust-lang/crates.io-index)" = "57781ed845b8e742fc2bf306aba8e3b408fe8c366b900e3769fbc39f49eb8b39" 610 | "checksum skeptic 0.13.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c8431f8fca168e2db4be547bd8329eac70d095dff1444fee4b0fa0fabc7df75a" 611 | "checksum ssh2 0.3.2 (registry+https://github.com/rust-lang/crates.io-index)" = "85d5183afb29cb2323ffcf0f0e026c30ed9d8fb5d8e95a9f1ccb5dbe1c032d9b" 612 | "checksum strsim 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bb4f380125926a99e52bc279241539c018323fab05ad6368b56f93d9369ff550" 613 | "checksum syn 0.11.11 (registry+https://github.com/rust-lang/crates.io-index)" = "d3b891b9015c88c576343b9b3e41c2c11a51c219ef067b264bd9c8aa9b441dad" 614 | "checksum synom 0.11.3 (registry+https://github.com/rust-lang/crates.io-index)" = "a393066ed9010ebaed60b9eafa373d4b1baac186dd7e008555b0f702b51945b6" 615 | "checksum tempdir 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "f73eebdb68c14bcb24aef74ea96079830e7fa7b31a6106e42ea7ee887c1e134e" 616 | "checksum termion 1.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "689a3bdfaab439fd92bc87df5c4c78417d3cbe537487274e9b0b2dce76e92096" 617 | "checksum textwrap 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c0b59b6b4b44d867f1370ef1bd91bfb262bf07bf0ae65c202ea2fbc16153b693" 618 | "checksum time 0.1.39 (registry+https://github.com/rust-lang/crates.io-index)" = "a15375f1df02096fb3317256ce2cee6a1f42fc84ea5ad5fc8c421cfe40c73098" 619 | "checksum unicode-width 0.1.4 (registry+https://github.com/rust-lang/crates.io-index)" = "bf3a113775714a22dcb774d8ea3655c53a32debae63a063acc00a91cc586245f" 620 | "checksum unicode-xid 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8c1f860d7d29cf02cb2f3f359fd35991af3d30bac52c57d265a3c461074cb4dc" 621 | "checksum vcpkg 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9e0a7d8bed3178a8fb112199d466eeca9ed09a14ba8ad67718179b4fd5487d0b" 622 | "checksum vec_map 0.8.0 (registry+https://github.com/rust-lang/crates.io-index)" = "887b5b631c2ad01628bbbaa7dd4c869f80d3186688f8d0b6f58774fbe324988c" 623 | "checksum walkdir 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)" = "bb08f9e670fab86099470b97cd2b252d6527f0b3cc1401acdb595ffc9dd288ff" 624 | "checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 625 | "checksum winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "04e3bd221fcbe8a271359c04f21a76db7d0c6028862d1bb5512d85e1e2eb5bb3" 626 | "checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 627 | "checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 628 | "checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 629 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ssh_permit_a38" 3 | description = "Central managment and deployment for SSH keys" 4 | version = "0.2.0" 5 | authors = ["Bernhard Janetzki "] 6 | 7 | [[bin]] 8 | name = "ssh-permit-a38" 9 | path = "src/main.rs" 10 | 11 | [dependencies] 12 | clap = "2.30" 13 | serde = "1.0" 14 | serde_json = "1.0" 15 | serde_derive = "1.0" 16 | chrono = "0.4" 17 | ssh2 = "0.3" 18 | colored = "1.6" 19 | difference = "2.0" 20 | rpassword = "1.0.0" 21 | 22 | [dev-dependencies] 23 | assert_cli = "0.5" 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2018 Bernhard Janetzki 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build test 2 | SHELL := /bin/sh 3 | 4 | # Linux 5 | UNAME_S := $(shell uname -s) 6 | ifeq ($(UNAME_S),Linux) 7 | UNAME_P := $(shell uname -p) 8 | ifeq ($(UNAME_P),x86_64) 9 | TARGET := x86_64-unknown-linux-gnu 10 | else 11 | TARGET := i686-unknown-linux-gnu 12 | endif 13 | endif 14 | 15 | # OS X 16 | ifeq ($(UNAME_S),Darwin) 17 | TARGET := x86_64-apple-darwin 18 | 19 | # use brew OpenSSL on Mac OSX 20 | export OPENSSL_ROOT_DIR = /usr/local/opt/openssl 21 | export OPENSSL_LIB_DIR = /usr/local/opt/openssl/lib 22 | export OPENSSL_INCLUDE_DIR = /usr/local/opt/openssl/include 23 | endif 24 | 25 | # move command line args to RUN_ARGS for the run command 26 | ifeq (run,$(firstword $(MAKECMDGOALS))) 27 | # use the rest as arguments for "run" 28 | RUN_ARGS := $(wordlist 2,$(words $(MAKECMDGOALS)),$(MAKECMDGOALS)) 29 | # ...and turn them into do-nothing targets 30 | $(eval $(RUN_ARGS):;@:) 31 | endif 32 | 33 | export RUST_BACKTRACE = 1 34 | 35 | fmt: 36 | cargo fmt 37 | 38 | pre_release: 39 | sed -ibak 's/^version = ".*"$$/version = "$(VERSION)"/' Cargo.toml 40 | sed '/^```$$/d;' examples/commands.md > examples/commands.txt 41 | 42 | run: 43 | cargo run -- $(RUN_ARGS) 44 | 45 | clean: 46 | cargo clean 47 | 48 | build: 49 | rustup update stable 50 | cargo build --release --target=$(TARGET) 51 | 52 | build_linux_x86_64: 53 | cd build && \ 54 | vagrant up linux_x86_64 --provision && \ 55 | vagrant ssh -c "cd /src && make build;" linux_x86_64 && \ 56 | vagrant halt linux_x86_64 57 | 58 | build_linux_i686: 59 | cd build && \ 60 | vagrant up linux_i686 --provision && \ 61 | vagrant ssh -c "cd /src && make build;" linux_i686 && \ 62 | vagrant halt linux_i686 63 | 64 | release: clean pre_release build_linux_x86_64 build_linux_i686 build 65 | # update release urls 66 | sed -ibak 's/releases\/download\/v[^/]*\/ssh-permit-a38-v[^/]*-/releases\/download\/v$(VERSION)\/ssh-permit-a38-v$(VERSION)-/' README.md 67 | # update release version an date 68 | sed -ibak 's/^## Latest release v.*/## Latest release v$(VERSION) - $(shell date +%Y-%m-%d)/' README.md 69 | 70 | rm README.mdbak 71 | rm Cargo.tomlbak 72 | 73 | git commit -a -m "bump $(VERSION)" 74 | git push 75 | git checkout master 76 | git merge develop 77 | git push origin master 78 | git tag v$(VERSION) 79 | git push origin v$(VERSION) 80 | rm build/binaries/*.zip || true 81 | 82 | # OS X 83 | cp target/x86_64-apple-darwin/release/ssh-permit-a38 build/binaries/ 84 | cd build/binaries/ && zip --move ssh-permit-a38-v$(VERSION)-x86_64-apple-darwin.zip ssh-permit-a38 85 | 86 | # Linux x86_64 87 | cp target/x86_64-unknown-linux-gnu/release/ssh-permit-a38 build/binaries/ 88 | cd build/binaries/ && zip --move ssh-permit-a38-v$(VERSION)-x86_64-unknown-linux-gnu.zip ssh-permit-a38 89 | 90 | # Linux i686 91 | cp target/i686-unknown-linux-gnu/release/ssh-permit-a38 build/binaries/ 92 | cd build/binaries/ && zip --move ssh-permit-a38-v$(VERSION)-i686-unknown-linux-gnu.zip ssh-permit-a38 93 | 94 | cd build/binaries/ && hub release create -a ssh-permit-a38-v$(VERSION)-x86_64-apple-darwin.zip -a ssh-permit-a38-v$(VERSION)-x86_64-unknown-linux-gnu.zip -a ssh-permit-a38-v$(VERSION)-i686-unknown-linux-gnu.zip v$(VERSION) 95 | 96 | git checkout develop 97 | open https://github.com/ierror/ssh-permit-a38/releases 98 | 99 | test: 100 | cargo test --jobs=4 -- --test-threads=4 101 | 102 | push: fmt 103 | git push 104 | 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SSH Permit A38 2 | 3 | Central management and deployment for SSH keys 4 | 5 | [![Build Status](https://travis-ci.org/ierror/ssh-permit-a38.svg?branch=master)](https://travis-ci.org/ierror/ssh-permit-a38) 6 | 7 | [![asciicast](https://asciinema.org/a/GyIB6XZj7Sotp9ZCekaQcLdDa.png)](https://asciinema.org/a/GyIB6XZj7Sotp9ZCekaQcLdDa) 8 | 9 | ### Features 10 | 11 | * Central management of public SSH keys and servers in a simple and readable JSON database 12 | * Sync authorized users to the servers authorized_keys 13 | * SSH config support for the sync command connection paramaters (Hostname, User, Port) 14 | * User Groups 15 | * Host aliases 16 | * Diff of authorized_keys to sync and the existing one 17 | 18 | ## Latest release v0.2.0 - 2018-08-18 19 | 20 | ### Download prebuilt binaries 21 | 22 | * [Linux x86_64](https://github.com/ierror/ssh-permit-a38/releases/download/v0.2.0/ssh-permit-a38-v0.2.0-x86_64-unknown-linux-gnu.zip) 23 | * [Linux i686](https://github.com/ierror/ssh-permit-a38/releases/download/v0.2.0/ssh-permit-a38-v0.2.0-i686-unknown-linux-gnu.zip) 24 | 25 | * [macOS](https://github.com/ierror/ssh-permit-a38/releases/download/v0.2.0/ssh-permit-a38-v0.2.0-x86_64-apple-darwin.zip) 26 | 27 | or you can install [this Homebrew package](http://formulae.brew.sh/formula/ssh-permit-a38): 28 | ``` 29 | brew install ssh-permit-a38 30 | ``` 31 | 32 | [Previous Releases](https://github.com/ierror/ssh-permit-a38/releases) 33 | 34 | ### Changelog 35 | 36 | ## v0.2.0 - 2018-08-18 37 | 38 | - Support for SSH config files [#5](https://github.com/ierror/ssh-permit-a38/issues/5) 39 | 40 | If a ssh-permit-a38 hostname or alias matches the ssh configs Host (or Hostname), User, Port and Host information are used for authorized_keys sync connection 41 | 42 | - sync command switch -y, --yes-authorized-keys-prompt: Automatic yes to authorized_keys location prompts 43 | 44 | 45 | [Previous Changes](https://github.com/ierror/ssh-permit-a38/blob/master/CHANGELOG.md) 46 | 47 | ## Build from source 48 | 49 | ### Prerequisites 50 | 51 | * [Rust](https://www.rust-lang.org/) 52 | * [Cargo](https://doc.rust-lang.org/cargo/) 53 | * [OpenSSL](https://www.openssl.org/) 54 | 55 | ### Build 56 | 57 | ``` 58 | make build 59 | ``` 60 | 61 | ## Quickstart 62 | 63 | ``` 64 | ssh-permit-a38 host urlsmash.403.io add 65 | ssh-permit-a38 user obelix add 66 | ssh-permit-a38 user obelix grant urlsmash.403.io 67 | ssh-permit-a38 sync 68 | ``` 69 | 70 | ## Documentation 71 | 72 | Run 73 | 74 | ``` 75 | ssh-permit-a38 howto 76 | ``` 77 | 78 | [or online](https://github.com/ierror/ssh-permit-a38/blob/master/examples/commands.md) 79 | 80 | ## Running the tests 81 | 82 | ``` 83 | make test 84 | ``` 85 | 86 | ### Coding style 87 | 88 | We use rustfmt to format the source. 89 | 90 | ``` 91 | make fmt 92 | ``` 93 | 94 | ## Contributing 95 | 96 | Pull requests welcome! ;) 97 | 98 | ## Versioning 99 | 100 | We use [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/ierror/ssh-permit-a38/tags). 101 | 102 | ## Authors 103 | 104 | * **Bernhard Janetzki** [@i_error](https://twitter.com/i_error) 105 | 106 | See also the list of [contributors](https://github.com/ierror/ssh-permit-a38/contributors) who participated in this project. 107 | 108 | ## License 109 | 110 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details 111 | 112 | ## FAQ 113 | 114 | * [Permit A38 ?](https://www.youtube.com/watch?v=GI5kwSap9Ug) 115 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | * user list -> with permissions? 2 | * Default "".to_owned() to None? 3 | * to_owned vs. to_string 4 | * .expect("Couldn't read public key.") 5 | * user_id => id? 6 | * DOC: "unknown => ... to get a list of hostnames available" 7 | * DOC: SSH v2 only 8 | * check for sync_todo = True 9 | * naming user_group vs. group 10 | 11 | -------------------------------------------------------------------------------- /build/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | 4 | # All Vagrant configuration is done below. The "2" in Vagrant.configure 5 | # configures the configuration version (we support older styles for 6 | # backwards compatibility). Please don't change it unless you know what 7 | # you're doing. 8 | Vagrant.configure("2") do |config| 9 | config.vm.synced_folder "../", "/src/" 10 | 11 | # linux x86_64 12 | config.vm.define "linux_x86_64" do |linux_x86_64| 13 | # The most common configuration options are documented and commented below. 14 | linux_x86_64.vm.box = "ubuntu/artful64" 15 | linux_x86_64.disksize.size = "20GB" 16 | 17 | linux_x86_64.vm.provider "virtualbox" do |vb| 18 | # Display the VirtualBox GUI when booting the machine 19 | vb.gui = true 20 | 21 | # Customize the amount of memory on the VM: 22 | vb.memory = "1024" 23 | end 24 | 25 | linux_x86_64.vm.provision :shell, path: "bootstrap_vagrant.sh", privileged: false 26 | linux_x86_64.vm.provision :shell, path: "bootstrap_root.sh", privileged: true 27 | end 28 | 29 | # linux i686 30 | config.vm.define "linux_i686" do |linux_i686| 31 | # The most common configuration options are documented and commented below. 32 | linux_i686.vm.box = "ubuntu/artful32" 33 | linux_i686.disksize.size = "20GB" 34 | 35 | linux_i686.vm.provider "virtualbox" do |vb| 36 | # Display the VirtualBox GUI when booting the machine 37 | vb.gui = true 38 | 39 | # Customize the amount of memory on the VM: 40 | vb.memory = "1024" 41 | end 42 | 43 | linux_i686.vm.provision :shell, path: "bootstrap_vagrant.sh", privileged: false 44 | linux_i686.vm.provision :shell, path: "bootstrap_root.sh", privileged: true 45 | end 46 | 47 | end 48 | -------------------------------------------------------------------------------- /build/binaries/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ierror/ssh-permit-a38/ae88aa13206c7313d8aacfde2392a0931d00dafb/build/binaries/.gitkeep -------------------------------------------------------------------------------- /build/bootstrap_root.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | apt-get update --assume-yes 3 | apt-get dist-upgrade --assume-yes 4 | apt-get install build-essential pkg-config libssl-dev cmake ntpdate --assume-yes 5 | -------------------------------------------------------------------------------- /build/bootstrap_vagrant.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | curl https://sh.rustup.rs -sSf | sh -s -- -y 3 | -------------------------------------------------------------------------------- /ci/install.sh: -------------------------------------------------------------------------------- 1 | set -ex 2 | 3 | main() { 4 | local target= 5 | if [ $TRAVIS_OS_NAME = linux ]; then 6 | target=x86_64-unknown-linux-musl 7 | sort=sort 8 | else 9 | target=x86_64-apple-darwin 10 | sort=gsort # for `sort --sort-version`, from brew's coreutils. 11 | fi 12 | 13 | # Builds for iOS are done on OSX, but require the specific target to be 14 | # installed. 15 | case $TARGET in 16 | aarch64-apple-ios) 17 | rustup target install aarch64-apple-ios 18 | ;; 19 | armv7-apple-ios) 20 | rustup target install armv7-apple-ios 21 | ;; 22 | armv7s-apple-ios) 23 | rustup target install armv7s-apple-ios 24 | ;; 25 | i386-apple-ios) 26 | rustup target install i386-apple-ios 27 | ;; 28 | x86_64-apple-ios) 29 | rustup target install x86_64-apple-ios 30 | ;; 31 | esac 32 | 33 | # This fetches latest stable release 34 | local tag=$(git ls-remote --tags --refs --exit-code https://github.com/japaric/cross \ 35 | | cut -d/ -f3 \ 36 | | grep -E '^v[0.1.0-9.]+$' \ 37 | | $sort --version-sort \ 38 | | tail -n1) 39 | curl -LSfs https://japaric.github.io/trust/install.sh | \ 40 | sh -s -- \ 41 | --force \ 42 | --git japaric/cross \ 43 | --tag $tag \ 44 | --target $target 45 | } 46 | 47 | main -------------------------------------------------------------------------------- /examples/commands.md: -------------------------------------------------------------------------------- 1 | Global Options 2 | ============== 3 | 4 | ``` 5 | --database 6 | JSON database file. Default: ssh-permit.json 7 | ``` 8 | 9 | 10 | Host 11 | ==== 12 | 13 | ## new host 14 | ``` 15 | ssh-permit-a38 host urlsmash.403.io add 16 | ``` 17 | 18 | ## new host, special ssh port 19 | ``` 20 | ssh-permit-a38 host 10.211.55.7:2222 add 21 | ``` 22 | 23 | ## list all hosts 24 | ``` 25 | ssh-permit-a38 host list 26 | ``` 27 | 28 | ## list specific host 29 | ``` 30 | ssh-permit-a38 host urlsmash.403.io list 31 | ``` 32 | 33 | ## set host alias 34 | ``` 35 | ssh-permit-a38 host urlsmash.403.io alias um 36 | ``` 37 | 38 | After this point you can use the alias or the hostname for all host related commands 39 | 40 | ## remove host alias 41 | ``` 42 | ssh-permit-a38 host urlsmash.403.io alias 43 | ``` 44 | 45 | ## remove host 46 | ``` 47 | ssh-permit-a38 host example.com:2222 remove 48 | ``` 49 | 50 | 51 | User 52 | ==== 53 | 54 | ## new user 55 | ``` 56 | ssh-permit-a38 user obelix add 57 | ``` 58 | 59 | ## list all users 60 | ``` 61 | ssh-permit-a38 user list 62 | ``` 63 | 64 | ## list specific user 65 | ``` 66 | ssh-permit-a38 user obelix list 67 | ``` 68 | 69 | ## user remove 70 | ``` 71 | ssh-permit-a38 user obelix remove 72 | ``` 73 | 74 | ## grant access to host 75 | ``` 76 | ssh-permit-a38 user obelix grant urlsmash.403.io 77 | ``` 78 | 79 | ## revoke access 80 | ``` 81 | ssh-permit-a38 user obelix revoke urlsmash.403.io 82 | ``` 83 | 84 | ## remove user 85 | ``` 86 | ssh-permit-a38 user obelix remove 87 | ``` 88 | 89 | 90 | Group 91 | ===== 92 | 93 | ## new group 94 | ``` 95 | ssh-permit-a38 group gauls add 96 | ``` 97 | 98 | ## list all groups 99 | ``` 100 | ssh-permit-a38 group list 101 | ``` 102 | 103 | ## list specific group 104 | ``` 105 | ssh-permit-a38 group gauls list 106 | ``` 107 | 108 | ## add an user to group 109 | ``` 110 | ssh-permit-a38 group gauls add obelix 111 | ``` 112 | 113 | ## remove an user from group 114 | ``` 115 | ssh-permit-a38 group gauls remove obelix 116 | ``` 117 | 118 | ## Grant group to host 119 | ``` 120 | ssh-permit-a38 group gauls grant urlsmash.403.io 121 | ``` 122 | 123 | ## Revoke group from host 124 | ``` 125 | ssh-permit-a38 group gauls revoke urlsmash.403.io 126 | ``` 127 | 128 | 129 | Sync 130 | ==== 131 | 132 | ## With public key authentication 133 | ``` 134 | ssh-permit-a38 sync -y 135 | ``` 136 | 137 | ## With password authentication 138 | ``` 139 | ssh-permit-a38 sync --password -y 140 | ``` -------------------------------------------------------------------------------- /examples/commands.txt: -------------------------------------------------------------------------------- 1 | Global Options 2 | ============== 3 | 4 | --database 5 | JSON database file. Default: ssh-permit.json 6 | 7 | 8 | Host 9 | ==== 10 | 11 | ## new host 12 | ssh-permit-a38 host urlsmash.403.io add 13 | 14 | ## new host, special ssh port 15 | ssh-permit-a38 host 10.211.55.7:2222 add 16 | 17 | ## list all hosts 18 | ssh-permit-a38 host list 19 | 20 | ## list specific host 21 | ssh-permit-a38 host urlsmash.403.io list 22 | 23 | ## set host alias 24 | ssh-permit-a38 host urlsmash.403.io alias um 25 | 26 | After this point you can use the alias or the hostname for all host related commands 27 | 28 | ## remove host alias 29 | ssh-permit-a38 host urlsmash.403.io alias 30 | 31 | ## remove host 32 | ssh-permit-a38 host example.com:2222 remove 33 | 34 | 35 | User 36 | ==== 37 | 38 | ## new user 39 | ssh-permit-a38 user obelix add 40 | 41 | ## list all users 42 | ssh-permit-a38 user list 43 | 44 | ## list specific user 45 | ssh-permit-a38 user obelix list 46 | 47 | ## user remove 48 | ssh-permit-a38 user obelix remove 49 | 50 | ## grant access to host 51 | ssh-permit-a38 user obelix grant urlsmash.403.io 52 | 53 | ## revoke access 54 | ssh-permit-a38 user obelix revoke urlsmash.403.io 55 | 56 | ## remove user 57 | ssh-permit-a38 user obelix remove 58 | 59 | 60 | Group 61 | ===== 62 | 63 | ## new group 64 | ssh-permit-a38 group gauls add 65 | 66 | ## list all groups 67 | ssh-permit-a38 group list 68 | 69 | ## list specific group 70 | ssh-permit-a38 group gauls list 71 | 72 | ## add an user to group 73 | ssh-permit-a38 group gauls add obelix 74 | 75 | ## remove an user from group 76 | ssh-permit-a38 group gauls remove obelix 77 | 78 | ## Grant group to host 79 | ssh-permit-a38 group gauls grant urlsmash.403.io 80 | 81 | ## Revoke group from host 82 | ssh-permit-a38 group gauls revoke urlsmash.403.io 83 | 84 | 85 | Sync 86 | ==== 87 | 88 | ## With public key authentication 89 | ssh-permit-a38 sync -y 90 | 91 | ## With password authentication 92 | ssh-permit-a38 sync --password -y 93 | -------------------------------------------------------------------------------- /examples/ssh-permit.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosts": [ 3 | { 4 | "hostname": "urlsmash.403.io", 5 | "authorized_users": [ 6 | "bernhard@janetzki.eu" 7 | ], 8 | "authorized_user_groups": [], 9 | "sync_todo": false 10 | } 11 | ], 12 | "users": [ 13 | { 14 | "user_id": "bernhard@janetzki.eu", 15 | "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDCgIcHBcyRYjRq51YhcduL0ifmd6f/glH+LniKzKZwXfplO3T/G1hk7C1KDwPzfRMS/mVPEIn8pOJ0yd1Y+fbh0hwlUz8LwIuCuos38bBiRUDaUcrwxOvFkFV3cre9pfa7u3WaKt7VZT6ac0AoFyRz36s15uPXa2yH8AX+yybL1UYswO9V3P5s4kacTC/0CRr+ltSjSa7C7W6U349jOfhE2H3orr5+Fe8kY10zcyVJsohiuOaiCgaeeSddAcr2t2koTa2/Wl0tNr6d8RAEnloF7EI9TKlshyVzfMaRDGFx1NSJWVupCaG2wuZzGG3gvuDjduCNqF0z4oOlm2y2TUdk1oOLv5037YdQzhqodnrNDeGvS7ZqjapSOpgN7UzH86J/HAzR/39BXm4UMBGL8YZJtGjo1hVePtBdqviQFhFRlHMbQGwQyJJpq2Usmr+MPmy5xYHdllrD1JzrRmaeaiVLPgDhbh13MIago2SVsbe95eBMdGAE/Bast34i6QuKyDqV64DkEBvJFgBeUjFGZS82+4fR/ARVj1J6kofQbbSuDYdASbZNMSFAAcp+cYd930UTxF5gicaMkWLNdrTzsVJ0P3rgazVsYFkJOrp3wTLdfsceApJaI43dC4ZUphXRvo5BjmayJitupkr7pblepMYXNfCvcOgbC5yuQVHDm6P49Q== boerni@steppenwolf" 16 | }, 17 | { 18 | "user_id": "obelix", 19 | "public_key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCnytU8vYLYQhmkr+j12fPRkz04bmgSZ7aUcgPmNAtEkUBfpDlyHV/ABDe2jyY5iIJSw9q6r96kByzFn4e+GmCvwrSEqa22502aNlX3anhfN3N9OnLGulCsywOBnpQ6vQl8er5x4HeFjolTXImRe2X6pqZ/027m0BH5IZlPJWENbRRxhG2cXpeDl+xJjPVxwb8MDJmCxrHXl/qJhTjYLoHWpbKnPIVvspOU/4EdL4UL74mC6UJN1teYfwBXeD181CZjXOJlp6/Qo7FeHvVUVganr1WDB2UVxjZ5BzR9hH8lf/TNbQxsrhO+DiiXI8LiZR+LaEoOOJnVoF6CaM8JA5JN boerni@steppenwolf.fritz.box" 20 | } 21 | ], 22 | "user_groups": [], 23 | "modified_at": "2018-03-18 17:15:16.039270 UTC", 24 | "schema_version": "1.0.0" 25 | } -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 100 2 | hard_tabs = false 3 | tab_spaces = 4 4 | newline_style = "Unix" 5 | indent_style = "Block" 6 | format_strings = false 7 | wrap_comments = false 8 | comment_width = 80 9 | normalize_comments = false 10 | empty_item_single_line = true 11 | struct_lit_single_line = true 12 | fn_single_line = false 13 | where_single_line = false 14 | imports_indent = "Visual" 15 | imports_layout = "Mixed" 16 | reorder_extern_crates = true 17 | reorder_extern_crates_in_group = true 18 | reorder_imports = true 19 | reorder_imported_names = true 20 | reorder_modules = true 21 | space_before_colon = false 22 | space_after_colon = true 23 | spaces_within_parens_and_brackets = false 24 | struct_field_align_threshold = 0 25 | remove_blank_lines_at_start_or_end_of_block = true 26 | fn_args_density = "Tall" 27 | brace_style = "SameLineWhere" 28 | control_brace_style = "AlwaysSameLine" 29 | trailing_comma = "Vertical" 30 | blank_lines_upper_bound = 1 31 | blank_lines_lower_bound = 0 32 | merge_derives = true 33 | use_try_shorthand = false 34 | condense_wildcard_suffixes = false 35 | force_explicit_abi = true 36 | write_mode = "Overwrite" 37 | disable_all_formatting = false 38 | skip_children = false 39 | hide_parse_errors = false 40 | report_todo = "Never" 41 | report_fixme = "Never" 42 | -------------------------------------------------------------------------------- /src/cli_flow.rs: -------------------------------------------------------------------------------- 1 | use colored::Colorize; 2 | use std::io; 3 | use std::io::Write; 4 | use std::process::exit; 5 | 6 | pub fn errorln(msg: &str) { 7 | println!("{} {}", "Error:".red(), msg); 8 | exit(1); 9 | } 10 | 11 | pub fn prompt(msg: &str, colorful: bool) { 12 | if colorful { 13 | print!("{} ", msg.yellow()); 14 | } else { 15 | print!("{} ", msg); 16 | } 17 | io::stdout().flush().expect("Unable to flush"); 18 | } 19 | 20 | pub fn promptln(msg: &str) { 21 | println!("{}", msg); 22 | } 23 | 24 | pub fn prompt_yes_no(msg: &str, colorful: bool) -> String { 25 | let mut yes_no; 26 | 27 | loop { 28 | yes_no = String::from(""); 29 | 30 | prompt(&mut format!("{}", msg), colorful); 31 | io::stdin() 32 | .read_line(&mut yes_no) 33 | .ok() 34 | .expect("Couldn't read line (y/n)"); 35 | 36 | yes_no = yes_no.trim_right().trim_left().to_owned(); 37 | if yes_no == "n" || yes_no == "y" { 38 | break; 39 | } 40 | } 41 | 42 | yes_no 43 | } 44 | 45 | pub fn read_line<'a>(msg: &str, default: &'a String) -> String { 46 | prompt(msg, false); 47 | 48 | let mut input = String::new(); 49 | 50 | io::stdin() 51 | .read_line(&mut input) 52 | .ok() 53 | .expect("Couldn't read_line"); 54 | 55 | input = input.trim_right().trim_left().to_string(); 56 | if !input.is_empty() { 57 | return input; 58 | } 59 | 60 | default.to_owned() 61 | } 62 | 63 | pub fn okln(msg: &str) { 64 | println!("{}", msg.green().bold()); 65 | } 66 | 67 | pub fn warningln(msg: &str) { 68 | println!("{} {}", "Warning:".magenta(), msg); 69 | } 70 | 71 | pub fn infoln(msg: &str) { 72 | println!("{}", msg); 73 | } 74 | -------------------------------------------------------------------------------- /src/database.rs: -------------------------------------------------------------------------------- 1 | extern crate serde; 2 | extern crate serde_json; 3 | 4 | use chrono::Utc; 5 | use std::error::Error; 6 | use std::fmt; 7 | use std::fs::File; 8 | use std::path::Path; 9 | 10 | const SCHEMA_VERSION: &'static str = "0.1.0"; 11 | 12 | #[derive(Serialize, Deserialize)] 13 | pub struct Database { 14 | pub hosts: Vec, 15 | pub users: Vec, 16 | pub user_groups: Vec, 17 | 18 | pub modified_at: String, 19 | pub schema_version: String, 20 | } 21 | 22 | impl Default for Database { 23 | fn default() -> Database { 24 | Database { 25 | hosts: vec![], 26 | users: vec![], 27 | user_groups: vec![], 28 | modified_at: String::from(""), 29 | schema_version: SCHEMA_VERSION.to_owned(), 30 | } 31 | } 32 | } 33 | 34 | impl Database { 35 | pub fn load>(&self, path: P) -> Result> { 36 | let file = File::open(path)?; 37 | Ok(serde_json::from_reader(file)?) 38 | } 39 | 40 | pub fn save>(&mut self, path: P) { 41 | let file = File::create(path).unwrap(); 42 | let now = Utc::now(); 43 | 44 | self.modified_at = format!("{}", now.to_owned()); 45 | self.schema_version = SCHEMA_VERSION.to_owned(); 46 | 47 | serde_json::to_writer_pretty(&file, &self).expect("Unable to write database file."); 48 | } 49 | 50 | pub fn host_get(&self, hostname_or_alias: &str) -> Option<&Host> { 51 | self.hosts 52 | .iter() 53 | .position(|ref h| { 54 | h.hostname == hostname_or_alias || h.alias == Some(hostname_or_alias.to_owned()) 55 | }) 56 | .map(|i| &self.hosts[i]) 57 | } 58 | 59 | pub fn host_get_mut(&mut self, hostname_or_alias: &str) -> Option<&mut Host> { 60 | self.hosts 61 | .iter() 62 | .position(|ref h| { 63 | h.hostname == hostname_or_alias || h.alias == Some(hostname_or_alias.to_owned()) 64 | }) 65 | .map(move |i| &mut self.hosts[i]) 66 | } 67 | 68 | pub fn host_get_by_alias(&self, alias: &str) -> Option<&Host> { 69 | self.hosts 70 | .iter() 71 | .position(|ref h| h.alias == Some(alias.to_owned())) 72 | .map(|i| &self.hosts[i]) 73 | } 74 | 75 | pub fn user_get(&self, user_id: &str) -> Option<&User> { 76 | self.users 77 | .iter() 78 | .position(|u| u.user_id == user_id) 79 | .map(|i| &self.users[i]) 80 | } 81 | 82 | pub fn group_get(&self, group_id: &str) -> Option<&UserGroup> { 83 | self.user_groups 84 | .iter() 85 | .position(|g| g.group_id == group_id) 86 | .map(|i| &self.user_groups[i]) 87 | } 88 | 89 | pub fn group_get_mut(&mut self, group_id: &str) -> Option<&mut UserGroup> { 90 | self.user_groups 91 | .iter() 92 | .position(|g| g.group_id == group_id) 93 | .map(move |i| &mut self.user_groups[i]) 94 | } 95 | 96 | pub fn is_user_granted(&self, user: &User, host: &Host) -> bool { 97 | host.authorized_users 98 | .iter() 99 | .position(|au| au == &user.user_id) 100 | .is_some() 101 | } 102 | 103 | pub fn is_group_granted(&self, user_group: &UserGroup, host: &Host) -> bool { 104 | host.authorized_user_groups 105 | .iter() 106 | .position(|ag| ag == &user_group.group_id) 107 | .is_some() 108 | } 109 | 110 | pub fn is_user_group_member(&self, user: &User, user_group: &UserGroup) -> bool { 111 | user_group 112 | .members 113 | .iter() 114 | .position(|u| u == &user.user_id) 115 | .is_some() 116 | } 117 | } 118 | 119 | #[derive(Serialize, Deserialize, Debug)] 120 | pub struct Host { 121 | pub hostname: String, 122 | 123 | #[serde(default)] 124 | pub alias: Option, 125 | 126 | pub authorized_users: Vec, 127 | pub authorized_user_groups: Vec, 128 | pub sync_todo: bool, 129 | } 130 | 131 | impl Default for Host { 132 | fn default() -> Host { 133 | Host { 134 | hostname: String::from(""), 135 | alias: None, 136 | authorized_users: vec![], 137 | authorized_user_groups: vec![], 138 | sync_todo: true, 139 | } 140 | } 141 | } 142 | 143 | impl fmt::Display for Host { 144 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 145 | write!(f, "{}", self.hostname) 146 | } 147 | } 148 | 149 | #[derive(Serialize, Deserialize, Debug)] 150 | pub struct User { 151 | pub user_id: String, 152 | pub public_key: String, 153 | } 154 | 155 | impl fmt::Display for User { 156 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 157 | write!(f, "{}", self.user_id) 158 | } 159 | } 160 | 161 | #[derive(Serialize, Deserialize, Debug)] 162 | pub struct UserGroup { 163 | pub group_id: String, 164 | pub members: Vec, 165 | } 166 | 167 | impl fmt::Display for UserGroup { 168 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 169 | write!(f, "{}", self.group_id) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate serde_derive; 3 | 4 | extern crate chrono; 5 | extern crate clap; 6 | extern crate colored; 7 | extern crate difference; 8 | extern crate rpassword; 9 | extern crate serde; 10 | extern crate serde_json; 11 | extern crate ssh2; 12 | 13 | use clap::{App, Arg, SubCommand}; 14 | use std::path::Path; 15 | 16 | mod cli_flow; 17 | mod database; 18 | mod ssh_config; 19 | mod subcommand_group; 20 | mod subcommand_host; 21 | mod subcommand_howto; 22 | mod subcommand_sync; 23 | mod subcommand_user; 24 | 25 | fn main() { 26 | let matches = App::new("SSH Permit A38") 27 | // application info 28 | .version(env!("CARGO_PKG_VERSION")) 29 | .about(env!("CARGO_PKG_DESCRIPTION")) 30 | .author(env!("CARGO_PKG_AUTHORS")) 31 | 32 | // --database 33 | .arg( 34 | Arg::with_name("database") 35 | .short("d") 36 | .long("database") 37 | .value_name("FILE") 38 | .help("Database file to use") 39 | .takes_value(true), 40 | ) 41 | 42 | // host 43 | .subcommand( 44 | SubCommand::with_name("host") 45 | // host 46 | .about("Host related actions") 47 | .arg(Arg::with_name("host:port") 48 | .help("Host") 49 | .index(1)) 50 | .alias("hosts") 51 | 52 | // host add 53 | .subcommand( 54 | SubCommand::with_name("add") 55 | ) 56 | // host remove 57 | .subcommand( 58 | SubCommand::with_name("remove") 59 | ) 60 | // host list 61 | .subcommand( 62 | SubCommand::with_name("list") 63 | // --raw 64 | .arg( 65 | Arg::with_name("raw") 66 | .short("r") 67 | .long("raw") 68 | .help("Prints raw host struct") 69 | ) 70 | ) 71 | // host alias 72 | .subcommand( 73 | SubCommand::with_name("alias") 74 | .arg(Arg::with_name("alias") 75 | .help("Host alias") 76 | .index(1) 77 | .required(false)) 78 | ) 79 | ) 80 | 81 | // user 82 | .subcommand( 83 | SubCommand::with_name("user") 84 | // user 85 | .about("User related actions") 86 | .arg(Arg::with_name("user") 87 | .help("User") 88 | .index(1)) 89 | .alias("users") 90 | 91 | // user add 92 | .subcommand( 93 | SubCommand::with_name("add") 94 | ) 95 | // user remove 96 | .subcommand( 97 | SubCommand::with_name("remove") 98 | ) 99 | // user list 100 | .subcommand( 101 | SubCommand::with_name("list") 102 | // --raw 103 | .arg( 104 | Arg::with_name("raw") 105 | .short("r") 106 | .long("raw") 107 | .help("Prints raw host struct")) 108 | ) 109 | // user grant 110 | .subcommand( 111 | SubCommand::with_name("grant") 112 | .arg(Arg::with_name("host") 113 | .help("Host") 114 | .index(1) 115 | .required(true)) 116 | ) 117 | // user grant 118 | .subcommand( 119 | SubCommand::with_name("revoke") 120 | .arg(Arg::with_name("host") 121 | .help("Host") 122 | .index(1) 123 | .required(true)) 124 | ) 125 | ) 126 | 127 | // group 128 | .subcommand( 129 | SubCommand::with_name("group") 130 | // group 131 | .about("Group related actions") 132 | .arg(Arg::with_name("group") 133 | .help("Group") 134 | .index(1)) 135 | .alias("groups") 136 | 137 | // group add 138 | .subcommand( 139 | SubCommand::with_name("add") 140 | // group add 141 | .arg(Arg::with_name("user") 142 | .help("User") 143 | .index(1) 144 | .required(false)) 145 | ) 146 | // group remove 147 | .subcommand( 148 | SubCommand::with_name("remove") 149 | // group remove 150 | .arg(Arg::with_name("user") 151 | .help("User") 152 | .index(1) 153 | .required(false)) 154 | ) 155 | // group list 156 | .subcommand( 157 | SubCommand::with_name("list") 158 | // --raw 159 | .arg( 160 | Arg::with_name("raw") 161 | .short("r") 162 | .long("raw") 163 | .help("Prints raw host struct") 164 | ) 165 | ) 166 | // group grant 167 | .subcommand( 168 | SubCommand::with_name("grant") 169 | .arg(Arg::with_name("host") 170 | .help("Host") 171 | .index(1) 172 | .required(true)) 173 | ) 174 | // group revoke 175 | .subcommand( 176 | SubCommand::with_name("revoke") 177 | .arg(Arg::with_name("host") 178 | .help("Host") 179 | .index(1) 180 | .required(true)) 181 | ) 182 | ) 183 | 184 | // sync 185 | .subcommand( 186 | SubCommand::with_name("sync") 187 | .about("Sync pending changes to the related hosts") 188 | // --password 189 | .arg( 190 | Arg::with_name("password") 191 | .short("p") 192 | .long("password") 193 | .help("Use password authentication instead of public key") 194 | .takes_value(false), 195 | ) 196 | // --yes-authorized-keys-prompt 197 | .arg( 198 | Arg::with_name("yes_authorized_keys_prompt") 199 | .short("yakp") 200 | .long("yes-authorized-keys-prompt") 201 | .help("Automatic yes to authorized_keys location prompts") 202 | .takes_value(false), 203 | ) 204 | ) 205 | // howto 206 | .subcommand( 207 | SubCommand::with_name("howto") 208 | .about("Prints a Howto") 209 | ) 210 | .get_matches(); 211 | 212 | // load database 213 | let database_file = matches.value_of("database").unwrap_or("ssh-permit.json"); 214 | 215 | let mut db = database::Database { 216 | ..Default::default() 217 | }; 218 | 219 | if Path::new(database_file).exists() { 220 | db = match db.load(database_file) { 221 | Ok(t) => t, 222 | Err(e) => { 223 | cli_flow::errorln(&format!( 224 | "Unable to load {}: {}", 225 | database_file, 226 | e.to_string() 227 | )); 228 | return; 229 | } 230 | }; 231 | } else { 232 | cli_flow::warningln(&format!( 233 | "Database file {} does not exist. Created a new one.", 234 | database_file, 235 | )); 236 | } 237 | 238 | // host 239 | if let Some(matches) = matches.subcommand_matches("host") { 240 | let hostname = matches.value_of("host:port").unwrap_or(""); 241 | 242 | if matches.subcommand_matches("add").is_some() { 243 | subcommand_host::add(&mut db, &hostname); 244 | } else if matches.subcommand_matches("remove").is_some() { 245 | subcommand_host::remove(&mut db, &hostname); 246 | } else if let Some(matches) = matches.subcommand_matches("list") { 247 | subcommand_host::list(&mut db, &hostname, matches.is_present("raw")); 248 | } else if let Some(matches) = matches.subcommand_matches("alias") { 249 | subcommand_host::alias(&mut db, &hostname, matches.value_of("alias")); 250 | } 251 | } 252 | // user 253 | else if let Some(matches) = matches.subcommand_matches("user") { 254 | let user_id = matches.value_of("user").unwrap_or(""); 255 | 256 | if matches.subcommand_matches("add").is_some() { 257 | subcommand_user::add(&mut db, &user_id); 258 | } else if matches.subcommand_matches("remove").is_some() { 259 | subcommand_user::remove(&mut db, &user_id); 260 | } else if let Some(matches) = matches.subcommand_matches("list") { 261 | subcommand_user::list(&mut db, &user_id, matches.is_present("raw")); 262 | } else if let Some(matches) = matches.subcommand_matches("grant") { 263 | let hostname = matches.value_of("host").unwrap(); 264 | subcommand_user::grant(&mut db, &user_id, &hostname); 265 | } else if let Some(matches) = matches.subcommand_matches("revoke") { 266 | let hostname = matches.value_of("host").unwrap(); 267 | subcommand_user::revoke(&mut db, &user_id, &hostname); 268 | } 269 | } 270 | // group 271 | else if let Some(matches) = matches.subcommand_matches("group") { 272 | let group_id = matches.value_of("group").unwrap_or(""); 273 | 274 | if let Some(matches) = matches.subcommand_matches("add") { 275 | match matches.value_of("user") { 276 | Some(user_id) => subcommand_group::user_add(&mut db, &group_id, &user_id), 277 | None => subcommand_group::add(&mut db, &group_id), 278 | } 279 | } else if let Some(matches) = matches.subcommand_matches("remove") { 280 | match matches.value_of("user") { 281 | Some(user_id) => subcommand_group::user_remove(&mut db, &group_id, &user_id), 282 | None => subcommand_group::remove(&mut db, &group_id), 283 | } 284 | } else if let Some(matches) = matches.subcommand_matches("list") { 285 | subcommand_group::list(&mut db, &group_id, matches.is_present("raw")); 286 | } else if let Some(matches) = matches.subcommand_matches("grant") { 287 | let hostname = matches.value_of("host").unwrap(); 288 | subcommand_group::grant(&mut db, &group_id, &hostname); 289 | } else if let Some(matches) = matches.subcommand_matches("revoke") { 290 | let hostname = matches.value_of("host").unwrap(); 291 | subcommand_group::revoke(&mut db, &group_id, &hostname); 292 | } 293 | } 294 | // sync 295 | else if let Some(matches) = matches.subcommand_matches("sync") { 296 | subcommand_sync::sync( 297 | &mut db, 298 | matches.is_present("password"), 299 | matches.is_present("yes_authorized_keys_prompt"), 300 | ); 301 | } 302 | // howto 303 | else if matches.subcommand_matches("howto").is_some() { 304 | subcommand_howto::print(); 305 | } 306 | 307 | // save database 308 | db.save(&database_file); 309 | } 310 | -------------------------------------------------------------------------------- /src/ssh_config.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::env; 3 | use std::error::Error; 4 | use std::fs::File; 5 | use std::io::BufRead; 6 | use std::io::BufReader; 7 | use std::path::Path; 8 | 9 | #[derive(Debug)] 10 | pub struct SSHConfigHost { 11 | pub hostname: String, 12 | pub user: String, 13 | pub port: String, 14 | } 15 | 16 | impl Default for SSHConfigHost { 17 | fn default() -> SSHConfigHost { 18 | SSHConfigHost { 19 | hostname: String::new(), 20 | user: String::new(), 21 | port: "22".to_owned(), 22 | } 23 | } 24 | } 25 | 26 | pub fn get() -> Result, Box> { 27 | let mut ssh_config = HashMap::new(); 28 | 29 | // guess ~/.ssh/config path 30 | let ssh_config_path = match env::home_dir() { 31 | Some(path) => path.join(".ssh").join("config"), 32 | None => Path::new("").to_path_buf(), 33 | }; 34 | 35 | if !ssh_config_path.exists() { 36 | return Ok(ssh_config); 37 | } 38 | 39 | // open ssh config 40 | let ssh_config_file = match File::open(&ssh_config_path) { 41 | Ok(f) => f, 42 | Err(e) => { 43 | return Err(From::from(format!( 44 | "SSH config file {} exists but can't be read - {}", 45 | ssh_config_path.to_str().unwrap(), 46 | &e.to_string() 47 | ))); 48 | } 49 | }; 50 | 51 | // parse file 52 | let ssh_config_reader = BufReader::new(&ssh_config_file); 53 | let mut host = String::new(); 54 | for (_, line) in ssh_config_reader.lines().enumerate() { 55 | let mut line = line.unwrap(); 56 | 57 | // Host 58 | if line.starts_with("Host") { 59 | host = line.split(" ").collect::>()[1].to_owned(); 60 | ssh_config.insert( 61 | host.to_owned(), 62 | SSHConfigHost { 63 | ..Default::default() 64 | }, 65 | ); 66 | } 67 | // config option 68 | else if !host.is_empty() 69 | && !line.trim().is_empty() 70 | && [' ', '\t'].contains(&line.chars().next().unwrap()) 71 | { 72 | line = line.trim().to_owned(); 73 | let option_value = line.split(" ").collect::>(); 74 | let option = option_value[0].trim().to_lowercase(); 75 | let value = option_value[1].trim().to_owned(); 76 | let host_entry = ssh_config.get_mut(&host).unwrap(); 77 | 78 | match option.as_str() { 79 | "hostname" => host_entry.hostname = value, 80 | "user" => host_entry.user = value, 81 | "port" => { 82 | // port valid? 83 | host_entry.port = match value.parse::() { 84 | Ok(i) => i.to_string(), 85 | Err(_e) => { 86 | host_entry.port.to_owned() // keep default 87 | } 88 | } 89 | } 90 | _ => {} 91 | } 92 | } else { 93 | host = "".to_owned(); 94 | } 95 | } 96 | 97 | Ok(ssh_config) 98 | } 99 | -------------------------------------------------------------------------------- /src/subcommand_group.rs: -------------------------------------------------------------------------------- 1 | use cli_flow; 2 | use database::{Database, UserGroup}; 3 | 4 | pub fn add(db: &mut Database, group_id: &str) { 5 | // check group is not present 6 | if db.group_get(group_id).is_some() { 7 | cli_flow::errorln(&format!("Group {} already exists", group_id)); 8 | } 9 | 10 | // add new group 11 | let mut group_new = vec![UserGroup { 12 | group_id: group_id.to_owned(), 13 | members: vec![], 14 | }]; 15 | 16 | db.user_groups.append(&mut group_new); 17 | cli_flow::okln(&format!("Successfully added group {}", group_id)); 18 | } 19 | 20 | pub fn remove(db: &mut Database, group_id: &str) { 21 | // check group exist 22 | if db.group_get(group_id).is_none() { 23 | cli_flow::errorln(&format!("Group {} not known", group_id)); 24 | } 25 | 26 | // delete grouo 27 | db.user_groups.retain(|u| u.group_id != group_id); 28 | 29 | // delete user from user_groups.members 30 | for host in &mut db.hosts { 31 | host.authorized_user_groups.retain(move |g| g != group_id); 32 | } 33 | 34 | cli_flow::okln(&format!("Successfully removed group {}", group_id)); 35 | } 36 | 37 | pub fn list(db: &mut Database, group_filter: &str, print_raw: bool) { 38 | for group in &db.user_groups { 39 | if !group_filter.is_empty() && group_filter != group.group_id { 40 | continue; 41 | } 42 | 43 | if print_raw { 44 | println!("{:?}", group); 45 | continue; 46 | } 47 | 48 | println!("\n{}", group.group_id); 49 | println!( 50 | "{}", 51 | (0..group.group_id.len()).map(|_| "=").collect::() 52 | ); 53 | 54 | println!("\n## Members"); 55 | for user in &group.members { 56 | println!("* {}", user); 57 | } 58 | } 59 | 60 | println!(""); 61 | } 62 | 63 | pub fn grant(db: &mut Database, group_id: &str, hostname: &str) { 64 | if let Some(host) = db.host_get(hostname) { 65 | if let Some(group) = db.group_get(group_id) { 66 | if db.is_group_granted(&group, &host) { 67 | cli_flow::errorln(&format!( 68 | "{} already granted to access {}", 69 | group.group_id, hostname 70 | )); 71 | } 72 | } else { 73 | cli_flow::errorln(&format!("Group {} not known", group_id)); 74 | } 75 | } else { 76 | cli_flow::errorln(&format!("Hostname {} not known", hostname)); 77 | } 78 | 79 | // at this point it's save to mut db.host... 80 | { 81 | let host = db.host_get_mut(hostname).unwrap(); 82 | host.authorized_user_groups 83 | .append(&mut vec![String::from(group_id)]); 84 | host.sync_todo = true; 85 | } 86 | 87 | cli_flow::okln(&format!( 88 | "Successfully granted group {} for host {}", 89 | group_id, hostname 90 | )); 91 | } 92 | 93 | pub fn revoke(db: &mut Database, group_id: &str, hostname: &str) { 94 | if let Some(host) = db.host_get(hostname) { 95 | if let Some(group) = db.group_get(group_id) { 96 | if !db.is_group_granted(&group, &host) { 97 | cli_flow::errorln(&format!( 98 | "{} is not granted to access {}", 99 | group.group_id, hostname 100 | )); 101 | } 102 | } else { 103 | cli_flow::errorln(&format!("Group {} not known", group_id)); 104 | } 105 | } else { 106 | cli_flow::errorln(&format!("Hostname {} not known", hostname)); 107 | } 108 | 109 | // at this point it's save to mut db.host... 110 | { 111 | let host = db.host_get_mut(hostname).unwrap(); 112 | host.authorized_user_groups.retain(|u| u != group_id); 113 | host.sync_todo = true; 114 | } 115 | 116 | cli_flow::okln(&format!( 117 | "Successfully revoked group {} from host {}", 118 | group_id, hostname 119 | )); 120 | } 121 | 122 | pub fn user_add(db: &mut Database, group_id: &str, user_id: &str) { 123 | // check user and group exist 124 | if let Some(user) = db.user_get(user_id) { 125 | if let Some(group) = db.group_get(group_id) { 126 | if db.is_user_group_member(&user, &group) { 127 | cli_flow::errorln(&format!( 128 | "User {} is already member of group {}", 129 | user_id, group_id 130 | )); 131 | } 132 | } else { 133 | cli_flow::errorln(&format!("Group {} not known", group_id)); 134 | } 135 | } else { 136 | cli_flow::errorln(&format!("User {} not known", user_id)); 137 | } 138 | 139 | // at this point it's save to mut db.host... 140 | { 141 | let group = db.group_get_mut(group_id).unwrap(); 142 | group.members.append(&mut vec![String::from(user_id)]); 143 | } 144 | { 145 | // set sync todo for affected hosts 146 | for host in &mut db.hosts { 147 | for authorized_group in &mut host.authorized_user_groups { 148 | if authorized_group == group_id { 149 | host.sync_todo = true; 150 | } 151 | } 152 | } 153 | } 154 | 155 | cli_flow::okln(&format!( 156 | "Successfully added user {} to group {}", 157 | user_id, group_id 158 | )); 159 | } 160 | 161 | pub fn user_remove(db: &mut Database, group_id: &str, user_id: &str) { 162 | // check user and group exist 163 | if let Some(user) = db.user_get(user_id) { 164 | if let Some(group) = db.group_get(group_id) { 165 | if !db.is_user_group_member(&user, &group) { 166 | cli_flow::errorln(&format!( 167 | "User {} is not a member of group {}", 168 | user_id, group_id 169 | )); 170 | } 171 | } else { 172 | cli_flow::errorln(&format!("Group {} not known", group_id)); 173 | } 174 | } else { 175 | cli_flow::errorln(&format!("User {} not known", user_id)); 176 | } 177 | 178 | // at this point it's save to mut db.host... 179 | { 180 | let group = db.group_get_mut(group_id).unwrap(); 181 | group.members.retain(|u| u != user_id); 182 | } 183 | { 184 | // set sync todo for affected hosts 185 | for host in &mut db.hosts { 186 | for authorized_group in &mut host.authorized_user_groups { 187 | if authorized_group == group_id { 188 | host.sync_todo = true; 189 | } 190 | } 191 | } 192 | } 193 | 194 | cli_flow::okln(&format!( 195 | "Successfully removed user {} from group {}", 196 | group_id, group_id 197 | )); 198 | } 199 | -------------------------------------------------------------------------------- /src/subcommand_host.rs: -------------------------------------------------------------------------------- 1 | use cli_flow; 2 | use database::{Database, Host}; 3 | 4 | pub fn add(db: &mut Database, hostname: &str) { 5 | if db.host_get(hostname).is_some() { 6 | cli_flow::errorln(&format!( 7 | "Hostname or a host alias {} already exists", 8 | hostname 9 | )); 10 | } 11 | 12 | // <= 1 char ':' allowed 13 | if hostname.matches(":").count() > 1 { 14 | cli_flow::errorln("Hostname format invalid. More than than one ':' found"); 15 | } 16 | 17 | // check that port part is integer 18 | let host_splitted: Vec<&str> = hostname.split(':').collect(); 19 | if host_splitted.len() == 2 { 20 | if !host_splitted[1].parse::().is_ok() { 21 | cli_flow::errorln("Hostname format invalid. Port is not a integer"); 22 | } 23 | } 24 | 25 | // add new host 26 | let mut host_new = vec![Host { 27 | hostname: hostname.to_owned(), 28 | ..Default::default() 29 | }]; 30 | 31 | db.hosts.append(&mut host_new); 32 | cli_flow::okln(&format!("Successfully added host {}", hostname)); 33 | } 34 | 35 | pub fn remove(db: &mut Database, hostname: &str) { 36 | if !db.host_get(hostname).is_some() { 37 | cli_flow::errorln(&format!("Hostname {} not known", hostname)); 38 | } 39 | 40 | db.hosts.retain(|h| h.hostname != hostname); 41 | cli_flow::okln(&format!("Successfully removed host {}", hostname)); 42 | } 43 | 44 | pub fn list(db: &mut Database, hostname_filter: &str, print_raw: bool) { 45 | for host in &db.hosts { 46 | if !hostname_filter.is_empty() 47 | && (hostname_filter != host.hostname && Some(hostname_filter.to_owned()) != host.alias) 48 | { 49 | continue; 50 | } 51 | 52 | if print_raw { 53 | println!("{:?}", host); 54 | continue; 55 | } 56 | 57 | println!("\n{}", host.hostname); 58 | println!( 59 | "{}", 60 | (0..host.hostname.len()).map(|_| "=").collect::() 61 | ); 62 | 63 | println!("\n## Authorized Users"); 64 | for user in &host.authorized_users { 65 | println!("* {}", user); 66 | } 67 | 68 | println!("\n## Authorized Groups"); 69 | for group in &host.authorized_user_groups { 70 | println!("* {}", group); 71 | } 72 | 73 | println!(""); 74 | } 75 | } 76 | 77 | pub fn alias(db: &mut Database, hostname: &str, alias_opt: Option<&str>) { 78 | { 79 | // filter host by hostname only 80 | let host_lookup_by_hostname = db 81 | .hosts 82 | .iter() 83 | .position(|ref h| h.hostname == hostname) 84 | .map(|i| &db.hosts[i]); 85 | 86 | match host_lookup_by_hostname { 87 | Some(_h) => (), 88 | None => { 89 | cli_flow::errorln(&format!("Hostname {} does not exist", hostname)); 90 | return; 91 | } 92 | } 93 | } 94 | { 95 | if alias_opt.is_some() { 96 | let alias = alias_opt.unwrap(); 97 | 98 | if db.host_get(alias).is_some() { 99 | cli_flow::errorln(&format!("There is already a host with hostname or alias {} - You can't use an alias where a host with this hostname already exists.", alias)); 100 | } 101 | 102 | if db.host_get_by_alias(alias).is_some() { 103 | cli_flow::errorln(&format!("Host alias {} already exists", alias)); 104 | } 105 | { 106 | let host = db.host_get_mut(hostname).unwrap(); 107 | host.alias = Some(alias.to_owned()); 108 | 109 | cli_flow::okln(&format!( 110 | "Successfully set alias {} for host {}", 111 | alias, hostname 112 | )); 113 | } 114 | } else { 115 | let host = db.host_get_mut(hostname).unwrap(); 116 | if !host.alias.is_some() { 117 | cli_flow::errorln(&format!("No alias set for host {}", hostname)); 118 | } 119 | 120 | host.alias = None; 121 | cli_flow::okln(&format!("Successfully removed alias for host {}", hostname)); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/subcommand_howto.rs: -------------------------------------------------------------------------------- 1 | pub fn print() { 2 | println!(include_str!("../examples/commands.txt")); 3 | } 4 | -------------------------------------------------------------------------------- /src/subcommand_sync.rs: -------------------------------------------------------------------------------- 1 | use cli_flow; 2 | use colored::Colorize; 3 | use database::Database; 4 | use difference::{Changeset, Difference}; 5 | use rpassword; 6 | use ssh2::Session; 7 | use ssh_config; 8 | use std::collections::HashMap; 9 | use std::env; 10 | use std::error::Error; 11 | use std::io::prelude::*; 12 | use std::io::Read; 13 | use std::net::TcpStream; 14 | use std::path::Path; 15 | use std::str; 16 | 17 | fn userauth_agent(sess: &mut Session, ssh_user: &str) -> Result> { 18 | let mut agent = try!(sess.agent()); 19 | try!(agent.connect()); 20 | agent.list_identities().unwrap(); 21 | 22 | for identity in agent.identities() { 23 | let identity = try!(identity); 24 | if agent.userauth(&ssh_user, &identity).is_ok() { 25 | return Ok(true); 26 | } 27 | } 28 | 29 | Ok(false) 30 | } 31 | 32 | pub fn sync(db: &mut Database, password_auth: bool, yes_authorized_keys_prompt: bool) { 33 | let ssh_config = match ssh_config::get() { 34 | Ok(c) => c, 35 | Err(e) => { 36 | cli_flow::warningln(&e.to_string()); 37 | HashMap::new() 38 | } 39 | }; 40 | 41 | let mut syned_sth = false; 42 | 43 | for host in &mut db.hosts { 44 | // sync needed for host? 45 | if !host.sync_todo { 46 | continue; 47 | } 48 | 49 | println!(""); 50 | cli_flow::infoln(&format!("# Syncing host {}...", host.hostname)); 51 | println!(""); 52 | 53 | syned_sth = true; 54 | 55 | // ssh connect to host 56 | // defaults for connection 57 | let mut ssh_host = &*host.hostname; 58 | let mut ssh_port = "22"; 59 | 60 | let mut ssh_user = String::new(); 61 | let ssh_user_default = "root"; 62 | 63 | let mut ssh_config_used = false; 64 | 65 | // is host to connect found in ssh_config? 66 | for (cfg_host_label, cfg_host) in &ssh_config { 67 | if cfg_host_label == ssh_host 68 | || cfg_host_label == &(host.alias.clone()).unwrap_or("".to_string()) 69 | || cfg_host.hostname == ssh_host 70 | { 71 | cli_flow::infoln(&format!( 72 | "Found hostname or alias {} in ssh_config, using config parameters (hostname, user, port) for connection", 73 | ssh_host 74 | )); 75 | 76 | ssh_host = &cfg_host.hostname; 77 | ssh_user = cfg_host.user.to_owned(); 78 | ssh_port = &cfg_host.port; 79 | ssh_config_used = true; 80 | 81 | break; 82 | } 83 | } 84 | 85 | // hostname:port format? 86 | if !ssh_config_used { 87 | let host_splitted: Vec<&str> = host.hostname.split(':').collect(); 88 | 89 | // found one ':' in hostname 90 | if host_splitted.len() == 2 { 91 | if host_splitted[1].parse::().is_ok() { 92 | ssh_host = &*host_splitted[0]; 93 | ssh_port = &*host_splitted[1]; 94 | } 95 | } 96 | } 97 | 98 | // connect! 99 | let ssh_tcp = match TcpStream::connect(&format!("{}:{}", ssh_host, ssh_port)) { 100 | Ok(t) => t, 101 | Err(e) => { 102 | cli_flow::errorln(&e.to_string()); 103 | continue; 104 | } 105 | }; 106 | 107 | // create ssh session 108 | let mut ssh_sess = match Session::new() { 109 | Some(s) => s, 110 | None => { 111 | cli_flow::errorln("Unable to create SSH session."); 112 | continue; 113 | } 114 | }; 115 | 116 | // ssh handshake 117 | match ssh_sess.handshake(&ssh_tcp) { 118 | Ok(h) => h, 119 | Err(e) => { 120 | cli_flow::errorln(&e.to_string()); 121 | continue; 122 | } 123 | }; 124 | 125 | // prompt for remote user 126 | if !ssh_config_used { 127 | ssh_user = cli_flow::read_line( 128 | &format!("SSH User ({}):", ssh_user_default), 129 | &ssh_user_default.to_owned(), 130 | ).to_owned(); 131 | } else { 132 | cli_flow::infoln(&format!("SSH User: {}", ssh_user)); 133 | } 134 | 135 | if password_auth { 136 | // prompt for password 137 | cli_flow::prompt("Password:", false); 138 | let password = rpassword::prompt_password_stdout("").unwrap(); 139 | 140 | match ssh_sess.userauth_password(&ssh_user, &password) { 141 | Ok(t) => { 142 | // drop ssh_password 143 | drop(password); 144 | t 145 | } 146 | Err(e) => { 147 | cli_flow::errorln(&e.to_string()); 148 | // drop passphrase 149 | drop(password); 150 | continue; 151 | } 152 | }; 153 | } else { 154 | let agent_authed = match userauth_agent(&mut ssh_sess, &ssh_user) { 155 | Ok(true) => true, 156 | Ok(false) | Err(_) => false, 157 | }; 158 | 159 | if !agent_authed { 160 | // guess ssh key location 161 | let private_key_path = match env::home_dir() { 162 | Some(path) => path.join(".ssh").join("id_rsa"), 163 | None => Path::new("").to_path_buf(), 164 | }; 165 | 166 | let mut private_key_file_default = private_key_path.to_str().unwrap(); 167 | let private_key_file = &cli_flow::read_line( 168 | &format!("Private key ({}):", private_key_file_default), 169 | &private_key_file_default.to_owned(), 170 | ); 171 | 172 | // prompt for passphrase 173 | cli_flow::prompt("Passphrase (empty for no passphrase):", false); 174 | let private_key_pass = rpassword::prompt_password_stdout("").unwrap(); 175 | 176 | // public key auth 177 | match ssh_sess.userauth_pubkey_file( 178 | &ssh_user, 179 | None, 180 | Path::new(&private_key_file), 181 | Some(&private_key_pass), 182 | ) { 183 | Ok(t) => { 184 | // drop passphrase 185 | drop(private_key_pass); 186 | t 187 | } 188 | Err(e) => { 189 | cli_flow::errorln(&e.to_string()); 190 | // drop passphrase 191 | drop(private_key_pass); 192 | continue; 193 | } 194 | } 195 | } 196 | } 197 | 198 | // read current authorized_keys from host 199 | let mut remote_authorized_keys_file_default = String::new(); 200 | 201 | if let Ok(mut channel) = ssh_sess.channel_session() { 202 | let mut r_get_home = channel.exec("echo $HOME"); 203 | 204 | if r_get_home.is_ok() { 205 | let mut home = String::new(); 206 | let r_read = channel.read_to_string(&mut home); 207 | 208 | if r_read.is_ok() { 209 | remote_authorized_keys_file_default = format!( 210 | "{}/.ssh/authorized_keys", 211 | home.trim_right().trim_left().to_owned() 212 | ); 213 | channel.wait_close().is_ok(); 214 | } 215 | } 216 | }; 217 | 218 | // prompt for remote authorized_keys file 219 | let mut remote_authorized_keys_file = String::new(); 220 | 221 | if yes_authorized_keys_prompt { 222 | cli_flow::infoln(&format!( 223 | "Remote authorized_keys: {}", 224 | remote_authorized_keys_file_default 225 | )); 226 | } else { 227 | remote_authorized_keys_file = cli_flow::read_line( 228 | &format!( 229 | "Remote authorized_keys ({}):", 230 | remote_authorized_keys_file_default 231 | ), 232 | &remote_authorized_keys_file_default, 233 | ).to_owned(); 234 | } 235 | 236 | let authorized_keys_res = ssh_sess.scp_recv(Path::new(&remote_authorized_keys_file)); 237 | let mut authorized_keys_remote = Vec::new(); 238 | let mut authorized_keys_remote_str = ""; 239 | 240 | match authorized_keys_res { 241 | Ok(r) => { 242 | let (mut ch, _stat) = r; 243 | ch.read_to_end(&mut authorized_keys_remote).unwrap(); 244 | 245 | authorized_keys_remote_str = match str::from_utf8(&authorized_keys_remote) { 246 | Ok(v) => v, 247 | Err(e) => { 248 | cli_flow::warningln(&format!( 249 | "{}: Invalid UTF-8 sequence: {}", 250 | host.hostname, e 251 | )); 252 | continue; 253 | } 254 | }; 255 | } 256 | Err(e) => { 257 | cli_flow::warningln(&format!( 258 | "Unable to read remote {} - {}", 259 | remote_authorized_keys_file, 260 | e.to_string() 261 | )); 262 | } 263 | }; 264 | 265 | // collect authorized_keys to sync ... 266 | let mut authorized_keys_sync_vec: Vec = Vec::new(); 267 | 268 | // ... 1. on user level 269 | for authorized_user_id in &host.authorized_users { 270 | for user in &db.users { 271 | if &user.user_id == authorized_user_id { 272 | // build e.g. 273 | // # mail@example.com 274 | // ssh-rsa ... 275 | authorized_keys_sync_vec.append(&mut vec![format!( 276 | "# {}\n{}", 277 | authorized_user_id, 278 | String::from(&*user.public_key) 279 | )]); 280 | } 281 | } 282 | } 283 | 284 | // ... 2. on group level 285 | for authorized_group_id in &host.authorized_user_groups { 286 | for group in &db.user_groups { 287 | if authorized_group_id == &group.group_id { 288 | for user_id in &group.members { 289 | for user in &db.users { 290 | if user_id == &user.user_id { 291 | authorized_keys_sync_vec.append(&mut vec![format!( 292 | "# {}\n{}", 293 | user_id, 294 | String::from(&*user.public_key) 295 | )]); 296 | break; 297 | } 298 | } 299 | } 300 | break; 301 | } 302 | } 303 | } 304 | 305 | authorized_keys_sync_vec.sort(); 306 | authorized_keys_sync_vec.dedup(); 307 | 308 | // show diff of authorized_keys of host <-> to sync 309 | let authorized_keys_sync_str = format!("{}\n", authorized_keys_sync_vec.join("\n\n")); 310 | let Changeset { diffs, .. } = 311 | Changeset::new(&authorized_keys_remote_str, &authorized_keys_sync_str, "\n"); 312 | 313 | println!(""); 314 | for i in 0..diffs.len() { 315 | match diffs[i] { 316 | Difference::Same(ref x) => { 317 | println!("{}", x); 318 | } 319 | Difference::Add(ref x) => { 320 | println!("{}", format!("+{}", x).green()); 321 | } 322 | Difference::Rem(ref x) => { 323 | println!("{}", format!("-{}", x).red()); 324 | } 325 | } 326 | } 327 | 328 | // sync confirmation 329 | if cli_flow::prompt_yes_no( 330 | &mut format!( 331 | "Verify changes. Do you want to sync to {}? (y/n):", 332 | remote_authorized_keys_file 333 | ), 334 | true, 335 | ) == "n" 336 | { 337 | cli_flow::warningln(&format!("Skipping sync of {} as you told so\n\n", ssh_host)); 338 | continue; 339 | } 340 | 341 | // sync! 342 | let mut remote_authorized_keys_fh = match ssh_sess.scp_send( 343 | Path::new(&remote_authorized_keys_file), 344 | 0o600, 345 | authorized_keys_sync_str.len() as u64, 346 | None, 347 | ) { 348 | Ok(r) => r, 349 | Err(e) => { 350 | cli_flow::errorln(&format!( 351 | "Unable to upload {} - {}", 352 | remote_authorized_keys_file, 353 | &e.to_string() 354 | )); 355 | return; 356 | } 357 | }; 358 | 359 | match remote_authorized_keys_fh.write(authorized_keys_sync_str.as_bytes()) { 360 | Ok(r) => r, 361 | Err(e) => { 362 | cli_flow::errorln(&format!( 363 | "Unable to upload {} - {}", 364 | remote_authorized_keys_file, 365 | &e.to_string() 366 | )); 367 | return; 368 | } 369 | }; 370 | 371 | // mark as synced 372 | host.sync_todo = false; 373 | 374 | cli_flow::okln(&format!( 375 | "Successfully synced to {}\n", 376 | remote_authorized_keys_file 377 | )); 378 | } 379 | 380 | if !syned_sth { 381 | cli_flow::okln("All hosts up to date. Nothing to sync, bye bye"); 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /src/subcommand_user.rs: -------------------------------------------------------------------------------- 1 | use cli_flow; 2 | use database::{Database, User}; 3 | use std::io; 4 | 5 | pub fn add(db: &mut Database, user_id: &str) { 6 | // check user is not present 7 | if db.user_get(user_id).is_some() { 8 | cli_flow::errorln(&format!("User {} already exists", user_id)); 9 | } 10 | 11 | // read public key 12 | cli_flow::promptln(&format!( 13 | "Paste the public key of {} and press the Enter key:", 14 | user_id 15 | )); 16 | 17 | let mut public_key = String::new(); 18 | io::stdin() 19 | .read_line(&mut public_key) 20 | .ok() 21 | .expect("Couldn't read public key"); 22 | 23 | // TODO:; daring assumption, validate... 24 | if !public_key.starts_with("ssh-") { 25 | cli_flow::errorln("Invalid public ssh key format") 26 | } 27 | 28 | // add new user 29 | let mut user_new = vec![User { 30 | user_id: user_id.to_owned(), 31 | public_key: public_key.trim_right().trim_left().to_owned(), 32 | }]; 33 | 34 | db.users.append(&mut user_new); 35 | cli_flow::okln(&format!("Successfully added user {}", user_id)); 36 | } 37 | 38 | pub fn remove(db: &mut Database, user_id: &str) { 39 | // check user exist 40 | if db.user_get(user_id).is_none() { 41 | cli_flow::errorln(&format!("User {} not known", user_id)); 42 | } 43 | 44 | // rm user 45 | db.users.retain(|u| u.user_id != user_id); 46 | 47 | // delete user from hosts.authorized_users 48 | for host in &mut db.hosts { 49 | host.authorized_users.retain(move |u| u != user_id); 50 | } 51 | 52 | // delete user from user_groups.members 53 | for user_group in &mut db.user_groups { 54 | user_group.members.retain(move |u| u != user_id); 55 | } 56 | 57 | cli_flow::okln(&format!("Successfully removed user {}", user_id)); 58 | } 59 | 60 | pub fn list(db: &mut Database, user_id_filter: &str, print_raw: bool) { 61 | for user in &db.users { 62 | if !user_id_filter.is_empty() && user_id_filter != user.user_id { 63 | continue; 64 | } 65 | 66 | if print_raw { 67 | println!("{:?}", user); 68 | continue; 69 | } 70 | 71 | println!("\n{}", user.user_id); 72 | println!( 73 | "{}", 74 | (0..user.user_id.len()).map(|_| "=").collect::() 75 | ); 76 | } 77 | 78 | println!(""); 79 | } 80 | 81 | pub fn grant(db: &mut Database, user_id: &str, hostname: &str) { 82 | if let Some(host) = db.host_get(hostname) { 83 | if let Some(user) = db.user_get(user_id) { 84 | if db.is_user_granted(&user, &host) { 85 | cli_flow::errorln(&format!( 86 | "{} already granted to access {}", 87 | user.user_id, hostname 88 | )); 89 | } 90 | } else { 91 | cli_flow::errorln(&format!("User {} not known", user_id)); 92 | } 93 | } else { 94 | cli_flow::errorln(&format!("Hostname {} not known", hostname)); 95 | } 96 | 97 | // at this point it's save to mut db.host... 98 | { 99 | let host = db.host_get_mut(hostname).unwrap(); 100 | host.authorized_users 101 | .append(&mut vec![String::from(user_id)]); 102 | host.sync_todo = true; 103 | } 104 | 105 | cli_flow::okln(&format!( 106 | "Successfully granted user {} to host {}", 107 | user_id, hostname 108 | )); 109 | } 110 | 111 | pub fn revoke(db: &mut Database, user_id: &str, hostname: &str) { 112 | if let Some(host) = db.host_get(hostname) { 113 | if let Some(user) = db.user_get(user_id) { 114 | if !db.is_user_granted(&user, &host) { 115 | cli_flow::errorln(&format!( 116 | "{} is not granted to access {}", 117 | user.user_id, hostname 118 | )); 119 | } 120 | } else { 121 | cli_flow::errorln(&format!("User {} not known", user_id)); 122 | } 123 | } else { 124 | cli_flow::errorln(&format!("Hostname {} not known", hostname)); 125 | } 126 | 127 | // at this point it's save to mut db.host... 128 | { 129 | let host = db.host_get_mut(hostname).unwrap(); 130 | host.authorized_users.retain(|u| u != user_id); 131 | host.sync_todo = true; 132 | } 133 | 134 | cli_flow::okln(&format!( 135 | "Successfully revoked user {} from host {}", 136 | user_id, hostname 137 | )); 138 | } 139 | -------------------------------------------------------------------------------- /tests/fixtures/ssh-permit.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosts": [ 3 | { 4 | "hostname": "existing.example.com", 5 | "alias": "existing", 6 | "authorized_users": [], 7 | "authorized_user_groups": [], 8 | "sync_todo": true 9 | } 10 | ], 11 | "users": [], 12 | "user_groups": [], 13 | "modified_at": "2018-04-01 20:35:56.910957 UTC", 14 | "schema_version": "0.1.0" 15 | } -------------------------------------------------------------------------------- /tests/integration.rs: -------------------------------------------------------------------------------- 1 | extern crate assert_cli; 2 | 3 | use std::fs; 4 | use std::panic; 5 | use std::path::{Path, PathBuf}; 6 | 7 | fn tests_dir() -> PathBuf { 8 | Path::new(env!("CARGO_MANIFEST_DIR")).join("tests") 9 | } 10 | 11 | fn tests_tmp_dir() -> PathBuf { 12 | tests_dir().join("tmp") 13 | } 14 | 15 | fn settings_fixtures_src() -> PathBuf { 16 | tests_dir().join("fixtures").join("ssh-permit.json") 17 | } 18 | 19 | fn settings_fixtures_copy(test_id: u32) -> PathBuf { 20 | tests_tmp_dir().join(format!("ssh-permit-{}.json", test_id)) 21 | } 22 | 23 | fn setup(test_id: u32) { 24 | fs::remove_file(&settings_fixtures_copy(test_id).as_path()).is_ok(); 25 | fs::copy( 26 | &settings_fixtures_src().as_path(), 27 | &settings_fixtures_copy(test_id).as_path(), 28 | ).unwrap(); 29 | } 30 | 31 | fn teardown(test_id: u32) { 32 | fs::remove_file(&settings_fixtures_copy(test_id).as_path()).unwrap(); 33 | } 34 | 35 | fn assert_cli_bin(test_id: u32) -> assert_cli::Assert { 36 | assert_cli::Assert::main_binary().with_args(&[ 37 | "--database", 38 | &settings_fixtures_copy(test_id).to_str().unwrap(), 39 | ]) 40 | } 41 | 42 | fn run_test(test_id: u32, test: T) -> () 43 | where 44 | T: FnOnce() -> () + panic::UnwindSafe, 45 | { 46 | setup(test_id); 47 | let result = panic::catch_unwind(|| test()); 48 | teardown(test_id); 49 | assert!(result.is_ok()) 50 | } 51 | 52 | #[test] 53 | fn host_add_remove() { 54 | let test_id = line!(); 55 | 56 | run_test(test_id, || { 57 | // host foo1@example.com add 58 | assert_cli_bin(test_id) 59 | .with_args(&["host", "1.example.com", "add"]) 60 | .succeeds() 61 | .stdin("ssh-") 62 | .unwrap(); 63 | 64 | // host foo1@exmap2e.com add 65 | assert_cli_bin(test_id) 66 | .with_args(&["host", "2.example.com", "add"]) 67 | .succeeds() 68 | .stdin("ssh-") 69 | .unwrap(); 70 | 71 | // host list (check for existing host in fixture) 72 | assert_cli_bin(test_id) 73 | .with_args(&["host", "list"]) 74 | .succeeds() 75 | .stdout() 76 | .contains("existing.example.com") 77 | .unwrap(); 78 | 79 | // remove existing host 80 | assert_cli_bin(test_id) 81 | .with_args(&["host", "existing.example.com", "remove"]) 82 | .succeeds() 83 | .unwrap(); 84 | 85 | // host list 86 | assert_cli_bin(test_id) 87 | .with_args(&["host", "list"]) 88 | .succeeds() 89 | .stdout() 90 | .contains("1.example.com") 91 | .unwrap(); 92 | 93 | // host list 94 | assert_cli_bin(test_id) 95 | .with_args(&["host", "list"]) 96 | .succeeds() 97 | .stdout() 98 | .contains("2.example.com") 99 | .unwrap(); 100 | 101 | // host foo1@example.com remove 102 | assert_cli_bin(test_id) 103 | .with_args(&["host", "1.example.com", "remove"]) 104 | .succeeds() 105 | .unwrap(); 106 | 107 | // host foo2@example.com remove 108 | assert_cli_bin(test_id) 109 | .with_args(&["host", "2.example.com", "remove"]) 110 | .succeeds() 111 | .unwrap(); 112 | 113 | // host list 114 | assert_cli_bin(test_id) 115 | .with_args(&["host", "list"]) 116 | .succeeds() 117 | .stdout() 118 | .doesnt_contain("1.example.com") 119 | .stdout() 120 | .doesnt_contain("2.example.com") 121 | .unwrap(); 122 | }) 123 | } 124 | 125 | #[test] 126 | fn host_add_duplicate_deny() { 127 | let test_id = line!(); 128 | 129 | run_test(test_id, || { 130 | // host example.com add 131 | assert_cli_bin(test_id) 132 | .with_args(&["host", "example.com", "add"]) 133 | .succeeds() 134 | .stdin("ssh-") 135 | .unwrap(); 136 | 137 | // host examaple.com add 138 | assert_cli_bin(test_id) 139 | .with_args(&["host", "example.com", "add"]) 140 | .fails() 141 | .unwrap(); 142 | 143 | // host list 144 | assert_cli_bin(test_id) 145 | .with_args(&["host", "list"]) 146 | .succeeds() 147 | .stdout() 148 | .contains("example.com") 149 | .unwrap(); 150 | }) 151 | } 152 | 153 | #[test] 154 | fn host_alias() { 155 | let test_id = line!(); 156 | 157 | run_test(test_id, || { 158 | // add two hosts 159 | assert_cli_bin(test_id) 160 | .with_args(&["host", "1.example.com", "add"]) 161 | .succeeds() 162 | .unwrap(); 163 | assert_cli_bin(test_id) 164 | .with_args(&["host", "2.example.com", "add"]) 165 | .succeeds() 166 | .unwrap(); 167 | 168 | // try to alias unknown host 169 | assert_cli_bin(test_id) 170 | .with_args(&["host", "foo.example.com", "alias", "1"]) 171 | .fails() 172 | .unwrap(); 173 | 174 | // try to alias with an alias where the hostname already exists 175 | assert_cli_bin(test_id) 176 | .with_args(&["host", "foo.example.com", "alias", "1.example.com"]) 177 | .fails() 178 | .unwrap(); 179 | 180 | // check no alias were set 181 | assert_cli_bin(test_id) 182 | .with_args(&["host", "list", "--raw"]) 183 | .succeeds() 184 | .stdout() 185 | .contains("alias: None") 186 | .stdout() 187 | .doesnt_contain("alias: \"") 188 | .unwrap(); 189 | 190 | // alias 1.example.com 191 | assert_cli_bin(test_id) 192 | .with_args(&["host", "1.example.com", "alias", "1"]) 193 | .succeeds() 194 | .unwrap(); 195 | 196 | // check alias was set 197 | assert_cli_bin(test_id) 198 | .with_args(&["host", "1.example.com", "list", "--raw"]) 199 | .succeeds() 200 | .stdout() 201 | .contains("alias: Some(\"1\")") 202 | .stdout() 203 | .doesnt_contain("alias: None") 204 | .unwrap(); 205 | 206 | // check lookup by alias works 207 | assert_cli_bin(test_id) 208 | .with_args(&["host", "1", "list"]) 209 | .succeeds() 210 | .stdout() 211 | .contains("1.example.com") 212 | .stdout() 213 | .doesnt_contain("2.example.com") 214 | .unwrap(); 215 | 216 | // overwrite alias 217 | assert_cli_bin(test_id) 218 | .with_args(&["host", "1.example.com", "alias", "one"]) 219 | .succeeds() 220 | .unwrap(); 221 | 222 | // check alias was set 223 | assert_cli_bin(test_id) 224 | .with_args(&["host", "1.example.com", "list", "--raw"]) 225 | .succeeds() 226 | .stdout() 227 | .contains("alias: Some(\"one\")") 228 | .stdout() 229 | .doesnt_contain("alias: None") 230 | .unwrap(); 231 | 232 | // remove alias 233 | assert_cli_bin(test_id) 234 | .with_args(&["host", "1.example.com", "alias"]) 235 | .succeeds() 236 | .unwrap(); 237 | 238 | // check alias was removed 239 | assert_cli_bin(test_id) 240 | .with_args(&["host", "1.example.com", "list", "--raw"]) 241 | .succeeds() 242 | .stdout() 243 | .contains("alias: None") 244 | .stdout() 245 | .doesnt_contain("alias: Some(\"one\")") 246 | .unwrap(); 247 | 248 | // try to remove alias again results in error msg 249 | assert_cli_bin(test_id) 250 | .with_args(&["host", "1.example.com", "alias"]) 251 | .fails() 252 | .unwrap(); 253 | }) 254 | } 255 | 256 | #[test] 257 | fn user_add_remove() { 258 | let test_id = line!(); 259 | 260 | run_test(test_id, || { 261 | // user foo1 add 262 | assert_cli_bin(test_id) 263 | .with_args(&["user", "foo1", "add"]) 264 | .stdin("ssh-123") 265 | .succeeds() 266 | .unwrap(); 267 | 268 | // user foo2 add 269 | assert_cli_bin(test_id) 270 | .with_args(&["user", "foo2", "add"]) 271 | .stdin("ssh-456") 272 | .succeeds() 273 | .unwrap(); 274 | 275 | // user foo3 add (missing key) 276 | assert_cli_bin(test_id) 277 | .with_args(&["user", "foo3", "add"]) 278 | .fails() 279 | .unwrap(); 280 | 281 | // user list 282 | assert_cli_bin(test_id) 283 | .with_args(&["user", "list"]) 284 | .succeeds() 285 | .stdout() 286 | .contains("foo1") 287 | .unwrap(); 288 | 289 | // user list 290 | assert_cli_bin(test_id) 291 | .with_args(&["user", "list"]) 292 | .succeeds() 293 | .stdout() 294 | .contains("foo1") 295 | .stdout() 296 | .contains("foo2") 297 | .unwrap(); 298 | 299 | // user list --raw 300 | assert_cli_bin(test_id) 301 | .with_args(&["user", "foo1", "list", "--raw"]) 302 | .succeeds() 303 | .stdout() 304 | .contains("ssh-123") 305 | .stdout() 306 | .doesnt_contain("ssh-456") 307 | .unwrap(); 308 | 309 | // user foo1 remove 310 | assert_cli_bin(test_id) 311 | .with_args(&["user", "foo1", "remove"]) 312 | .succeeds() 313 | .unwrap(); 314 | 315 | // user foo2 remove 316 | assert_cli_bin(test_id) 317 | .with_args(&["user", "foo2", "remove"]) 318 | .succeeds() 319 | .unwrap(); 320 | 321 | // user list 322 | assert_cli_bin(test_id) 323 | .with_args(&["user", "list"]) 324 | .succeeds() 325 | .stdout() 326 | .doesnt_contain("foo1") 327 | .stdout() 328 | .doesnt_contain("foo2") 329 | .unwrap(); 330 | }) 331 | } 332 | 333 | #[test] 334 | fn user_add_duplicate_deny() { 335 | let test_id = line!(); 336 | 337 | run_test(test_id, || { 338 | // user foo add 339 | assert_cli_bin(test_id) 340 | .with_args(&["user", "foo1", "add"]) 341 | .stdin("ssh-") 342 | .succeeds() 343 | .unwrap(); 344 | 345 | // user foo add 346 | assert_cli_bin(test_id) 347 | .with_args(&["user", "foo1", "add"]) 348 | .stdin("ssh-") 349 | .fails() 350 | .unwrap(); 351 | 352 | // user list 353 | assert_cli_bin(test_id) 354 | .with_args(&["user", "list"]) 355 | .succeeds() 356 | .stdout() 357 | .contains("foo1") 358 | .unwrap(); 359 | }) 360 | } 361 | 362 | #[test] 363 | fn group_add_remove() { 364 | let test_id = line!(); 365 | 366 | run_test(test_id, || { 367 | // group dev-ops add 368 | assert_cli_bin(test_id) 369 | .with_args(&["group", "dev-ops", "add"]) 370 | .succeeds() 371 | .unwrap(); 372 | 373 | // group fsupport add 374 | assert_cli_bin(test_id) 375 | .with_args(&["group", "support", "add"]) 376 | .succeeds() 377 | .unwrap(); 378 | 379 | // group list 380 | assert_cli_bin(test_id) 381 | .with_args(&["group", "list"]) 382 | .succeeds() 383 | .stdout() 384 | .contains("dev-ops") 385 | .unwrap(); 386 | 387 | // group list 388 | assert_cli_bin(test_id) 389 | .with_args(&["group", "list"]) 390 | .succeeds() 391 | .stdout() 392 | .contains("support") 393 | .unwrap(); 394 | 395 | // group dev-ops .com remove 396 | assert_cli_bin(test_id) 397 | .with_args(&["group", "dev-ops", "remove"]) 398 | .succeeds() 399 | .unwrap(); 400 | 401 | // group support remove 402 | assert_cli_bin(test_id) 403 | .with_args(&["group", "support", "remove"]) 404 | .succeeds() 405 | .unwrap(); 406 | 407 | // group support remove 408 | assert_cli_bin(test_id) 409 | .with_args(&["group", "support", "remove"]) 410 | .fails() 411 | .unwrap(); 412 | 413 | // group list 414 | assert_cli_bin(test_id) 415 | .with_args(&["group", "list"]) 416 | .succeeds() 417 | .stdout() 418 | .doesnt_contain("dev-ops") 419 | .stdout() 420 | .doesnt_contain("support") 421 | .unwrap(); 422 | }) 423 | } 424 | 425 | #[test] 426 | fn group_add_duplicate_deny() { 427 | let test_id = line!(); 428 | 429 | run_test(test_id, || { 430 | // group dev-ops add 431 | assert_cli_bin(test_id) 432 | .with_args(&["group", "dev-ops", "add"]) 433 | .succeeds() 434 | .unwrap(); 435 | 436 | // group dev-ops add (duplicate) 437 | assert_cli_bin(test_id) 438 | .with_args(&["group", "dev-ops", "add"]) 439 | .fails() 440 | .unwrap(); 441 | 442 | // group list 443 | assert_cli_bin(test_id) 444 | .with_args(&["group", "list"]) 445 | .succeeds() 446 | .stdout() 447 | .contains("dev-ops") 448 | .unwrap(); 449 | }) 450 | } 451 | 452 | #[test] 453 | fn user_grant_revoke() { 454 | let test_id = line!(); 455 | 456 | run_test(test_id, || { 457 | // user foo1 add 458 | assert_cli_bin(test_id) 459 | .with_args(&["user", "foo1", "add"]) 460 | .stdin("ssh-") 461 | .succeeds() 462 | .unwrap(); 463 | 464 | // user foo2 add 465 | assert_cli_bin(test_id) 466 | .with_args(&["user", "foo2", "add"]) 467 | .stdin("ssh-") 468 | .succeeds() 469 | .unwrap(); 470 | 471 | // user foo3 add 472 | assert_cli_bin(test_id) 473 | .with_args(&["user", "foo3", "add"]) 474 | .stdin("ssh-") 475 | .succeeds() 476 | .unwrap(); 477 | 478 | // host 1.example.com add 479 | assert_cli_bin(test_id) 480 | .with_args(&["host", "1.example.com", "add"]) 481 | .succeeds() 482 | .unwrap(); 483 | 484 | // host 2.example.com add 485 | assert_cli_bin(test_id) 486 | .with_args(&["host", "2.example.com", "add"]) 487 | .succeeds() 488 | .unwrap(); 489 | 490 | // user foo1 grant 1.example.com 491 | assert_cli_bin(test_id) 492 | .with_args(&["user", "foo1", "grant", "1.example.com"]) 493 | .succeeds() 494 | .unwrap(); 495 | 496 | // user foo1 grant 1.example.com (fail, second add) 497 | assert_cli_bin(test_id) 498 | .with_args(&["user", "foo1", "grant", "1.example.com"]) 499 | .fails() 500 | .unwrap(); 501 | 502 | // user foo1 grant 2.example.com 503 | assert_cli_bin(test_id) 504 | .with_args(&["user", "foo1", "grant", "2.example.com"]) 505 | .succeeds() 506 | .unwrap(); 507 | 508 | // user foo2 grant 2.example.com 509 | assert_cli_bin(test_id) 510 | .with_args(&["user", "foo2", "grant", "1.example.com"]) 511 | .succeeds() 512 | .unwrap(); 513 | 514 | // host list 515 | assert_cli_bin(test_id) 516 | .with_args(&["host", "list"]) 517 | .succeeds() 518 | .stdout() 519 | .contains("foo1") 520 | .stdout() 521 | .contains("foo2") 522 | .stdout() 523 | .doesnt_contain("foo3") 524 | .unwrap(); 525 | 526 | // host 1.example.com list 527 | assert_cli_bin(test_id) 528 | .with_args(&["host", "1.example.com", "list"]) 529 | .succeeds() 530 | .stdout() 531 | .contains("foo1") 532 | .stdout() 533 | .contains("foo2") 534 | .stdout() 535 | .doesnt_contain("foo3") 536 | .unwrap(); 537 | 538 | // host 2.example.com list 539 | assert_cli_bin(test_id) 540 | .with_args(&["host", "2.example.com", "list"]) 541 | .succeeds() 542 | .stdout() 543 | .contains("foo1") 544 | .stdout() 545 | .doesnt_contain("foo2") 546 | .stdout() 547 | .doesnt_contain("foo3") 548 | .unwrap(); 549 | 550 | // user foo1 revoke 2.example.com 551 | assert_cli_bin(test_id) 552 | .with_args(&["user", "foo1", "revoke", "2.example.com"]) 553 | .succeeds() 554 | .unwrap(); 555 | 556 | // host list 557 | assert_cli_bin(test_id) 558 | .with_args(&["host", "list"]) 559 | .succeeds() 560 | .stdout() 561 | .contains("foo1") 562 | .stdout() 563 | .contains("foo2") 564 | .unwrap(); 565 | 566 | // host 2.example.com list 567 | assert_cli_bin(test_id) 568 | .with_args(&["host", "2.example.com", "list"]) 569 | .succeeds() 570 | .stdout() 571 | .doesnt_contain("foo1") 572 | .stdout() 573 | .doesnt_contain("foo2") 574 | .unwrap(); 575 | 576 | // host 1.example.com list 577 | assert_cli_bin(test_id) 578 | .with_args(&["host", "1.example.com", "list"]) 579 | .succeeds() 580 | .stdout() 581 | .contains("foo1") 582 | .unwrap(); 583 | 584 | // user foo1 revoke 1.example.com 585 | assert_cli_bin(test_id) 586 | .with_args(&["user", "foo1", "revoke", "1.example.com"]) 587 | .succeeds() 588 | .unwrap(); 589 | 590 | // host list 591 | assert_cli_bin(test_id) 592 | .with_args(&["host", "list"]) 593 | .succeeds() 594 | .stdout() 595 | .doesnt_contain("foo1") 596 | .stdout() 597 | .contains("foo2") 598 | .unwrap(); 599 | 600 | // user foo2 revoke 1.example.com 601 | assert_cli_bin(test_id) 602 | .with_args(&["user", "foo2", "revoke", "1.example.com"]) 603 | .succeeds() 604 | .unwrap(); 605 | 606 | // host list 607 | assert_cli_bin(test_id) 608 | .with_args(&["host", "list"]) 609 | .succeeds() 610 | .stdout() 611 | .doesnt_contain("foo2") 612 | .unwrap(); 613 | }) 614 | } 615 | -------------------------------------------------------------------------------- /tests/tmp/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ierror/ssh-permit-a38/ae88aa13206c7313d8aacfde2392a0931d00dafb/tests/tmp/.gitkeep --------------------------------------------------------------------------------