Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/quality.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
python-version: "3.13"

- name: Install uv
run: pip install uv
Expand Down
21 changes: 20 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,16 @@ This is a monorepo with:
- Backend API at repository root.
- Frontend Admin Console at `frontend/`.

## Instruction hierarchy (authoritative)

- This root `AGENTS.md` is mandatory baseline guidance for the entire monorepo.
- Subproject `AGENTS.md` files are also mandatory when working inside their directory tree.
- Always load and follow the nearest `AGENTS.md` for every file you touch, in addition to this root file.
- If guidance conflicts, the nearest (most specific) `AGENTS.md` for the changed file wins, and root rules still apply where they do not conflict.
- Continuously capture non-trivial lessons from implementation work and document the most important ones in the appropriate `AGENTS.md` (root and/or subproject) so future sessions accelerate delivery and workflow quality.
- Known subproject instruction files in this repo:
- `frontend/AGENTS.md` for all work under `frontend/`.

## Backend working rules

- Run backend quality checks with `./scripts/quality.sh check`.
Expand Down Expand Up @@ -36,6 +46,16 @@ This is a monorepo with:
- Playwright MCP tool, when accessible, can be used for testing front-end components and changes.
- Always scan related components to ensure consistency. Keep things stupid simple (KISS) and don't repeat yourself (DRY). This prevents code bloat. We need to avoid overengineering.

## Session lessons (non-trivial)

- `git push` can be blocked by a pre-push hook when the working tree is dirty, even if the dirty files are unrelated to the commit being pushed.
- Practical workflow: temporarily stash unrelated local edits, push, then restore with `git stash pop`.
- Keep React Query derived views under the same key prefix when possible (for example, `['secrets', projectSlug, configSlug, ...]`) so existing prefix invalidation still refreshes all related views.
- Secret icon persistence now distinguishes `icon_source` as `auto` or `manual`; project-wide icon recompute should only rewrite `auto` entries so explicit manual overrides remain stable.
- When preparing legacy API removal, deprecate in layers first: code-level `@warnings.deprecated`, OpenAPI `deprecated=true`, and API response deprecation headers, then remove only in a later major release.
- For complexity-heavy logic in engines, prefer a module-private per-use-case service class (for example, comparison or icon resolution) with small single-purpose methods, while keeping the public engine/API method signature and response shape unchanged.
- DeepSource `PYL-R0201` maps to pylint's optional extension check `R6301/no-self-use`; local verification requires `--load-plugins=pylint.extensions.no_self_use`.


## Conventional Commit Guidelines

