Skip to content

Commit f7e5162

Browse files
Preliminary Working version of secret management with basic validation and resolution
1 parent c190586 commit f7e5162

File tree

7 files changed

+108
-53
lines changed

7 files changed

+108
-53
lines changed

executor/migrations/0046_secret_secret_executor_se_key_60cc82_idx_and_more.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Generated by Django 4.1.13 on 2025-03-05 09:24
1+
# Generated by Django 4.1.13 on 2025-03-05 11:35
22

33
from django.conf import settings
44
from django.db import migrations, models
@@ -27,7 +27,7 @@ class Migration(migrations.Migration):
2727
('is_active', models.BooleanField(default=True)),
2828
('description', models.TextField(blank=True)),
2929
('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.account')),
30-
('creator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_secrets', to=settings.AUTH_USER_MODEL)),
30+
('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_secrets', to=settings.AUTH_USER_MODEL)),
3131
('last_updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_secrets', to=settings.AUTH_USER_MODEL)),
3232
],
3333
),

executor/models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -679,7 +679,7 @@ class Secret(models.Model):
679679
key = models.CharField(max_length=255, help_text="Reference key for the secret")
680680
value = EncryptedTextField()
681681
account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE)
682-
creator = models.ForeignKey('accounts.User', on_delete=models.SET_NULL, null=True, related_name='created_secrets')
682+
created_by = models.ForeignKey('accounts.User', on_delete=models.SET_NULL, null=True, related_name='created_secrets')
683683
created_at = models.DateTimeField(auto_now_add=True)
684684
updated_at = models.DateTimeField(auto_now=True)
685685
last_updated_by = models.ForeignKey('accounts.User', on_delete=models.SET_NULL, null=True, related_name='updated_secrets')

executor/secrets/views.py

Lines changed: 55 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,13 @@
1010
from playbooks.utils.decorators import web_api
1111
from playbooks.utils.meta import get_meta
1212
from playbooks.utils.queryset import filter_page
13-
from protos.base_pb2 import Message
13+
from protos.base_pb2 import Message, Meta, Page
1414
from protos.secrets.api_pb2 import (
1515
GetSecretsRequest, GetSecretsResponse,
1616
GetSecretRequest, GetSecretResponse,
1717
CreateSecretRequest, CreateSecretResponse,
1818
UpdateSecretRequest, UpdateSecretResponse,
1919
Secret as SecretProto,
20-
UpdateSecretOp
2120
)
2221

2322
logger = logging.getLogger(__name__)
@@ -37,27 +36,60 @@ def _secret_to_proto(secret: Secret) -> SecretProto:
3736
key=StringValue(value=secret.key),
3837
masked_value=StringValue(value=_mask_secret_value(secret.value)),
3938
description=StringValue(value=secret.description or ""),
40-
creator=StringValue(value=secret.creator.email if secret.creator else ""),
39+
created_by=StringValue(value=secret.created_by.email if secret.created_by else ""),
4140
last_updated_by=StringValue(value=secret.last_updated_by.email if secret.last_updated_by else ""),
4241
created_at=int(secret.created_at.replace(tzinfo=timezone.utc).timestamp()) if secret.created_at else 0,
4342
updated_at=int(secret.updated_at.replace(tzinfo=timezone.utc).timestamp()) if secret.updated_at else 0,
4443
is_active=secret.is_active
4544
)
4645

46+
def _secret_to_proto_partial(secret: Secret) -> SecretProto:
47+
"""Convert a Secret model to a partial Secret proto (for list views)"""
48+
return SecretProto(
49+
id=StringValue(value=str(secret.id)),
50+
key=StringValue(value=secret.key),
51+
description=StringValue(value=secret.description or ""),
52+
created_by=StringValue(value=secret.created_by.email if secret.created_by else ""),
53+
created_at=int(secret.created_at.replace(tzinfo=timezone.utc).timestamp()) if secret.created_at else 0,
54+
is_active=secret.is_active
55+
)
56+
57+
4758

