Skip to content

Commit dc176c9

Browse files
committed
Secret manager: Passwords
Added the initial part of Secret Manager to manager passwords. The first implementation is supported only plain passwords but a particular `constructor` interface allow to implement more strict solutions in the future. The secret manager follows the universal agent way so actual password generation and saving is on the driver side. Single driver `PasswordCapabilityDriver' has been added in the first implementation that uses the PG database as a storage for passwords. An example, of adding a password: Request: ```curl curl --location 'http://10.20.0.2:11010/v1/secret/passwords/' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer <TOKEN>' \ --data '{ "name": "my-password", "project_id": "c27f59fe-d3d3-4625-804c-5c9fd3bb3142", "method": "AUTO_HEX", "constructor": { "kind": "plain" } }' ``` Response: ```curl { "uuid": "985dad4b-161c-432c-8d9f-be78763df758", "created_at": "2025-06-25T20:39:50.095203Z", "updated_at": "2025-06-25T20:39:56.335798Z", "project_id": "c27f59fe-d3d3-4625-804c-5c9fd3bb3142", "name": "my-password", "description": "", "method": "AUTO_HEX", "status": "ACTIVE", "constructor": { "kind": "plain" }, "value": "755eca35ece5bd921852c72996d1a790" } ``` There are two supported method for generation: AUTO_HEX, AUTO_URL_SAFE Signed-off-by: Anton Kremenetsky <anton.kremenetsky@gmail.com>
1 parent f36e037 commit dc176c9

27 files changed

Lines changed: 1258 additions & 7 deletions

File tree

etc/genesis_universal_agent/genesis_universal_agent.conf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@ connection_pool_size = 2
1111
[universal_agent]
1212
orch_endpoint = http://localhost:11011
1313
status_endpoint = http://localhost:11012
14-
caps_drivers = CoreCapabilityDriver
14+
caps_drivers = CoreCapabilityDriver,PasswordCapabilityDriver
1515

1616

1717
[universal_agent_scheduler]
18-
capabilities = em_core_*
18+
capabilities = em_core_*,password
1919

2020

2121
[CoreCapabilityDriver]
File renamed without changes.

genesis_core/agent/universal/drivers/secret/__init__.py

Whitespace-only changes.

genesis_core/agent/universal/drivers/secret/backend/__init__.py

Whitespace-only changes.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Copyright 2025 Genesis Corporation.
2+
#
3+
# All Rights Reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License. You may obtain
7+
# a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations
15+
# under the License.
16+
from __future__ import annotations
17+
18+
import secrets
19+
import logging
20+
import typing as tp
21+
22+
from restalchemy.dm import filters as dm_filters
23+
from restalchemy.storage import exceptions as ra_exc
24+
from gcl_sdk.agents.universal.dm import models
25+
from gcl_sdk.agents.universal.clients.backend import base
26+
from gcl_sdk.agents.universal.clients.backend import exceptions
27+
28+
from genesis_core.secret import constants as sc
29+
from genesis_core.secret.dm import models as secret_dm
30+
from genesis_core.agent.universal.drivers.secret.dm import models as driver_dm
31+
32+
33+
LOG = logging.getLogger(__name__)
34+
35+
36+
class DatabaseSecretBackendClient(base.AbstractBackendClient):
37+
"""Secret Backend client based on SQL database."""
38+
39+
def get(self, resource: models.Resource) -> dict[str, tp.Any]:
40+
"""Get the resource value in dictionary format."""
41+
try:
42+
driver_password = driver_dm.Password.objects.get_one(
43+
filters={
44+
"uuid": dm_filters.EQ(resource.uuid),
45+
},
46+
)
47+
except ra_exc.RecordNotFound:
48+
raise exceptions.ResourceNotFound(resource=resource)
49+
50+
return driver_password.meta
51+
52+
def create(self, resource: models.Resource) -> dict[str, tp.Any]:
53+
"""Creates the resource. Returns the created resource."""
54+
try:
55+
self.get(resource)
56+
except exceptions.ResourceNotFound:
57+
pass
58+
else:
59+
raise exceptions.ResourceAlreadyExists(resource=resource)
60+
61+
password = secret_dm.Password.from_ua_resource(resource)
62+
63+
# Validate structure of password model
64+
if (
65+
sc.SecretMethod[password.method].is_auto
66+
and password.value is not None
67+
):
68+
raise ValueError("Cannot create auto-generated password.")
69+
70+
if (
71+
not sc.SecretMethod[password.method].is_auto
72+
and password.value is None
73+
):
74+
raise ValueError("Cannot create non-auto-generated password.")
75+
76+
# Generate plain password
77+
if sc.SecretMethod[password.method].is_auto:
78+
if password.method == sc.SecretMethod.AUTO_HEX:
79+
plain_password = secrets.token_hex(16)
80+
elif password.method == sc.SecretMethod.AUTO_URL_SAFE:
81+
plain_password = secrets.token_urlsafe(16)
82+
else:
83+
raise ValueError("Unknown auto-generated password method.")
84+
else:
85+
plain_password = password.value
86+
87+
# Build password from the plain view
88+
pass_value = password.constructor.build(plain_password)
89+
90+
# Build storagable password and save
91+
driver_password = driver_dm.Password.from_password_resource(
92+
resource, pass_value
93+
)
94+
driver_password.save()
95+
return driver_password.meta
96+
97+
def update(self, resource: models.Resource) -> dict[str, tp.Any]:
98+
"""Update the resource. Returns the updated resource."""
99+
100+
# The simplest implementation. Update via recreation.
101+
self.delete(resource)
102+
return self.create(resource)
103+
104+
def list(self, kind: str, **kwargs) -> list[dict[str, tp.Any]]:
105+
"""Lists all resources by kind."""
106+
secrets = driver_dm.Password.objects.get_all()
107+
return [s.meta for s in secrets]
108+
109+
def delete(self, resource: models.Resource) -> None:
110+
"""Delete the resource."""
111+
try:
112+
self.get(resource)
113+
except exceptions.ResourceNotFound:
114+
raise exceptions.ResourceNotFound(resource=resource)
115+
116+
password = driver_dm.Password.objects.get_one(
117+
filters={
118+
"uuid": dm_filters.EQ(resource.uuid),
119+
}
120+
)
121+
password.delete()

