Skip to content

Commit 2fd36e5

Browse files
working to preserve SOPS-ness of stored data.
1 parent 77f553e commit 2fd36e5

File tree

7 files changed

+87
-15
lines changed

7 files changed

+87
-15
lines changed

docs/changelog.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## 2.3.1 - 2025-12-XX
4+
- Fix a bug where sops protected files would be rewritten without preserving
5+
their sops protection.
6+
37
## 2.3.0 - 2025-10-20
48
- Improve the user experience around old stale sessions that appear to be
59
initialized, but are actually expired. This is done by providing the new

src/planet_auth/storage_utils.py

Lines changed: 62 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import subprocess
2020
import time
2121
from abc import ABC, abstractmethod
22+
from enum import Enum
2223
from typing import Optional, Dict, Any
2324

2425
from planet_auth.auth_exception import AuthException
@@ -96,9 +97,15 @@ def _default_storage_provider():
9697
class _SOPSAwareFilesystemObjectStorageProvider(ObjectStorageProvider):
9798
"""
9899
Storage provider geared around backing a single object in a single file
99-
with paths take from the root of the local file system.
100+
with paths taken from the root of the local file system.
100101
"""
101102

103+
_STORAGE_TYPE_KEY = "___SOPSAwareFilesystemObjectStorageProvider__storage_type"
104+
105+
class _StorageType(Enum):
106+
PLAINTEXT = "plaintext"
107+
SOPS = "sops"
108+
102109
def __init__(self, root: Optional[pathlib.Path] = None):
103110
if root:
104111
self._storage_root = root
@@ -116,13 +123,34 @@ def _obj_filepath(self, obj_key):
116123
return obj_path
117124

118125
@staticmethod
119-
def _is_sops_path(file_path):
126+
def _is_sops_path(file_path: pathlib.Path) -> bool:
120127
# TODO: Could be ".json.sops", or ".sops.json", depending on file
121128
# level or field level encryption, respectively. We currently
122129
# only look for and support field level encryption in json
123130
# files with a ".sops.json" suffix.
124131
return bool(file_path.suffixes == [".sops", ".json"])
125132

133+
# @staticmethod
134+
# def _convert_to_sops_path(file_path: pathlib.Path) -> pathlib.Path:
135+
# if _SOPSAwareFilesystemObjectStorageProvider._is_sops_path(file_path):
136+
# return file_path
137+
# else:
138+
# if bool(file_path.suffix == ".json"):
139+
# return file_path.with_name(file_path.stem + ".sops" + file_path.suffix)
140+
# else:
141+
# return file_path.with_suffix(file_path.suffix + ".sops.json")
142+
143+
@staticmethod
144+
def _filter_write_object(data: dict) -> dict:
145+
# TODO: consider making this part of the base class?
146+
final_data = {
147+
k: v
148+
for k, v in data.items()
149+
# if k not in [_SOPSAwareFilesystemObjectStorageProvider._STORAGE_TYPE_KEY]
150+
if not (isinstance(k, str) and k.startswith("__"))
151+
}
152+
return final_data
153+
126154
@staticmethod
127155
def _read_json(file_path: pathlib.Path):
128156
auth_logger.debug(msg="Loading JSON data from file {}".format(file_path))
@@ -137,7 +165,7 @@ def _read_json_sops(file_path: pathlib.Path):
137165

