cnos-firebase 1.12.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cnos_firebase-1.12.0/.gitignore +33 -0
- cnos_firebase-1.12.0/PKG-INFO +7 -0
- cnos_firebase-1.12.0/pyproject.toml +19 -0
- cnos_firebase-1.12.0/src/cnos_firebase/__init__.py +4 -0
- cnos_firebase-1.12.0/src/cnos_firebase/provider.py +56 -0
- cnos_firebase-1.12.0/tests/__init__.py +0 -0
- cnos_firebase-1.12.0/tests/test_provider.py +135 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
.coop/logs/
|
|
2
|
+
.coop/tmp/
|
|
3
|
+
node_modules/
|
|
4
|
+
dist/
|
|
5
|
+
coverage/
|
|
6
|
+
.artifacts/
|
|
7
|
+
.turbo/
|
|
8
|
+
.DS_Store
|
|
9
|
+
Thumbs.db
|
|
10
|
+
.idea/
|
|
11
|
+
.vscode/
|
|
12
|
+
*.log
|
|
13
|
+
.env
|
|
14
|
+
.env.local
|
|
15
|
+
.env.*.local
|
|
16
|
+
!.env.example
|
|
17
|
+
pnpm-debug.log*
|
|
18
|
+
|
|
19
|
+
# Claude Code worktrees and session scratch
|
|
20
|
+
.claude/
|
|
21
|
+
.tmp/
|
|
22
|
+
|
|
23
|
+
# Python bytecode
|
|
24
|
+
__pycache__/
|
|
25
|
+
*.pyc
|
|
26
|
+
*.pyo
|
|
27
|
+
*.pyd
|
|
28
|
+
.pytest_cache/
|
|
29
|
+
|
|
30
|
+
# Java build output
|
|
31
|
+
target/
|
|
32
|
+
*.class
|
|
33
|
+
*.jar
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "cnos-firebase"
|
|
7
|
+
version = "1.12.0"
|
|
8
|
+
description = "CNOS Firebase Secrets provider (thin wrapper over cnos-gcp)"
|
|
9
|
+
requires-python = ">=3.8"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"cnos>=1.12.0",
|
|
12
|
+
"cnos-gcp>=1.12.0",
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
[tool.hatch.build.targets.wheel]
|
|
16
|
+
packages = ["src/cnos_firebase"]
|
|
17
|
+
|
|
18
|
+
[tool.pytest.ini_options]
|
|
19
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Firebase Secrets CNOS vault provider.
|
|
2
|
+
|
|
3
|
+
This is a thin wrapper over GcpSecretManagerProvider. Firebase projects use
|
|
4
|
+
the same GCP Secret Manager API; the only difference is the provider name
|
|
5
|
+
reported to the CNOS runtime ("firebase-secrets" instead of "gcp-secret-manager").
|
|
6
|
+
|
|
7
|
+
All authentication and secret-fetching logic is delegated entirely to
|
|
8
|
+
GcpSecretManagerProvider.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any, Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from cnos.types import (
|
|
15
|
+
SecretVaultProvider,
|
|
16
|
+
SecretVaultProviderFactory,
|
|
17
|
+
VaultAuthConfig,
|
|
18
|
+
VaultDefinition,
|
|
19
|
+
)
|
|
20
|
+
from cnos_gcp.provider import GcpSecretManagerProvider
|
|
21
|
+
|
|
22
|
+
PROVIDER_NAME = "firebase-secrets"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class FirebaseSecretsProvider(SecretVaultProvider):
|
|
26
|
+
"""Delegates entirely to GcpSecretManagerProvider; swaps the provider name."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
vault_id: str,
|
|
31
|
+
definition: VaultDefinition,
|
|
32
|
+
client: Any = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
self._vault_id = vault_id
|
|
35
|
+
self._definition = definition
|
|
36
|
+
self._delegate = GcpSecretManagerProvider(vault_id, definition, client=client)
|
|
37
|
+
|
|
38
|
+
def authenticate(self, auth: VaultAuthConfig) -> None:
|
|
39
|
+
self._delegate.authenticate(auth)
|
|
40
|
+
|
|
41
|
+
def batch_get(self, refs: List[str]) -> Dict[str, Any]:
|
|
42
|
+
return self._delegate.batch_get(refs)
|
|
43
|
+
|
|
44
|
+
def get(self, ref: str) -> Optional[Any]:
|
|
45
|
+
return self._delegate.get(ref)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def factory(client: Any = None) -> SecretVaultProviderFactory:
|
|
49
|
+
"""Return a SecretVaultProviderFactory for Firebase Secrets.
|
|
50
|
+
|
|
51
|
+
Pass client= to inject a mock GCP Secret Manager client for tests.
|
|
52
|
+
"""
|
|
53
|
+
def create(vault_id: str, definition: VaultDefinition) -> FirebaseSecretsProvider:
|
|
54
|
+
return FirebaseSecretsProvider(vault_id, definition, client=client)
|
|
55
|
+
|
|
56
|
+
return SecretVaultProviderFactory(provider=PROVIDER_NAME, create=create)
|
|
File without changes
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Tests for the Firebase Secrets provider.
|
|
2
|
+
|
|
3
|
+
FirebaseSecretsProvider is a thin wrapper over GcpSecretManagerProvider;
|
|
4
|
+
all functional logic is tested in cnos-gcp. These tests verify:
|
|
5
|
+
- the provider name constant is "firebase-secrets"
|
|
6
|
+
- delegation to the GCP provider works end-to-end
|
|
7
|
+
- factory() returns a usable SecretVaultProviderFactory
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
from cnos.types import VaultAuthConfig, VaultAuthDefinition, VaultDefinition
|
|
16
|
+
from cnos_firebase.provider import FirebaseSecretsProvider, PROVIDER_NAME, factory
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ---------------------------------------------------------------------------
|
|
20
|
+
# Mock GCP Secret Manager client (reused from cnos-gcp tests)
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
class _NotFoundError(Exception):
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class MockGcpClient:
|
|
28
|
+
def __init__(self, secrets: Dict[str, str], project_id: str = "test-project") -> None:
|
|
29
|
+
self._secrets = secrets
|
|
30
|
+
self.project_id = project_id
|
|
31
|
+
|
|
32
|
+
def access_secret_version(self, name: str) -> str:
|
|
33
|
+
for key, val in self._secrets.items():
|
|
34
|
+
if key in name:
|
|
35
|
+
return val
|
|
36
|
+
raise _NotFoundError(f"not found: {name}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _make_provider(
|
|
40
|
+
secrets: Dict[str, str] = None,
|
|
41
|
+
project_id: str = "test-project",
|
|
42
|
+
mapping: dict = None,
|
|
43
|
+
) -> FirebaseSecretsProvider:
|
|
44
|
+
definition = VaultDefinition(
|
|
45
|
+
provider=PROVIDER_NAME,
|
|
46
|
+
auth=VaultAuthDefinition(method="iam", config={"projectId": project_id}),
|
|
47
|
+
mapping=mapping or {},
|
|
48
|
+
)
|
|
49
|
+
client = MockGcpClient(secrets or {}, project_id=project_id)
|
|
50
|
+
p = FirebaseSecretsProvider("test-vault", definition, client=client)
|
|
51
|
+
p._delegate._authenticated = True
|
|
52
|
+
return p
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ---------------------------------------------------------------------------
|
|
56
|
+
# Tests
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
class TestProviderName:
|
|
60
|
+
def test_constant(self):
|
|
61
|
+
assert PROVIDER_NAME == "firebase-secrets"
|
|
62
|
+
|
|
63
|
+
def test_factory_provider_name(self):
|
|
64
|
+
f = factory()
|
|
65
|
+
assert f.provider == "firebase-secrets"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class TestAuthenticate:
|
|
69
|
+
def test_iam_accepted(self):
|
|
70
|
+
p = _make_provider()
|
|
71
|
+
p._delegate._authenticated = False
|
|
72
|
+
p.authenticate(VaultAuthConfig(method="iam"))
|
|
73
|
+
assert p._delegate._authenticated
|
|
74
|
+
|
|
75
|
+
def test_environment_accepted(self):
|
|
76
|
+
p = _make_provider()
|
|
77
|
+
p._delegate._authenticated = False
|
|
78
|
+
p.authenticate(VaultAuthConfig(method="environment"))
|
|
79
|
+
assert p._delegate._authenticated
|
|
80
|
+
|
|
81
|
+
def test_wrong_method_raises(self):
|
|
82
|
+
p = _make_provider()
|
|
83
|
+
p._delegate._authenticated = False
|
|
84
|
+
with pytest.raises(Exception, match="iam authentication"):
|
|
85
|
+
p.authenticate(VaultAuthConfig(method="token", token="t"))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class TestBatchGet:
|
|
89
|
+
def test_delegates_to_gcp(self):
|
|
90
|
+
p = _make_provider({"my-secret": "firebase-value"})
|
|
91
|
+
result = p.batch_get(["my-secret"])
|
|
92
|
+
assert result == {"my-secret": "firebase-value"}
|
|
93
|
+
|
|
94
|
+
def test_not_found_excluded(self):
|
|
95
|
+
p = _make_provider({})
|
|
96
|
+
result = p.batch_get(["missing"])
|
|
97
|
+
assert "missing" not in result
|
|
98
|
+
|
|
99
|
+
def test_multiple_secrets(self):
|
|
100
|
+
p = _make_provider({"s1": "v1", "s2": "v2"})
|
|
101
|
+
result = p.batch_get(["s1", "s2"])
|
|
102
|
+
assert result == {"s1": "v1", "s2": "v2"}
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class TestGet:
|
|
106
|
+
def test_existing_secret(self):
|
|
107
|
+
p = _make_provider({"sec": "val"})
|
|
108
|
+
assert p.get("sec") == "val"
|
|
109
|
+
|
|
110
|
+
def test_missing_returns_none(self):
|
|
111
|
+
p = _make_provider({})
|
|
112
|
+
assert p.get("missing") is None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class TestMapping:
|
|
116
|
+
def test_external_mapping(self):
|
|
117
|
+
p = _make_provider(
|
|
118
|
+
{"firebase-project/external-name": "mapped-value"},
|
|
119
|
+
mapping={"firebase-project/external-name": "logical.key"},
|
|
120
|
+
)
|
|
121
|
+
result = p.batch_get(["logical.key"])
|
|
122
|
+
assert result.get("logical.key") == "mapped-value"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TestFactory:
|
|
126
|
+
def test_factory_creates_provider(self):
|
|
127
|
+
client = MockGcpClient({"key": "val"})
|
|
128
|
+
definition = VaultDefinition(
|
|
129
|
+
provider=PROVIDER_NAME,
|
|
130
|
+
auth=VaultAuthDefinition(method="iam", config={"projectId": "proj"}),
|
|
131
|
+
)
|
|
132
|
+
f = factory(client=client)
|
|
133
|
+
p = f.create("my-vault", definition)
|
|
134
|
+
assert isinstance(p, FirebaseSecretsProvider)
|
|
135
|
+
assert f.provider == PROVIDER_NAME
|