main
py 164 lines 4.85 KB
Raw
1 #!/usr/bin/env python3
2
3 import re
4 import subprocess
5 import sys
6 from pathlib import Path
7 from textwrap import dedent, indent
8
9 import click
10
11
12 def find_project_root(start: Path) -> Path:
13 # Can search for `flake.nix` because there are multiple in this project.
14 root_indicator = start / ".git/config"
15 if root_indicator.exists():
16 return start
17
18 return find_project_root(start.parent)
19
20
21 def find_relative_project_root() -> Path:
22 return find_project_root(Path.cwd()).relative_to(Path.cwd(), walk_up=True)
23
24
25 PROJECT_ROOT = find_relative_project_root()
26 NON_CRITICAL_INFRA_DIR = PROJECT_ROOT / "non-critical-infra"
27 MAILING_LISTS_NIX = NON_CRITICAL_INFRA_DIR / "modules/mailserver/mailing-lists.nix"
28 assert MAILING_LISTS_NIX.exists()
29
30
31 def encrypt_to_file(plaintext: str, secret_path: Path, force: bool) -> None:
32 if secret_path.exists():
33 if not force:
34 msg = f"Refusing to clobber existing {secret_path}. Use `--force` to override."
35 raise click.ClickException(msg)
36 click.secho(f"Clobbering existing {secret_path}", fg="yellow")
37
38 cp = subprocess.run(
39 [
40 "sops",
41 "--encrypt",
42 "--filename-override",
43 secret_path,
44 "/dev/stdin",
45 ],
46 cwd=secret_path.parent,
47 text=True,
48 check=True,
49 stdout=subprocess.PIPE,
50 input=plaintext,
51 )
52
53 secret_path.write_text(cp.stdout)
54 subprocess.run(
55 ["git", "add", "--intent-to-add", "--force", "--", secret_path], check=True
56 )
57
58 click.secho(f"Successfully generated {secret_path}", fg="green")
59
60
61 def hash_password(plaintext: str) -> str:
62 cp = subprocess.run(
63 ["mkpasswd", "--stdin", "--method=bcrypt"],
64 stdout=subprocess.PIPE,
65 input=plaintext,
66 text=True,
67 check=True,
68 )
69 return cp.stdout
70
71
72 @click.group()
73 def main() -> None:
74 pass
75
76
77 @main.command()
78 @click.argument("address_id")
79 @click.argument("email")
80 @click.option("--force/--no-force", "-f/ ", default=False)
81 def address(address_id: str, email: str, force: bool) -> None:
82 """
83 Encrypt an email address (or email addresses) for inclusion in a mailing list.
84
85 Example:
86
87 \bencrypt-email address some-token 'me@example.com,you@example.com'
88
89 Then follow the instructions for what to do next.
90 """
91 # Feel free to make the regex less restrictive if you need to.
92 id_re = re.compile("[A-Za-z0-9-]+")
93 if not id_re.fullmatch(address_id):
94 msg = f"Given ID: {address_id!r} is invalid. Must match regex: {id_re.pattern}"
95 raise click.ClickException(msg)
96
97 # Make sure we aren't being given a text file that happens to have a newline at the end.
98 clean_email = email.strip()
99 if clean_email != email:
100 click.secho("Removed whitespace surrounding given email address", fg="yellow")
101 email = clean_email
102
103 secret_path = NON_CRITICAL_INFRA_DIR / f"secrets/{address_id}-email-address.umbriel"
104 encrypt_to_file(email, secret_path, force)
105
106 click.secho()
107 click.secho("Now add `", nl=False)
108 click.secho(
109 secret_path.relative_to(MAILING_LISTS_NIX.parent, walk_up=True),
110 fg="blue",
111 nl=False,
112 )
113 click.secho("` to the relevant mailing list in '", nl=False)
114 click.secho(MAILING_LISTS_NIX, fg="blue")
115
116
117 @main.command()
118 @click.argument("address_id")
119 @click.option("--force/--no-force", "-f/ ", default=False)
120 def login(address_id: str, force: bool) -> None:
121 """
122 Encrypt a password to set up a login account for a mailing list. The password must be given via stdin.
123
124 Example:
125
126 \bencrypt-email login test-sender < file-with-password
127
128 Then follow the instructions for what to do next.
129 """
130 # Make sure we aren't being given a text file that happens to have a newline at the end.
131 password = sys.stdin.read()
132 clean_password = password.strip()
133 if clean_password != password:
134 click.secho("Removed whitespace surrounding given password", fg="yellow")
135 password = clean_password
136
137 hashed_password = hash_password(password)
138
139 secret_path = NON_CRITICAL_INFRA_DIR / f"secrets/{address_id}-email-login.umbriel"
140 encrypt_to_file(hashed_password, secret_path, force)
141
142 nix_code = dedent(
143 f"""\
144 "{address_id}@nixos.org" = {{
145 forwardTo = [
146 # Add emails here
147 ];
148 loginAccount = {{
149 encryptedHashedPassword = ../../secrets/{address_id}-email-login.umbriel;
150 storeEmail = false; # Set to `true` if you want to store email in a mailbox accessible via IMAP.
151 }};
152 }};
153 """
154 )
155 click.secho()
156 click.secho("Now add this login account to ", nl=False)
157 click.secho(MAILING_LISTS_NIX, fg="blue", nl=False)
158 click.secho("'. Add or edit an entry that looks like this:")
159 click.secho()
160 click.secho(indent(nix_code, prefix=" " * 4), fg="blue")
161
162
163 if __name__ == "__main__":
164 main()