Add client_secret_filepath option
To allow supplying the `client_secret` in a file, instead of directly in the configuration. I've found that ESS prefer this method in the past, so the entire config does not need to be a secret.
This commit is contained in:
@@ -51,6 +51,7 @@ API](https://element-hq.github.io/matrix-authentication-service/topics/admin-api
|
||||
- `oauth_base_url` - Base URL for MAS' OAuth endpoints (defaults to `admin_api_base_url` if not set). Trailing slashes will be automatically stripped.
|
||||
- `client_id` - client ID for the automated tool. Must be a valid [ULID](https://github.com/ulid/spec). Generate one [here](https://ulidtools.com/).
|
||||
- `client_secret` - client secret for the automated tool. Ideally long and cryptographically secure. Keep it a secret!
|
||||
- `client_secret_filepath` - path to a plaintext file containing the client secret. If set, this is used instead of `client_secret`.
|
||||
|
||||
Example configuration:
|
||||
|
||||
@@ -70,6 +71,8 @@ modules:
|
||||
# https://ulidtools.com/
|
||||
client_id: 000000000000000000000G0EST
|
||||
client_secret: your-client-secret
|
||||
# Alternatively, load the secret from a file:
|
||||
# client_secret_filepath: /run/secrets/mas-client-secret
|
||||
```
|
||||
|
||||
Enable [the Admin API on a MAS
|
||||
|
||||
@@ -17,8 +17,8 @@ class MasConfig:
|
||||
admin_api_base_url: str
|
||||
oauth_base_url: str
|
||||
client_id: str
|
||||
# TODO: Add a filepath option for the secret as well.
|
||||
client_secret: str
|
||||
client_secret: Optional[str] = None
|
||||
client_secret_filepath: Optional[str] = None
|
||||
|
||||
|
||||
@attr.s(frozen=True, auto_attribs=True)
|
||||
|
||||
@@ -120,14 +120,37 @@ class GuestModule:
|
||||
raise ConfigError("Config option 'mas.client_id' is required and must be a string")
|
||||
|
||||
client_secret = mas_config.get("client_secret")
|
||||
if not isinstance(client_secret, str) or len(client_secret.strip()) == 0:
|
||||
raise ConfigError("Config option 'mas.client_secret' is required and must be a string")
|
||||
if client_secret is not None:
|
||||
if not isinstance(client_secret, str) or len(client_secret.strip()) == 0:
|
||||
raise ConfigError(
|
||||
"Config option 'mas.client_secret' must be a string"
|
||||
)
|
||||
client_secret = client_secret.strip()
|
||||
|
||||
client_secret_filepath = mas_config.get("client_secret_filepath")
|
||||
if client_secret_filepath is not None:
|
||||
if not isinstance(client_secret_filepath, str) or len(client_secret_filepath.strip()) == 0:
|
||||
raise ConfigError(
|
||||
"Config option 'mas.client_secret_filepath' must be a string"
|
||||
)
|
||||
client_secret_filepath = client_secret_filepath.strip()
|
||||
|
||||
if client_secret is None and client_secret_filepath is None:
|
||||
raise ConfigError(
|
||||
"Config option 'mas.client_secret' or 'mas.client_secret_filepath' is required"
|
||||
)
|
||||
|
||||
if client_secret is not None and client_secret_filepath is not None:
|
||||
raise ConfigError(
|
||||
"Config option 'mas.client_secret' and 'mas.client_secret_filepath' are mutually exclusive"
|
||||
)
|
||||
|
||||
mas = MasConfig(
|
||||
admin_api_base_url.strip(),
|
||||
oauth_base_url.strip(),
|
||||
client_id.strip(),
|
||||
client_secret.strip(),
|
||||
client_secret,
|
||||
client_secret_filepath,
|
||||
)
|
||||
|
||||
return GuestModuleConfig(
|
||||
|
||||
@@ -18,9 +18,10 @@ class MasAdminClient:
|
||||
def __init__(self, api: ModuleApi, config: MasConfig):
|
||||
self._api = api
|
||||
self._config = config
|
||||
# Strip trailing any slashes if present
|
||||
# Strip any trailing slashes if present
|
||||
self._admin_api_base_url = config.admin_api_base_url.rstrip("/")
|
||||
self._oauth_base_url = config.oauth_base_url.rstrip("/")
|
||||
self._client_secret = self._load_client_secret()
|
||||
|
||||
async def create_user(self, username: str) -> str:
|
||||
"""Creates a new user in MAS with the given username.
|
||||
@@ -56,7 +57,7 @@ class MasAdminClient:
|
||||
"actor_user_id": mas_user_id,
|
||||
"expires_in": expires_in,
|
||||
"scope": "openid urn:matrix:client:api:*",
|
||||
"human_name": "guest user",
|
||||
"human_name": "guest session",
|
||||
}
|
||||
|
||||
response = await self._api.http_client.post_json_get_json(
|
||||
@@ -92,7 +93,7 @@ class MasAdminClient:
|
||||
async def request_admin_token(self) -> str:
|
||||
url = self._build_oauth_url("/oauth2/token")
|
||||
basic_auth = base64.b64encode(
|
||||
f"{self._config.client_id}:{self._config.client_secret}".encode("utf-8")
|
||||
f"{self._config.client_id}:{self._client_secret}".encode("utf-8")
|
||||
).decode("ascii")
|
||||
headers = {
|
||||
"Authorization": [f"Basic {basic_auth}"],
|
||||
@@ -108,6 +109,33 @@ class MasAdminClient:
|
||||
if not isinstance(access_token, str) or len(access_token) == 0:
|
||||
raise ValueError("MAS token response missing access_token")
|
||||
return access_token
|
||||
|
||||
def _load_client_secret(self) -> str:
|
||||
"""Source the MAS client secret from either configuration or a file."""
|
||||
if self._config.client_secret_filepath is not None:
|
||||
try:
|
||||
with open(
|
||||
self._config.client_secret_filepath, "r", encoding="utf-8"
|
||||
) as secret_file:
|
||||
client_secret = secret_file.read().strip()
|
||||
except Exception as err:
|
||||
raise ValueError(
|
||||
f"Failed to read MAS client secret file: {err}"
|
||||
) from err
|
||||
|
||||
if len(client_secret) == 0:
|
||||
raise ValueError("MAS client secret file is empty")
|
||||
|
||||
return client_secret
|
||||
|
||||
if self._config.client_secret is None:
|
||||
raise ValueError("MAS client secret is not configured")
|
||||
|
||||
client_secret = self._config.client_secret.strip()
|
||||
if len(client_secret) == 0:
|
||||
raise ValueError("MAS client secret is empty")
|
||||
|
||||
return client_secret
|
||||
|
||||
async def _post_urlencoded_get_json(
|
||||
self, url: str, data: Dict[str, str], headers: Dict[str, Any]
|
||||
|
||||
Reference in New Issue
Block a user