Setting the file. One moment.
Record · Operate On AWS · aws/agent-toolkit-for-aws · Skills Docs
ContentsBack to the top of the page 93
Creating Amazon Aurora Db Cluster With Instances
Shieldadvanced
Resilience Program Design
Connecting Lambda To DynamoDB
references/vendored/telemetry/consent/ record.py
Python · 204 lines · 7 KB
16 Fail-closed on read, fail-loud on write. A missing, unreadable or schema-invalid
17 record reads as *no consent*, never as acceptance. Writes raise, so `accept.py`
18 cannot report success it did not achieve.
19
20 `notice.py` owns the wording and knows nothing about any of this; `disclaimer()`
21 is the seam between the two.
22 """
23
24 import json
25 import os
26 import re
27 import sys
28 import tempfile
29 import uuid
30 from pathlib import Path
31
32 sys.path.insert( 0 , str (Path( __file__ ).resolve().parent))
33
34 import notice # noqa: E402 (path set above so this resolves in-plugin)
35
36 STATE_DIR_NAME = ".aws-startup-advisor"
37 STATE_FILE_NAME = "plugin-telemetry.json"
38
39 # Two answers, and only ACCEPTED permits emitting anything.
40 ACCEPTED = "ACCEPTED"
41 OPT_OUT = "OPT_OUT"
42
43 VALID_STATUSES = ( ACCEPTED , OPT_OUT )
44
45 # The service's UUID shape, from model/types/scalars.smithy. installId is sent
46 # verbatim as a @required UUID, so a record holding anything else is unusable.
47 _UUID_PATTERN = re.compile(
48 r " ^[ 0-9a-fA-F ] {8} - [ 0-9a-fA-F ] {4} - [ 0-9a-fA-F ] {4} - [ 0-9a-fA-F ] {4} - [ 0-9a-fA-F ] {12} $ "
49 )
50
51 # The record as a human should see it: `~`, not an expanded home directory.
52 DISPLAY_STATE_PATH = "~/ %s / %s " % ( STATE_DIR_NAME , STATE_FILE_NAME )
53
54 # The opt-out the notice names: a config setting, not a command. It is prose, so
55 # it wraps; it names no absolute install path; and it needs no Python.
56 OPT_OUT_INSTRUCTION = 'setting "consentStatus" to " %s " in %s ' % (
57 OPT_OUT ,
58 DISPLAY_STATE_PATH ,
59 )
60
61 # The one-step equivalent, for a human reading script output rather than the
62 # notice. It preserves the install ID, which a hand-edit cannot. `py -3` on
63 # Windows, where python3 is usually not on PATH at all.
64 OPT_OUT_SCRIPT = Path( __file__ ).resolve().parent / "opt_out.py"
65 INTERPRETER = "py -3" if os.name == "nt" else "python3"
66 OPT_OUT_COMMAND = ' %s " %s "' % ( INTERPRETER , OPT_OUT_SCRIPT )
67
68
69 def disclaimer ():
70 """The notice, with the opt-out mechanism filled in."""
71 return notice.render( opt_out = OPT_OUT_INSTRUCTION )
72
73
74 def opt_out_help ():
75 """The copy-pasteable opt-out, for a human reading script output."""
76 return " \n " .join(
77 [
78 "To opt out later, run:" ,
79 " %s " % OPT_OUT_COMMAND ,
80 'Or edit %s by hand and set "consentStatus" to " %s ".'
81 % (state_path(), OPT_OUT ),
82 "That file is the only thing that decides whether anything is sent." ,
83 "Opting out disables no plugin features." ,
84 ]
85 )
86
87
88 def state_dir ():
89 return Path.home() / STATE_DIR_NAME
90
91
92 def state_path ():
93 return state_dir() / STATE_FILE_NAME
94
95
96 def _valid_install_id (value):
97 return isinstance (value, str ) and bool ( _UUID_PATTERN .match(value))
98
99
100 def read_state ():
101 """The record, or None if absent, unparseable, or invalid.
102
103 In order: the file exists, parses as JSON, is an object, and carries an allowed
104 `consentStatus`. Every caller has to treat "cannot establish consent" and "no
105 consent" identically, so failure is None rather than an exception.
106
107 `installId` is required only when the status is ACCEPTED, because that is the
108 only status that sends anything and it is sent verbatim as a @required UUID.
109 The notice tells the user to opt out by setting `consentStatus` to OPT_OUT in
110 this file; demanding a UUID they have no way to invent would make that
111 instruction produce a record we then read as corrupt, and the notice would be
112 raised at them again next session.
113 """
114 # utf-8-sig, not utf-8: the notice tells the user to edit this file, and a
115 # Windows editor adds a BOM that json.load rejects. Reading that as "nothing
116 # recorded" would raise the notice again at someone who just did what it asked.
117 # It reads a BOM-less file identically, so this costs nothing.
118 try :
119 with open (state_path(), "r" , encoding = "utf-8-sig" ) as handle:
120 data = json.load(handle)
121 except ( OSError , ValueError ):
122 return None
123
124 if not isinstance (data, dict ):
125 return None
126 if data.get( "consentStatus" ) not in VALID_STATUSES :
127 return None
128 if data[ "consentStatus" ] == ACCEPTED and not _valid_install_id(
129 data.get( "installId" )
130 ):
131 return None
132 return data
133
134
135 def consent_status ():
136 """'ACCEPTED', 'OPT_OUT', or None when nothing is decided."""
137 state = read_state()
138 return state[ "consentStatus" ] if state else None
139
140
141 def is_accepted ():
142 """The single gate every telemetry path must call.
143
144 Deliberately not `!= OPT_OUT`: a missing or corrupt record must block too.
145 """
146 return consent_status() == ACCEPTED
147
148
149 def accepted_notice_version ():
150 """The notice version an ACCEPTED answer covers, or 0 when nothing is accepted."""
151 state = read_state()
152 if not state or state[ "consentStatus" ] != ACCEPTED :
153 return 0
154 version = state.get( "noticeVersion" , 1 )
155 return version if isinstance (version, int ) and not isinstance (version, bool ) and version >= 1 else 1
156
157
158 def write_state (status):
159 """Record `status`, return the record written, raise on failure.
160
161 Atomic, so an interrupted write cannot leave a half-written record that
162 `read_state` rejects and the hook then re-prompts on forever. An existing
163 `installId` is preserved, so changing the answer is not a new install — but
164 only if it is a UUID, since a hand-edited record may carry no ID at all or
165 something the service would reject.
166 """
167 if status not in VALID_STATUSES :
168 raise ValueError (
169 "consentStatus must be one of %s , got %r " % ( VALID_STATUSES , status)
170 )
171
172 existing = read_state() or {}
173 install_id = existing.get( "installId" )
174 if not _valid_install_id(install_id):
175 install_id = str (uuid.uuid4())
176 record = { "installId" : install_id, "consentStatus" : status, "noticeVersion" : notice. NOTICE_VERSION }
177
178 directory = state_dir()
179 directory.mkdir( parents = True , exist_ok = True )
180
181 # Renamed into place from the same directory, so the replace is atomic.
182 handle = tempfile.NamedTemporaryFile(
183 mode = "w" ,
184 encoding = "utf-8" ,
185 dir = str (directory),
186 prefix = STATE_FILE_NAME + "." ,
187 suffix = ".tmp" ,
188 delete = False ,
189 )
190 try :
191 with handle:
192 json.dump(record, handle, indent = 2 )
193 handle.write( " \n " )
194 handle.flush()
195 os.fsync(handle.fileno())
196 os.replace(handle.name, str (state_path()))
197 except BaseException :
198 try :
199 os.unlink(handle.name)
200 except OSError :
201 pass
202 raise
203
204 return record