138166
@staticmethod
139167
def _write_json(file_path: pathlib.Path, data: dict):
140-
auth_logger.debug(msg="Writing JSON data to file {}".format(file_path))
168+
auth_logger.debug(msg="Writing JSON data to cleartext file {}".format(file_path))
141169
with open(file_path, mode="w", encoding="UTF-8") as file_w:
142170
os.chmod(file_path, stat.S_IREAD | stat.S_IWRITE)
143171
_no_none_data = {key: value for key, value in data.items() if value is not None}
@@ -155,26 +183,53 @@ def _write_json_sops(file_path: pathlib.Path, data: dict):
155183
# ['sops', '-e', '--input-type', 'json', '--output-type',
156184
# 'json', '--output', file_path, '/dev/stdin'],
157185
# stdin=data_f)
158-
auth_logger.debug(msg="Writing JSON data to SOPS encrypted file {}".format(file_path))
159186
_SOPSAwareFilesystemObjectStorageProvider._write_json(file_path, data)
187+
auth_logger.debug(msg="Writing JSON data to SOPS encrypted file {}".format(file_path))
160188
subprocess.check_call(["sops", "-e", "--input-type", "json", "--output-type", "json", "-i", file_path])
161189

162190
@staticmethod
163191
def _load_file(file_path: pathlib.Path) -> dict:
164192
if _SOPSAwareFilesystemObjectStorageProvider._is_sops_path(file_path):
165193
new_data = _SOPSAwareFilesystemObjectStorageProvider._read_json_sops(file_path)
194+
new_data[_SOPSAwareFilesystemObjectStorageProvider._STORAGE_TYPE_KEY] = (
195+
_SOPSAwareFilesystemObjectStorageProvider._StorageType.SOPS.value
196+
)
166197
else:
167198
new_data = _SOPSAwareFilesystemObjectStorageProvider._read_json(file_path)
199+
new_data[_SOPSAwareFilesystemObjectStorageProvider._STORAGE_TYPE_KEY] = (
200+
_SOPSAwareFilesystemObjectStorageProvider._StorageType.PLAINTEXT.value
201+
)
168202

169203
return new_data
170204

205+
@staticmethod
206+
def _do_sops(file_path: pathlib.Path, data: dict) -> bool:
207+
if _SOPSAwareFilesystemObjectStorageProvider._is_sops_path(file_path):
208+
return True
209+
if (
210+
data
211+
and data.get(_SOPSAwareFilesystemObjectStorageProvider._STORAGE_TYPE_KEY)
212+
== _SOPSAwareFilesystemObjectStorageProvider._StorageType.SOPS.value
213+
):
214+
auth_logger.warning(msg=f"Data sourced from SOPS being written cleartext to the file {file_path}.")
215+
# Upgrading to SOPS would be great, but also problematic.
216+
# return True
217+
218+
return False
219+
171220
@staticmethod
172221
def _save_file(file_path: pathlib.Path, data: dict):
173222
file_path.parent.mkdir(parents=True, exist_ok=True)
174-
if _SOPSAwareFilesystemObjectStorageProvider._is_sops_path(file_path):
175-
_SOPSAwareFilesystemObjectStorageProvider._write_json_sops(file_path, data)
223+
do_sops = _SOPSAwareFilesystemObjectStorageProvider._do_sops(file_path, data)
224+
write_data = _SOPSAwareFilesystemObjectStorageProvider._filter_write_object(data)
225+
226+
if do_sops:
227+
# This has to be with the caller, otherwise the caller would not know
228+
# where we actually wrote the data and would likely not be able to find it again.
229+
# sops_file_path = _SOPSAwareFilesystemObjectStorageProvider._convert_to_sops_path(file_path)
230+
_SOPSAwareFilesystemObjectStorageProvider._write_json_sops(file_path, write_data)
176231
else:
177-
_SOPSAwareFilesystemObjectStorageProvider._write_json(file_path, data)
232+
_SOPSAwareFilesystemObjectStorageProvider._write_json(file_path, write_data)
178233

179234
def load_obj(self, key: ObjectStorageProvider_KeyType) -> dict:
180235
obj_filepath = self._obj_filepath(key)

src/planet_auth_utils/commands/cli/main.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,8 +227,7 @@ def cmd_plauth_login(
227227
)
228228
print("Login succeeded.") # Errors should throw.
229229