Expand Down Expand Up @@ -128,4 +148,3 @@ git commit -m "type(scope): description"
<note>Replace with your constructed message. Include body and footer if needed.</note>
</final-step>
```

3 changes: 2 additions & 1 deletion Access/tokens.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,8 @@ def revoke(self, token=None, username=None, token_id=None):
)
return {"status": "OK"}, 200

def _serialize_token_metadata(self, doc):
@staticmethod
def _serialize_token_metadata(doc):
return {
"token_id": oid_to_str(doc.get("_id")),
"type": doc.get("type"),
Expand Down
3 changes: 3 additions & 0 deletions Api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@
SecretExportResource,
SecretItemResource,
)
from Api.resources.secrets.project_icons_resource import ( # noqa: F401
ProjectSecretIconsRecomputeResource,
)
from Api.resources.compare.compare_secret_resource import ( # noqa: F401
CompareSecretResource,
)
Expand Down
6 changes: 4 additions & 2 deletions Api/resources/auth/onboarding_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@

@onboarding_ns.route("/status")
class OnboardingStatusResource(Resource):
def get(self):
@staticmethod
def get():
return {"status": "OK", "onboarding": conn.onboarding.get_state()}, 200


@onboarding_ns.route("/bootstrap")
class OnboardingBootstrapResource(Resource):
@staticmethod
@api.doc(parser=bootstrap_parser)
def post(self):
def post():
args = bootstrap_parser.parse_args()
result, code = conn.onboarding.bootstrap(
username=args["username"],
Expand Down
3 changes: 2 additions & 1 deletion Api/resources/meta/version_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@

@meta_ns.route("")
class VersionResource(Resource):
def get(self):
@staticmethod
def get():
return {"status": "OK", "version": get_application_version()}, 200
52 changes: 45 additions & 7 deletions Api/resources/secrets/kv_resource.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,30 @@
#!/usr/bin/env python3
# Key-Value (KV) Secrets Engines API Resource
from warnings import deprecated

from flask_restx import fields, Resource
from Api.core import api, conn
from Access.is_auth import require_token

KV_DEPRECATION_MESSAGE = (
"The /api/secrets/kv endpoints are deprecated and will be removed in the "
"next major release. "
"Use /api/projects/{project}/configs/{config}/secrets/{key}."
)
KV_DEPRECATION_HEADERS = {
"Deprecation": "true",
"Warning": f'299 - "{KV_DEPRECATION_MESSAGE}"',
"Link": (
"<https://github.com/bearlike/Simple-Secrets-Manager/blob/main/"
'docs/DEPRECATIONS.md>; rel="deprecation"'
),
}


def _with_deprecation_headers(payload, code=200):
return payload, code, KV_DEPRECATION_HEADERS


# KV Namespace
kv_ns = api.namespace(
name="secrets/kv",
Expand Down Expand Up @@ -47,19 +68,25 @@

@kv_ns.route("/<string:path>/<string:key>")
@api.doc(
deprecated=True,
responses={401: "Unauthorized", 404: "Path or KV not found"},
params={
"path": "Path to a KV store",
"key": "Key (index) in path where a secret (value) is stored",
},
)
@deprecated(KV_DEPRECATION_MESSAGE)
class Engine_KV(Resource):
"""Key-Value API operations"""

@api.doc(
description="Update a kv in a path", security="Token", parser=kv_parser
description="Update a kv in a path",
security="Token",
parser=kv_parser,
deprecated=True,
)
@api.marshal_with(kv_model)
@deprecated(KV_DEPRECATION_MESSAGE)
def put(self, path, key):
"""Update a given resource"""
args = kv_parser.parse_args()
Expand All @@ -68,26 +95,32 @@ def put(self, path, key):
if code != 200:
api.abort(code, status)
return None
return status
return _with_deprecation_headers(status, code)

@api.doc(
description="Delete a KV from a path",
security="Token",
responses={200: "Secrets deleted"},
deprecated=True,
)
@deprecated(KV_DEPRECATION_MESSAGE)
def delete(self, path, key):
"""Delete a given kv"""
require_token()
status, code = conn.kv.delete(path, key)
if code != 200:
api.abort(code, status)
return None
return status
return _with_deprecation_headers(status, code)

@api.doc(
description="Add a KV to a path", security="Token", parser=kv_parser
description="Add a KV to a path",
security="Token",
parser=kv_parser,
deprecated=True,
)
@api.marshal_with(kv_model)
@deprecated(KV_DEPRECATION_MESSAGE)
def post(self, path, key):
"""Add a new kv to a path"""
args = kv_parser.parse_args()
Expand All @@ -96,14 +129,19 @@ def post(self, path, key):
if code != 200:
api.abort(code, status)
return None
return status
return _with_deprecation_headers(status, code)

@api.doc(description="Return a KV from a path", security="Token")
@api.doc(
description="Return a KV from a path",
security="Token",
deprecated=True,
)
@api.marshal_with(kv_model)
@deprecated(KV_DEPRECATION_MESSAGE)
def get(self, path, key):
"""Fetch a given KV from a path"""
require_token()
status, code = conn.kv.get(path, key)
if code != 200:
api.abort(code, str(status))
return status
return _with_deprecation_headers(status, code)
32 changes: 32 additions & 0 deletions Api/resources/secrets/project_icons_resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env python3
from flask_restx import Resource

from Api.core import api, conn
from Api.resources.helpers import resolve_project_config
from Access.is_auth import with_token, require_scope, audit_event

project_icons_ns = api.namespace(
"projects/<string:project_slug>/secrets/icons",
description="Project secret icon maintenance",
)


@project_icons_ns.route("/recompute")
class ProjectSecretIconsRecomputeResource(Resource):
@api.doc(security=["Bearer", "Token"])
@with_token
def post(self, project_slug):
project, _ = resolve_project_config(project_slug)
require_scope("secrets:write", project_id=project["_id"])

summary, msg, code = conn.secrets_v2.recompute_project_icon_slugs(
project["_id"]
)
audit_event(
"secrets.icons.recompute",
project_slug=project_slug,
status_code=code,
)
if code >= 400:
api.abort(code, msg)
return {"status": "OK", "summary": summary}, 200
3 changes: 2 additions & 1 deletion Api/resources/secrets/references.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,8 @@ def _resolve_key(
self._resolved_cache[node] = resolved
return resolved

def _parse_reference(self, token: str, current: _Context) -> _Node | None:
@staticmethod
def _parse_reference(token: str, current: _Context) -> _Node | None:
parts = token.split(".")
if len(parts) == 1:
key = parts[0]
Expand Down
2 changes: 2 additions & 0 deletions Api/resources/secrets/secrets_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ def put(self, project_slug, config_slug, key):
)
args = secret_parser.parse_args()
value = args["value"]
if not isinstance(value, str):
api.abort(400, "value must be a string")
raw_payload = request.get_json(silent=True)
payload = raw_payload if isinstance(raw_payload, dict) else {}
icon_slug_provided = "icon_slug" in payload
Expand Down
2 changes: 1 addition & 1 deletion Engines/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ def write_event(self, event: dict):
}
self._events.insert_one(payload)

@staticmethod
def _build_query(
self,
project_slug=None,
config_slug=None,
since=None,
Expand Down
31 changes: 31 additions & 0 deletions Engines/kv.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@
"""KV (Key-Value) Secret Engine for Secrets Manager"""

import re
from warnings import deprecated


@deprecated(
"Key_Value_Secrets is deprecated and will be removed in the next major "
"release. "
"Use config-scoped secrets via "
"/api/projects/{project}/configs/{config}/secrets/{key}."
)
class Key_Value_Secrets:
def __init__(self, kv_col):
"""KV stands for Key-Value collection
Expand All @@ -14,6 +21,12 @@ def __init__(self, kv_col):
# * db.kv.createIndex( { "path": 1 }, { unique: true } )
self._kv = kv_col

@deprecated(
"Key_Value_Secrets.get is deprecated and will be removed in the next "
"major release. "
"Use config-scoped secrets via "
"/api/projects/{project}/configs/{config}/secrets/{key}."
)
def get(self, path, key):
finder = self._kv.find_one({"path": path})
if not finder:
Expand All @@ -28,6 +41,12 @@ def get(self, path, key):
}
return result, 200

@deprecated(
"Key_Value_Secrets.add is deprecated and will be removed in the next "
"major release. "
"Use config-scoped secrets via "
"/api/projects/{project}/configs/{config}/secrets/{key}."
)
def add(self, path, key, value):
pattern = "[a-zA-Z0-9_]+"
if not (re.fullmatch(pattern, key) and re.fullmatch(pattern, path)):
Expand All @@ -54,6 +73,12 @@ def add(self, path, key, value):
return {"status": f"Key already exist in '{path}'"}, 400
return result, 200

@deprecated(
"Key_Value_Secrets.delete is deprecated and will be removed in the "
"next major release. "
"Use config-scoped secrets via "
"/api/projects/{project}/configs/{config}/secrets/{key}."
)
def delete(self, path, key):
finder = self._kv.find_one({"path": path})
if not finder:
Expand All @@ -65,6 +90,12 @@ def delete(self, path, key):
result = {"status": "OK", "path": path, "key": key}
return result, 200

@deprecated(
"Key_Value_Secrets.update is deprecated and will be removed in the "
"next major release. "
"Use config-scoped secrets via "
"/api/projects/{project}/configs/{config}/secrets/{key}."
)
def update(self, path, key, value):
finder = self._kv.find_one({"path": path})
if not finder:
Expand Down
Loading