Skip to content

Commit

Permalink
Support loading secret and email password args from a file (#13)
Browse files Browse the repository at this point in the history
* DRY up usages of mocks in cli tests
  • Loading branch information
flaeppe authored Oct 15, 2021
1 parent 79988e8 commit 8bbd4f2
Show file tree
Hide file tree
Showing 3 changed files with 316 additions and 89 deletions.
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ optional arguments:
Required arguments:
-s SECRET, --secret SECRET
Secret key
-sf PATH_TO_FILE, --secret-file PATH_TO_FILE
Secret key file
-a AUTH_HOST, --auth-host AUTH_HOST
The entrypoint domain name for access guard (without protocol or path)
-t TRUSTED_HOST [TRUSTED_HOST ...], --trusted-hosts TRUSTED_HOST [TRUSTED_HOST ...]
Expand All @@ -80,6 +82,8 @@ Optional email arguments:
Username to login with on configured SMTP server [default: unset]
--email-password EMAIL_PASSWORD
Password to login with on configured SMTP server [default: unset]
--email-password-file PATH_TO_FILE
File containing password to login with on configured SMTP server [default: unset]
--email-use-tls Make the _initial_ connection to the SMTP server over TLS/SSL [default: false]
--email-start-tls Make the initial connection to the SMTP server over plaintext, and then upgrade the connection to TLS/SSL [default: false]
--email-no-validate-certs
Expand Down Expand Up @@ -127,6 +131,23 @@ Optional cookie arguments:

Should be set to a unique, unpredictable value. Is used for cryptographic signing.

Both `--secret` and `--secret-file` can _not_ be passed at the same time

- `-sf/--secret-file PATH_TO_FILE`

As an alternative to passing the secret via command line, the value can be loaded
from a file present in the container. For example:

```
--secret-file /run/secrets/access-guard-secret
```

Only the _first line_ of the secret file will be read and any newline character at
the end of it will be removed. If the first line is _empty_ after any newline
character has been removed, an error will be raised.

Both `--secret-file` and `--secret` can _not_ be passed at the same time.

- `-a/--auth-host AUTH_HOST`

The configured domain name for the access guard service, without protocol or path. The service
Expand Down Expand Up @@ -239,6 +260,25 @@ Optional cookie arguments:

Password to login with on configured SMTP server

Both `--email-password` and `--email-password-file` can _not_ be passed at the same
time

- `--email-password-file PATH_TO_FILE` [default: unset]

As an alternative to passing a password via command line, the value can be loaded
from a file present in the container. For example:

```
--email-password-file /run/secrets/email-passwd
```

Only the _first line_ of the password file will be read and any newline character at
the end of it will be removed. If the first line is _empty_ after any newline
character has been removed, an error will be raised.

Both `--email-password-file` and `--email-password` can _not_ be passed at the same
time

- `--email-use-tls` [default: false]

Make the _initial_ connection to the SMTP server over TLS/SSL.
Expand Down
64 changes: 54 additions & 10 deletions access_guard/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,17 @@
def command(argv: list[str] | None = None) -> None:
from access_guard.environ import environ

environ.load(vars(parse_argv(argv if argv is not None else sys.argv[1:])))
parsed = vars(parse_argv(argv if argv is not None else sys.argv[1:]))
parsed["secret"] = (
read_first_line(secret_file)
if (secret_file := parsed.pop("secret_file", None))
else parsed["secret"]
)
if email_password_file := parsed.pop("email_password_file", None):
parsed["email_password"] = read_first_line(email_password_file)

environ.load(parsed)

if not healthcheck():
exit(666)
start_server()
Expand Down Expand Up @@ -48,7 +58,18 @@ def parse_argv(argv: list[str]) -> argparse.Namespace:
help="Email addresses to match, each compiled to a regex",
)
# Required arguments
required.add_argument("-s", "--secret", required=True, type=str, help="Secret key")
secret_mutex = required.add_mutually_exclusive_group(required=True)
secret_mutex.add_argument(
"-s", "--secret", type=str, dest="secret", help="Secret key"
)
secret_mutex.add_argument(
"-sf",
"--secret-file",
type=Path,
dest="secret_file",
metavar="PATH_TO_FILE",
help="Secret key file",
)
# TODO: Validate auth_host is subdomain of cookie_domain
required.add_argument(
"-a",
Expand Down Expand Up @@ -143,15 +164,27 @@ def parse_argv(argv: list[str]) -> argparse.Namespace:
default=argparse.SUPPRESS,
help="Username to login with on configured SMTP server [default: unset]",
)
email_optional.add_argument(
email_password_mutex = email_optional.add_mutually_exclusive_group()
email_password_mutex.add_argument(
"--email-password",
type=str,
dest="email_password",
default=argparse.SUPPRESS,
help="Password to login with on configured SMTP server [default: unset]",
)
email_mutex = email_optional.add_mutually_exclusive_group()
email_mutex.add_argument(
email_password_mutex.add_argument(
"--email-password-file",
type=Path,
dest="email_password_file",
metavar="PATH_TO_FILE",
default=argparse.SUPPRESS,
help=(
"File containing password to login with on configured SMTP server"
" [default: unset]"
),
)
email_tls_mutex = email_optional.add_mutually_exclusive_group()
email_tls_mutex.add_argument(
"--email-use-tls",
dest="email_use_tls",
action="store_true",
Expand All @@ -160,7 +193,7 @@ def parse_argv(argv: list[str]) -> argparse.Namespace:
" [default: false]"
),
)
email_mutex.add_argument(
email_tls_mutex.add_argument(
"--email-start-tls",
dest="email_start_tls",
action="store_true",
Expand Down Expand Up @@ -261,16 +294,27 @@ def parse_argv(argv: list[str]) -> argparse.Namespace:
return args


class HealthcheckFailed(Exception):
...


def start_server() -> None:
from access_guard import server

server.run()


def read_first_line(path: Path) -> str:
with path.open() as f:
value = f.readline().rstrip("\n")

if not value:
sys.stderr.write(f"Encountered empty first line in {str(path)}\n")
sys.exit(2)

return value


class HealthcheckFailed(Exception):
...


async def _check_smtp() -> None:
from access_guard.emails import get_connection

Expand Down
Loading

0 comments on commit 8bbd4f2

Please sign in to comment.