230-
# FIXME: sops
231-
post_login_cmd_helper(override_auth_context=override_auth_context, use_sops=sops, prompt_pre_selection=yes)
230+
post_login_cmd_helper(override_auth_context=override_auth_context, use_sops_opt=sops, prompt_pre_selection=yes)
232231

233232

234233
cmd_plauth.add_command(cmd_oauth)

src/planet_auth_utils/commands/cli/oauth_cmd.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,7 @@ def cmd_oauth_login(
128128
extra=login_extra,
129129
)
130130
print("Login succeeded.") # Errors should throw.
131-
# FIXME sops-ness of profiles is being lost
132-
post_login_cmd_helper(override_auth_context=current_auth_context, use_sops=sops, prompt_pre_selection=yes)
131+
post_login_cmd_helper(override_auth_context=current_auth_context, use_sops_opt=sops, prompt_pre_selection=yes)
133132

134133

135134
@cmd_oauth.command("refresh")

src/planet_auth_utils/commands/cli/planet_legacy_auth_cmd.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,7 @@ def cmd_pllegacy_login(ctx, username, password, sops, yes):
8181
password=password,
8282
)
8383
print("Login succeeded.") # Errors should throw.
84-
# FIXME: sops-ness is being lost
85-
post_login_cmd_helper(override_auth_context=current_auth_context, use_sops=sops, prompt_pre_selection=yes)
84+
post_login_cmd_helper(override_auth_context=current_auth_context, use_sops_opt=sops, prompt_pre_selection=yes)
8685

8786

8887
@cmd_pllegacy.command("print-api-key")

src/planet_auth_utils/commands/cli/util.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,18 @@
1717
import json
1818
from typing import List, Optional
1919

20+
import planet_auth.logging.auth_logger
2021
import planet_auth
2122
from planet_auth.constants import AUTH_CONFIG_FILE_SOPS, AUTH_CONFIG_FILE_PLAIN
23+
from planet_auth.storage_utils import _SOPSAwareFilesystemObjectStorageProvider
2224
from planet_auth.util import custom_json_class_dumper
2325

2426
from planet_auth_utils.builtins import Builtins
2527
from planet_auth_utils.profile import Profile
2628
from .prompts import prompt_and_change_user_default_profile_if_different
2729

30+
auth_logger = planet_auth.logging.auth_logger.getAuthLogger()
31+
2832

2933
def monkeypatch_hide_click_cmd_options(cmd, hide_options: List[str]):
3034
"""
@@ -68,7 +72,7 @@ def print_obj(obj):
6872

6973

7074
def post_login_cmd_helper(
71-
override_auth_context: planet_auth.Auth, use_sops, prompt_pre_selection: Optional[bool] = None
75+
override_auth_context: planet_auth.Auth, use_sops_opt, prompt_pre_selection: Optional[bool] = None
7276
):
7377
override_profile_name = override_auth_context.profile_name()
7478
if not override_profile_name:
@@ -88,6 +92,18 @@ def post_login_cmd_helper(
8892
# helping CLI commands, we can be more opinionated about what is to
8993
# be done.
9094

95+
# If the profile was read from sops, selected sops even if wasn't an
96+
# explict command line option.
97+
use_sops = use_sops_opt
98+
if not use_sops:
99+
_config_data = override_auth_context.auth_client().config().data() or {}
100+
if (
101+
_config_data.get(_SOPSAwareFilesystemObjectStorageProvider._STORAGE_TYPE_KEY)
102+
== _SOPSAwareFilesystemObjectStorageProvider._StorageType.SOPS.value
103+
):
104+
auth_logger.debug(msg="Implicitly selecting SOPS based on data originating from SOPS protected storage.")
105+
use_sops = True
106+
91107
# Don't clobber built-in profiles.
92108
if not Builtins.is_builtin_profile(override_profile_name):
93109
if use_sops:

version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.3.0
1+
2.3.1

0 commit comments

Comments
 (0)