genesis_core/agent/universal/drivers/secret/dm/__init__.py

Whitespace-only changes.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Copyright 2025 Genesis Corporation.
2+
#
3+
# All Rights Reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License. You may obtain
7+
# a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations
15+
# under the License.
16+
from __future__ import annotations
17+
18+
import typing as tp
19+
20+
from restalchemy.dm import properties
21+
from restalchemy.dm import models
22+
from restalchemy.dm import types
23+
from restalchemy.storage.sql import orm
24+
from gcl_sdk.agents.universal.dm import models as ua_models
25+
26+
from genesis_core.secret import constants as sc
27+
from genesis_core.secret.dm import models as secret_dm
28+
29+
30+
class Password(
31+
models.ModelWithUUID,
32+
models.ModelWithTimestamp,
33+
orm.SQLStorableMixin,
34+
):
35+
__tablename__ = "storage_passwords"
36+
37+
status = properties.property(
38+
types.Enum([s.value for s in sc.SecretStatus]),
39+
default=sc.SecretStatus.NEW.value,
40+
)
41+
value = properties.property(
42+
types.String(min_length=1, max_length=512),
43+
required=True,
44+
)
45+
# Some additional metadata about the secret
46+
meta = properties.property(types.Dict(), default=lambda: {})
47+
48+
@classmethod
49+
def from_password_resource(
50+
cls, resource: ua_models.TargetResource, password_value: str
51+
) -> Password:
52+
meta = resource.value.copy()
53+
meta["value"] = password_value
54+
meta["status"] = sc.SecretStatus.ACTIVE.value
55+
56+
return cls(
57+
uuid=resource.uuid,
58+
value=password_value,
59+
status=sc.SecretStatus.ACTIVE.value,
60+
meta=meta,
61+
)
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright 2025 Genesis Corporation.
2+
#
3+
# All Rights Reserved.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License"); you may
6+
# not use this file except in compliance with the License. You may obtain
7+
# a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14+
# License for the specific language governing permissions and limitations
15+
# under the License.
16+
from __future__ import annotations
17+
18+
import logging
19+
20+
from gcl_sdk.agents.universal.drivers import direct
21+
from gcl_sdk.agents.universal.storage import fs
22+
23+
from genesis_core.agent.universal.drivers.secret.backend import db as db_back
24+
25+
26+
LOG = logging.getLogger(__name__)
27+
28+
AGENT_WORK_DIR = "/var/lib/genesis/universal_agent/"
29+
30+
31+
class PasswordCapabilityDriver(direct.DirectAgentDriver):
32+
"""Password capability driver."""
33+
34+
def __init__(self):
35+
storage = fs.FileAgentStorage(
36+
AGENT_WORK_DIR, "password_cap_storage.json"
37+
)
38+
client = db_back.DatabaseSecretBackendClient()
39+
40+
super().__init__(storage=storage, client=client)
41+
42+
def get_capabilities(self) -> list[str]:
43+
"""Returns a list of capabilities supported by the driver."""
44+
return ["password"]

genesis_core/config/service.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@
3939
DEF_OUTDATE_MIN_PERIOD = datetime.timedelta(minutes=10)
4040

4141

42-
class ConfigService(basic.BasicService):
42+
class ConfigServiceBuilder(basic.BasicService):
4343

4444
def _get_new_configs(
4545
self,

genesis_core/gservice/service.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from genesis_core.node.machine import service as n_machine_service
2727
from genesis_core.network import service as n_network_service
2828
from genesis_core.config import service as config_service
29+
from genesis_core.secret import service as secret_service
2930

3031

3132
LOG = logging.getLogger(__name__)
@@ -70,7 +71,8 @@ def __init__(self, iter_min_period=1, iter_pause=0.1):
7071
n_machine = n_machine_service.MachineAgentService(
7172
iter_min_period=1, iter_pause=0.1
7273
)
73-
cfg_service = config_service.ConfigService()
74+
cfg_service = config_service.ConfigServiceBuilder()
75+
secret_svc = secret_service.SecretServiceBuilder()
7476
event_sender = senders.EventSenderService.build_from_config()
7577

7678
self._services = [
@@ -79,6 +81,7 @@ def __init__(self, iter_min_period=1, iter_pause=0.1):
7981
n_builder,
8082
n_machine,
8183
cfg_service,
84+
secret_svc,
8285
event_sender,
8386
]
8487

0 commit comments

Comments
 (0)