4859
@web_api(GetSecretsRequest)
4960
def secrets_list(request_message: GetSecretsRequest) -> Union[GetSecretsResponse, HttpResponse]:
50-
"""List all active secrets for the current account"""
61+
"""List secrets with optional filtering by IDs or key"""
5162
account: Account = get_request_account()
52-
53-
# Get all active secrets for this account
63+
meta: Meta = request_message.meta
64+
show_inactive = meta.show_inactive
65+
page: Page = meta.page
66+
list_all = True
67+
68+
# Base queryset
5469
qs = Secret.objects.filter(account=account, is_active=True)
55-
56-
# Apply pagination
70+
71+
# Filter by specific IDs if provided
72+
if request_message.secret_ids:
73+
qs = qs.filter(id__in=request_message.secret_ids)
74+
list_all = False
75+
# Filter by key if provided
76+
if request_message.key:
77+
qs = qs.filter(key__icontains=request_message.key.value.lower())
78+
list_all = False
79+
# Otherwise filter by active status unless show_inactive is True
80+
elif not show_inactive or not show_inactive.value:
81+
qs = qs.filter(is_active=True)
82+
5783
total_count = qs.count()
58-
page = request_message.meta.page
59-
secrets = [_secret_to_proto(secret) for secret in filter_page(qs.order_by("-created_at"), page)]
60-
84+
qs = qs.order_by('-created_at')
85+
qs = filter_page(qs, page)
86+
87+
# Use proto or proto_partial based on list_all flag
88+
if list_all:
89+
secrets = [_secret_to_proto_partial(secret) for secret in qs]
90+
else:
91+
secrets = [_secret_to_proto(secret) for secret in qs]
92+
6193
return GetSecretsResponse(
6294
meta=get_meta(page=page, total_count=total_count),
6395
success=BoolValue(value=True),
@@ -108,7 +140,16 @@ def secret_create(request_message: CreateSecretRequest) -> Union[CreateSecretRes
108140
return CreateSecretResponse(
109141
meta=get_meta(),
110142
success=BoolValue(value=False),
111-
message=Message(title="Invalid Request", description="Key, and value are required")
143+
message=Message(title="Invalid Request", description="Key and value are required")
144+
)
145+
146+
# Validate key format -> (single word, no spaces, only alphanumeric and underscores)
147+
if not key.isalnum() or ' ' in key:
148+
return CreateSecretResponse(
149+
meta=get_meta(),
150+
success=BoolValue(value=False),
151+
message=Message(title="Invalid Key",
152+
description="Key must be a single word containing only letters, numbers, and underscores")
112153
)
113154

114155
# Check if key already exists for this account
@@ -126,7 +167,7 @@ def secret_create(request_message: CreateSecretRequest) -> Union[CreateSecretRes
126167
key=key,
127168
value=value,
128169
description=description,
129-
creator=user,
170+
created_by=user,
130171
last_updated_by=user,
131172
is_active=True
132173
)
@@ -219,4 +260,4 @@ def secret_update(request_message: UpdateSecretRequest) -> Union[UpdateSecretRes
219260
meta=get_meta(),
220261
success=BoolValue(value=False),
221262
message=Message(title="Error", description="Failed to update secret")
222-
)
263+
)

protos/secrets/api.proto

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ message Secret {
99
google.protobuf.StringValue key = 2;
1010
google.protobuf.StringValue masked_value = 3;
1111
google.protobuf.StringValue description = 4;
12-
google.protobuf.StringValue creator = 5;
12+
google.protobuf.StringValue created_by = 5;
1313
google.protobuf.StringValue last_updated_by = 6;
1414
int64 created_at = 7;
1515
int64 updated_at = 8;
@@ -18,6 +18,8 @@ message Secret {
1818

1919
message GetSecretsRequest {
2020
Meta meta = 1;
21+
repeated string secret_ids = 2; // Optional list of secret IDs to fetch
22+
google.protobuf.StringValue key = 3; // Optional key to filter by (key_name)
2123
}
2224

2325
message GetSecretsResponse {

protos/secrets/api_pb2.py

Lines changed: 26 additions & 26 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)