Skip to content

Commit 49a939a

Browse files
Manage secrets outside of server_config.json (#4633)
* Manage secrets outside of server_config.json * Also read secrets from environmental variables. * Fix minor styling issue in resolve_variables Co-authored-by: bruntib <[email protected]> * Fix lint test * Extended docs with an example dictionary secret --------- Co-authored-by: bruntib <[email protected]>
1 parent 0b043e4 commit 49a939a

File tree

3 files changed

+75
-3
lines changed

3 files changed

+75
-3
lines changed

docs/web/server_config.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,32 @@ By default the server will use the value from your host configured by the
115115
## Authentication
116116
For authentication configuration options and which options can be reloaded see
117117
the [Authentication](authentication.md) documentation.
118+
119+
## Secrets
120+
121+
### server_secrets.json
122+
Optionally, one can store sensitive data (e.g., passwords, secret tokens) outside
123+
of `server_config.json`. To do this, create a separate `server_secrets.json` file
124+
in the server's *workspace* folder. In `server_config.json`, replace sensitive data
125+
with `$SECRET:NAME_OF_SECRET$`.
126+
Then, secrets can be defined in `server_secrets.json`, as an example:
127+
```json
128+
{
129+
"NAME_OF_SECRET": "MySecurePassword123"
130+
}
131+
```
132+
Alternatively, one can also define entire sections as a secret, for instance:
133+
```json
134+
{
135+
"NAME_OF_SECRET": {
136+
"enabled" : true,
137+
"client_id" : "<ExampleClientID>",
138+
"client_secret": "<ExampleClientSecret>"
139+
}
140+
}
141+
```
142+
143+
### Environmental variables
144+
Alternatively, CodeChecker can also read sensitive data from environmental variables.
145+
To do this, replace sensitive data in `server_config.json` with `$ENV:VARIABLE_NAME$`.
146+
In this case, value will be read from environmental variable `VARIABLE_NAME`.

web/server/codechecker_server/server.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,9 +1127,12 @@ def start_server(config_directory, package_data, port, config_sql_server,
11271127
"created at '%s'", server_cfg_file)
11281128
shutil.copyfile(example_cfg_file, server_cfg_file)
11291129

1130+
server_secrets_file = os.path.join(config_directory, 'server_secrets.json')
1131+
11301132
try:
11311133
manager = session_manager.SessionManager(
11321134
server_cfg_file,
1135+
server_secrets_file,
11331136
force_auth)
11341137

11351138
except IOError as ioerr:
@@ -1138,8 +1141,7 @@ def start_server(config_directory, package_data, port, config_sql_server,
11381141
"is missing or can not be read!")
11391142
sys.exit(1)
11401143
except ValueError as verr:
1141-
LOG.debug(verr)
1142-
LOG.error("The server's configuration file is invalid!")
1144+
LOG.error(verr)
11431145
sys.exit(1)
11441146

11451147
if not skip_db_cleanup:

web/server/codechecker_server/session_manager.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ class SessionManager:
164164
CodeChecker server.
165165
"""
166166

167-
def __init__(self, configuration_file, force_auth=False):
167+
def __init__(self, configuration_file, secrets_file, force_auth=False):
168168
"""
169169
Initialise a new Session Manager on the server.
170170
@@ -177,6 +177,7 @@ def __init__(self, configuration_file, force_auth=False):
177177
self.__logins_since_prune = 0
178178
self.__sessions = []
179179
self.__configuration_file = configuration_file
180+
self.__secrets_file = secrets_file
180181

181182
self.scfg_dict = self.__get_config_dict()
182183

@@ -410,6 +411,46 @@ def __get_config_dict(self):
410411
# have been parsed from it.
411412
raise ValueError("Server configuration file was invalid, or "
412413
"empty.")
414+
415+
LOG.debug(self.__secrets_file)
416+
if os.path.exists(self.__secrets_file):
417+
secrets_dict = load_json(self.__secrets_file, {})
418+
check_file_owner_rw(self.__secrets_file)
419+
else:
420+
secrets_dict = {}
421+
422+
secret_re = re.compile(r'^\$SECRET:[a-zA-Z0-9_-]+\$$')
423+
env_re = re.compile(r'^\$ENV:[a-zA-Z0-9_-]+\$$')
424+
425+
def resolve_variables_failed(var):
426+
if (secret_re.search(var) and
427+
not os.path.exists(self.__secrets_file)):
428+
LOG.error("Secrets were used in server configuration file, "
429+
f"but {self.__secrets_file} does not exist!")
430+
431+
raise ValueError(f"Variable '{var}' could not "
432+
"be resolved in server configuration file.")
433+
434+
def resolve_variables(d):
435+
items = d.items() if isinstance(d, dict) else enumerate(d)
436+
437+
for k, v in items:
438+
if isinstance(v, (dict, list)):
439+
resolve_variables(v)
440+
elif isinstance(v, str):
441+
secret_matched = secret_re.search(v)
442+
env_matched = env_re.search(v)
443+
444+
if secret_matched or env_matched:
445+
var_name = v.split(':')[1][:-1]
446+
if secret_matched and var_name in secrets_dict:
447+
d[k] = secrets_dict[var_name]
448+
elif env_matched and var_name in os.environ:
449+
d[k] = os.environ[var_name]
450+
else:
451+
resolve_variables_failed(v)
452+
453+
resolve_variables(cfg_dict)
413454
return cfg_dict
414455

415456
def reload_config(self):

0 commit comments

Comments
 (0)