Merge ~wck0/landscape-charm:set-secret-token into landscape-charm:main

Proposed by Bill Kronholm
Status: Merged
Merged at revision: e2d697561b2e653aa904c6c55dcc06bd9bda6621
Proposed branch: ~wck0/landscape-charm:set-secret-token
Merge into: landscape-charm:main
Diff against target: 132 lines (+52/-1)
3 files modified
config.yaml (+6/-0)
src/ (+35/-1)
src/ (+11/-0)
Reviewer Review Type Date Requested Status
Kevin Nasto Approve
Review via email:

Commit message

Add secret_token to the config.

To post a comment you must log in.
Revision history for this message
Bill Kronholm (wck0) wrote :

Manual Testing Instructions

1. make build

2. Wait for the charm to settle and check the secret token is set.
juju ssh landscape-server/0
sudo less /etc/landscape/service.conf

3. Log in to the UI, click around, and check you can access the Organization page and the new beta UI.

4. Change the secret token:
juju config landscape-server secret_token="itsasecrettoeverybody"

5. Wait for the charm to settle and check the value was changed in service.conf.

6. Click around the UI and see that you are logged out of the old and new UIs.

7. Log back in and check things still work.

8. Change the secret token again:
juju config landscape-server secret_token=""

9. Wait for the charm to settle and check the value was changed in service.conf to something random.

10. Try to break something.

Revision history for this message
Kevin Nasto (silverdrake11) wrote :

I see the generate_secret_token code is from Landscape server. It's pretty outdated though. The following is from here and I think would be better. Note there are only 52 so not sure why the original code has [0:52] there.

import string
import secrets
alphabet = string.ascii_letters + string.digits
password = ''.join(secrets.choice(alphabet) for i in range(172))

Revision history for this message
Kevin Nasto (silverdrake11) wrote :

Regarding step 8, this is a bit undefined behavior. I'm not sure what it should do. One one hand setting it to empty will break it, on the other hand setting something to blank and then something being there doesn't make sense to me.

I would suggest to only on_install to generate it. And on_config change let the user do w/e

Revision history for this message
Bill Kronholm (wck0) wrote :

I need a sanity check on this.

With these updates, all units get the same secret-token set in the service.conf file.
However, I can't log in to the new UI as I get 401 {"message": "Auth token invalid. Please log in again.", "error": "AuthTokenInvalid"}

The login process is definitely getting and setting a JWT, and subsequent requests from the new UI set the appropriate request header.


To test:
Set num_units: 3 (or something bigger than 1 anyways) in the landscape-server section of the bundle.yaml file.
make build
Poke around in the browser.

Revision history for this message
Kevin Nasto (silverdrake11) wrote :

So 1 works but 2 doesn't?

Revision history for this message
Bill Kronholm (wck0) wrote :

> So 1 works but 2 doesn't?

I can't even log in to the new UI with only 1 landscape-server unit. Maybe it's an orthogonal issue.

5d9eb87... by Bill Kronholm

Use `secrets`

Revision history for this message
Kevin Nasto (silverdrake11) wrote :

When running my tests, I actually removed your changes in the _leader_elected function. This is because the token shouldn't change when a new leader is elected unlike the other stuff in that function that only runs on the leader. Despite removing all the units one by one and readding them, everything works

Revision history for this message
Kevin Nasto (silverdrake11) :
review: Needs Fixing
ba26c36... by Bill Kronholm


Revision history for this message
Bill Kronholm (wck0) wrote :


cb5e0aa... by Bill Kronholm

Write secret only if we need to.

d1700d4... by Bill Kronholm

Update help text.

Revision history for this message
Bill Kronholm (wck0) wrote :

Updated again.

Revision history for this message
Kevin Nasto (silverdrake11) wrote :

