Skip to content
This repository has been archived by the owner on Apr 26, 2024. It is now read-only.

Support registration_shared_secret in a file #13614

Merged
merged 3 commits into from
Aug 25, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/13614.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support setting the registration shared secret in a file, via a new `registration_shared_secret_path` configuration option.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels very similar to me to simply putting the registration_shared_secret option in a separate configuration file -- can you describe a bit more why this is useful?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, it's particularly useful in combination with #13615. With both PRs in place, I can have a k8s deployment which creates a homeserver.yaml with registration_shared_secret_path pointing to a non-existent file. Then Synapse will magically generate me a shared secret on first run (which register_new_matrix_user can slurp).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will that cause the shared secret to be regenerated each time the container is rebuilt? I'm struggling to see why you would want to generate these on first run.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will that cause the shared secret to be regenerated each time the container is rebuilt?

That depends if you use a persistent volume to keep registration_shared_secret_path on. If you do, you'll keep the same secret between rebuilds. But it doesn't matter anyway: there is little benefit to using the same shared secret on each run, provided register_new_matrix_user can find the current secret. Indeed, maybe it's better to rotate it frequently.

I'm struggling to see why you would want to generate these on first run.

... well, because if you don't have synapse generate them, you have to generate them yourself with some annoying scripting.

18 changes: 18 additions & 0 deletions docs/usage/configuration/config_documentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -2124,10 +2124,28 @@ registration_requires_token: true
If set, allows registration of standard or admin accounts by anyone who
has the shared secret, even if registration is otherwise disabled.

See also [`registration_shared_secret_path`](#registration_shared_secret_path).

Example configuration:
```yaml
registration_shared_secret: <PRIVATE STRING>
```

---
### `registration_shared_secret_path`

An alternative to [`registration_shared_secret`](#registration_shared_secret):
allows the shared secret to be specified in an external file.

The file should be a plain text file, containing only the shared secret.

Example configuration:
```yaml
registration_shared_secret_file: /path/to/secrets/file
```

_Added in Synapse 1.67.0._

---
### `bcrypt_rounds`

Expand Down
46 changes: 43 additions & 3 deletions synapse/_scripts/register_new_matrix_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,20 @@
import hmac
import logging
import sys
from typing import Callable, Optional
from typing import Any, Callable, Optional

import requests
import yaml

_CONFLICTING_SHARED_SECRET_OPTS_ERROR = """\
Conflicting options 'registration_shared_secret' and 'registration_shared_secret_path'
are both defined in config file.
"""
Comment on lines +28 to +31
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kind of a shame that we need to do the same checks that we have in RegistrationConfig. I don't think there's an easy way around that though.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I'm not keen to drag in the whole config-parsing infrastructure


_NO_SHARED_SECRET_OPTS_ERROR = """\
No 'registration_shared_secret' or 'registration_shared_secret_path' defined in config.
"""


def request_registration(
user: str,
Expand Down Expand Up @@ -213,9 +222,16 @@ def main() -> None:

if "config" in args and args.config:
config = yaml.safe_load(args.config)
secret = config.get("registration_shared_secret", None)
secret = config.get("registration_shared_secret")
secret_file = config.get("registration_shared_secret_path")
if secret_file:
if secret:
print(_CONFLICTING_SHARED_SECRET_OPTS_ERROR, file=sys.stderr)
sys.exit(1)
secret = _read_file(secret_file, "registration_shared_secret_path").strip()

if not secret:
print("No 'registration_shared_secret' defined in config.")
print(_NO_SHARED_SECRET_OPTS_ERROR, file=sys.stderr)
sys.exit(1)
else:
secret = args.shared_secret
Expand All @@ -229,5 +245,29 @@ def main() -> None:
)


def _read_file(file_path: Any, config_path: str) -> str:
"""Check the given file exists, and read it into a string

If it does not, exit with an error indicating the problem

Args:
file_path: the file to be read
config_path: where in the configuration file_path came from, so that a useful
error can be emitted if it does not exist.
Returns:
content of the file.
"""
if not isinstance(file_path, str):
print(f"{config_path} setting is not a string", file=sys.stderr)
sys.exit(1)

try:
with open(file_path) as file_stream:
return file_stream.read()
except OSError as e:
print(f"Error accessing file {file_path}", file=sys.stderr)
sys.exit(1)


if __name__ == "__main__":
main()
33 changes: 31 additions & 2 deletions synapse/config/registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,10 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
from typing import Any, Optional
from typing import Any, Dict, Optional

from synapse.api.constants import RoomCreationPreset
from synapse.config._base import Config, ConfigError
from synapse.config._base import Config, ConfigError, read_file
from synapse.types import JsonDict, RoomAlias, UserID
from synapse.util.stringutils import random_string_with_symbols, strtobool

Expand All @@ -27,6 +27,11 @@
remove `account_threepid_delegates.email`.
"""

CONFLICTING_SHARED_SECRET_OPTS_ERROR = """\
You have configured both `registration_shared_secret` and
`registration_shared_secret_path`. These are mutually incompatible.
"""


class RegistrationConfig(Config):
section = "registration"
Expand All @@ -53,7 +58,16 @@ def read_config(self, config: JsonDict, **kwargs: Any) -> None:
self.enable_registration_token_3pid_bypass = config.get(
"enable_registration_token_3pid_bypass", False
)

# read the shared secret, either inline or from an external file
self.registration_shared_secret = config.get("registration_shared_secret")
registration_shared_secret_path = config.get("registration_shared_secret_path")
if registration_shared_secret_path:
if self.registration_shared_secret:
raise ConfigError(CONFLICTING_SHARED_SECRET_OPTS_ERROR)
self.registration_shared_secret = read_file(
registration_shared_secret_path, ("registration_shared_secret_path",)
).strip()

self.bcrypt_rounds = config.get("bcrypt_rounds", 12)

Expand Down Expand Up @@ -218,6 +232,21 @@ def generate_config_section(
else:
return ""

def generate_files(self, config: Dict[str, Any], config_dir_path: str) -> None:
# if 'registration_shared_secret_path' is specified, and the target file
# does not exist, generate it.
registration_shared_secret_path = config.get("registration_shared_secret_path")
if registration_shared_secret_path and not self.path_exists(
registration_shared_secret_path
):
print(
"Generating registration shared secret file "
+ registration_shared_secret_path
)
secret = random_string_with_symbols(50)
with open(registration_shared_secret_path, "w") as f:
f.write(f"{secret}\n")

@staticmethod
def add_arguments(parser: argparse.ArgumentParser) -> None:
reg_group = parser.add_argument_group("registration")
Expand Down