Skip to content

Commit

Permalink
Add source IP support in REST auth (#165)
Browse files Browse the repository at this point in the history
  • Loading branch information
robklg authored Dec 24, 2023
1 parent ba56dc5 commit 1bf22e2
Show file tree
Hide file tree
Showing 9 changed files with 248 additions and 161 deletions.
296 changes: 165 additions & 131 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ http = "0.2.9"
hyper = { version = "0.14.27", features = ["server", "http1"] }
hyper-rustls = "0.23.2"
lazy_static = "1.4.0"
libunftp = "0.19.0"
libunftp = "0.19.1"
prometheus = { version = "0.13.3", features = ["process"] }
serde = { version = "1.0.188", features = ["derive"] }
serde_json = "1.0.107"
Expand All @@ -46,10 +46,10 @@ slog-async = "2.8.0"
slog-term = "2.9.0"
thiserror = "1.0.48"
tokio = { version = "1.32.0", features = ["signal", "rt-multi-thread"] }
unftp-sbe-fs = "0.2.3"
unftp-sbe-gcs = { version = "0.2.4", optional = true }
unftp-auth-rest = { version = "0.2.3", optional = true }
unftp-auth-jsonfile = { version = "0.3.2", optional = true }
unftp-sbe-fs = "0.2.4"
unftp-sbe-gcs = { version = "0.2.5", optional = true }
unftp-auth-rest = { version = "0.2.4", optional = true }
unftp-auth-jsonfile = { version = "0.3.3", optional = true }
unftp-sbe-rooter = "0.2.0"
unftp-sbe-restrict = "0.1.1"

Expand Down
2 changes: 1 addition & 1 deletion RELEASE-CHECKLIST.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,5 @@
> slog-redis-v0.1.2
* Wait for the Github Actions pipeline to finish. You should see all artifacts in the release page.
* Build and push the docker containers
* Publish the docs site unftp.rs by running `make site`
* Publish the docs site unftp.rs by running `make site`.
* Notify the Telegram channel.
2 changes: 1 addition & 1 deletion crates/redislog/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ pub struct Builder {
connection_pool_size: u32,
}

/// Errors returned by the [`Builder`](crate::Builder) and the [`Logger`](crate::Logger)
/// Errors returned by the [`Builder`] and the [`Logger`]
#[derive(Debug)]
pub enum Error {
ConnectionPoolErr(r2d2::Error),
Expand Down
43 changes: 43 additions & 0 deletions docs/server/rest.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
title: REST auth
---

There are multiple ways to externalize authentication, rather than [local JSON authentication](/server/jsonconfig).
This page explains how to use the REST authenticator.
With the REST method, you provide your own HTTP endpoint.
You can use any method like GET or POST, and you can customize unFTP to support your API.

## Set up

Example:

```sh
unftp \
--auth-type rest \
--auth-rest-method POST \
--auth-rest-url http://localhost:5000/v1/ftp-auth \
--auth-rest-body '{"username":"{USER}","password":"{PASS}"}' \
--auth-rest-selector /status \
--auth-rest-regex successful
```

Let's say, a user `alice` logs in on the unFTP server.
With the above configuration unFTP will build an HTTP POST authentication request for http://localhost:5000/v1/ftp-auth
It will replace the placeholders `{USER}`, and `{PASS}` with the given FTP username and password:

```json
{"username":"alice","password":"abc1234"}
```

And, if the login was successful, the server should respond with something like:

```json
{"message":"User logged in.","status":"successful"}
```

The REST authenticator uses the `/status` JSON pointer, and matches it to `"successful"`.

Aside from the placeholders `{USER}` and `{PASS}` you can use `{IP}`.
That will add the source IP address of the connected client.
That is in case if you want to perform client-IP whitelisting, next to regular username and password.

1 change: 1 addition & 0 deletions doctave.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ navigation:
- path: docs/server/ftps.md
- path: docs/server/gcs.md
- path: docs/server/jsonconfig.md
- path: docs/server/rest.md
- path: docs/server/pubsub.md
- path: docs/server/anti-brute.md
- path: docs/server/proxy-protocol.md
Expand Down
8 changes: 4 additions & 4 deletions src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ pub(crate) fn clap_app(tmp_dir: &str) -> clap::Command {
Arg::new(AUTH_REST_URL)
.long("auth-rest-url")
.value_name("URL")
.help("Define REST endpoint. {USER} and {PASS} are replaced by provided credentials.")
.help("Define REST endpoint. {USER}, {PASS} and/or {IP} are replaced by provided credentials and source IP respectively.")
.env("UNFTP_AUTH_REST_URL")
.takes_value(true),
)
Expand All @@ -389,9 +389,9 @@ pub(crate) fn clap_app(tmp_dir: &str) -> clap::Command {
.arg(
Arg::new(AUTH_REST_BODY)
.long("auth-rest-body")
.value_name("URL")
.help("If HTTP method contains body, it can be specified here. {USER} and {PASS} \
are replaced by provided credentials.")
.value_name("TEMPLATE")
.help("If HTTP method contains body, it can be specified here. {USER}, {PASS} and/or {IP}\
are replaced by provided credentials and source IP respectively.")
.env("UNFTP_AUTH_REST_BODY")
.takes_value(true),
)
Expand Down
2 changes: 1 addition & 1 deletion src/infra/pubsub.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use std::sync::Arc;
// - API Docs for publishing: https://cloud.google.com/pubsub/docs/reference/rest/v1/projects.topics/publish
//

/// An [EventDispatcher](crate::domain::EventDispatcher) that dispatches to Google Pub/sub
/// An [EventDispatcher] that dispatches to Google Pub/sub
#[derive(Debug)]
pub struct PubsubEventDispatcher {
log: Arc<slog::Logger>,
Expand Down
45 changes: 27 additions & 18 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,6 @@ fn make_pam_auth(m: &clap::ArgMatches) -> Result<LookupAuthenticator, String> {
}
}

// FIXME: add user support
fn make_rest_auth(m: &clap::ArgMatches) -> Result<LookupAuthenticator, String> {
#[cfg(not(feature = "rest_auth"))]
{
Expand All @@ -161,23 +160,33 @@ fn make_rest_auth(m: &clap::ArgMatches) -> Result<LookupAuthenticator, String> {
);
}

let authenticator: unftp_auth_rest::RestAuthenticator =
match unftp_auth_rest::Builder::new()
.with_username_placeholder("{USER}".to_string())
.with_password_placeholder("{PASS}".to_string())
.with_url(String::from(url))
.with_method(
hyper::Method::from_str(method)
.map_err(|e| format!("error creating REST auth: {}", e))?,
)
.with_body(String::from(m.value_of(args::AUTH_REST_BODY).unwrap_or("")))
.with_selector(String::from(selector))
.with_regex(String::from(regex))
.build()
{
Ok(res) => res,
Err(e) => return Err(format!("Unable to create RestAuthenticator: {}", e)),
};
let body = String::from(m.value_of(args::AUTH_REST_BODY).unwrap_or(""));
let mut builder = unftp_auth_rest::Builder::new()
.with_url(String::from(url))
.with_method(
hyper::Method::from_str(method)
.map_err(|e| format!("error creating REST auth: {}", e))?,
)
.with_body(String::from(m.value_of(args::AUTH_REST_BODY).unwrap_or("")))
.with_selector(String::from(selector))
.with_regex(String::from(regex));

if url.contains("{USER}") || body.contains("{USER}") {
builder = builder.with_username_placeholder("{USER}".to_string());
}

if url.contains("{PASS}") || body.contains("{PASS}") {
builder = builder.with_password_placeholder("{PASS}".to_string());
}

if url.contains("{IP}") || body.contains("{IP}") {
builder = builder.with_source_ip_placeholder("{IP}".to_string());
}

let authenticator: unftp_auth_rest::RestAuthenticator = match builder.build() {
Ok(res) => res,
Err(e) => return Err(format!("Unable to create RestAuthenticator: {}", e)),
};

Ok(LookupAuthenticator::new(authenticator))
}
Expand Down

0 comments on commit 1bf22e2

Please sign in to comment.