Setting the file. One moment.
Get Admin Email · Reading Mailcatcher API · bitwarden/ai-plugins · Skills Docs
ContentsBack to the top of the page Reference
scripts/ get_admin_email.py
Python · 204 lines · 7 KB
a dev/secrets.json path).
16 Exit 3: secrets file not found, unreadable, invalid JSON, or missing the
17 adminSettings.admins key.
18
19 Usage:
20 get_admin_email.py [--secrets-file <path>] [--all]
21
22 --secrets-file defaults to server/dev/secrets.json, relative to the current
23 directory (run from the workspace root that holds your bitwarden/server
24 checkout). Any override must resolve, after symlinks, to a file named
25 secrets.json inside a directory named dev: the skill grants this script without
26 a prompt, so the argument must not become a probe of arbitrary files. --all lists every configured admin address; the default prints only
27 the first, which is the Admin Portal login address.
28 """
29 import argparse
30 import json
31 import os
32 import sys
33
34 DEFAULT_SECRETS_FILE = os.path.join( "server" , "dev" , "secrets.json" )
35
36 EXIT_OK = 0
37 EXIT_USAGE = 2
38 EXIT_ERROR = 3
39
40 # The trailing path components every --secrets-file must resolve to.
41 REQUIRED_PATH_TAIL = ( "dev" , "secrets.json" )
42
43
44 class SecretsError ( Exception ):
45 """The secrets file could not be read or held no admin address."""
46
47
48 class SecretsPathError ( Exception ):
49 """The --secrets-file argument does not name a dev/secrets.json file."""
50
51
52 def check_secrets_path (path):
53 """Refuse any path that does not resolve to a dev/secrets.json file.
54
55 The skill's Bash grant pre-approves every invocation of this script, and a
56 grant cannot constrain an argument, so the constraint lives here. Without
57 it, --secrets-file would answer "does this file exist, and is it JSON?" for
58 any path on disk without a prompt. The path is resolved first, so neither
59 `..` segments nor a symlink can dress another file up as dev/secrets.json.
60 """
61 resolved = os.path.realpath(path)
62 parts = tuple (os.path.normcase(part) for part in resolved.split(os.sep))
63 if parts[ - len ( REQUIRED_PATH_TAIL ):] != REQUIRED_PATH_TAIL :
64 raise SecretsPathError(
65 f "--secrets-file must point at a bitwarden/server checkout's "
66 f "dev/secrets.json, got: { path } "
67 )
68
69
70 def extract_admins (data):
71 """Return the admin addresses from parsed secrets JSON.
72
73 The value lives at adminSettings.admins as a comma-separated string, e.g.
74 "admin@localhost,owner@localhost". Only this key is read out; the rest of
75 the parsed object is never touched, so no other secret can reach stdout.
76 """
77 if not isinstance (data, dict ):
78 raise SecretsError( "secrets file is not a JSON object" )
79 admin_settings = data.get( "adminSettings" )
80 if not isinstance (admin_settings, dict ) or "admins" not in admin_settings:
81 raise SecretsError( "secrets file has no adminSettings.admins key" )
82 raw = admin_settings[ "admins" ]
83 if not isinstance (raw, str ):
84 raise SecretsError( "adminSettings.admins is not a string" )
85 admins = [entry.strip() for entry in raw.split( "," ) if entry.strip()]
86 if not admins:
87 raise SecretsError( "adminSettings.admins is empty" )
88 return admins
89
90
91 def strip_jsonc (text):
92 """Return `text` with JSONC extras removed so json.loads can parse it.
93
94 Removes `//` line comments, `/* */` block comments, and trailing commas
95 (a comma whose next non-space token is `}` or `]`). The scan is
96 string-aware: comment markers, commas, and braces inside string literals
97 are left untouched, and backslash escapes inside strings are honored. This
98 matters because Bitwarden's dev secrets.json holds values such as
99 "bitwarden://premium-checkout-result" that a naive `//` strip would corrupt.
100 """
101 out = []
102 i, n = 0 , len (text)
103 in_string = False
104 pending_comma = None # index in `out` of a comma awaiting its next token
105 while i < n:
106 char = text[i]
107 if in_string:
108 out.append(char)
109 if char == " \\ " and i + 1 < n:
110 out.append(text[i + 1 ])
111 i += 2
112 continue
113 if char == '"' :
114 in_string = False
115 i += 1
116 continue
117 if char == '"' :
118 pending_comma = None
119 in_string = True
120 out.append(char)
121 i += 1
122 continue
123 if char == "/" and i + 1 < n and text[i + 1 ] == "/" :
124 i += 2
125 while i < n and text[i] != " \n " :
126 i += 1
127 continue
128 if char == "/" and i + 1 < n and text[i + 1 ] == "*" :
129 i += 2
130 while i + 1 < n and not (text[i] == "*" and text[i + 1 ] == "/" ):
131 i += 1
132 i += 2 # skip the closing */
133 continue
134 if char.isspace():
135 out.append(char)
136 i += 1
137 continue
138 if char == "," :
139 pending_comma = len (out)
140 out.append(char)
141 i += 1
142 continue
143 if char in "}]" :
144 if pending_comma is not None :
145 out[pending_comma] = "" # drop the trailing comma
146 pending_comma = None
147 out.append(char)
148 i += 1
149 continue
150 pending_comma = None
151 out.append(char)
152 i += 1
153 return "" .join(out)
154
155
156 def read_admins (path):
157 try :
158 with open (path, encoding = "utf-8" ) as handle:
159 text = handle.read()
160 except FileNotFoundError :
161 raise SecretsError(
162 f "secrets file not found: { path } . Pass --secrets-file to point at "
163 "your bitwarden/server checkout's dev/secrets.json."
164 )
165 except OSError as err:
166 raise SecretsError( f "could not read { path } : { err } " )
167 try :
168 data = json.loads(strip_jsonc(text))
169 except ValueError as err:
170 raise SecretsError( f "invalid JSON in { path } : { err } " )
171 return extract_admins(data)
172
173
174 def main (argv):
175 parser = argparse.ArgumentParser(
176 prog = "get_admin_email.py" ,
177 description = "Print the Bitwarden dev admin email(s) from secrets.json." ,
178 )
179 parser.add_argument( "--secrets-file" , default = DEFAULT_SECRETS_FILE )
180 parser.add_argument( "--all" , action = "store_true" )
181 try :
182 args = parser.parse_args(argv)
183 except SystemExit as exc:
184 return exc.code if isinstance (exc.code, int ) else EXIT_USAGE
185
186 try :
187 check_secrets_path(args.secrets_file)
188 except SecretsPathError as err:
189 print ( f "ERROR: { err } " , file = sys.stderr)
190 return EXIT_USAGE
191
192 try :
193 admins = read_admins(args.secrets_file)
194 except SecretsError as err:
195 print ( f "ERROR: { err } " , file = sys.stderr)
196 return EXIT_ERROR
197
198 for address in (admins if args.all else admins[: 1 ]):
199 print (address)
200 return EXIT_OK
201
202
203 if __name__ == "__main__" :
204 sys.exit(main(sys.argv[ 1 :]))