I tried to take down the leader so the others finish first, I thought it would not write the secret. But the others are waiting for the leader, then config-changed is triggered again. Anyway LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/config.yaml b/config.yaml
2index 119b2aa..e6aa062 100644
3--- a/config.yaml
4+++ b/config.yaml
5@@ -187,3 +187,9 @@ options:
6 description: |
7 Additional service.conf settings to be merged with the default
8 configuration.
9+ secret_token:
10+ type: string
11+ default:
12+ description: |
13+ A secret token for the landscape service. If not set one will be
14+ generated securely.
15diff --git a/src/ b/src/
16index 569201f..eb3a64d 100755
17--- a/src/
18+++ b/src/
19@@ -49,8 +49,10 @@ from ops.model import (
20 from settings_files import (
22 configure_for_deployment_mode,
23+ generate_secret_token,
24 merge_service_conf,
25 prepend_default_settings,
26+ SecretTokenMissing,
27 update_db_conf,
28 update_default_settings,
29 update_service_conf,
30@@ -175,6 +177,7 @@ class LandscapeServerCharm(CharmBase):
31 self._stored.set_default(paused=False)
32 self._stored.set_default(default_root_url="")
33 self._stored.set_default(account_bootstrapped=False)
34+ self._stored.set_default(secret_token=None)
36 self.landscape_uid = user_exists("landscape").pw_uid
37 self.root_gid = group_exists("root").gr_gid
38@@ -263,11 +266,36 @@ class LandscapeServerCharm(CharmBase):
40 self._bootstrap_account()
42+ secret_token = self._get_secret_token()
43+ if self.unit.is_leader():
44+ if not secret_token:
45+ # If the secret token wasn't in the config, and we don't have one
46+ # in the peer relation data, then the leader needs to generate one
47+ # for all of the units to use.
48+"Generating new random secret token")
49+ secret_token = generate_secret_token()
50+ peer_relation = self.model.get_relation("replicas")
51+[].update({"secret-token": secret_token})
52+ if (secret_token) and (secret_token != self._stored.secret_token):
53+ self._write_secret_token(secret_token)
54+ self._stored.secret_token = secret_token
56 if isinstance(prev_status, BlockedStatus):
57 self.unit.status = prev_status
59 self._update_ready_status(restart_services=True)
61+ def _get_secret_token(self):
62+ secret_token = self.model.config.get("secret_token")
63+ if not secret_token:
64+ peer_relation = self.model.get_relation("replicas")
65+ secret_token =[].get("secret-token", None)
66+ return secret_token
68+ def _write_secret_token(self, secret_token):
69+"Writing secret token")
70+ update_service_conf({"landscape": {"secret-token": secret_token}})
72 def _on_install(self, event: InstallEvent) -> None:
73 """Handle the install event."""
74 self.unit.status = MaintenanceStatus("Installing apt packages")
75@@ -658,7 +686,7 @@ class LandscapeServerCharm(CharmBase):
76 self.unit.status = MaintenanceStatus("Configuring HAProxy")
77 haproxy_ssl_cert =[event.unit]["ssl_cert"]
79- # Sometimes the data has not been encoded propery in the HA charm
80+ # Sometimes the data has not been encoded properly in the HA charm
81 if haproxy_ssl_cert.startswith("b'"):
82 haproxy_ssl_cert = haproxy_ssl_cert.strip('b').strip("'")
84@@ -847,6 +875,12 @@ command[check_{service}]=/usr/local/lib/nagios/plugins/ {service
85 for relation in haproxy_relations:
86 self._update_haproxy_connection(relation)
88+ secret_token = self._get_secret_token()
89+ if (secret_token) and (secret_token != self._stored.secret_token):
90+ self._write_secret_token(secret_token)
91+ self._stored.secret_token = secret_token
92+ self._update_ready_status(restart_services=True)
94 def _configure_smtp(self, relay_host: str) -> None:
96 # Rewrite postfix config.
97diff --git a/src/ b/src/
98index ba8211d..b4d1189 100644
99--- a/src/
100+++ b/src/
101@@ -9,6 +9,8 @@ import os
102 from base64 import b64decode, binascii
103 from collections import defaultdict
104 from configparser import ConfigParser
105+import secrets
106+from string import ascii_letters, digits
107 from urllib.request import urlopen
108 from urllib.error import URLError
110@@ -41,6 +43,10 @@ class ServiceConfMissing(Exception):
111 pass
114+class SecretTokenMissing(Exception):
115+ pass
118 def configure_for_deployment_mode(mode: str) -> None:
119 """
120 Places files where Landscape expects to find them for different deployment
121@@ -134,6 +140,11 @@ def update_service_conf(updates: dict) -> None:
122 config.write(config_fp)
125+def generate_secret_token():
126+ alphanumerics = ascii_letters + digits
127+ return "".join(secrets.choice(alphanumerics) for _ in range(172))
130 def write_license_file(license_file: str, uid: int, gid: int) -> None:
131 """
132 Reads or decodes `license_file` to LICENSE_FILE and sets it up


People subscribed via source and target branches

to all changes: