From 8abe8cba0821ebe064c33278d8019c2f97d18576 Mon Sep 17 00:00:00 2001 From: droid-dhruv Date: Tue, 4 Mar 2025 17:14:39 +0530 Subject: [PATCH 01/16] initial requirements for secrets --- .DS_Store | Bin 8196 -> 0 bytes executor/models.py | 31 ++++++++++ executor/playbook_source_manager.py | 14 ++++- executor/secret_resolver.py | 85 ++++++++++++++++++++++++++++ web/.DS_Store | Bin 6148 -> 0 bytes web/public/.DS_Store | Bin 8196 -> 0 bytes 6 files changed, 128 insertions(+), 2 deletions(-) delete mode 100644 .DS_Store create mode 100644 executor/secret_resolver.py delete mode 100644 web/.DS_Store delete mode 100644 web/public/.DS_Store diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index fec41c975104fb4ddb103df4eea62266dd7b91ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMU2GIp6u#fI(3yeCl;6&#l(icHrNEZI6i~YD7KIdOr7isf*4@qsBeOGQXLc88 znlu`fpotHPf3LKlhs#_G^&!3x$6iTXzg6x?$mb2X)aTju9H)%(L zC<0Lgq6kD0h$0Y0;BJTj?b*D@x7qhaZ&XJSh$3)LMu2}G;`DHu3hAVv!KZ_&;0QpJ z9|3|wZH+gG#zUG4>7=04Lj}T=s4zu%#DFj-c|6ohg>+I-Va^a9J`m1~@PvY3b{an( zs57Jljp`@@Q3P&|08gJPR%Ry4<;pjn-)+nDlgTTPs;Xzru90ixIr4$t!*03P&-z8L zBU>2ZT%YUOxk@{m+h>{OjJ7cCI)2u2%tByb878UhAFv$5EqAy@$M6E%UeO^dvXas2 z$H!Y*ldaA1*7Xz3@$t>;lGNPPGBKgZixO+M?#&#r^NxE;IDqgYz-nhW^um-kwpU7v z+lZ>V+?ep@>xwsuI!Eu{rw$HUqps6yoiH=nJdfPjNo-HSERh=nqpn@dJ6=Ys8_n9I zMLX-8?Y7O8y+PkR=JVFD=N9~QpqSCLqG#?kNzW>oouo~s^Z7{KW4`4&-afP75ly6$ z;-!_da~CabTD~f|am%*!V_n^sYww$<)u{{AVTz06TTfbMVJPd3nvP*Phx*Hw;U69- zS)P@%%|0iaH(WuAO{RX|@Zr9+m z7_xR=33?KGTp!^*>4HhMAKe802ra0hc|y{nuGjnRY|gY5 z%@Agr)K-0P z1+%+MfC}Ge2T6k^BRMzPZdks{JA-J%^*ypY#0P9Q4AZz=yQF?`&*M5-XqHp52*9)s z0>|1HvMeW34~ZoaVO=-_RD5gFSrgm99%emkh#8bFFS66@RrWSJ%id=nvrpLP>?ou zp2jnH7SG`&ypA{UCf>q1yo>XA4HOrJ#niQ~VEZL$^>S$FVyJ{n`5RAHci$Y1HRflZDHey-8#9|$3 z8s?(*FA#c$onsf+N9+8QAPT~|^#w$3D*FwC$gEKP_xN|xJ@xc@V zOT7ihcAY~cso-_Ga>U)C2vmu2>-qVA`|a=l@6hbguu%k}2;2=3Ky_ztX9vB>?Y_VK ztR1K433_S&$ Cp$T^Y diff --git a/executor/models.py b/executor/models.py index 87c245ab2..3ff585159 100644 --- a/executor/models.py +++ b/executor/models.py @@ -25,6 +25,8 @@ from accounts.models import Account from utils.proto_utils import dict_to_proto +from encrypted_model_fields.fields import EncryptedTextField +import uuid class PlayBookTask(models.Model): @@ -670,3 +672,32 @@ def proto(self) -> PlaybookStepRelationExecutionLogProto: InterpretationProto) if self.interpretation else InterpretationProto(), ) + + +class Secret(models.Model): + id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) + name = models.CharField(max_length=255, help_text="Human-readable name") + key = models.CharField(max_length=255, help_text="Reference key for the secret") + value = EncryptedTextField() + account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE) + creator = models.ForeignKey('accounts.User', on_delete=models.SET_NULL, null=True, related_name='created_secrets') + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + last_updated_by = models.ForeignKey('accounts.User', on_delete=models.SET_NULL, null=True, related_name='updated_secrets') + is_active = models.BooleanField(default=True) + description = models.TextField(blank=True) + + class Meta: + constraints = [ + models.UniqueConstraint( + fields=['key', 'account', 'is_active'], + condition=models.Q(is_active=True), + name='unique_active_key_per_account' + ) + ] + indexes = [ + models.Index(fields=['key', 'account', 'is_active']), + ] + + def __str__(self): + return f"{self.account.name}:{self.key}" diff --git a/executor/playbook_source_manager.py b/executor/playbook_source_manager.py index 01b349c3f..e5da8d0a3 100644 --- a/executor/playbook_source_manager.py +++ b/executor/playbook_source_manager.py @@ -15,6 +15,7 @@ from protos.playbooks.source_task_definitions.lambda_function_task_pb2 import Lambda from protos.ui_definition_pb2 import FormField from utils.proto_utils import proto_to_dict, dict_to_proto +from executor.secret_resolver import SecretResolver def apply_result_transformer(result_dict, lambda_function: Lambda.Function) -> Dict: @@ -148,9 +149,18 @@ def execute_task(self, account_id, time_range: TimeRange, global_variable_set: S form_fields = self.task_type_callable_map[task_type]['form_fields'] - # Resolve global variables in source_type_task_def + # First resolve secrets + resolved_source_type_task_def = SecretResolver.resolve_secrets( + form_fields=form_fields, + account_id=account_id, + source_type_task_def=source_type_task_def + ) + + # Then resolve global variables resolved_source_type_task_def, task_local_variable_map = resolve_global_variables( - form_fields, global_variable_set, source_type_task_def) + form_fields, global_variable_set, resolved_source_type_task_def + ) + source_task[task_type_name] = resolved_source_type_task_def resolved_task_def_proto = dict_to_proto(source_task, self.task_proto) diff --git a/executor/secret_resolver.py b/executor/secret_resolver.py new file mode 100644 index 000000000..94f6c2e17 --- /dev/null +++ b/executor/secret_resolver.py @@ -0,0 +1,85 @@ +import re +import logging +from typing import Dict, Set +from protos.ui_definition_pb2 import FormField +from protos.literal_pb2 import LiteralType +from executor.models import Secret + +logger = logging.getLogger(__name__) + +class SecretResolver: + SECRET_PATTERN = r'!([\w-]+)' + + @classmethod + def extract_secret_refs(cls, value: str) -> Set[str]: + """Extract secret references from a string value""" + return set(re.findall(cls.SECRET_PATTERN, str(value))) + + @classmethod + def replace_secret_refs(cls, value: str, secret_map: Dict[str, str]) -> str: + """Replace secret references with their values""" + result = str(value) + for secret_key, secret_value in secret_map.items(): + result = result.replace(f"!{secret_key}", secret_value) + return result + + @classmethod + def resolve_secrets(cls, form_fields: [FormField], account_id: int, source_type_task_def: Dict) -> Dict: + """ + Resolves secret references (prefixed with !) in task definition + + Args: + form_fields: List of form fields defining the task structure + account_id: ID of the account owning the secrets + source_type_task_def: Task definition dictionary to resolve secrets in + + Returns: + Dict with secrets resolved + """ + try: + # Get fields that might contain secrets + string_fields = [ff.key_name.value for ff in form_fields if ff.data_type == LiteralType.STRING] + string_array_fields = [ff.key_name.value for ff in form_fields if ff.data_type == LiteralType.STRING_ARRAY] + + # Collect all secret references + secret_refs = set() + for field_name, value in source_type_task_def.items(): + if field_name in string_fields: + secret_refs.update(cls.extract_secret_refs(value)) + elif field_name in string_array_fields: + for item in value: + secret_refs.update(cls.extract_secret_refs(item)) + + if not secret_refs: + return source_type_task_def + + # Get referenced secrets from DB + secrets = Secret.objects.filter( + account_id=account_id, + is_active=True, + key__in=secret_refs + ).values('key', 'value') + + if not secrets: + logger.warning(f"No secrets found for references: {secret_refs}") + return source_type_task_def + + # Create mapping of secret keys to values + secret_map = {s['key']: s['value'] for s in secrets} + + # Replace secret references with values + resolved_def = source_type_task_def.copy() + for field_name, value in source_type_task_def.items(): + if field_name in string_fields: + resolved_def[field_name] = cls.replace_secret_refs(value, secret_map) + elif field_name in string_array_fields: + resolved_def[field_name] = [ + cls.replace_secret_refs(item, secret_map) + for item in value + ] + + return resolved_def + + except Exception as e: + logger.error(f"Error resolving secrets: {str(e)}") + return source_type_task_def \ No newline at end of file diff --git a/web/.DS_Store b/web/.DS_Store deleted file mode 100644 index a0418a4e50f4146d69d6ef27e7f66c54b4cbd3a3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK%}T>S5Z<-5O({YS3Oz1(Eto1u#Y>3w1&ruHr6#6mFlI}W*h4AgtS{t~_&m<+ zZVthKHxWAnyWi~m>}Ed5{xHV4S41PmY{r-c4UwbLAZTuMZJA(1uI7l@L%7W5VIsqZ ziTmXH`A28aP- zVDlI-XMxq@Z$`^Z2!wO^nPajwBq VBhG?$l@3T30YwOP#K12w@CEjdOd0?H diff --git a/web/public/.DS_Store b/web/public/.DS_Store deleted file mode 100644 index 0bac881801e629bdf458c317591799d3ef83a58c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8196 zcmeHMU2GIp6h3EK=$$FZlolv13kwCpPg%AUvHZz)+k!~7!1kBFGP^Uh1JjwZGrL=; zHGMLQCTa{seO02oU}7R6zKAcz2VO)(jfpS%pgtNCV}d{D&YdlE`HAsCjm%B%J@?!> z-`u%pzB8w13jmnP>rDXl0KlkosaDf)m&UL2en*pn6`3Rw9v}-2*boPkW?>6G5PBf= zK%hppd-{Iv6lU75^}J zFalN?=G`*PEFa?$W(ab$r$;*9(VdRN=wHQUitcFejF&T`mg#zl#0``xDrd~BVpXh~ z9ZnBB#k8k+c{ioy4sxyAajZ;fu4Q^nqd2J6wK=w@nYNMh0j6${$)P^e)}3O?$=kZ? zW7FaSlbJlI){l=jwIo^^V=c`Sjj{2~8xl0#)HE?6vw02cx9%T2VU5_%8SX6lAO#;T zm@V|F@dcXQ(=V1=XcOnFs41QA%hLH|r8?5ntMvDqS;tPBr;R~%j!OsJP3qp9QJ{nM zWgRO&V!MNCO;)qAc}w$*R?FhXZog-Yd3<)jb#h*tZ#bx`dDqx)keyjD+R2)rbNQXR zMm^K9-CiT-l59vO!~x`sv*tyYtz4UUWXof1yY_TlpZ(ArRZ(i|`U<9NW-MbsbF+r6 zoAyvo(bT=+1LQ&5Gmn`@PL^x1vD?;0jO*(B1@#LbjzombEllYy{rQY}GH-gP)CR`H z9+B8mnW^s|(&-_ebaZ}4-Mw9<5?Q^509v<8M-Cn{zFwA9xBCRW zW&u^2H!@k(dn_$uSiw@uW?I_QR}d!;7HhXNxj*ZSjL>0Ru@-4fF{XZUgm2ubxn9?( zVNX3$>rTcFxWih`=qeJ)N{A;I^IsaRb4Z77dQQ&wMrSM%8Q`~S)pbLsL)Xt=phosZ z(oDTZ7}c~3CefPSg8_&MIdFYpKa32)+W z_`6gkF-eiCrNz<`X_d52ii-!AN`W&l{Y26Z((e~I!G}Sz?iM)Fj-ATm|7zfTHH(Kz z&D`3$d5arXt=q6^>y9Zx3I3HRcRQ7R_DW#hv;pL-3lrZWTntj zt9Tf)>)d*YvNWpBpa2l+a%E*yok`>u>gwp)s47v+2{j%~C~5^olpWT7OZsi?{$h5dcbUV!)DGI91QarPVd7QTmH5O5}{#L>kV!=<y+lfzM7w*ITcn}X0XOE(W$I-$Ow6Tb%h`CSW1o8Gcd>+r> zt9TaA;cNK1&)|3PLK!D_Pv<0la6e8K(mC65>>;w@oSsbyQ={Z(Qu;N(rDPa~<0L5| zmZigA$$EO~&5>1xx<~eL8ppt!j*<8-w_`{xZxv#pp4a~!_x}CA92V-K2SN}0k34{t z?dkRu9jKIP%g@GZ?HKi=)Oq7}lS29s8vJscq+gDcocf0$&0}QBG08wq3P~%}{_!6I Ss4$>EoWuMd=Klve|NjX Date: Tue, 4 Mar 2025 18:29:13 +0530 Subject: [PATCH 02/16] crud endpoints for the secrets and refactoring of the code --- ...ret_executor_se_key_60cc82_idx_and_more.py | 43 +++ executor/playbook_source_manager.py | 2 +- executor/secrets/__init__.py | 1 + executor/{ => secrets}/secret_resolver.py | 4 +- executor/secrets/urls.py | 10 + executor/secrets/views.py | 242 +++++++++++++ playbooks/base_settings.py | 3 + playbooks/urls.py | 1 + protos/secrets/api.proto | 81 +++++ protos/secrets/api_pb2.py | 47 +++ protos/secrets/api_pb2.pyi | 331 ++++++++++++++++++ requirements.txt | 1 + web/src/constants/api/secrets.ts | 6 + 13 files changed, 769 insertions(+), 3 deletions(-) create mode 100644 executor/migrations/0046_secret_secret_executor_se_key_60cc82_idx_and_more.py create mode 100644 executor/secrets/__init__.py rename executor/{ => secrets}/secret_resolver.py (99%) create mode 100644 executor/secrets/urls.py create mode 100644 executor/secrets/views.py create mode 100644 protos/secrets/api.proto create mode 100644 protos/secrets/api_pb2.py create mode 100644 protos/secrets/api_pb2.pyi create mode 100644 web/src/constants/api/secrets.ts diff --git a/executor/migrations/0046_secret_secret_executor_se_key_60cc82_idx_and_more.py b/executor/migrations/0046_secret_secret_executor_se_key_60cc82_idx_and_more.py new file mode 100644 index 000000000..6dada3601 --- /dev/null +++ b/executor/migrations/0046_secret_secret_executor_se_key_60cc82_idx_and_more.py @@ -0,0 +1,43 @@ +# Generated by Django 4.1.13 on 2025-03-04 12:00 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import encrypted_model_fields.fields +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('accounts', '0003_accountuseroauth2sessioncodestore'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('executor', '0045_upgrade_step_relation_conditions'), + ] + + operations = [ + migrations.CreateModel( + name='Secret', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(help_text='Human-readable name', max_length=255)), + ('key', models.CharField(help_text='Reference key for the secret', max_length=255)), + ('value', encrypted_model_fields.fields.EncryptedTextField()), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('is_active', models.BooleanField(default=True)), + ('description', models.TextField(blank=True)), + ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.account')), + ('creator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_secrets', to=settings.AUTH_USER_MODEL)), + ('last_updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_secrets', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.AddIndex( + model_name='secret', + index=models.Index(fields=['key', 'account', 'is_active'], name='executor_se_key_60cc82_idx'), + ), + migrations.AddConstraint( + model_name='secret', + constraint=models.UniqueConstraint(condition=models.Q(('is_active', True)), fields=('key', 'account', 'is_active'), name='unique_active_key_per_account'), + ), + ] diff --git a/executor/playbook_source_manager.py b/executor/playbook_source_manager.py index e5da8d0a3..8e61492f2 100644 --- a/executor/playbook_source_manager.py +++ b/executor/playbook_source_manager.py @@ -15,7 +15,7 @@ from protos.playbooks.source_task_definitions.lambda_function_task_pb2 import Lambda from protos.ui_definition_pb2 import FormField from utils.proto_utils import proto_to_dict, dict_to_proto -from executor.secret_resolver import SecretResolver +from executor.secrets.secret_resolver import SecretResolver def apply_result_transformer(result_dict, lambda_function: Lambda.Function) -> Dict: diff --git a/executor/secrets/__init__.py b/executor/secrets/__init__.py new file mode 100644 index 000000000..eb0a76e96 --- /dev/null +++ b/executor/secrets/__init__.py @@ -0,0 +1 @@ +# This file is intentionally left empty to make the directory a Python package \ No newline at end of file diff --git a/executor/secret_resolver.py b/executor/secrets/secret_resolver.py similarity index 99% rename from executor/secret_resolver.py rename to executor/secrets/secret_resolver.py index 94f6c2e17..5d74b5c90 100644 --- a/executor/secret_resolver.py +++ b/executor/secrets/secret_resolver.py @@ -27,12 +27,12 @@ def replace_secret_refs(cls, value: str, secret_map: Dict[str, str]) -> str: def resolve_secrets(cls, form_fields: [FormField], account_id: int, source_type_task_def: Dict) -> Dict: """ Resolves secret references (prefixed with !) in task definition - + Args: form_fields: List of form fields defining the task structure account_id: ID of the account owning the secrets source_type_task_def: Task definition dictionary to resolve secrets in - + Returns: Dict with secrets resolved """ diff --git a/executor/secrets/urls.py b/executor/secrets/urls.py new file mode 100644 index 000000000..46e148034 --- /dev/null +++ b/executor/secrets/urls.py @@ -0,0 +1,10 @@ +from django.urls import path +from executor.secrets import views + +urlpatterns = [ + path('list', views.secrets_list, name='secrets_list'), + path('get', views.secret_get, name='secret_get'), + path('create', views.secret_create, name='secret_create'), + path('update', views.secret_update, name='secret_update'), + path('delete', views.secret_delete, name='secret_delete'), +] \ No newline at end of file diff --git a/executor/secrets/views.py b/executor/secrets/views.py new file mode 100644 index 000000000..64b3443cd --- /dev/null +++ b/executor/secrets/views.py @@ -0,0 +1,242 @@ +import logging +from typing import Union +from datetime import timezone + +from django.http import HttpResponse +from google.protobuf.wrappers_pb2 import BoolValue, StringValue + +from accounts.models import Account, get_request_account, get_request_user +from executor.models import Secret +from playbooks.utils.decorators import web_api +from playbooks.utils.meta import get_meta +from playbooks.utils.queryset import filter_page +from protos.base_pb2 import Message +from protos.secrets.api_pb2 import ( + GetSecretsRequest, GetSecretsResponse, + GetSecretRequest, GetSecretResponse, + CreateSecretRequest, CreateSecretResponse, + UpdateSecretRequest, UpdateSecretResponse, + DeleteSecretRequest, DeleteSecretResponse, + Secret as SecretProto +) + +logger = logging.getLogger(__name__) + + +def _mask_secret_value(value): + """Mask the secret value, showing only the first and last characters""" + if not value or len(value) <= 8: + return "••••••••" + return value[:2] + "••••••" + value[-2:] + + +def _secret_to_proto(secret: Secret) -> SecretProto: + """Convert a Secret model to a Secret proto""" + return SecretProto( + id=StringValue(value=str(secret.id)), + name=StringValue(value=secret.name), + key=StringValue(value=secret.key), + masked_value=StringValue(value=_mask_secret_value(secret.value)), + description=StringValue(value=secret.description or ""), + creator=StringValue(value=secret.creator.email if secret.creator else ""), + last_updated_by=StringValue(value=secret.last_updated_by.email if secret.last_updated_by else ""), + created_at=int(secret.created_at.replace(tzinfo=timezone.utc).timestamp()) if secret.created_at else 0, + updated_at=int(secret.updated_at.replace(tzinfo=timezone.utc).timestamp()) if secret.updated_at else 0, + is_active=secret.is_active + ) + + +@web_api(GetSecretsRequest) +def secrets_list(request_message: GetSecretsRequest) -> Union[GetSecretsResponse, HttpResponse]: + """List all active secrets for the current account""" + account: Account = get_request_account() + + # Get all active secrets for this account + qs = Secret.objects.filter(account=account, is_active=True) + + # Apply pagination + total_count = qs.count() + page = request_message.meta.page + secrets = [_secret_to_proto(secret) for secret in filter_page(qs.order_by("-created_at"), page)] + + return GetSecretsResponse( + meta=get_meta(page=page, total_count=total_count), + success=BoolValue(value=True), + secrets=secrets + ) + + +@web_api(GetSecretRequest) +def secret_get(request_message: GetSecretRequest) -> Union[GetSecretResponse, HttpResponse]: + """Get a specific secret by ID""" + account: Account = get_request_account() + secret_id = request_message.secret_id.value + + if not secret_id: + return GetSecretResponse( + meta=get_meta(), + success=BoolValue(value=False), + message=Message(title="Invalid Request", description="Secret ID is required") + ) + + try: + secret = Secret.objects.get(id=secret_id, account=account, is_active=True) + return GetSecretResponse( + meta=get_meta(), + success=BoolValue(value=True), + secret=_secret_to_proto(secret) + ) + except Secret.DoesNotExist: + return GetSecretResponse( + meta=get_meta(), + success=BoolValue(value=False), + message=Message(title="Not Found", description="Secret not found") + ) + + +@web_api(CreateSecretRequest) +def secret_create(request_message: CreateSecretRequest) -> Union[CreateSecretResponse, HttpResponse]: + """Create a new secret""" + account: Account = get_request_account() + user = get_request_user() + + name = request_message.name.value + key = request_message.key.value + value = request_message.value.value + description = request_message.description.value + + # Validate required fields + if not name or not key or not value: + return CreateSecretResponse( + meta=get_meta(), + success=BoolValue(value=False), + message=Message(title="Invalid Request", description="Name, key, and value are required") + ) + + # Check if key already exists for this account + if Secret.objects.filter(account=account, key=key, is_active=True).exists(): + return CreateSecretResponse( + meta=get_meta(), + success=BoolValue(value=False), + message=Message(title="Duplicate Key", description=f"A secret with key '{key}' already exists") + ) + + # Create the secret + try: + secret = Secret.objects.create( + account=account, + name=name, + key=key, + value=value, + description=description, + creator=user, + last_updated_by=user, + is_active=True + ) + + return CreateSecretResponse( + meta=get_meta(), + success=BoolValue(value=True), + message=Message(title="Success", description="Secret created successfully"), + secret=_secret_to_proto(secret) + ) + except Exception as e: + logger.error(f"Error creating secret: {str(e)}") + return CreateSecretResponse( + meta=get_meta(), + success=BoolValue(value=False), + message=Message(title="Error", description="Failed to create secret") + ) + + +@web_api(UpdateSecretRequest) +def secret_update(request_message: UpdateSecretRequest) -> Union[UpdateSecretResponse, HttpResponse]: + """Update a secret's name or description (not the value)""" + account: Account = get_request_account() + user = get_request_user() + + secret_id = request_message.secret_id.value + name = request_message.name.value + description = request_message.description.value + + if not secret_id: + return UpdateSecretResponse( + meta=get_meta(), + success=BoolValue(value=False), + message=Message(title="Invalid Request", description="Secret ID is required") + ) + + try: + secret = Secret.objects.get(id=secret_id, account=account, is_active=True) + + # Update fields if provided + if name: + secret.name = name + if description is not None: # Allow empty description + secret.description = description + + secret.last_updated_by = user + secret.save() + + return UpdateSecretResponse( + meta=get_meta(), + success=BoolValue(value=True), + message=Message(title="Success", description="Secret updated successfully"), + secret=_secret_to_proto(secret) + ) + except Secret.DoesNotExist: + return UpdateSecretResponse( + meta=get_meta(), + success=BoolValue(value=False), + message=Message(title="Not Found", description="Secret not found") + ) + except Exception as e: + logger.error(f"Error updating secret: {str(e)}") + return UpdateSecretResponse( + meta=get_meta(), + success=BoolValue(value=False), + message=Message(title="Error", description="Failed to update secret") + ) + + +@web_api(DeleteSecretRequest) +def secret_delete(request_message: DeleteSecretRequest) -> Union[DeleteSecretResponse, HttpResponse]: + """Soft delete a secret by setting is_active to False""" + account: Account = get_request_account() + user = get_request_user() + + secret_id = request_message.secret_id.value + + if not secret_id: + return DeleteSecretResponse( + meta=get_meta(), + success=BoolValue(value=False), + message=Message(title="Invalid Request", description="Secret ID is required") + ) + + try: + secret = Secret.objects.get(id=secret_id, account=account, is_active=True) + + # Soft delete by setting is_active to False + secret.is_active = False + secret.last_updated_by = user + secret.save() + + return DeleteSecretResponse( + meta=get_meta(), + success=BoolValue(value=True), + message=Message(title="Success", description="Secret deleted successfully") + ) + except Secret.DoesNotExist: + return DeleteSecretResponse( + meta=get_meta(), + success=BoolValue(value=False), + message=Message(title="Not Found", description="Secret not found") + ) + except Exception as e: + logger.error(f"Error deleting secret: {str(e)}") + return DeleteSecretResponse( + meta=get_meta(), + success=BoolValue(value=False), + message=Message(title="Error", description="Failed to delete secret") + ) \ No newline at end of file diff --git a/playbooks/base_settings.py b/playbooks/base_settings.py index 6004d8944..3b348fc39 100644 --- a/playbooks/base_settings.py +++ b/playbooks/base_settings.py @@ -32,6 +32,9 @@ DEBUG = env.bool("DJANGO_DEBUG", default=True) DATA_UPLOAD_MAX_MEMORY_SIZE = 10485760 +# Set your own field encryption key +FIELD_ENCRYPTION_KEY = env.list("FIELD_ENCRYPTION_KEY", default=['Bb25dPKXrIRmwNG7u0FosUGUFQjcb8ER']) + ALLOWED_HOSTS = ['*'] # Application definition diff --git a/playbooks/urls.py b/playbooks/urls.py index ef4b29467..d155baf0d 100644 --- a/playbooks/urls.py +++ b/playbooks/urls.py @@ -32,4 +32,5 @@ path('media/', include('media.urls')), path('', include('django_prometheus.urls')), path('', views.index), + path('secrets/', include('executor.secrets.urls')), ] diff --git a/protos/secrets/api.proto b/protos/secrets/api.proto new file mode 100644 index 000000000..7cc57a70b --- /dev/null +++ b/protos/secrets/api.proto @@ -0,0 +1,81 @@ +syntax = "proto3"; +package protos.secrets; + +import "google/protobuf/wrappers.proto"; +import "protos/base.proto"; + +message Secret { + google.protobuf.StringValue id = 1; + google.protobuf.StringValue name = 2; + google.protobuf.StringValue key = 3; + google.protobuf.StringValue masked_value = 4; + google.protobuf.StringValue description = 5; + google.protobuf.StringValue creator = 6; + google.protobuf.StringValue last_updated_by = 7; + int64 created_at = 8; + int64 updated_at = 9; + bool is_active = 10; +} + +message GetSecretsRequest { + Meta meta = 1; +} + +message GetSecretsResponse { + Meta meta = 1; + google.protobuf.BoolValue success = 2; + Message message = 3; + repeated Secret secrets = 4; +} + +message GetSecretRequest { + Meta meta = 1; + google.protobuf.StringValue secret_id = 2; +} + +message GetSecretResponse { + Meta meta = 1; + google.protobuf.BoolValue success = 2; + Message message = 3; + Secret secret = 4; +} + +message CreateSecretRequest { + Meta meta = 1; + google.protobuf.StringValue name = 2; + google.protobuf.StringValue key = 3; + google.protobuf.StringValue value = 4; + google.protobuf.StringValue description = 5; +} + +message CreateSecretResponse { + Meta meta = 1; + google.protobuf.BoolValue success = 2; + Message message = 3; + Secret secret = 4; +} + +message UpdateSecretRequest { + Meta meta = 1; + google.protobuf.StringValue secret_id = 2; + google.protobuf.StringValue name = 3; + google.protobuf.StringValue description = 4; +} + +message UpdateSecretResponse { + Meta meta = 1; + google.protobuf.BoolValue success = 2; + Message message = 3; + Secret secret = 4; +} + +message DeleteSecretRequest { + Meta meta = 1; + google.protobuf.StringValue secret_id = 2; +} + +message DeleteSecretResponse { + Meta meta = 1; + google.protobuf.BoolValue success = 2; + Message message = 3; +} \ No newline at end of file diff --git a/protos/secrets/api_pb2.py b/protos/secrets/api_pb2.py new file mode 100644 index 000000000..4564f4978 --- /dev/null +++ b/protos/secrets/api_pb2.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: protos/secrets/api.proto +"""Generated protocol buffer code.""" +from google.protobuf.internal import builder as _builder +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import wrappers_pb2 as google_dot_protobuf_dot_wrappers__pb2 +from protos import base_pb2 as protos_dot_base__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18protos/secrets/api.proto\x12\x0eprotos.secrets\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x11protos/base.proto\"\x91\x03\n\x06Secret\x12(\n\x02id\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12*\n\x04name\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12)\n\x03key\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x32\n\x0cmasked_value\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12-\n\x07\x63reator\x18\x06 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x35\n\x0flast_updated_by\x18\x07 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x12\n\ncreated_at\x18\x08 \x01(\x03\x12\x12\n\nupdated_at\x18\t \x01(\x03\x12\x11\n\tis_active\x18\n \x01(\x08\"/\n\x11GetSecretsRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\"\xa8\x01\n\x12GetSecretsResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12\'\n\x07secrets\x18\x04 \x03(\x0b\x32\x16.protos.secrets.Secret\"_\n\x10GetSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa6\x01\n\x11GetSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"\xe8\x01\n\x13\x43reateSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12*\n\x04name\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12)\n\x03key\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12+\n\x05value\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa9\x01\n\x14\x43reateSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"\xc1\x01\n\x13UpdateSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12*\n\x04name\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa9\x01\n\x14UpdateSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"b\n\x13\x44\x65leteSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\x81\x01\n\x14\x44\x65leteSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Messageb\x06proto3') + +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'protos.secrets.api_pb2', globals()) +if _descriptor._USE_C_DESCRIPTORS == False: + + DESCRIPTOR._options = None + _SECRET._serialized_start=96 + _SECRET._serialized_end=497 + _GETSECRETSREQUEST._serialized_start=499 + _GETSECRETSREQUEST._serialized_end=546 + _GETSECRETSRESPONSE._serialized_start=549 + _GETSECRETSRESPONSE._serialized_end=717 + _GETSECRETREQUEST._serialized_start=719 + _GETSECRETREQUEST._serialized_end=814 + _GETSECRETRESPONSE._serialized_start=817 + _GETSECRETRESPONSE._serialized_end=983 + _CREATESECRETREQUEST._serialized_start=986 + _CREATESECRETREQUEST._serialized_end=1218 + _CREATESECRETRESPONSE._serialized_start=1221 + _CREATESECRETRESPONSE._serialized_end=1390 + _UPDATESECRETREQUEST._serialized_start=1393 + _UPDATESECRETREQUEST._serialized_end=1586 + _UPDATESECRETRESPONSE._serialized_start=1589 + _UPDATESECRETRESPONSE._serialized_end=1758 + _DELETESECRETREQUEST._serialized_start=1760 + _DELETESECRETREQUEST._serialized_end=1858 + _DELETESECRETRESPONSE._serialized_start=1861 + _DELETESECRETRESPONSE._serialized_end=1990 +# @@protoc_insertion_point(module_scope) diff --git a/protos/secrets/api_pb2.pyi b/protos/secrets/api_pb2.pyi new file mode 100644 index 000000000..4c01b1610 --- /dev/null +++ b/protos/secrets/api_pb2.pyi @@ -0,0 +1,331 @@ +""" +@generated by mypy-protobuf. Do not edit manually! +isort:skip_file +""" +import builtins +import collections.abc +import google.protobuf.descriptor +import google.protobuf.internal.containers +import google.protobuf.message +import google.protobuf.wrappers_pb2 +import protos.base_pb2 +import sys + +if sys.version_info >= (3, 8): + import typing as typing_extensions +else: + import typing_extensions + +DESCRIPTOR: google.protobuf.descriptor.FileDescriptor + +@typing_extensions.final +class Secret(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + ID_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int + KEY_FIELD_NUMBER: builtins.int + MASKED_VALUE_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + CREATOR_FIELD_NUMBER: builtins.int + LAST_UPDATED_BY_FIELD_NUMBER: builtins.int + CREATED_AT_FIELD_NUMBER: builtins.int + UPDATED_AT_FIELD_NUMBER: builtins.int + IS_ACTIVE_FIELD_NUMBER: builtins.int + @property + def id(self) -> google.protobuf.wrappers_pb2.StringValue: ... + @property + def name(self) -> google.protobuf.wrappers_pb2.StringValue: ... + @property + def key(self) -> google.protobuf.wrappers_pb2.StringValue: ... + @property + def masked_value(self) -> google.protobuf.wrappers_pb2.StringValue: ... + @property + def description(self) -> google.protobuf.wrappers_pb2.StringValue: ... + @property + def creator(self) -> google.protobuf.wrappers_pb2.StringValue: ... + @property + def last_updated_by(self) -> google.protobuf.wrappers_pb2.StringValue: ... + created_at: builtins.int + updated_at: builtins.int + is_active: builtins.bool + def __init__( + self, + *, + id: google.protobuf.wrappers_pb2.StringValue | None = ..., + name: google.protobuf.wrappers_pb2.StringValue | None = ..., + key: google.protobuf.wrappers_pb2.StringValue | None = ..., + masked_value: google.protobuf.wrappers_pb2.StringValue | None = ..., + description: google.protobuf.wrappers_pb2.StringValue | None = ..., + creator: google.protobuf.wrappers_pb2.StringValue | None = ..., + last_updated_by: google.protobuf.wrappers_pb2.StringValue | None = ..., + created_at: builtins.int = ..., + updated_at: builtins.int = ..., + is_active: builtins.bool = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["creator", b"creator", "description", b"description", "id", b"id", "key", b"key", "last_updated_by", b"last_updated_by", "masked_value", b"masked_value", "name", b"name"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["created_at", b"created_at", "creator", b"creator", "description", b"description", "id", b"id", "is_active", b"is_active", "key", b"key", "last_updated_by", b"last_updated_by", "masked_value", b"masked_value", "name", b"name", "updated_at", b"updated_at"]) -> None: ... + +global___Secret = Secret + +@typing_extensions.final +class GetSecretsRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + META_FIELD_NUMBER: builtins.int + @property + def meta(self) -> protos.base_pb2.Meta: ... + def __init__( + self, + *, + meta: protos.base_pb2.Meta | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["meta", b"meta"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta"]) -> None: ... + +global___GetSecretsRequest = GetSecretsRequest + +@typing_extensions.final +class GetSecretsResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + META_FIELD_NUMBER: builtins.int + SUCCESS_FIELD_NUMBER: builtins.int + MESSAGE_FIELD_NUMBER: builtins.int + SECRETS_FIELD_NUMBER: builtins.int + @property + def meta(self) -> protos.base_pb2.Meta: ... + @property + def success(self) -> google.protobuf.wrappers_pb2.BoolValue: ... + @property + def message(self) -> protos.base_pb2.Message: ... + @property + def secrets(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___Secret]: ... + def __init__( + self, + *, + meta: protos.base_pb2.Meta | None = ..., + success: google.protobuf.wrappers_pb2.BoolValue | None = ..., + message: protos.base_pb2.Message | None = ..., + secrets: collections.abc.Iterable[global___Secret] | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["message", b"message", "meta", b"meta", "success", b"success"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["message", b"message", "meta", b"meta", "secrets", b"secrets", "success", b"success"]) -> None: ... + +global___GetSecretsResponse = GetSecretsResponse + +@typing_extensions.final +class GetSecretRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + META_FIELD_NUMBER: builtins.int + SECRET_ID_FIELD_NUMBER: builtins.int + @property + def meta(self) -> protos.base_pb2.Meta: ... + @property + def secret_id(self) -> google.protobuf.wrappers_pb2.StringValue: ... + def __init__( + self, + *, + meta: protos.base_pb2.Meta | None = ..., + secret_id: google.protobuf.wrappers_pb2.StringValue | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["meta", b"meta", "secret_id", b"secret_id"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta", "secret_id", b"secret_id"]) -> None: ... + +global___GetSecretRequest = GetSecretRequest + +@typing_extensions.final +class GetSecretResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + META_FIELD_NUMBER: builtins.int + SUCCESS_FIELD_NUMBER: builtins.int + MESSAGE_FIELD_NUMBER: builtins.int + SECRET_FIELD_NUMBER: builtins.int + @property + def meta(self) -> protos.base_pb2.Meta: ... + @property + def success(self) -> google.protobuf.wrappers_pb2.BoolValue: ... + @property + def message(self) -> protos.base_pb2.Message: ... + @property + def secret(self) -> global___Secret: ... + def __init__( + self, + *, + meta: protos.base_pb2.Meta | None = ..., + success: google.protobuf.wrappers_pb2.BoolValue | None = ..., + message: protos.base_pb2.Message | None = ..., + secret: global___Secret | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["message", b"message", "meta", b"meta", "secret", b"secret", "success", b"success"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["message", b"message", "meta", b"meta", "secret", b"secret", "success", b"success"]) -> None: ... + +global___GetSecretResponse = GetSecretResponse + +@typing_extensions.final +class CreateSecretRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + META_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int + KEY_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + @property + def meta(self) -> protos.base_pb2.Meta: ... + @property + def name(self) -> google.protobuf.wrappers_pb2.StringValue: ... + @property + def key(self) -> google.protobuf.wrappers_pb2.StringValue: ... + @property + def value(self) -> google.protobuf.wrappers_pb2.StringValue: ... + @property + def description(self) -> google.protobuf.wrappers_pb2.StringValue: ... + def __init__( + self, + *, + meta: protos.base_pb2.Meta | None = ..., + name: google.protobuf.wrappers_pb2.StringValue | None = ..., + key: google.protobuf.wrappers_pb2.StringValue | None = ..., + value: google.protobuf.wrappers_pb2.StringValue | None = ..., + description: google.protobuf.wrappers_pb2.StringValue | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["description", b"description", "key", b"key", "meta", b"meta", "name", b"name", "value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "key", b"key", "meta", b"meta", "name", b"name", "value", b"value"]) -> None: ... + +global___CreateSecretRequest = CreateSecretRequest + +@typing_extensions.final +class CreateSecretResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + META_FIELD_NUMBER: builtins.int + SUCCESS_FIELD_NUMBER: builtins.int + MESSAGE_FIELD_NUMBER: builtins.int + SECRET_FIELD_NUMBER: builtins.int + @property + def meta(self) -> protos.base_pb2.Meta: ... + @property + def success(self) -> google.protobuf.wrappers_pb2.BoolValue: ... + @property + def message(self) -> protos.base_pb2.Message: ... + @property + def secret(self) -> global___Secret: ... + def __init__( + self, + *, + meta: protos.base_pb2.Meta | None = ..., + success: google.protobuf.wrappers_pb2.BoolValue | None = ..., + message: protos.base_pb2.Message | None = ..., + secret: global___Secret | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["message", b"message", "meta", b"meta", "secret", b"secret", "success", b"success"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["message", b"message", "meta", b"meta", "secret", b"secret", "success", b"success"]) -> None: ... + +global___CreateSecretResponse = CreateSecretResponse + +@typing_extensions.final +class UpdateSecretRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + META_FIELD_NUMBER: builtins.int + SECRET_ID_FIELD_NUMBER: builtins.int + NAME_FIELD_NUMBER: builtins.int + DESCRIPTION_FIELD_NUMBER: builtins.int + @property + def meta(self) -> protos.base_pb2.Meta: ... + @property + def secret_id(self) -> google.protobuf.wrappers_pb2.StringValue: ... + @property + def name(self) -> google.protobuf.wrappers_pb2.StringValue: ... + @property + def description(self) -> google.protobuf.wrappers_pb2.StringValue: ... + def __init__( + self, + *, + meta: protos.base_pb2.Meta | None = ..., + secret_id: google.protobuf.wrappers_pb2.StringValue | None = ..., + name: google.protobuf.wrappers_pb2.StringValue | None = ..., + description: google.protobuf.wrappers_pb2.StringValue | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["description", b"description", "meta", b"meta", "name", b"name", "secret_id", b"secret_id"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "meta", b"meta", "name", b"name", "secret_id", b"secret_id"]) -> None: ... + +global___UpdateSecretRequest = UpdateSecretRequest + +@typing_extensions.final +class UpdateSecretResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + META_FIELD_NUMBER: builtins.int + SUCCESS_FIELD_NUMBER: builtins.int + MESSAGE_FIELD_NUMBER: builtins.int + SECRET_FIELD_NUMBER: builtins.int + @property + def meta(self) -> protos.base_pb2.Meta: ... + @property + def success(self) -> google.protobuf.wrappers_pb2.BoolValue: ... + @property + def message(self) -> protos.base_pb2.Message: ... + @property + def secret(self) -> global___Secret: ... + def __init__( + self, + *, + meta: protos.base_pb2.Meta | None = ..., + success: google.protobuf.wrappers_pb2.BoolValue | None = ..., + message: protos.base_pb2.Message | None = ..., + secret: global___Secret | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["message", b"message", "meta", b"meta", "secret", b"secret", "success", b"success"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["message", b"message", "meta", b"meta", "secret", b"secret", "success", b"success"]) -> None: ... + +global___UpdateSecretResponse = UpdateSecretResponse + +@typing_extensions.final +class DeleteSecretRequest(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + META_FIELD_NUMBER: builtins.int + SECRET_ID_FIELD_NUMBER: builtins.int + @property + def meta(self) -> protos.base_pb2.Meta: ... + @property + def secret_id(self) -> google.protobuf.wrappers_pb2.StringValue: ... + def __init__( + self, + *, + meta: protos.base_pb2.Meta | None = ..., + secret_id: google.protobuf.wrappers_pb2.StringValue | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["meta", b"meta", "secret_id", b"secret_id"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta", "secret_id", b"secret_id"]) -> None: ... + +global___DeleteSecretRequest = DeleteSecretRequest + +@typing_extensions.final +class DeleteSecretResponse(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + META_FIELD_NUMBER: builtins.int + SUCCESS_FIELD_NUMBER: builtins.int + MESSAGE_FIELD_NUMBER: builtins.int + @property + def meta(self) -> protos.base_pb2.Meta: ... + @property + def success(self) -> google.protobuf.wrappers_pb2.BoolValue: ... + @property + def message(self) -> protos.base_pb2.Message: ... + def __init__( + self, + *, + meta: protos.base_pb2.Meta | None = ..., + success: google.protobuf.wrappers_pb2.BoolValue | None = ..., + message: protos.base_pb2.Message | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["message", b"message", "meta", b"meta", "success", b"success"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["message", b"message", "meta", b"meta", "success", b"success"]) -> None: ... + +global___DeleteSecretResponse = DeleteSecretResponse diff --git a/requirements.txt b/requirements.txt index 393550702..3d114a77c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -20,6 +20,7 @@ django-allauth==0.52.0 django-celery-beat==2.4.0 django-celery-email==3.0.0 django-cors-headers==3.13.0 +django-encrypted-model-fields==0.6.5 django-filter==22.1 django-prometheus==2.4.0.dev5 django-redis==5.2.0 diff --git a/web/src/constants/api/secrets.ts b/web/src/constants/api/secrets.ts new file mode 100644 index 000000000..8e07ae9b9 --- /dev/null +++ b/web/src/constants/api/secrets.ts @@ -0,0 +1,6 @@ +// Secrets management +export const SECRETS_LIST = "/secrets/list"; +export const SECRET_GET = "/secrets/get"; +export const SECRET_CREATE = "/secrets/create"; +export const SECRET_UPDATE = "/secrets/update"; +export const SECRET_DELETE = "/secrets/delete"; \ No newline at end of file From 1464039bee5b3f711f1c4ceb4001ebfe84a415b1 Mon Sep 17 00:00:00 2001 From: droid-dhruv Date: Tue, 4 Mar 2025 19:35:02 +0530 Subject: [PATCH 03/16] fixes to URLs and fernet key default --- executor/secrets/urls.py | 10 +++++----- executor/secrets/views.py | 6 ++++-- playbooks/base_settings.py | 2 +- playbooks/urls.py | 2 +- protos/secrets/api.proto | 1 + protos/secrets/api_pb2.py | 16 ++++++++-------- protos/secrets/api_pb2.pyi | 8 ++++++-- 7 files changed, 26 insertions(+), 19 deletions(-) diff --git a/executor/secrets/urls.py b/executor/secrets/urls.py index 46e148034..ef29683dd 100644 --- a/executor/secrets/urls.py +++ b/executor/secrets/urls.py @@ -2,9 +2,9 @@ from executor.secrets import views urlpatterns = [ - path('list', views.secrets_list, name='secrets_list'), - path('get', views.secret_get, name='secret_get'), - path('create', views.secret_create, name='secret_create'), - path('update', views.secret_update, name='secret_update'), - path('delete', views.secret_delete, name='secret_delete'), + path('list/', views.secrets_list, name='secrets_list'), + path('get/', views.secret_get, name='secret_get'), + path('create/', views.secret_create, name='secret_create'), + path('update/', views.secret_update, name='secret_update'), + path('delete/', views.secret_delete, name='secret_delete'), ] \ No newline at end of file diff --git a/executor/secrets/views.py b/executor/secrets/views.py index 64b3443cd..9721f1b45 100644 --- a/executor/secrets/views.py +++ b/executor/secrets/views.py @@ -158,7 +158,8 @@ def secret_update(request_message: UpdateSecretRequest) -> Union[UpdateSecretRes secret_id = request_message.secret_id.value name = request_message.name.value description = request_message.description.value - + key = request_message.key.value + if not secret_id: return UpdateSecretResponse( meta=get_meta(), @@ -174,7 +175,8 @@ def secret_update(request_message: UpdateSecretRequest) -> Union[UpdateSecretRes secret.name = name if description is not None: # Allow empty description secret.description = description - + if key: + secret.key = key secret.last_updated_by = user secret.save() diff --git a/playbooks/base_settings.py b/playbooks/base_settings.py index 3b348fc39..fc7235cec 100644 --- a/playbooks/base_settings.py +++ b/playbooks/base_settings.py @@ -33,7 +33,7 @@ DATA_UPLOAD_MAX_MEMORY_SIZE = 10485760 # Set your own field encryption key -FIELD_ENCRYPTION_KEY = env.list("FIELD_ENCRYPTION_KEY", default=['Bb25dPKXrIRmwNG7u0FosUGUFQjcb8ER']) +FIELD_ENCRYPTION_KEY = env.list("FIELD_ENCRYPTION_KEY", default=['sY2EtYEgvPZk7Fu3pgl-BoVLN4hoXYmpJQdJiNNRlRE=']) ALLOWED_HOSTS = ['*'] diff --git a/playbooks/urls.py b/playbooks/urls.py index d155baf0d..636d1c788 100644 --- a/playbooks/urls.py +++ b/playbooks/urls.py @@ -30,7 +30,7 @@ path('pb/', include('executor.urls')), path('management/', include('management.urls')), path('media/', include('media.urls')), + path('secrets/', include('executor.secrets.urls')), path('', include('django_prometheus.urls')), path('', views.index), - path('secrets/', include('executor.secrets.urls')), ] diff --git a/protos/secrets/api.proto b/protos/secrets/api.proto index 7cc57a70b..148a0db5f 100644 --- a/protos/secrets/api.proto +++ b/protos/secrets/api.proto @@ -60,6 +60,7 @@ message UpdateSecretRequest { google.protobuf.StringValue secret_id = 2; google.protobuf.StringValue name = 3; google.protobuf.StringValue description = 4; + google.protobuf.StringValue key = 5; } message UpdateSecretResponse { diff --git a/protos/secrets/api_pb2.py b/protos/secrets/api_pb2.py index 4564f4978..6c9229a97 100644 --- a/protos/secrets/api_pb2.py +++ b/protos/secrets/api_pb2.py @@ -15,7 +15,7 @@ from protos import base_pb2 as protos_dot_base__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18protos/secrets/api.proto\x12\x0eprotos.secrets\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x11protos/base.proto\"\x91\x03\n\x06Secret\x12(\n\x02id\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12*\n\x04name\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12)\n\x03key\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x32\n\x0cmasked_value\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12-\n\x07\x63reator\x18\x06 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x35\n\x0flast_updated_by\x18\x07 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x12\n\ncreated_at\x18\x08 \x01(\x03\x12\x12\n\nupdated_at\x18\t \x01(\x03\x12\x11\n\tis_active\x18\n \x01(\x08\"/\n\x11GetSecretsRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\"\xa8\x01\n\x12GetSecretsResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12\'\n\x07secrets\x18\x04 \x03(\x0b\x32\x16.protos.secrets.Secret\"_\n\x10GetSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa6\x01\n\x11GetSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"\xe8\x01\n\x13\x43reateSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12*\n\x04name\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12)\n\x03key\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12+\n\x05value\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa9\x01\n\x14\x43reateSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"\xc1\x01\n\x13UpdateSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12*\n\x04name\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa9\x01\n\x14UpdateSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"b\n\x13\x44\x65leteSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\x81\x01\n\x14\x44\x65leteSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Messageb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18protos/secrets/api.proto\x12\x0eprotos.secrets\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x11protos/base.proto\"\x91\x03\n\x06Secret\x12(\n\x02id\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12*\n\x04name\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12)\n\x03key\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x32\n\x0cmasked_value\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12-\n\x07\x63reator\x18\x06 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x35\n\x0flast_updated_by\x18\x07 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x12\n\ncreated_at\x18\x08 \x01(\x03\x12\x12\n\nupdated_at\x18\t \x01(\x03\x12\x11\n\tis_active\x18\n \x01(\x08\"/\n\x11GetSecretsRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\"\xa8\x01\n\x12GetSecretsResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12\'\n\x07secrets\x18\x04 \x03(\x0b\x32\x16.protos.secrets.Secret\"_\n\x10GetSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa6\x01\n\x11GetSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"\xe8\x01\n\x13\x43reateSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12*\n\x04name\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12)\n\x03key\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12+\n\x05value\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa9\x01\n\x14\x43reateSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"\xec\x01\n\x13UpdateSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12*\n\x04name\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12)\n\x03key\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa9\x01\n\x14UpdateSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"b\n\x13\x44\x65leteSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\x81\x01\n\x14\x44\x65leteSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Messageb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'protos.secrets.api_pb2', globals()) @@ -37,11 +37,11 @@ _CREATESECRETRESPONSE._serialized_start=1221 _CREATESECRETRESPONSE._serialized_end=1390 _UPDATESECRETREQUEST._serialized_start=1393 - _UPDATESECRETREQUEST._serialized_end=1586 - _UPDATESECRETRESPONSE._serialized_start=1589 - _UPDATESECRETRESPONSE._serialized_end=1758 - _DELETESECRETREQUEST._serialized_start=1760 - _DELETESECRETREQUEST._serialized_end=1858 - _DELETESECRETRESPONSE._serialized_start=1861 - _DELETESECRETRESPONSE._serialized_end=1990 + _UPDATESECRETREQUEST._serialized_end=1629 + _UPDATESECRETRESPONSE._serialized_start=1632 + _UPDATESECRETRESPONSE._serialized_end=1801 + _DELETESECRETREQUEST._serialized_start=1803 + _DELETESECRETREQUEST._serialized_end=1901 + _DELETESECRETRESPONSE._serialized_start=1904 + _DELETESECRETRESPONSE._serialized_end=2033 # @@protoc_insertion_point(module_scope) diff --git a/protos/secrets/api_pb2.pyi b/protos/secrets/api_pb2.pyi index 4c01b1610..7a407d477 100644 --- a/protos/secrets/api_pb2.pyi +++ b/protos/secrets/api_pb2.pyi @@ -234,6 +234,7 @@ class UpdateSecretRequest(google.protobuf.message.Message): SECRET_ID_FIELD_NUMBER: builtins.int NAME_FIELD_NUMBER: builtins.int DESCRIPTION_FIELD_NUMBER: builtins.int + KEY_FIELD_NUMBER: builtins.int @property def meta(self) -> protos.base_pb2.Meta: ... @property @@ -242,6 +243,8 @@ class UpdateSecretRequest(google.protobuf.message.Message): def name(self) -> google.protobuf.wrappers_pb2.StringValue: ... @property def description(self) -> google.protobuf.wrappers_pb2.StringValue: ... + @property + def key(self) -> google.protobuf.wrappers_pb2.StringValue: ... def __init__( self, *, @@ -249,9 +252,10 @@ class UpdateSecretRequest(google.protobuf.message.Message): secret_id: google.protobuf.wrappers_pb2.StringValue | None = ..., name: google.protobuf.wrappers_pb2.StringValue | None = ..., description: google.protobuf.wrappers_pb2.StringValue | None = ..., + key: google.protobuf.wrappers_pb2.StringValue | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["description", b"description", "meta", b"meta", "name", b"name", "secret_id", b"secret_id"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "meta", b"meta", "name", b"name", "secret_id", b"secret_id"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["description", b"description", "key", b"key", "meta", b"meta", "name", b"name", "secret_id", b"secret_id"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "key", b"key", "meta", b"meta", "name", b"name", "secret_id", b"secret_id"]) -> None: ... global___UpdateSecretRequest = UpdateSecretRequest From 268c9cccb191779a8432318909b22555748f61a9 Mon Sep 17 00:00:00 2001 From: droid-dhruv Date: Tue, 4 Mar 2025 19:51:50 +0530 Subject: [PATCH 04/16] remove potentially sensitive log --- executor/secrets/secret_resolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/executor/secrets/secret_resolver.py b/executor/secrets/secret_resolver.py index 5d74b5c90..7e9dde288 100644 --- a/executor/secrets/secret_resolver.py +++ b/executor/secrets/secret_resolver.py @@ -61,7 +61,7 @@ def resolve_secrets(cls, form_fields: [FormField], account_id: int, source_type_ ).values('key', 'value') if not secrets: - logger.warning(f"No secrets found for references: {secret_refs}") + logger.warning(f"No secrets found for given references") return source_type_task_def # Create mapping of secret keys to values From 13fdfcfa315e3990ae031c0c8dcb579cb3788d28 Mon Sep 17 00:00:00 2001 From: jayeshsadhwani99 Date: Wed, 5 Mar 2025 13:34:53 +0530 Subject: [PATCH 05/16] adds basic page+redux setup --- web/src/components.ts | 1 + .../components/Inputs/HandleInputRender.tsx | 3 + .../Inputs/InputTypes/SearchInput.tsx | 25 +++ web/src/components/Sidebar/index.tsx | 11 +- .../create/CreateSecretButton.tsx | 32 +++ .../create/CreateSecretForm.tsx | 183 ++++++++++++++++++ .../create/SecretCreateOverlay.tsx | 32 +++ web/src/constants/api/index.ts | 1 + web/src/pageKeys.ts | 2 + .../pages/secret-management/hooks/index.ts | 1 + .../hooks/useSecretsData.tsx | 60 ++++++ web/src/pages/secret-management/index.tsx | 79 ++++++++ .../pages/secret-management/utils/index.ts | 1 + .../secret-management/utils/secretsColumns.ts | 5 + web/src/routes.ts | 1 + web/src/store/app/apiSlice.ts | 1 + .../store/features/secrets/actions/index.ts | 2 + .../secrets/actions/resetSecretStateAction.ts | 8 + .../secrets/actions/setSecretKeyAction.ts | 16 ++ .../features/secrets/api/getSecretsListApi.ts | 23 +++ web/src/store/features/secrets/api/index.ts | 1 + .../store/features/secrets/initialState.ts | 20 ++ .../store/features/secrets/secretsSlice.ts | 18 ++ .../store/features/secrets/selectors/index.ts | 1 + .../secrets/selectors/secretsSelector.ts | 3 + web/src/store/index.ts | 2 + web/src/types/inputs/inputTypes.ts | 1 + 27 files changed, 532 insertions(+), 1 deletion(-) create mode 100644 web/src/components/Inputs/InputTypes/SearchInput.tsx create mode 100644 web/src/components/secret-management/create/CreateSecretButton.tsx create mode 100644 web/src/components/secret-management/create/CreateSecretForm.tsx create mode 100644 web/src/components/secret-management/create/SecretCreateOverlay.tsx create mode 100644 web/src/pages/secret-management/hooks/index.ts create mode 100644 web/src/pages/secret-management/hooks/useSecretsData.tsx create mode 100644 web/src/pages/secret-management/index.tsx create mode 100644 web/src/pages/secret-management/utils/index.ts create mode 100644 web/src/pages/secret-management/utils/secretsColumns.ts create mode 100644 web/src/store/features/secrets/actions/index.ts create mode 100644 web/src/store/features/secrets/actions/resetSecretStateAction.ts create mode 100644 web/src/store/features/secrets/actions/setSecretKeyAction.ts create mode 100644 web/src/store/features/secrets/api/getSecretsListApi.ts create mode 100644 web/src/store/features/secrets/api/index.ts create mode 100644 web/src/store/features/secrets/initialState.ts create mode 100644 web/src/store/features/secrets/secretsSlice.ts create mode 100644 web/src/store/features/secrets/selectors/index.ts create mode 100644 web/src/store/features/secrets/selectors/secretsSelector.ts diff --git a/web/src/components.ts b/web/src/components.ts index 45c18436d..b27008241 100644 --- a/web/src/components.ts +++ b/web/src/components.ts @@ -42,4 +42,5 @@ export const components = { import("./pages/dynamicAlerts/CreateDynamicAlert"), [PageKeys.DYNAMIC_ALERT_VIEW]: () => import("./pages/dynamicAlerts/CreateDynamicAlert"), + [PageKeys.SECRET_MANAGEMENT]: () => import("./pages/secret-management"), }; diff --git a/web/src/components/Inputs/HandleInputRender.tsx b/web/src/components/Inputs/HandleInputRender.tsx index 7144ca69e..99c5e779a 100644 --- a/web/src/components/Inputs/HandleInputRender.tsx +++ b/web/src/components/Inputs/HandleInputRender.tsx @@ -15,6 +15,7 @@ import TypingDropdownMultipleSelectionInput from "./InputTypes/TypingDropdownMul import TextButton from "./InputTypes/TextButton.tsx"; import CronInput from "../common/CronInput/index.tsx"; import Checkbox from "../common/Checkbox/index.tsx"; +import SearchInput from "./InputTypes/SearchInput.tsx"; export type HandleInputRenderType = { inputType: InputType; @@ -48,6 +49,8 @@ function HandleInputRender({ inputType, ...props }: HandleInputRenderType) { switch (inputType) { case InputTypes.TEXT: return ; + case InputTypes.SEARCH: + return ; case InputTypes.MULTILINE: return ; case InputTypes.BUTTON: diff --git a/web/src/components/Inputs/InputTypes/SearchInput.tsx b/web/src/components/Inputs/InputTypes/SearchInput.tsx new file mode 100644 index 000000000..733038ea8 --- /dev/null +++ b/web/src/components/Inputs/InputTypes/SearchInput.tsx @@ -0,0 +1,25 @@ +import { SearchRounded } from "@mui/icons-material"; +import CustomInput from "../CustomInput.tsx"; +import { InputTypes } from "../../../types/index.ts"; +import { HandleInputRenderType } from "../HandleInputRender"; + +function SearchInput(props: Omit) { + return ( +
+
+ +
+ +
+ ); +} + +export default SearchInput; diff --git a/web/src/components/Sidebar/index.tsx b/web/src/components/Sidebar/index.tsx index 4b9456220..9659bbc4a 100644 --- a/web/src/components/Sidebar/index.tsx +++ b/web/src/components/Sidebar/index.tsx @@ -4,7 +4,11 @@ import useToggle from "../../hooks/common/useToggle"; import SlackConnectOverlay from "../SlackConnectOverlay"; import { elements } from "./utils"; import SidebarElement from "./SidebarElement"; -import { LogoutRounded, SettingsRounded } from "@mui/icons-material"; +import { + LogoutRounded, + SecurityRounded, + SettingsRounded, +} from "@mui/icons-material"; import SidebarButtonElement from "./SidebarButtonElement"; import HeadElement from "./HeadElement"; import useSidebar from "../../hooks/common/sidebar/useSidebar"; @@ -36,6 +40,11 @@ function Sidebar() {
+ } + /> { + toggle(); + }; + + return ( +
+ + {buttonText} + + + +
+ ); +} + +export default CreateSecretButton; diff --git a/web/src/components/secret-management/create/CreateSecretForm.tsx b/web/src/components/secret-management/create/CreateSecretForm.tsx new file mode 100644 index 000000000..e8d36fd60 --- /dev/null +++ b/web/src/components/secret-management/create/CreateSecretForm.tsx @@ -0,0 +1,183 @@ +import CustomInput from "../../Inputs/CustomInput"; +import { InputTypes } from "../../../types"; +import { useDispatch, useSelector } from "react-redux"; +import CustomButton from "../../common/CustomButton"; +import { showSnackbar } from "../../../store/features/snackbar/snackbarSlice"; +import { CircularProgress } from "@mui/material"; +import React, { useEffect } from "react"; +// import { +// useCreateVariableMutation, +// useGetVariableApiQuery, +// useUpdateVariableMutation, +// } from "../../../store/features/variables/api"; +import TableLoader from "../../Skeleton/TableLoader"; +import { secretsSelector } from "../../../store/features/secrets/selectors"; +import { + resetSecretState, + setSecretKey, +} from "../../../store/features/secrets/secretsSlice"; +import { SecretsInitialState } from "../../../store/features/secrets/initialState"; + +type CreateSecretFormProps = { + toggleOverlay?: () => void; + id?: string; +}; + +function CreateSecretForm({ + toggleOverlay = () => {}, + id, +}: CreateSecretFormProps) { + const dispatch = useDispatch(); + // const [triggerCreate, { isLoading }] = useCreateVariableMutation(); + // const [triggerUpdate, { isLoading: updateLoading }] = + // useUpdateVariableMutation(); + const { name, description, options } = useSelector(secretsSelector); + // const { isLoading: isLoadingVariable } = useGetVariableApiQuery( + // { + // id: id!, + // }, + // { + // skip: !id, + // }, + // ); + + const handleChange = (key: keyof SecretsInitialState, value: any) => { + dispatch( + setSecretKey({ + key, + value, + }), + ); + }; + + const validate = () => { + let error = ""; + if (!name || !description || !options) { + error = "Please fill all fields"; + } + + if (error) { + dispatch(showSnackbar({ message: error, type: "error" })); + } + return error; + }; + + const handleCreate = async ( + e: React.MouseEvent, + ) => { + e.preventDefault(); + if (validate()) return; + + try { + // await triggerCreate({ + // name, + // description, + // options: options.split(", "), + // }).unwrap(); + dispatch(resetSecretState()); + toggleOverlay(); + } catch (e: any) { + dispatch( + showSnackbar({ + message: `Failed to create variable: ${JSON.stringify( + e?.message ?? e, + )}`, + type: "error", + }), + ); + } + }; + + const handleUpdate = async ( + e: React.MouseEvent, + ) => { + e.preventDefault(); + if (validate()) return; + + try { + // await triggerUpdate({ + // id: id!, + // name, + // description, + // options: options.split(", "), + // }).unwrap(); + dispatch(resetSecretState()); + toggleOverlay(); + } catch (e: any) { + dispatch( + showSnackbar({ + message: `Failed to update variable: ${JSON.stringify( + e?.message ?? e, + )}`, + type: "error", + }), + ); + } + }; + + useEffect(() => { + return () => { + dispatch(resetSecretState()); + }; + }, [dispatch]); + + // if (isLoadingVariable) { + // return ( + //
+ // + //
+ // ); + // } + + return ( +
+ handleChange("name", value)} + containerClassName="!w-full" + className="w-full" + /> + handleChange("description", value)} + containerClassName="!w-full" + className="w-full !h-20" + /> + handleChange("options", value)} + containerClassName="!w-full" + className="w-full" + /> +
+ {id ? ( + + Update + + ) : ( + + Save + + )} + {/* {(isLoading || updateLoading) && } */} +
+ + ); +} + +export default CreateSecretForm; diff --git a/web/src/components/secret-management/create/SecretCreateOverlay.tsx b/web/src/components/secret-management/create/SecretCreateOverlay.tsx new file mode 100644 index 000000000..cd90dba25 --- /dev/null +++ b/web/src/components/secret-management/create/SecretCreateOverlay.tsx @@ -0,0 +1,32 @@ +import { CloseRounded } from "@mui/icons-material"; +import Overlay from "../../Overlay"; +import CreateSecretForm from "./CreateSecretForm"; + +type SecretCreateOverlayProps = { + isOpen: boolean; + toggleOverlay: () => void; + id?: string; +}; + +const SecretCreateOverlay = ({ + isOpen, + toggleOverlay, + id, +}: SecretCreateOverlayProps) => { + return ( + <> + {isOpen && ( + +
+
+ +
+ +
+
+ )} + + ); +}; + +export default SecretCreateOverlay; diff --git a/web/src/constants/api/index.ts b/web/src/constants/api/index.ts index 809729aa3..2c95e14c8 100644 --- a/web/src/constants/api/index.ts +++ b/web/src/constants/api/index.ts @@ -11,3 +11,4 @@ export * from "./templates.ts"; export * from "./management.ts"; export * from "./search.ts"; export * from "./dynamicAlerts"; +export * from "./secrets.ts"; diff --git a/web/src/pageKeys.ts b/web/src/pageKeys.ts index 2cf50fadc..63d098164 100644 --- a/web/src/pageKeys.ts +++ b/web/src/pageKeys.ts @@ -30,4 +30,6 @@ export enum PageKeys { DYNAMIC_ALERTS = "DYNAMIC_ALERTS", CREATE_DYNAMIC_ALERTS = "CREATE_DYNAMIC_ALERTS", DYNAMIC_ALERT_VIEW = "DYNAMIC_ALERT", + + SECRET_MANAGEMENT = "SECRET_MANAGEMENT", } diff --git a/web/src/pages/secret-management/hooks/index.ts b/web/src/pages/secret-management/hooks/index.ts new file mode 100644 index 000000000..176a1ffa5 --- /dev/null +++ b/web/src/pages/secret-management/hooks/index.ts @@ -0,0 +1 @@ +export * from "./useSecretsData"; diff --git a/web/src/pages/secret-management/hooks/useSecretsData.tsx b/web/src/pages/secret-management/hooks/useSecretsData.tsx new file mode 100644 index 000000000..bbca8bf7b --- /dev/null +++ b/web/src/pages/secret-management/hooks/useSecretsData.tsx @@ -0,0 +1,60 @@ +import { renderTimestamp } from "../../../utils/common/dateUtils"; +import { DeleteRounded } from "@mui/icons-material"; +import useToggle from "../../../hooks/common/useToggle"; +import { useState } from "react"; + +export const useSecretsData = (data: any[]) => { + const { isOpen: isActionOpen, toggle } = useToggle(); + const { isOpen: isConfigOpen, toggle: toggleConfig } = useToggle(); + const [selectedSecret, setSelectedSecret] = useState({}); + const [selectedId, setSelectedId] = useState(""); + + const handleDeleteSecret = (variable: any) => { + setSelectedSecret(variable); + toggle(); + }; + + const handleUpdateVariable = (variable: any) => { + setSelectedId(variable.id); + toggleConfig(); + }; + + const actions = [ + { + icon: , + label: "Delete", + action: (item: any) => handleDeleteSecret(item), + tooltip: "Remove this Secret", + }, + ]; + + const rows = data?.map((item: any) => ({ + ...item, + name: ( +

handleUpdateVariable(item)} + className={ + "text-violet-500 dark:text-purple-400 hover:text-violet-800 dark:hover:text-purple-300" + }> + {item.key} +

+ ), + createdAt: ( +

+ {renderTimestamp(item.created_at)} +

+ ), + createdBy:

{item.created_by}

, + })); + + return { + rows, + actions, + selectedSecret, + isActionOpen, + toggle, + isConfigOpen, + toggleConfig, + selectedId, + }; +}; diff --git a/web/src/pages/secret-management/index.tsx b/web/src/pages/secret-management/index.tsx new file mode 100644 index 000000000..e3ef22096 --- /dev/null +++ b/web/src/pages/secret-management/index.tsx @@ -0,0 +1,79 @@ +import Heading from "../../components/Heading"; +import { useState } from "react"; +import useDebounce from "../../hooks/common/useDebounce"; +import { useGetSecretsListQuery } from "../../store/features/secrets/api"; +import usePaginationComponent from "../../hooks/common/usePaginationComponent"; +import CustomInput from "../../components/Inputs/CustomInput"; +import { InputTypes } from "../../types"; +import SuspenseLoader from "../../components/Skeleton/SuspenseLoader"; +import TableSkeleton from "../../components/Skeleton/TableLoader"; +import PaginatedTable from "../../components/common/Table/PaginatedTable"; +import { secretsColumns } from "./utils"; +import { useSecretsData } from "./hooks"; +import CreateSecretButton from "../../components/secret-management/create/CreateSecretButton"; + +function SecretManagement() { + const [query, setQuery] = useState(""); + const debouncedQuery = useDebounce(query, 500); + const { data, isFetching, refetch } = useGetSecretsListQuery({ + key: debouncedQuery, + }); + const secretsList = data?.secrets ?? []; + const total = data?.meta?.total_count ?? 0; + const { + rows, + actions, + isActionOpen, + selectedSecret, + toggle, + isConfigOpen, + toggleConfig, + selectedId, + } = useSecretsData(secretsList ?? []); + usePaginationComponent(refetch); + + return ( + <> + +
+
+ +
+ + }> + + +
+ + {/* {isActionOpen && ( + + )} + {isConfigOpen && ( + + )} */} + + ); +} + +export default SecretManagement; diff --git a/web/src/pages/secret-management/utils/index.ts b/web/src/pages/secret-management/utils/index.ts new file mode 100644 index 000000000..8ef0e3cb6 --- /dev/null +++ b/web/src/pages/secret-management/utils/index.ts @@ -0,0 +1 @@ +export * from "./secretsColumns"; diff --git a/web/src/pages/secret-management/utils/secretsColumns.ts b/web/src/pages/secret-management/utils/secretsColumns.ts new file mode 100644 index 000000000..a183cf773 --- /dev/null +++ b/web/src/pages/secret-management/utils/secretsColumns.ts @@ -0,0 +1,5 @@ +export const secretsColumns = [ + { header: "Name", key: "name", isMain: true }, + { header: "Created At", key: "createdAt" }, + { header: "Created By", key: "createdBy" }, +]; diff --git a/web/src/routes.ts b/web/src/routes.ts index 7eab6f37d..36012994b 100644 --- a/web/src/routes.ts +++ b/web/src/routes.ts @@ -31,5 +31,6 @@ export const routes = { [PageKeys.INVITE_TEAM]: "/settings/invite-team", [PageKeys.SUPPORT]: "/support", [PageKeys.DYNAMIC_ALERT_VIEW]: "/dynamic-alerts/:alert_id", + [PageKeys.SECRET_MANAGEMENT]: "/secret-management", [PageKeys.NOT_FOUND]: "*", }; diff --git a/web/src/store/app/apiSlice.ts b/web/src/store/app/apiSlice.ts index 26d65a320..e1adbadff 100644 --- a/web/src/store/app/apiSlice.ts +++ b/web/src/store/app/apiSlice.ts @@ -12,5 +12,6 @@ export const apiSlice = createApi({ "Workflows", "Templates", "SiteURL", + "Secrets", ], }); diff --git a/web/src/store/features/secrets/actions/index.ts b/web/src/store/features/secrets/actions/index.ts new file mode 100644 index 000000000..24a8771c1 --- /dev/null +++ b/web/src/store/features/secrets/actions/index.ts @@ -0,0 +1,2 @@ +export * from "./resetSecretStateAction"; +export * from "./setSecretKeyAction"; diff --git a/web/src/store/features/secrets/actions/resetSecretStateAction.ts b/web/src/store/features/secrets/actions/resetSecretStateAction.ts new file mode 100644 index 000000000..929b5ea44 --- /dev/null +++ b/web/src/store/features/secrets/actions/resetSecretStateAction.ts @@ -0,0 +1,8 @@ +import { secretsInitialState, SecretsInitialState } from "../initialState"; + +export const resetSecretStateAction = (state: SecretsInitialState) => { + state.name = secretsInitialState.name; + state.description = secretsInitialState.name; + state.options = secretsInitialState.options; + state.type = secretsInitialState.type; +}; diff --git a/web/src/store/features/secrets/actions/setSecretKeyAction.ts b/web/src/store/features/secrets/actions/setSecretKeyAction.ts new file mode 100644 index 000000000..87c3abb00 --- /dev/null +++ b/web/src/store/features/secrets/actions/setSecretKeyAction.ts @@ -0,0 +1,16 @@ +import { PayloadAction } from "@reduxjs/toolkit"; +import { secretsInitialState, SecretsInitialState } from "../initialState"; + +type PayloadType = { + key: keyof SecretsInitialState; + value: (typeof secretsInitialState)[keyof SecretsInitialState]; +}; + +export const setSecretKeyAction = ( + state: SecretsInitialState, + { payload }: PayloadAction, +) => { + const { key, value } = payload; + + state[key] = value as any; +}; diff --git a/web/src/store/features/secrets/api/getSecretsListApi.ts b/web/src/store/features/secrets/api/getSecretsListApi.ts new file mode 100644 index 000000000..4777b550f --- /dev/null +++ b/web/src/store/features/secrets/api/getSecretsListApi.ts @@ -0,0 +1,23 @@ +import { SECRETS_LIST } from "../../../../constants/index.ts"; +import { apiSlice } from "../../../app/apiSlice.ts"; + +export const getSecretsListApi = apiSlice.injectEndpoints({ + endpoints: (builder) => ({ + getSecretsList: builder.query< + any, + { + meta?: any; + key?: string; + } + >({ + query: () => ({ + url: SECRETS_LIST, + method: "POST", + body: {}, + }), + providesTags: ["Secrets"], + }), + }), +}); + +export const { useGetSecretsListQuery } = getSecretsListApi; diff --git a/web/src/store/features/secrets/api/index.ts b/web/src/store/features/secrets/api/index.ts new file mode 100644 index 000000000..75fe7187c --- /dev/null +++ b/web/src/store/features/secrets/api/index.ts @@ -0,0 +1 @@ +export * from "./getSecretsListApi"; diff --git a/web/src/store/features/secrets/initialState.ts b/web/src/store/features/secrets/initialState.ts new file mode 100644 index 000000000..454689c7d --- /dev/null +++ b/web/src/store/features/secrets/initialState.ts @@ -0,0 +1,20 @@ +export const VariableOption = { + DROPDOWN: "DROPDOWN", +} as const; + +export type VariableOptionType = + (typeof VariableOption)[keyof typeof VariableOption]; + +export type SecretsInitialState = { + name: string; + description: string; + options: string; + type: VariableOptionType; +}; + +export const secretsInitialState: SecretsInitialState = { + name: "", + description: "", + options: "", + type: VariableOption.DROPDOWN, +}; diff --git a/web/src/store/features/secrets/secretsSlice.ts b/web/src/store/features/secrets/secretsSlice.ts new file mode 100644 index 000000000..9d02b595a --- /dev/null +++ b/web/src/store/features/secrets/secretsSlice.ts @@ -0,0 +1,18 @@ +import { createSlice } from "@reduxjs/toolkit"; +import { secretsInitialState } from "./initialState"; +import * as Actions from "./actions"; + +export * from "./selectors"; + +const secretsSlice = createSlice({ + name: "secrets", + initialState: secretsInitialState, + reducers: { + resetSecretState: Actions.resetSecretStateAction, + setSecretKey: Actions.setSecretKeyAction, + }, +}); + +export const { resetSecretState, setSecretKey } = secretsSlice.actions; + +export default secretsSlice.reducer; diff --git a/web/src/store/features/secrets/selectors/index.ts b/web/src/store/features/secrets/selectors/index.ts new file mode 100644 index 000000000..b876e4388 --- /dev/null +++ b/web/src/store/features/secrets/selectors/index.ts @@ -0,0 +1 @@ +export * from "./secretsSelector"; diff --git a/web/src/store/features/secrets/selectors/secretsSelector.ts b/web/src/store/features/secrets/selectors/secretsSelector.ts new file mode 100644 index 000000000..ec0b0bebe --- /dev/null +++ b/web/src/store/features/secrets/selectors/secretsSelector.ts @@ -0,0 +1,3 @@ +import { RootState } from "../../.."; + +export const secretsSelector = (state: RootState) => state.secrets; diff --git a/web/src/store/index.ts b/web/src/store/index.ts index 7525a1bf6..f6a3d65f6 100644 --- a/web/src/store/index.ts +++ b/web/src/store/index.ts @@ -14,6 +14,7 @@ import paginationSlice from "./features/pagination/paginationSlice.ts"; import commonSlice from "./features/common/commonSlice.ts"; import dynamicAlertsSlice from "./features/dynamicAlerts/dynamicAlertsSlice.ts"; import sidebarSlice from "./features/sidebar/sidebarSlice.ts"; +import secretsSlice from "./features/secrets/secretsSlice.ts"; export const store = configureStore({ reducer: { @@ -31,6 +32,7 @@ export const store = configureStore({ common: commonSlice, dynamicAlerts: dynamicAlertsSlice, sidebar: sidebarSlice, + secerts: secretsSlice, }, middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(apiSlice.middleware), diff --git a/web/src/types/inputs/inputTypes.ts b/web/src/types/inputs/inputTypes.ts index aa0719e63..db7b063ea 100644 --- a/web/src/types/inputs/inputTypes.ts +++ b/web/src/types/inputs/inputTypes.ts @@ -14,6 +14,7 @@ export const InputTypes = { TEXT_BUTTON: "TEXT_BUTTON_FT", CRON: "CRON_FT", CHECKBOX: "CHECKBOX_FT", + SEARCH: "SEARCH_FT", } as const; export type InputType = (typeof InputTypes)[keyof typeof InputTypes]; From 58efda134ccc5fa8f5e48504ba3dc13022e4a423 Mon Sep 17 00:00:00 2001 From: jayeshsadhwani99 Date: Wed, 5 Mar 2025 13:35:32 +0530 Subject: [PATCH 06/16] fix typo --- web/src/store/features/secrets/selectors/secretsSelector.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/store/features/secrets/selectors/secretsSelector.ts b/web/src/store/features/secrets/selectors/secretsSelector.ts index ec0b0bebe..09eb561eb 100644 --- a/web/src/store/features/secrets/selectors/secretsSelector.ts +++ b/web/src/store/features/secrets/selectors/secretsSelector.ts @@ -1,3 +1,3 @@ import { RootState } from "../../.."; -export const secretsSelector = (state: RootState) => state.secrets; +export const secretsSelector = (state: RootState) => state.secerts; From 772d9bef05ea3bc9d9cafa4458e03e2c6c1ead9e Mon Sep 17 00:00:00 2001 From: jayeshsadhwani99 Date: Wed, 5 Mar 2025 14:04:11 +0530 Subject: [PATCH 07/16] add create/update/delete apis --- .../secret-management/SecretActionOverlay.tsx | 55 ++ .../create/CreateSecretForm.tsx | 98 +- .../create/SecretCreateOverlay.tsx | 2 +- .../secret-management/index.module.css | 887 ++++++++++++++++++ web/src/constants/api/secrets.ts | 10 +- web/src/pages/secret-management/index.tsx | 12 +- .../features/secrets/api/createSecretApi.ts | 20 + .../features/secrets/api/deleteSecretApi.ts | 27 + .../features/secrets/api/getSecretApi.ts | 49 + web/src/store/features/secrets/api/index.ts | 4 + .../features/secrets/api/updateSecretApi.ts | 31 + .../store/features/secrets/initialState.ts | 18 +- 12 files changed, 1140 insertions(+), 73 deletions(-) create mode 100644 web/src/components/secret-management/SecretActionOverlay.tsx create mode 100644 web/src/components/secret-management/index.module.css create mode 100644 web/src/store/features/secrets/api/createSecretApi.ts create mode 100644 web/src/store/features/secrets/api/deleteSecretApi.ts create mode 100644 web/src/store/features/secrets/api/getSecretApi.ts create mode 100644 web/src/store/features/secrets/api/updateSecretApi.ts diff --git a/web/src/components/secret-management/SecretActionOverlay.tsx b/web/src/components/secret-management/SecretActionOverlay.tsx new file mode 100644 index 000000000..875744806 --- /dev/null +++ b/web/src/components/secret-management/SecretActionOverlay.tsx @@ -0,0 +1,55 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { useEffect } from "react"; +import styles from "./index.module.css"; +import { CircularProgress } from "@mui/material"; +import Overlay from "../Overlay/index.js"; +import CustomButton from "../common/CustomButton/index.tsx"; +import { useDeleteSecretMutation } from "../../store/features/secrets/api/deleteSecretApi.ts"; + +const SecretActionOverlay = ({ + secret, + isOpen, + toggleOverlay, + refreshTable, +}: any) => { + const [deleteVariable, { isLoading, isSuccess, status }] = + useDeleteSecretMutation(); + const handleSuccess = () => { + deleteVariable({ id: secret.id }); + }; + + useEffect(() => { + if (isSuccess) { + toggleOverlay(); + refreshTable(); + } + }, [status]); + + return ( + <> + {isOpen && ( + +
+
+ Delete {secret.name}? +
+
+ Cancel + Yes + {isLoading && ( + + )} +
+
+
+ )} + + ); +}; + +export default SecretActionOverlay; diff --git a/web/src/components/secret-management/create/CreateSecretForm.tsx b/web/src/components/secret-management/create/CreateSecretForm.tsx index e8d36fd60..919df9c45 100644 --- a/web/src/components/secret-management/create/CreateSecretForm.tsx +++ b/web/src/components/secret-management/create/CreateSecretForm.tsx @@ -5,11 +5,6 @@ import CustomButton from "../../common/CustomButton"; import { showSnackbar } from "../../../store/features/snackbar/snackbarSlice"; import { CircularProgress } from "@mui/material"; import React, { useEffect } from "react"; -// import { -// useCreateVariableMutation, -// useGetVariableApiQuery, -// useUpdateVariableMutation, -// } from "../../../store/features/variables/api"; import TableLoader from "../../Skeleton/TableLoader"; import { secretsSelector } from "../../../store/features/secrets/selectors"; import { @@ -17,6 +12,12 @@ import { setSecretKey, } from "../../../store/features/secrets/secretsSlice"; import { SecretsInitialState } from "../../../store/features/secrets/initialState"; +import { + useCreateSecretMutation, + useGetSecretQuery, + useUpdateSecretMutation, +} from "../../../store/features/secrets/api"; +import { SaveRounded } from "@mui/icons-material"; type CreateSecretFormProps = { toggleOverlay?: () => void; @@ -28,18 +29,19 @@ function CreateSecretForm({ id, }: CreateSecretFormProps) { const dispatch = useDispatch(); - // const [triggerCreate, { isLoading }] = useCreateVariableMutation(); - // const [triggerUpdate, { isLoading: updateLoading }] = - // useUpdateVariableMutation(); - const { name, description, options } = useSelector(secretsSelector); - // const { isLoading: isLoadingVariable } = useGetVariableApiQuery( - // { - // id: id!, - // }, - // { - // skip: !id, - // }, - // ); + const [triggerCreate, { isLoading }] = useCreateSecretMutation(); + const [triggerUpdate, { isLoading: updateLoading }] = + useUpdateSecretMutation(); + const secret = useSelector(secretsSelector); + const { key, description, value } = secret; + const { isLoading: isLoadingSecret } = useGetSecretQuery( + { + id: id!, + }, + { + skip: !id, + }, + ); const handleChange = (key: keyof SecretsInitialState, value: any) => { dispatch( @@ -52,8 +54,8 @@ function CreateSecretForm({ const validate = () => { let error = ""; - if (!name || !description || !options) { - error = "Please fill all fields"; + if (!key || !value) { + error = "Please fill all the required fields"; } if (error) { @@ -69,11 +71,7 @@ function CreateSecretForm({ if (validate()) return; try { - // await triggerCreate({ - // name, - // description, - // options: options.split(", "), - // }).unwrap(); + await triggerCreate(secret).unwrap(); dispatch(resetSecretState()); toggleOverlay(); } catch (e: any) { @@ -95,12 +93,12 @@ function CreateSecretForm({ if (validate()) return; try { - // await triggerUpdate({ - // id: id!, - // name, - // description, - // options: options.split(", "), - // }).unwrap(); + await triggerUpdate({ + id: id!, + key, + description, + value: value, + }).unwrap(); dispatch(resetSecretState()); toggleOverlay(); } catch (e: any) { @@ -121,22 +119,22 @@ function CreateSecretForm({ }; }, [dispatch]); - // if (isLoadingVariable) { - // return ( - //
- // - //
- // ); - // } + if (isLoadingSecret) { + return ( +
+ +
+ ); + } return (
handleChange("name", value)} + placeholder="Enter secret name" + handleChange={(value) => handleChange("key", value)} containerClassName="!w-full" className="w-full" /> @@ -144,37 +142,39 @@ function CreateSecretForm({ inputType={InputTypes.MULTILINE} value={description} label="Description" - placeholder="What is this variable for?" + placeholder="What is this secret for?" handleChange={(value) => handleChange("description", value)} containerClassName="!w-full" className="w-full !h-20" /> handleChange("options", value)} + inputType={InputTypes.TEXT} + value={value} + label="Value" + placeholder="Enter the secret value..." + handleChange={(value) => handleChange("value", value)} containerClassName="!w-full" className="w-full" />
{id ? ( + Update ) : ( + Save )} - {/* {(isLoading || updateLoading) && } */} + {(isLoading || updateLoading) && }
); diff --git a/web/src/components/secret-management/create/SecretCreateOverlay.tsx b/web/src/components/secret-management/create/SecretCreateOverlay.tsx index cd90dba25..6310d6cd2 100644 --- a/web/src/components/secret-management/create/SecretCreateOverlay.tsx +++ b/web/src/components/secret-management/create/SecretCreateOverlay.tsx @@ -17,7 +17,7 @@ const SecretCreateOverlay = ({ <> {isOpen && ( -
+
diff --git a/web/src/components/secret-management/index.module.css b/web/src/components/secret-management/index.module.css new file mode 100644 index 000000000..71a144ae7 --- /dev/null +++ b/web/src/components/secret-management/index.module.css @@ -0,0 +1,887 @@ +.link { + text-decoration: underline; + color: #9553fe; +} + +div.ruleBox { + padding-left: 10px; + padding-right: 10px; + padding-top: 10px; + border: thin solid black; + padding-bottom: 0px; + background-color: #efebf4; + border-radius: 10px; + margin-top: 5px; +} + +.row__container { + padding: 12px 12px; + border-bottom: 1px solid #e5e7eb; + padding: 12px 12px; + /* background-color: #f8f8f8; */ + /* background-color: #fafafa; */ + display: flex; + flex-direction: row; + /* border-radius: 4px; */ + /* box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.05); */ +} + +.row__container .eventTypeSelectionSection { + display: flex; + flex-direction: row; + gap: 10px; + margin-bottom: 10px; + min-width: 200px; +} + +.row__container .eventTypeSelectionSection input { + width: 200px; +} + +.row__container .eventTypeConditionsSection { +} + +/* .row__container .content { + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 150%; + color: #6b7280; + padding-top: 0px !important; + min-width: 100px; + } */ + +.right { + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 150%; + color: #6b7280; + padding-top: 4px; + min-width: 100px; + text-align: right; +} + +.halfContainer { + width: 50%; +} + +.horizontal { + flex-direction: row; + display: flex; +} + +.row__container .heading { + padding-bottom: 12px; + gap: 10px; + padding: 10px; + background-color: #f8fafc; +} + +.row__container .heading-h { + padding-bottom: 12px; + gap: 10px; + padding: 15px; + flex-direction: row; + display: flex; + border-bottom: 1px solid lightgray; +} + +.row__container .content { + font-style: normal; + font-weight: 600; + font-size: 12px; + line-height: 150%; + color: #6b7280; + /* padding-top: 4px; */ + padding-top: 0px; + min-width: 100px; + margin-bottom: 8px; + /* width: 200px; */ +} + +.row__container .panelContent { + /* display: flex; + flex-direction: row; */ + /* min-height: 100vh; */ +} + +.row__container .panelConfig { + /* width: 40%; */ + /* float: left; */ +} + +.select__container { + width: 400px; + display: flex; + flex-direction: row; +} + +.groupByList { + width: 400px; + display: flex; + flex-direction: row; + margin-bottom: 8px; +} + +.row__container .panelPlot { + /* width: 60%; + float: right; + margin: 10px; */ +} + +.metricLabelName { + color: #ffffff; + background-color: #9553fe; + font-style: normal; + font-weight: 600; + font-size: 12px; + line-height: 150%; + min-width: 100px; + padding: 5px; + border-radius: 5px; +} + +.vl { + border-left: 1px solid lightgray; +} + +.tableChart { + display: flex; + flex-direction: column; + margin-top: 20px; + gap: 0.5rem; +} +.showTitle { + text-align: initial; + font-weight: 600; +} +.reactChart { + height: 600px !important; +} + +.flexDisplay { + display: flex; + flex-direction: row; +} +.selectContainer { + min-width: max-content; +} + +.crossIcon { + margin: 2px; + cursor: pointer; +} + +.submitButton { + color: #9553fe; + border: 1px solid #9553fe; + border-radius: 5px; + padding-left: 12px; + padding-right: 12px; + padding-top: 5px; + padding-bottom: 5px; + font-size: 0.75rem; + line-height: 1rem; + margin-top: 5px; +} + +.submitButtonRight { + color: #9553fe; + border: 1px solid #9553fe; + border-radius: 5px; + padding-left: 12px; + padding-right: 12px; + padding-top: 5px; + padding-bottom: 5px; + font-size: 0.75rem; + line-height: 1rem; + margin-top: 5px; + margin-left: 5px; +} + +.row__container .subHeading { + font-size: 12px; + padding-top: 5px; + color: #6b7280; + margin-left: 5px; + font-style: italic; +} + +.tableTitle { + color: black; + font-weight: bold; +} + +.link { + color: #9553fe; +} + +.addConditionStyle { + cursor: pointer; + margin-bottom: 10px; +} + +.crossIcon { + margin: 2px; +} + +.arrow-down-icon { + margin-left: 20px; + transform: rotate(0deg); + transition: all 0.3s ease-in-out; +} + +.arrow-down-icon.open { + transform: rotate(-180deg); +} + +.kvCell { + paddingbottom: 0; + paddingtop: 0; +} + +.toolTip { + padding: 1px; +} + +.addGroupingButton { + font-style: normal; + font-weight: 600; + font-size: 12px; + line-height: 150%; + color: #8993a6; + padding-top: 4px; + min-width: 100px; + margin-bottom: 10px; + cursor: pointer; +} + +.searchBox { + padding: 10px; + display: flex; + gap: 20px; +} + +.dataCount { + padding-top: 4px; + color: #6b7280; + font-size: 12px; + font-style: italic; +} + +.addGroupingStyle { + cursor: pointer; + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 150%; + color: #6b7280; + padding-top: 4px; + min-width: 120px; +} + +.groupingLabel { + margin-left: 50px; +} + +.attributesTypeContainer { + display: flex; + flex-direction: column; + gap: 4px; + width: 250px; +} + +.timeRangeContext { + /* font-style: italic; */ + padding-left: 10px; + color: #6b7280; + font-size: 12px; +} + +.saveOverlay { + box-sizing: border-box; + width: calc(400px); + min-height: calc(100px); + max-height: 500px; + border-radius: 8px; + background-color: #ffffff; + padding: 0px 24px 24px 24px; + position: relative; + overflow: scroll; + display: flex; + flex-direction: column; +} + +.dashName { + margin-top: 20px; +} +.dashboardSaveOverlay { + box-sizing: border-box; + min-width: calc(600px); + min-height: calc(200px); + height: max-content; + border-radius: 8px; + background-color: #ffffff; + padding: 0px 24px 24px 24px; + position: relative; + display: flex; + flex-direction: column; + box-sizing: border-box; +} +.dashboardSaveOverlay__content { + margin-top: 2rem; + display: flex; + flex-direction: column; + gap: 2rem; +} + +.dashboardSaveOverlay__content .panel__description { + display: flex; + gap: 2rem; +} + +.dashboardSaveOverlay__select { + width: 220px; + color: #6b7280; +} +.dashboardSaveOverlay__title { + font-style: normal; + font-weight: 600; + font-size: 16px; + line-height: 150%; + color: #6b7280; +} + +.actions { + display: flex; + flex-direction: row; + align-items: center; + margin-top: 48px; +} + +.mr-3 { + margin-right: 12px; +} + +.panel__heading { + display: flex; + flex-direction: row; + gap: 20px; +} + +.actionOverlay { + box-sizing: border-box; + width: calc(400px); + min-height: calc(100px); + max-height: 500px; + border-radius: 8px; + background-color: #ffffff; + padding: 18px 18px 18px 18px; + position: relative; + overflow: scroll; + display: flex; + flex-direction: column; +} + +.panel__container { + display: flex; + flex-direction: column; +} +.panelConfig { + border: 1px solid #d1d5db; + margin: 1rem; + background-color: #ffffff; + border-radius: 8px; + box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.05); +} +.panelPlot { + margin: 1rem; + background-color: #ffffff; + padding: 1rem; + border: 2px solid #e5e7eb; +} +.content { + font-style: normal; + font-weight: 400; + font-size: 14px; + line-height: 150%; + color: #6b7280; + padding-top: 4px; + min-width: 100px; +} + +.config_panel__action__container { + padding: 12px 12px; + background-color: #f9fafb; +} + +.config_panel__action__container .viz-button { + background-color: #fff; + border: 1px solid #9333ea; + color: #9554ff; + font-size: 14px; + padding: 4px 12px; + border-radius: 4px; + box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.05); + transition: all 75ms ease-in-out; +} + +.config_panel__action__container .viz-button:hover { + background-color: #fafafa; + color: #9333ea; +} + +.secondary-btn { + color: #535253; + font-size: 14px; + font-weight: 500; + padding: 4px 12px 4px 12px; + border: 1px solid #e5e7eb; + background-color: #fff; + border-radius: 4px; + box-shadow: 0px 1px 2px 0px rgba(0, 0, 0, 0.05); + transition: all 75ms ease-in-out; +} + +.secondary-btn:hover { + color: #535253; + border: 1px solid #c0c2c6; + + /* text-decoration: underline; */ +} + +.filter__container { + margin: 0px !important; + background-color: #fff !important; + border: 0px !important; + border-bottom: 1px solid #e5e7eb !important; + padding-bottom: 4px !important; +} + +.query__builder { + margin-left: 0px !important; +} + +.status-chip-FINISHED { + background-color: #10b981; + color: white; + padding: 2px 4px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; +} + +.status-chip-CREATED { + background-color: rgb(0, 114, 76); + color: white; + padding: 2px 4px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; +} + +.status-chip-RUNNING { + background-color: #b96a10; + color: white; + padding: 2px 4px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; +} + +.status-chip-FAILED { + background-color: #b91010; + color: white; + padding: 2px 4px; + border-radius: 4px; + font-size: 12px; + font-weight: 500; +} + +.pbRunSummary { + flex-direction: row; + justify-content: space-between; + align-items: center; + margin: 10px; + border: 1px solid #8080804a; + background-color: white; + padding: 10px; + border-radius: 10px; +} + +.pbRunLogsTitle { + font-size: 14px; + font-weight: 800; + color: #6b7280; + margin: 10px; +} + +.pbRunLog { + flex-direction: row; + justify-content: space-between; + align-items: center; + margin: 10px; + border: 1px solid #8080804a; + background-color: white; + padding: 10px; + border-radius: 10px; +} + +.pbRunLogMetrics { + flex-direction: row; + justify-content: space-between; + align-items: center; + margin: 10px; + border: 1px solid #8080804a; + background-color: #d3d3d366; + padding: 10px; + border-radius: 10px; +} + +.pbRunLogDesc { + font-size: 15px; + font-weight: 400; + color: black; +} + +.container .eventTypeSelectionSection { + display: flex; + flex-direction: row; + gap: 10px; + margin-bottom: 10px; + min-width: 200px; +} + +.container .eventTypeConditionsSection { +} + +.container .content { + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 150%; + color: #6b7280; + padding-top: 4px; + min-width: 100px; +} + +.container .content-centre { + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 150%; + color: #6b7280; + padding-top: 4px; + min-width: 100px; + text-align: center; +} + +.container { + padding-left: 8px; + padding-top: 12px; + background-color: #fff; + margin: 12px; + border-width: 1px; + border-color: hsl(240deg 21% 87%); + border-style: solid; + border-radius: 5px; +} + +.container .heading { + padding-bottom: 12px; + display: flex; + flex-direction: row; + gap: 10px; +} + +.container .subHeading { + font-size: 11px; + padding-top: 4px; + color: #6b7280; +} + +.addConditionStyle { + cursor: pointer; + margin-bottom: 10px; +} + +.crossIcon { + cursor: pointer; +} + +.container .content { + font-style: normal; + font-weight: 400; + font-size: 12px; + line-height: 150%; + color: #6b7280; + padding-top: 4px; + min-width: 100px; +} + +.textValueContainer { + background: #fff; + border: 1px solid #d3d3d3; + border-radius: 5px; + color: #1f2937; + padding: 4px 8px; + margin-right: 10px; + outline: none; + width: -webkit-fill-available; + font-size: 12px; + height: 500px; +} + +.pb-explore-container { + display: flex; + flex-direction: row; + height: 100vh; +} + +.pb-explore-title { + margin: 5px 10px -5px 10px; + padding: 5px 5px 5px 5px; + font-size: 16px; + font-weight: 600; + color: #58595b; +} + +.alert-search-field { + margin: 10px 5px 10px 5px; + background: #fff; +} + +.alert-loading { + display: flex; + justify-content: center; + align-items: center; + height: 100%; +} + +.il-output-loading { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + padding: 20px; +} + +.il-output { + display: flex; + flex-direction: column; + margin: 10px 5px 5px 5px; + padding: 5px 5px 5px 5px; + border: 1px solid #d3d3d3; + border-radius: 5px; + background: #f8fafc; +} + +.alert-cards-pane { + width: 25%; + margin: 10px 5px 10px 10px; + border: 1px solid #d3d3d3; + border-radius: 5px; + background: #fff; + overflow-y: auto; +} + +.alert-actions-pane { + width: 75%; + margin: 10px 10px 10px 5px; + border: 1px solid #d3d3d3; + border-radius: 5px; + background: #fff; + overflow-y: auto; +} + +.alert-card { + margin: 5px 5px 5px 5px; + padding: 5px 5px 5px 5px; + border: 1px solid #d3d3d3; + border-radius: 5px; + background: #fff; + cursor: pointer; +} + +.alert-card-selected { + margin: 5px 5px 5px 5px; + padding: 5px 5px 5px 5px; + border: 1px solid #d3d3d3; + border-radius: 5px; + background: #9553fe2e; + cursor: pointer; +} + +.alert-title { + font-size: 16px; + font-weight: 600; + color: #58595b; + text-wrap: pretty; + width: 100%; +} + +.alert-type { + font-size: 14px; + font-weight: 400; + color: #6b7280; +} + +.alert-channel { + font-size: 12px; + font-weight: 400; + color: #6b7280; +} + +.alert-timestamp { + font-size: 12px; + font-weight: 400; + color: #8c8585; +} + +.alert-not-selected { + display: flex; + justify-content: center; + align-items: center; + height: 100%; + margin: 10px; + font-size: 16px; + font-weight: 600; + color: #58595b; +} + +.il-alert-details { + margin: 5px 5px 5px 5px; + padding: 5px 5px 5px 5px; + background: #fff; +} + +.il-card { + display: flex; + flex-direction: column; + margin: 5px 0px 5px 0px; + padding: 5px 5px 5px 5px; + border: 1px solid #d3d3d3; + border-radius: 5px; + background: #fff; +} + +.il-step-section { + display: flex; + flex-direction: row; +} + +.il-output-section { + display: flex; + flex-direction: column; +} + +.il-message { + font-size: 14px; + font-weight: 400; + color: #6b7280; + align-items: center; + display: flex; +} + +.il-step-info { + display: flex; + flex-direction: row; + justify-content: space-between; + border-left: 1px solid #babdc4; + margin-left: 20px; + padding-left: 20px; + border-right: 1px solid #babdc4; + margin-right: 20px; + padding-right: 20px; +} + +.il-step-name { + min-width: 60px; + display: flex; + padding-left: 5px; +} + +.alert-tags { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 5px; +} + +.alert-tag { + padding: 1px 5px 0px 5px; + background-color: #9553fe2e; + color: #58595b; + border-radius: 5px; + font-size: 14px; + margin-bottom: 5px; +} + +.add-step { + width: 110px; + height: 32px; + margin-top: 5px; + font-size: 14px; +} + +.graph-ts { + font-size: 12px; + text-align: center; + margin-top: 10px; + color: darkgray; + text-decoration: italic; +} + +.graph-title { + font-size: 13px; + text-align: center; + color: black; + font-weight: 800; +} + +.graph-box { + border: 1px solid #d3d3d3; + max-width: 100%; + width: 100%; + min-width: fit-content; + border-radius: 5px; + margin-bottom: 5px; + padding: 10px; + display: flex; + flex-direction: column; + align-items: stretch; +} + +.graph-error { + font-size: 14px; + text-align: center; + margin-top: 20px; + margin-bottom: 20px; + color: red; + font-weight: 800; +} + +.graph-ts-error { + font-size: 12px; + text-align: center; + color: darkgray; + text-decoration: italic; +} + +.tableData { + font-size: 12px; + min-width: 50px; +} + +.tableDataMsg { + font-size: 12px; + min-width: 100px; + width: 100px; +} + +.tableLogDataTitle { + font-size: 12px; + min-width: 50px; + width: 50px; +} diff --git a/web/src/constants/api/secrets.ts b/web/src/constants/api/secrets.ts index 8e07ae9b9..f70f6e231 100644 --- a/web/src/constants/api/secrets.ts +++ b/web/src/constants/api/secrets.ts @@ -1,6 +1,6 @@ // Secrets management -export const SECRETS_LIST = "/secrets/list"; -export const SECRET_GET = "/secrets/get"; -export const SECRET_CREATE = "/secrets/create"; -export const SECRET_UPDATE = "/secrets/update"; -export const SECRET_DELETE = "/secrets/delete"; \ No newline at end of file +export const SECRETS_LIST = "/secrets/list/"; +export const SECRET_GET = "/secrets/get/"; +export const SECRET_CREATE = "/secrets/create/"; +export const SECRET_UPDATE = "/secrets/update/"; +export const SECRET_DELETE = "/secrets/update/"; diff --git a/web/src/pages/secret-management/index.tsx b/web/src/pages/secret-management/index.tsx index e3ef22096..e2257e528 100644 --- a/web/src/pages/secret-management/index.tsx +++ b/web/src/pages/secret-management/index.tsx @@ -11,6 +11,8 @@ import PaginatedTable from "../../components/common/Table/PaginatedTable"; import { secretsColumns } from "./utils"; import { useSecretsData } from "./hooks"; import CreateSecretButton from "../../components/secret-management/create/CreateSecretButton"; +import SecretCreateOverlay from "../../components/secret-management/create/SecretCreateOverlay"; +import SecretActionOverlay from "../../components/secret-management/SecretActionOverlay"; function SecretManagement() { const [query, setQuery] = useState(""); @@ -57,21 +59,21 @@ function SecretManagement() { - {/* {isActionOpen && ( - )} {isConfigOpen && ( - - )} */} + )} ); } diff --git a/web/src/store/features/secrets/api/createSecretApi.ts b/web/src/store/features/secrets/api/createSecretApi.ts new file mode 100644 index 000000000..aa9bdeca9 --- /dev/null +++ b/web/src/store/features/secrets/api/createSecretApi.ts @@ -0,0 +1,20 @@ +import { SECRET_CREATE } from "../../../../constants/index.ts"; +import { apiSlice } from "../../../app/apiSlice.ts"; +import { SecretsInitialState } from "../initialState.ts"; + +export const createSecretApi = apiSlice.injectEndpoints({ + endpoints: (builder) => ({ + createSecret: builder.mutation>({ + query: (body) => ({ + url: SECRET_CREATE, + body: { + secret: body, + }, + method: "POST", + }), + invalidatesTags: ["Secrets"], + }), + }), +}); + +export const { useCreateSecretMutation } = createSecretApi; diff --git a/web/src/store/features/secrets/api/deleteSecretApi.ts b/web/src/store/features/secrets/api/deleteSecretApi.ts new file mode 100644 index 000000000..2ef9c2f15 --- /dev/null +++ b/web/src/store/features/secrets/api/deleteSecretApi.ts @@ -0,0 +1,27 @@ +import { SECRET_DELETE } from "../../../../constants/index.ts"; +import { apiSlice } from "../../../app/apiSlice.ts"; + +export const deleteSecretApi = apiSlice.injectEndpoints({ + endpoints: (builder) => ({ + deleteSecret: builder.mutation({ + query: ({ id }) => ({ + url: SECRET_DELETE, + method: "POST", + body: { + secret_id: id, + update_secret_ops: [ + { + op: "UPDATE_STATUS", + update_status: { + is_active: false, + }, + }, + ], + }, + }), + invalidatesTags: ["Secrets"], + }), + }), +}); + +export const { useDeleteSecretMutation } = deleteSecretApi; diff --git a/web/src/store/features/secrets/api/getSecretApi.ts b/web/src/store/features/secrets/api/getSecretApi.ts new file mode 100644 index 000000000..3b227f6b4 --- /dev/null +++ b/web/src/store/features/secrets/api/getSecretApi.ts @@ -0,0 +1,49 @@ +import { SECRETS_LIST } from "../../../../constants/index.ts"; +import { apiSlice } from "../../../app/apiSlice.ts"; +import { setSecretKey } from "../secretsSlice.ts"; + +export const getSecretApi = apiSlice.injectEndpoints({ + endpoints: (builder) => ({ + getSecret: builder.query({ + query: ({ id }) => ({ + url: SECRETS_LIST, + body: { + secret_ids: [id], + }, + method: "POST", + }), + transformResponse: (response) => { + const secret = response?.secrets?.[0] ?? {}; + return secret; + }, + onQueryStarted: async (_, { queryFulfilled, dispatch }) => { + try { + const { data } = await queryFulfilled; + dispatch( + setSecretKey({ + key: "key", + value: data?.name ?? "", + }), + ); + dispatch( + setSecretKey({ + key: "description", + value: data?.description ?? "", + }), + ); + dispatch( + setSecretKey({ + key: "value", + value: (data?.options ?? []).join(", "), + }), + ); + } catch (error) { + console.log(error); + } + }, + providesTags: ["Secrets"], + }), + }), +}); + +export const { useGetSecretQuery } = getSecretApi; diff --git a/web/src/store/features/secrets/api/index.ts b/web/src/store/features/secrets/api/index.ts index 75fe7187c..14d166936 100644 --- a/web/src/store/features/secrets/api/index.ts +++ b/web/src/store/features/secrets/api/index.ts @@ -1 +1,5 @@ export * from "./getSecretsListApi"; +export * from "./createSecretApi"; +export * from "./deleteSecretApi"; +export * from "./updateSecretApi"; +export * from "./getSecretApi"; diff --git a/web/src/store/features/secrets/api/updateSecretApi.ts b/web/src/store/features/secrets/api/updateSecretApi.ts new file mode 100644 index 000000000..75be4828d --- /dev/null +++ b/web/src/store/features/secrets/api/updateSecretApi.ts @@ -0,0 +1,31 @@ +import { SECRET_UPDATE } from "../../../../constants/index.ts"; +import { apiSlice } from "../../../app/apiSlice.ts"; +import { SecretsInitialState } from "../initialState.ts"; + +export const updateSecretApi = apiSlice.injectEndpoints({ + endpoints: (builder) => ({ + updateSecret: builder.mutation({ + query: (secret) => ({ + url: SECRET_UPDATE, + body: { + secret_id: secret.id, + update_secret_ops: [ + { + op: "UPDATE_SECRET", + update_secret: { + secret: { + value: secret.value, + description: secret.description, + }, + }, + }, + ], + }, + method: "POST", + }), + invalidatesTags: ["Secrets"], + }), + }), +}); + +export const { useUpdateSecretMutation } = updateSecretApi; diff --git a/web/src/store/features/secrets/initialState.ts b/web/src/store/features/secrets/initialState.ts index 454689c7d..251441fb1 100644 --- a/web/src/store/features/secrets/initialState.ts +++ b/web/src/store/features/secrets/initialState.ts @@ -1,20 +1,12 @@ -export const VariableOption = { - DROPDOWN: "DROPDOWN", -} as const; - -export type VariableOptionType = - (typeof VariableOption)[keyof typeof VariableOption]; - export type SecretsInitialState = { - name: string; + id?: string; + key: string; + value: string; description: string; - options: string; - type: VariableOptionType; }; export const secretsInitialState: SecretsInitialState = { - name: "", + key: "", description: "", - options: "", - type: VariableOption.DROPDOWN, + value: "", }; From c0a08d766b5b54a1bb7d8f1f8c21b9d3ab306d9d Mon Sep 17 00:00:00 2001 From: jayeshsadhwani99 Date: Wed, 5 Mar 2025 14:06:50 +0530 Subject: [PATCH 08/16] fix build issues --- .../store/features/secrets/actions/resetSecretStateAction.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/web/src/store/features/secrets/actions/resetSecretStateAction.ts b/web/src/store/features/secrets/actions/resetSecretStateAction.ts index 929b5ea44..f47824a91 100644 --- a/web/src/store/features/secrets/actions/resetSecretStateAction.ts +++ b/web/src/store/features/secrets/actions/resetSecretStateAction.ts @@ -1,8 +1,5 @@ import { secretsInitialState, SecretsInitialState } from "../initialState"; export const resetSecretStateAction = (state: SecretsInitialState) => { - state.name = secretsInitialState.name; - state.description = secretsInitialState.name; - state.options = secretsInitialState.options; - state.type = secretsInitialState.type; + Object.assign(state, secretsInitialState); }; From 4ac105ff33764d28a2338772b9456f2a335de025 Mon Sep 17 00:00:00 2001 From: jayeshsadhwani99 Date: Wed, 5 Mar 2025 14:12:50 +0530 Subject: [PATCH 09/16] change ui icon/name --- web/src/components/Sidebar/index.tsx | 3 ++- web/src/pages/secret-management/index.tsx | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/web/src/components/Sidebar/index.tsx b/web/src/components/Sidebar/index.tsx index 9659bbc4a..28b7733b3 100644 --- a/web/src/components/Sidebar/index.tsx +++ b/web/src/components/Sidebar/index.tsx @@ -5,6 +5,7 @@ import SlackConnectOverlay from "../SlackConnectOverlay"; import { elements } from "./utils"; import SidebarElement from "./SidebarElement"; import { + LockRounded, LogoutRounded, SecurityRounded, SettingsRounded, @@ -43,7 +44,7 @@ function Sidebar() { } + icon={} /> - +
From 38b39878f29a9a6b3dd09c3842f9455e4d53066a Mon Sep 17 00:00:00 2001 From: droid-dhruv Date: Wed, 5 Mar 2025 15:06:34 +0530 Subject: [PATCH 10/16] Update Processor pattern used in secret updation --- ...ret_executor_se_key_60cc82_idx_and_more.py | 5 +- executor/models.py | 1 - executor/secrets/crud/__init__.py | 1 + .../secrets/crud/secrets_update_processor.py | 60 +++++++ executor/secrets/urls.py | 3 +- executor/secrets/views.py | 122 ++++++-------- playbooks/urls.py | 2 +- protos/secrets/api.proto | 64 +++---- protos/secrets/api_pb2.py | 48 +++--- protos/secrets/api_pb2.pyi | 156 ++++++++++-------- web/src/constants/api/secrets.ts | 9 +- 11 files changed, 266 insertions(+), 205 deletions(-) create mode 100644 executor/secrets/crud/__init__.py create mode 100644 executor/secrets/crud/secrets_update_processor.py diff --git a/executor/migrations/0046_secret_secret_executor_se_key_60cc82_idx_and_more.py b/executor/migrations/0046_secret_secret_executor_se_key_60cc82_idx_and_more.py index 6dada3601..879729d58 100644 --- a/executor/migrations/0046_secret_secret_executor_se_key_60cc82_idx_and_more.py +++ b/executor/migrations/0046_secret_secret_executor_se_key_60cc82_idx_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.13 on 2025-03-04 12:00 +# Generated by Django 4.1.13 on 2025-03-05 09:24 from django.conf import settings from django.db import migrations, models @@ -10,8 +10,8 @@ class Migration(migrations.Migration): dependencies = [ - ('accounts', '0003_accountuseroauth2sessioncodestore'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('accounts', '0003_accountuseroauth2sessioncodestore'), ('executor', '0045_upgrade_step_relation_conditions'), ] @@ -20,7 +20,6 @@ class Migration(migrations.Migration): name='Secret', fields=[ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), - ('name', models.CharField(help_text='Human-readable name', max_length=255)), ('key', models.CharField(help_text='Reference key for the secret', max_length=255)), ('value', encrypted_model_fields.fields.EncryptedTextField()), ('created_at', models.DateTimeField(auto_now_add=True)), diff --git a/executor/models.py b/executor/models.py index 3ff585159..5d963bf78 100644 --- a/executor/models.py +++ b/executor/models.py @@ -676,7 +676,6 @@ def proto(self) -> PlaybookStepRelationExecutionLogProto: class Secret(models.Model): id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - name = models.CharField(max_length=255, help_text="Human-readable name") key = models.CharField(max_length=255, help_text="Reference key for the secret") value = EncryptedTextField() account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE) diff --git a/executor/secrets/crud/__init__.py b/executor/secrets/crud/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/executor/secrets/crud/__init__.py @@ -0,0 +1 @@ + diff --git a/executor/secrets/crud/secrets_update_processor.py b/executor/secrets/crud/secrets_update_processor.py new file mode 100644 index 000000000..287a0d18f --- /dev/null +++ b/executor/secrets/crud/secrets_update_processor.py @@ -0,0 +1,60 @@ +import logging + +from executor.models import Secret +from protos.secrets.api_pb2 import UpdateSecretOp +from utils.update_processor_mixin import UpdateProcessorMixin + +logger = logging.getLogger(__name__) + + +class SecretsUpdateProcessor(UpdateProcessorMixin): + update_op_cls = UpdateSecretOp + + @staticmethod + def update_secret(elem: Secret, update_op: UpdateSecretOp.UpdateSecret) -> Secret: + """Update a secret's description and/or value""" + update_fields = ['updated_at'] + + # Update description if provided and has a value + if hasattr(update_op, 'description') and update_op.description.value: + elem.description = update_op.description.value + update_fields.append('description') + + # Update value if provided and has a value + if hasattr(update_op, 'value') and update_op.value.value: + elem.value = update_op.value.value + update_fields.append('value') + + logger.info(f"Updating secret {elem.key} with fields: {update_fields}, {len(update_fields)}") + if len(update_fields) > 1: # Only save if we have fields to update + try: + elem.save(update_fields=update_fields) + except Exception as ex: + logger.exception(f"Error occurred updating secret {elem.key}") + raise Exception(f"Error updating secret: {str(ex)}") + + return elem + + @staticmethod + def update_secret_status(elem: Secret, update_op: UpdateSecretOp.UpdateSecretStatus) -> Secret: + """Update a secret's active status (soft delete)""" + is_active = update_op.is_active.value + + # Can't reactivate a secret that's been deactivated + if not elem.is_active and is_active: + raise Exception(f"Secret {elem.key} cannot be reactivated") + + # No change needed + if elem.is_active == is_active: + return elem + + elem.is_active = is_active + try: + elem.save(update_fields=['is_active', 'updated_at']) + except Exception as ex: + logger.exception(f"Error occurred updating secret status for {elem.key}") + raise Exception(f"Error updating secret status: {str(ex)}") + return elem + + +secrets_update_processor = SecretsUpdateProcessor() diff --git a/executor/secrets/urls.py b/executor/secrets/urls.py index ef29683dd..c6a3de645 100644 --- a/executor/secrets/urls.py +++ b/executor/secrets/urls.py @@ -5,6 +5,5 @@ path('list/', views.secrets_list, name='secrets_list'), path('get/', views.secret_get, name='secret_get'), path('create/', views.secret_create, name='secret_create'), - path('update/', views.secret_update, name='secret_update'), - path('delete/', views.secret_delete, name='secret_delete'), + path('update/', views.secret_update, name='secret_update') ] \ No newline at end of file diff --git a/executor/secrets/views.py b/executor/secrets/views.py index 9721f1b45..f832079c5 100644 --- a/executor/secrets/views.py +++ b/executor/secrets/views.py @@ -16,8 +16,8 @@ GetSecretRequest, GetSecretResponse, CreateSecretRequest, CreateSecretResponse, UpdateSecretRequest, UpdateSecretResponse, - DeleteSecretRequest, DeleteSecretResponse, - Secret as SecretProto + Secret as SecretProto, + UpdateSecretOp ) logger = logging.getLogger(__name__) @@ -34,7 +34,6 @@ def _secret_to_proto(secret: Secret) -> SecretProto: """Convert a Secret model to a Secret proto""" return SecretProto( id=StringValue(value=str(secret.id)), - name=StringValue(value=secret.name), key=StringValue(value=secret.key), masked_value=StringValue(value=_mask_secret_value(secret.value)), description=StringValue(value=secret.description or ""), @@ -99,18 +98,17 @@ def secret_create(request_message: CreateSecretRequest) -> Union[CreateSecretRes """Create a new secret""" account: Account = get_request_account() user = get_request_user() - - name = request_message.name.value + key = request_message.key.value value = request_message.value.value description = request_message.description.value # Validate required fields - if not name or not key or not value: + if not key or not value: return CreateSecretResponse( meta=get_meta(), success=BoolValue(value=False), - message=Message(title="Invalid Request", description="Name, key, and value are required") + message=Message(title="Invalid Request", description="Key, and value are required") ) # Check if key already exists for this account @@ -125,7 +123,6 @@ def secret_create(request_message: CreateSecretRequest) -> Union[CreateSecretRes try: secret = Secret.objects.create( account=account, - name=name, key=key, value=value, description=description, @@ -151,94 +148,75 @@ def secret_create(request_message: CreateSecretRequest) -> Union[CreateSecretRes @web_api(UpdateSecretRequest) def secret_update(request_message: UpdateSecretRequest) -> Union[UpdateSecretResponse, HttpResponse]: - """Update a secret's name or description (not the value)""" + """Update a secret using operations""" account: Account = get_request_account() user = get_request_user() - secret_id = request_message.secret_id.value - name = request_message.name.value - description = request_message.description.value - key = request_message.key.value - - if not secret_id: - return UpdateSecretResponse( - meta=get_meta(), - success=BoolValue(value=False), - message=Message(title="Invalid Request", description="Secret ID is required") - ) + update_secret_ops = request_message.update_secret_ops - try: - secret = Secret.objects.get(id=secret_id, account=account, is_active=True) - - # Update fields if provided - if name: - secret.name = name - if description is not None: # Allow empty description - secret.description = description - if key: - secret.key = key - secret.last_updated_by = user - secret.save() - - return UpdateSecretResponse( - meta=get_meta(), - success=BoolValue(value=True), - message=Message(title="Success", description="Secret updated successfully"), - secret=_secret_to_proto(secret) - ) - except Secret.DoesNotExist: + if not update_secret_ops: return UpdateSecretResponse( meta=get_meta(), success=BoolValue(value=False), - message=Message(title="Not Found", description="Secret not found") + message=Message(title="Invalid Request", description="No update operations provided") ) - except Exception as e: - logger.error(f"Error updating secret: {str(e)}") + + # All operations should reference the same secret + secret_ids = set(op.secret_id.value for op in update_secret_ops) + if len(secret_ids) != 1: return UpdateSecretResponse( meta=get_meta(), success=BoolValue(value=False), - message=Message(title="Error", description="Failed to update secret") + message=Message(title="Invalid Request", description="All operations must reference the same secret") ) - - -@web_api(DeleteSecretRequest) -def secret_delete(request_message: DeleteSecretRequest) -> Union[DeleteSecretResponse, HttpResponse]: - """Soft delete a secret by setting is_active to False""" - account: Account = get_request_account() - user = get_request_user() - secret_id = request_message.secret_id.value - - if not secret_id: - return DeleteSecretResponse( - meta=get_meta(), - success=BoolValue(value=False), - message=Message(title="Invalid Request", description="Secret ID is required") - ) + secret_id = list(secret_ids)[0] try: secret = Secret.objects.get(id=secret_id, account=account, is_active=True) - # Soft delete by setting is_active to False - secret.is_active = False + # Store the original user for later restoration + original_last_updated_by = secret.last_updated_by + + # Set the user who is making the update secret.last_updated_by = user - secret.save() + secret.save(update_fields=['last_updated_by']) - return DeleteSecretResponse( - meta=get_meta(), - success=BoolValue(value=True), - message=Message(title="Success", description="Secret deleted successfully") - ) + try: + # Apply all update operations + from executor.secrets.crud.secrets_update_processor import secrets_update_processor + secrets_update_processor.update(secret, update_secret_ops) + + # Get the updated secret + updated_secret = Secret.objects.get(id=secret_id) + + return UpdateSecretResponse( + meta=get_meta(), + success=BoolValue(value=True), + message=Message(title="Success", description="Secret updated successfully"), + secret=_secret_to_proto(updated_secret) + ) + except Exception as e: + # Restore the original user if update fails + secret.last_updated_by = original_last_updated_by + secret.save(update_fields=['last_updated_by']) + + logger.error(f"Error updating secret: {str(e)}") + return UpdateSecretResponse( + meta=get_meta(), + success=BoolValue(value=False), + message=Message(title="Error", description=str(e)) + ) except Secret.DoesNotExist: - return DeleteSecretResponse( + return UpdateSecretResponse( meta=get_meta(), success=BoolValue(value=False), message=Message(title="Not Found", description="Secret not found") ) except Exception as e: - logger.error(f"Error deleting secret: {str(e)}") - return DeleteSecretResponse( + logger.error(f"Error updating secret: {str(e)}") + return UpdateSecretResponse( meta=get_meta(), success=BoolValue(value=False), - message=Message(title="Error", description="Failed to delete secret") - ) \ No newline at end of file + message=Message(title="Error", description="Failed to update secret") + ) \ No newline at end of file diff --git a/playbooks/urls.py b/playbooks/urls.py index 636d1c788..351182ee5 100644 --- a/playbooks/urls.py +++ b/playbooks/urls.py @@ -27,10 +27,10 @@ path('executor/', include('executor.urls')), path('executor/workflows/', include('executor.workflows.urls')), path('executor/engine/', include('executor.engine_manager.urls')), + path('executor/secrets/', include('executor.secrets.urls')), path('pb/', include('executor.urls')), path('management/', include('management.urls')), path('media/', include('media.urls')), - path('secrets/', include('executor.secrets.urls')), path('', include('django_prometheus.urls')), path('', views.index), ] diff --git a/protos/secrets/api.proto b/protos/secrets/api.proto index 148a0db5f..5385fa80b 100644 --- a/protos/secrets/api.proto +++ b/protos/secrets/api.proto @@ -6,15 +6,14 @@ import "protos/base.proto"; message Secret { google.protobuf.StringValue id = 1; - google.protobuf.StringValue name = 2; - google.protobuf.StringValue key = 3; - google.protobuf.StringValue masked_value = 4; - google.protobuf.StringValue description = 5; - google.protobuf.StringValue creator = 6; - google.protobuf.StringValue last_updated_by = 7; - int64 created_at = 8; - int64 updated_at = 9; - bool is_active = 10; + google.protobuf.StringValue key = 2; + google.protobuf.StringValue masked_value = 3; + google.protobuf.StringValue description = 4; + google.protobuf.StringValue creator = 5; + google.protobuf.StringValue last_updated_by = 6; + int64 created_at = 7; + int64 updated_at = 8; + bool is_active = 9; } message GetSecretsRequest { @@ -42,10 +41,9 @@ message GetSecretResponse { message CreateSecretRequest { Meta meta = 1; - google.protobuf.StringValue name = 2; - google.protobuf.StringValue key = 3; - google.protobuf.StringValue value = 4; - google.protobuf.StringValue description = 5; + google.protobuf.StringValue key = 2; + google.protobuf.StringValue value = 3; + google.protobuf.StringValue description = 4; } message CreateSecretResponse { @@ -55,12 +53,33 @@ message CreateSecretResponse { Secret secret = 4; } +message UpdateSecretOp { + enum Op { + UNKNOWN = 0; + UPDATE_SECRET = 1; + UPDATE_SECRET_STATUS = 2; + } + + message UpdateSecret { + google.protobuf.StringValue description = 1; + google.protobuf.StringValue value = 2; + } + + message UpdateSecretStatus { + google.protobuf.BoolValue is_active = 1; + } + + Op op = 1; + google.protobuf.StringValue secret_id = 2; + oneof update { + UpdateSecret update_secret = 3; + UpdateSecretStatus update_secret_status = 4; + } +} + message UpdateSecretRequest { Meta meta = 1; - google.protobuf.StringValue secret_id = 2; - google.protobuf.StringValue name = 3; - google.protobuf.StringValue description = 4; - google.protobuf.StringValue key = 5; + repeated UpdateSecretOp update_secret_ops = 2; } message UpdateSecretResponse { @@ -69,14 +88,3 @@ message UpdateSecretResponse { Message message = 3; Secret secret = 4; } - -message DeleteSecretRequest { - Meta meta = 1; - google.protobuf.StringValue secret_id = 2; -} - -message DeleteSecretResponse { - Meta meta = 1; - google.protobuf.BoolValue success = 2; - Message message = 3; -} \ No newline at end of file diff --git a/protos/secrets/api_pb2.py b/protos/secrets/api_pb2.py index 6c9229a97..7a06ceaf3 100644 --- a/protos/secrets/api_pb2.py +++ b/protos/secrets/api_pb2.py @@ -15,7 +15,7 @@ from protos import base_pb2 as protos_dot_base__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18protos/secrets/api.proto\x12\x0eprotos.secrets\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x11protos/base.proto\"\x91\x03\n\x06Secret\x12(\n\x02id\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12*\n\x04name\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12)\n\x03key\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x32\n\x0cmasked_value\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12-\n\x07\x63reator\x18\x06 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x35\n\x0flast_updated_by\x18\x07 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x12\n\ncreated_at\x18\x08 \x01(\x03\x12\x12\n\nupdated_at\x18\t \x01(\x03\x12\x11\n\tis_active\x18\n \x01(\x08\"/\n\x11GetSecretsRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\"\xa8\x01\n\x12GetSecretsResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12\'\n\x07secrets\x18\x04 \x03(\x0b\x32\x16.protos.secrets.Secret\"_\n\x10GetSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa6\x01\n\x11GetSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"\xe8\x01\n\x13\x43reateSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12*\n\x04name\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12)\n\x03key\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12+\n\x05value\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa9\x01\n\x14\x43reateSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"\xec\x01\n\x13UpdateSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12*\n\x04name\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12)\n\x03key\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa9\x01\n\x14UpdateSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"b\n\x13\x44\x65leteSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\x81\x01\n\x14\x44\x65leteSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Messageb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18protos/secrets/api.proto\x12\x0eprotos.secrets\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x11protos/base.proto\"\xe5\x02\n\x06Secret\x12(\n\x02id\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12)\n\x03key\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x32\n\x0cmasked_value\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12-\n\x07\x63reator\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x35\n\x0flast_updated_by\x18\x06 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x12\n\ncreated_at\x18\x07 \x01(\x03\x12\x12\n\nupdated_at\x18\x08 \x01(\x03\x12\x11\n\tis_active\x18\t \x01(\x08\"/\n\x11GetSecretsRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\"\xa8\x01\n\x12GetSecretsResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12\'\n\x07secrets\x18\x04 \x03(\x0b\x32\x16.protos.secrets.Secret\"_\n\x10GetSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa6\x01\n\x11GetSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"\xbc\x01\n\x13\x43reateSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12)\n\x03key\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12+\n\x05value\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa9\x01\n\x14\x43reateSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"\x88\x04\n\x0eUpdateSecretOp\x12-\n\x02op\x18\x01 \x01(\x0e\x32!.protos.secrets.UpdateSecretOp.Op\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x44\n\rupdate_secret\x18\x03 \x01(\x0b\x32+.protos.secrets.UpdateSecretOp.UpdateSecretH\x00\x12Q\n\x14update_secret_status\x18\x04 \x01(\x0b\x32\x31.protos.secrets.UpdateSecretOp.UpdateSecretStatusH\x00\x1an\n\x0cUpdateSecret\x12\x31\n\x0b\x64\x65scription\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12+\n\x05value\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x1a\x43\n\x12UpdateSecretStatus\x12-\n\tis_active\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\">\n\x02Op\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x11\n\rUPDATE_SECRET\x10\x01\x12\x18\n\x14UPDATE_SECRET_STATUS\x10\x02\x42\x08\n\x06update\"l\n\x13UpdateSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12\x39\n\x11update_secret_ops\x18\x02 \x03(\x0b\x32\x1e.protos.secrets.UpdateSecretOp\"\xa9\x01\n\x14UpdateSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secretb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'protos.secrets.api_pb2', globals()) @@ -23,25 +23,29 @@ DESCRIPTOR._options = None _SECRET._serialized_start=96 - _SECRET._serialized_end=497 - _GETSECRETSREQUEST._serialized_start=499 - _GETSECRETSREQUEST._serialized_end=546 - _GETSECRETSRESPONSE._serialized_start=549 - _GETSECRETSRESPONSE._serialized_end=717 - _GETSECRETREQUEST._serialized_start=719 - _GETSECRETREQUEST._serialized_end=814 - _GETSECRETRESPONSE._serialized_start=817 - _GETSECRETRESPONSE._serialized_end=983 - _CREATESECRETREQUEST._serialized_start=986 - _CREATESECRETREQUEST._serialized_end=1218 - _CREATESECRETRESPONSE._serialized_start=1221 - _CREATESECRETRESPONSE._serialized_end=1390 - _UPDATESECRETREQUEST._serialized_start=1393 - _UPDATESECRETREQUEST._serialized_end=1629 - _UPDATESECRETRESPONSE._serialized_start=1632 - _UPDATESECRETRESPONSE._serialized_end=1801 - _DELETESECRETREQUEST._serialized_start=1803 - _DELETESECRETREQUEST._serialized_end=1901 - _DELETESECRETRESPONSE._serialized_start=1904 - _DELETESECRETRESPONSE._serialized_end=2033 + _SECRET._serialized_end=453 + _GETSECRETSREQUEST._serialized_start=455 + _GETSECRETSREQUEST._serialized_end=502 + _GETSECRETSRESPONSE._serialized_start=505 + _GETSECRETSRESPONSE._serialized_end=673 + _GETSECRETREQUEST._serialized_start=675 + _GETSECRETREQUEST._serialized_end=770 + _GETSECRETRESPONSE._serialized_start=773 + _GETSECRETRESPONSE._serialized_end=939 + _CREATESECRETREQUEST._serialized_start=942 + _CREATESECRETREQUEST._serialized_end=1130 + _CREATESECRETRESPONSE._serialized_start=1133 + _CREATESECRETRESPONSE._serialized_end=1302 + _UPDATESECRETOP._serialized_start=1305 + _UPDATESECRETOP._serialized_end=1825 + _UPDATESECRETOP_UPDATESECRET._serialized_start=1572 + _UPDATESECRETOP_UPDATESECRET._serialized_end=1682 + _UPDATESECRETOP_UPDATESECRETSTATUS._serialized_start=1684 + _UPDATESECRETOP_UPDATESECRETSTATUS._serialized_end=1751 + _UPDATESECRETOP_OP._serialized_start=1753 + _UPDATESECRETOP_OP._serialized_end=1815 + _UPDATESECRETREQUEST._serialized_start=1827 + _UPDATESECRETREQUEST._serialized_end=1935 + _UPDATESECRETRESPONSE._serialized_start=1938 + _UPDATESECRETRESPONSE._serialized_end=2107 # @@protoc_insertion_point(module_scope) diff --git a/protos/secrets/api_pb2.pyi b/protos/secrets/api_pb2.pyi index 7a407d477..3e8844a25 100644 --- a/protos/secrets/api_pb2.pyi +++ b/protos/secrets/api_pb2.pyi @@ -6,12 +6,14 @@ import builtins import collections.abc import google.protobuf.descriptor import google.protobuf.internal.containers +import google.protobuf.internal.enum_type_wrapper import google.protobuf.message import google.protobuf.wrappers_pb2 import protos.base_pb2 import sys +import typing -if sys.version_info >= (3, 8): +if sys.version_info >= (3, 10): import typing as typing_extensions else: import typing_extensions @@ -23,7 +25,6 @@ class Secret(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor ID_FIELD_NUMBER: builtins.int - NAME_FIELD_NUMBER: builtins.int KEY_FIELD_NUMBER: builtins.int MASKED_VALUE_FIELD_NUMBER: builtins.int DESCRIPTION_FIELD_NUMBER: builtins.int @@ -35,8 +36,6 @@ class Secret(google.protobuf.message.Message): @property def id(self) -> google.protobuf.wrappers_pb2.StringValue: ... @property - def name(self) -> google.protobuf.wrappers_pb2.StringValue: ... - @property def key(self) -> google.protobuf.wrappers_pb2.StringValue: ... @property def masked_value(self) -> google.protobuf.wrappers_pb2.StringValue: ... @@ -53,7 +52,6 @@ class Secret(google.protobuf.message.Message): self, *, id: google.protobuf.wrappers_pb2.StringValue | None = ..., - name: google.protobuf.wrappers_pb2.StringValue | None = ..., key: google.protobuf.wrappers_pb2.StringValue | None = ..., masked_value: google.protobuf.wrappers_pb2.StringValue | None = ..., description: google.protobuf.wrappers_pb2.StringValue | None = ..., @@ -63,8 +61,8 @@ class Secret(google.protobuf.message.Message): updated_at: builtins.int = ..., is_active: builtins.bool = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["creator", b"creator", "description", b"description", "id", b"id", "key", b"key", "last_updated_by", b"last_updated_by", "masked_value", b"masked_value", "name", b"name"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["created_at", b"created_at", "creator", b"creator", "description", b"description", "id", b"id", "is_active", b"is_active", "key", b"key", "last_updated_by", b"last_updated_by", "masked_value", b"masked_value", "name", b"name", "updated_at", b"updated_at"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["creator", b"creator", "description", b"description", "id", b"id", "key", b"key", "last_updated_by", b"last_updated_by", "masked_value", b"masked_value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["created_at", b"created_at", "creator", b"creator", "description", b"description", "id", b"id", "is_active", b"is_active", "key", b"key", "last_updated_by", b"last_updated_by", "masked_value", b"masked_value", "updated_at", b"updated_at"]) -> None: ... global___Secret = Secret @@ -169,15 +167,12 @@ class CreateSecretRequest(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor META_FIELD_NUMBER: builtins.int - NAME_FIELD_NUMBER: builtins.int KEY_FIELD_NUMBER: builtins.int VALUE_FIELD_NUMBER: builtins.int DESCRIPTION_FIELD_NUMBER: builtins.int @property def meta(self) -> protos.base_pb2.Meta: ... @property - def name(self) -> google.protobuf.wrappers_pb2.StringValue: ... - @property def key(self) -> google.protobuf.wrappers_pb2.StringValue: ... @property def value(self) -> google.protobuf.wrappers_pb2.StringValue: ... @@ -187,13 +182,12 @@ class CreateSecretRequest(google.protobuf.message.Message): self, *, meta: protos.base_pb2.Meta | None = ..., - name: google.protobuf.wrappers_pb2.StringValue | None = ..., key: google.protobuf.wrappers_pb2.StringValue | None = ..., value: google.protobuf.wrappers_pb2.StringValue | None = ..., description: google.protobuf.wrappers_pb2.StringValue | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["description", b"description", "key", b"key", "meta", b"meta", "name", b"name", "value", b"value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "key", b"key", "meta", b"meta", "name", b"name", "value", b"value"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["description", b"description", "key", b"key", "meta", b"meta", "value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "key", b"key", "meta", b"meta", "value", b"value"]) -> None: ... global___CreateSecretRequest = CreateSecretRequest @@ -227,109 +221,129 @@ class CreateSecretResponse(google.protobuf.message.Message): global___CreateSecretResponse = CreateSecretResponse @typing_extensions.final -class UpdateSecretRequest(google.protobuf.message.Message): +class UpdateSecretOp(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor - META_FIELD_NUMBER: builtins.int + class _Op: + ValueType = typing.NewType("ValueType", builtins.int) + V: typing_extensions.TypeAlias = ValueType + + class _OpEnumTypeWrapper(google.protobuf.internal.enum_type_wrapper._EnumTypeWrapper[UpdateSecretOp._Op.ValueType], builtins.type): # noqa: F821 + DESCRIPTOR: google.protobuf.descriptor.EnumDescriptor + UNKNOWN: UpdateSecretOp._Op.ValueType # 0 + UPDATE_SECRET: UpdateSecretOp._Op.ValueType # 1 + UPDATE_SECRET_STATUS: UpdateSecretOp._Op.ValueType # 2 + + class Op(_Op, metaclass=_OpEnumTypeWrapper): ... + UNKNOWN: UpdateSecretOp.Op.ValueType # 0 + UPDATE_SECRET: UpdateSecretOp.Op.ValueType # 1 + UPDATE_SECRET_STATUS: UpdateSecretOp.Op.ValueType # 2 + + @typing_extensions.final + class UpdateSecret(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + DESCRIPTION_FIELD_NUMBER: builtins.int + VALUE_FIELD_NUMBER: builtins.int + @property + def description(self) -> google.protobuf.wrappers_pb2.StringValue: ... + @property + def value(self) -> google.protobuf.wrappers_pb2.StringValue: ... + def __init__( + self, + *, + description: google.protobuf.wrappers_pb2.StringValue | None = ..., + value: google.protobuf.wrappers_pb2.StringValue | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["description", b"description", "value", b"value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "value", b"value"]) -> None: ... + + @typing_extensions.final + class UpdateSecretStatus(google.protobuf.message.Message): + DESCRIPTOR: google.protobuf.descriptor.Descriptor + + IS_ACTIVE_FIELD_NUMBER: builtins.int + @property + def is_active(self) -> google.protobuf.wrappers_pb2.BoolValue: ... + def __init__( + self, + *, + is_active: google.protobuf.wrappers_pb2.BoolValue | None = ..., + ) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["is_active", b"is_active"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["is_active", b"is_active"]) -> None: ... + + OP_FIELD_NUMBER: builtins.int SECRET_ID_FIELD_NUMBER: builtins.int - NAME_FIELD_NUMBER: builtins.int - DESCRIPTION_FIELD_NUMBER: builtins.int - KEY_FIELD_NUMBER: builtins.int - @property - def meta(self) -> protos.base_pb2.Meta: ... + UPDATE_SECRET_FIELD_NUMBER: builtins.int + UPDATE_SECRET_STATUS_FIELD_NUMBER: builtins.int + op: global___UpdateSecretOp.Op.ValueType @property def secret_id(self) -> google.protobuf.wrappers_pb2.StringValue: ... @property - def name(self) -> google.protobuf.wrappers_pb2.StringValue: ... + def update_secret(self) -> global___UpdateSecretOp.UpdateSecret: ... @property - def description(self) -> google.protobuf.wrappers_pb2.StringValue: ... - @property - def key(self) -> google.protobuf.wrappers_pb2.StringValue: ... + def update_secret_status(self) -> global___UpdateSecretOp.UpdateSecretStatus: ... def __init__( self, *, - meta: protos.base_pb2.Meta | None = ..., + op: global___UpdateSecretOp.Op.ValueType = ..., secret_id: google.protobuf.wrappers_pb2.StringValue | None = ..., - name: google.protobuf.wrappers_pb2.StringValue | None = ..., - description: google.protobuf.wrappers_pb2.StringValue | None = ..., - key: google.protobuf.wrappers_pb2.StringValue | None = ..., + update_secret: global___UpdateSecretOp.UpdateSecret | None = ..., + update_secret_status: global___UpdateSecretOp.UpdateSecretStatus | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["description", b"description", "key", b"key", "meta", b"meta", "name", b"name", "secret_id", b"secret_id"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["description", b"description", "key", b"key", "meta", b"meta", "name", b"name", "secret_id", b"secret_id"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["secret_id", b"secret_id", "update", b"update", "update_secret", b"update_secret", "update_secret_status", b"update_secret_status"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["op", b"op", "secret_id", b"secret_id", "update", b"update", "update_secret", b"update_secret", "update_secret_status", b"update_secret_status"]) -> None: ... + def WhichOneof(self, oneof_group: typing_extensions.Literal["update", b"update"]) -> typing_extensions.Literal["update_secret", "update_secret_status"] | None: ... -global___UpdateSecretRequest = UpdateSecretRequest +global___UpdateSecretOp = UpdateSecretOp @typing_extensions.final -class UpdateSecretResponse(google.protobuf.message.Message): - DESCRIPTOR: google.protobuf.descriptor.Descriptor - - META_FIELD_NUMBER: builtins.int - SUCCESS_FIELD_NUMBER: builtins.int - MESSAGE_FIELD_NUMBER: builtins.int - SECRET_FIELD_NUMBER: builtins.int - @property - def meta(self) -> protos.base_pb2.Meta: ... - @property - def success(self) -> google.protobuf.wrappers_pb2.BoolValue: ... - @property - def message(self) -> protos.base_pb2.Message: ... - @property - def secret(self) -> global___Secret: ... - def __init__( - self, - *, - meta: protos.base_pb2.Meta | None = ..., - success: google.protobuf.wrappers_pb2.BoolValue | None = ..., - message: protos.base_pb2.Message | None = ..., - secret: global___Secret | None = ..., - ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["message", b"message", "meta", b"meta", "secret", b"secret", "success", b"success"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["message", b"message", "meta", b"meta", "secret", b"secret", "success", b"success"]) -> None: ... - -global___UpdateSecretResponse = UpdateSecretResponse - -@typing_extensions.final -class DeleteSecretRequest(google.protobuf.message.Message): +class UpdateSecretRequest(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor META_FIELD_NUMBER: builtins.int - SECRET_ID_FIELD_NUMBER: builtins.int + UPDATE_SECRET_OPS_FIELD_NUMBER: builtins.int @property def meta(self) -> protos.base_pb2.Meta: ... @property - def secret_id(self) -> google.protobuf.wrappers_pb2.StringValue: ... + def update_secret_ops(self) -> google.protobuf.internal.containers.RepeatedCompositeFieldContainer[global___UpdateSecretOp]: ... def __init__( self, *, meta: protos.base_pb2.Meta | None = ..., - secret_id: google.protobuf.wrappers_pb2.StringValue | None = ..., + update_secret_ops: collections.abc.Iterable[global___UpdateSecretOp] | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["meta", b"meta", "secret_id", b"secret_id"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta", "secret_id", b"secret_id"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["meta", b"meta"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta", "update_secret_ops", b"update_secret_ops"]) -> None: ... -global___DeleteSecretRequest = DeleteSecretRequest +global___UpdateSecretRequest = UpdateSecretRequest @typing_extensions.final -class DeleteSecretResponse(google.protobuf.message.Message): +class UpdateSecretResponse(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor META_FIELD_NUMBER: builtins.int SUCCESS_FIELD_NUMBER: builtins.int MESSAGE_FIELD_NUMBER: builtins.int + SECRET_FIELD_NUMBER: builtins.int @property def meta(self) -> protos.base_pb2.Meta: ... @property def success(self) -> google.protobuf.wrappers_pb2.BoolValue: ... @property def message(self) -> protos.base_pb2.Message: ... + @property + def secret(self) -> global___Secret: ... def __init__( self, *, meta: protos.base_pb2.Meta | None = ..., success: google.protobuf.wrappers_pb2.BoolValue | None = ..., message: protos.base_pb2.Message | None = ..., + secret: global___Secret | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["message", b"message", "meta", b"meta", "success", b"success"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["message", b"message", "meta", b"meta", "success", b"success"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["message", b"message", "meta", b"meta", "secret", b"secret", "success", b"success"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["message", b"message", "meta", b"meta", "secret", b"secret", "success", b"success"]) -> None: ... -global___DeleteSecretResponse = DeleteSecretResponse +global___UpdateSecretResponse = UpdateSecretResponse diff --git a/web/src/constants/api/secrets.ts b/web/src/constants/api/secrets.ts index 8e07ae9b9..2c3721bf5 100644 --- a/web/src/constants/api/secrets.ts +++ b/web/src/constants/api/secrets.ts @@ -1,6 +1,5 @@ // Secrets management -export const SECRETS_LIST = "/secrets/list"; -export const SECRET_GET = "/secrets/get"; -export const SECRET_CREATE = "/secrets/create"; -export const SECRET_UPDATE = "/secrets/update"; -export const SECRET_DELETE = "/secrets/delete"; \ No newline at end of file +export const SECRETS_LIST = "/executor/secrets/list"; +export const SECRET_GET = "/executor/secrets/get"; +export const SECRET_CREATE = "/executor/secrets/create"; +export const SECRET_UPDATE = "/executor/secrets/update"; From 90cdd8f4d6cbcf0c639e43d3984766541022d68f Mon Sep 17 00:00:00 2001 From: jayeshsadhwani99 Date: Wed, 5 Mar 2025 15:48:13 +0530 Subject: [PATCH 11/16] adds all apis and secrets --- .../components/secret-management/SecretActionOverlay.tsx | 2 +- .../secret-management/create/CreateSecretForm.tsx | 2 ++ web/src/constants/api/secrets.ts | 9 +++++---- web/src/pages/secret-management/hooks/useSecretsData.tsx | 8 ++++---- web/src/store/features/secrets/api/createSecretApi.ts | 4 +--- web/src/store/features/secrets/api/deleteSecretApi.ts | 6 +++--- web/src/store/features/secrets/api/getSecretApi.ts | 4 ++-- web/src/store/features/secrets/api/updateSecretApi.ts | 8 +++----- 8 files changed, 21 insertions(+), 22 deletions(-) diff --git a/web/src/components/secret-management/SecretActionOverlay.tsx b/web/src/components/secret-management/SecretActionOverlay.tsx index 875744806..681c216dc 100644 --- a/web/src/components/secret-management/SecretActionOverlay.tsx +++ b/web/src/components/secret-management/SecretActionOverlay.tsx @@ -30,7 +30,7 @@ const SecretActionOverlay = ({ {isOpen && (
-
+
Delete {secret.name}?
diff --git a/web/src/components/secret-management/create/CreateSecretForm.tsx b/web/src/components/secret-management/create/CreateSecretForm.tsx index 919df9c45..c70933631 100644 --- a/web/src/components/secret-management/create/CreateSecretForm.tsx +++ b/web/src/components/secret-management/create/CreateSecretForm.tsx @@ -40,6 +40,7 @@ function CreateSecretForm({ }, { skip: !id, + refetchOnMountOrArgChange: true, }, ); @@ -137,6 +138,7 @@ function CreateSecretForm({ handleChange={(value) => handleChange("key", value)} containerClassName="!w-full" className="w-full" + disabled={id !== undefined} /> { const [selectedSecret, setSelectedSecret] = useState({}); const [selectedId, setSelectedId] = useState(""); - const handleDeleteSecret = (variable: any) => { - setSelectedSecret(variable); + const handleDeleteSecret = (secret: any) => { + setSelectedSecret(secret); toggle(); }; - const handleUpdateVariable = (variable: any) => { - setSelectedId(variable.id); + const handleUpdateVariable = (secret: any) => { + setSelectedId(secret.id); toggleConfig(); }; diff --git a/web/src/store/features/secrets/api/createSecretApi.ts b/web/src/store/features/secrets/api/createSecretApi.ts index aa9bdeca9..7c921f9e9 100644 --- a/web/src/store/features/secrets/api/createSecretApi.ts +++ b/web/src/store/features/secrets/api/createSecretApi.ts @@ -7,9 +7,7 @@ export const createSecretApi = apiSlice.injectEndpoints({ createSecret: builder.mutation>({ query: (body) => ({ url: SECRET_CREATE, - body: { - secret: body, - }, + body, method: "POST", }), invalidatesTags: ["Secrets"], diff --git a/web/src/store/features/secrets/api/deleteSecretApi.ts b/web/src/store/features/secrets/api/deleteSecretApi.ts index 2ef9c2f15..9ff7fa5de 100644 --- a/web/src/store/features/secrets/api/deleteSecretApi.ts +++ b/web/src/store/features/secrets/api/deleteSecretApi.ts @@ -8,11 +8,11 @@ export const deleteSecretApi = apiSlice.injectEndpoints({ url: SECRET_DELETE, method: "POST", body: { - secret_id: id, update_secret_ops: [ { - op: "UPDATE_STATUS", - update_status: { + secret_id: id, + op: "UPDATE_SECRET_STATUS", + update_secret_status: { is_active: false, }, }, diff --git a/web/src/store/features/secrets/api/getSecretApi.ts b/web/src/store/features/secrets/api/getSecretApi.ts index 3b227f6b4..d2c7d76ad 100644 --- a/web/src/store/features/secrets/api/getSecretApi.ts +++ b/web/src/store/features/secrets/api/getSecretApi.ts @@ -22,7 +22,7 @@ export const getSecretApi = apiSlice.injectEndpoints({ dispatch( setSecretKey({ key: "key", - value: data?.name ?? "", + value: data?.key ?? "", }), ); dispatch( @@ -34,7 +34,7 @@ export const getSecretApi = apiSlice.injectEndpoints({ dispatch( setSecretKey({ key: "value", - value: (data?.options ?? []).join(", "), + value: (data?.value ?? []).join(", "), }), ); } catch (error) { diff --git a/web/src/store/features/secrets/api/updateSecretApi.ts b/web/src/store/features/secrets/api/updateSecretApi.ts index 75be4828d..6939484fe 100644 --- a/web/src/store/features/secrets/api/updateSecretApi.ts +++ b/web/src/store/features/secrets/api/updateSecretApi.ts @@ -8,15 +8,13 @@ export const updateSecretApi = apiSlice.injectEndpoints({ query: (secret) => ({ url: SECRET_UPDATE, body: { - secret_id: secret.id, update_secret_ops: [ { op: "UPDATE_SECRET", + secret_id: secret.id, update_secret: { - secret: { - value: secret.value, - description: secret.description, - }, + value: secret.value, + description: secret.description, }, }, ], From d31e4248197ea2b529098a4ebed7d4a87836a719 Mon Sep 17 00:00:00 2001 From: jayeshsadhwani99 Date: Wed, 5 Mar 2025 15:49:21 +0530 Subject: [PATCH 12/16] remove str from the model --- executor/models.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/executor/models.py b/executor/models.py index 5d963bf78..7328b6bb6 100644 --- a/executor/models.py +++ b/executor/models.py @@ -696,7 +696,4 @@ class Meta: ] indexes = [ models.Index(fields=['key', 'account', 'is_active']), - ] - - def __str__(self): - return f"{self.account.name}:{self.key}" + ] \ No newline at end of file From f7e51623eed5e1e79eb941a0386173e19784b380 Mon Sep 17 00:00:00 2001 From: droid-dhruv Date: Wed, 5 Mar 2025 17:54:51 +0530 Subject: [PATCH 13/16] Preliminary Working version of secret management with basic validation and resolution --- ...ret_executor_se_key_60cc82_idx_and_more.py | 4 +- executor/models.py | 2 +- executor/secrets/views.py | 69 +++++++++++++++---- protos/secrets/api.proto | 4 +- protos/secrets/api_pb2.py | 52 +++++++------- protos/secrets/api_pb2.pyi | 24 +++++-- .../features/secrets/api/getSecretsListApi.ts | 6 +- 7 files changed, 108 insertions(+), 53 deletions(-) diff --git a/executor/migrations/0046_secret_secret_executor_se_key_60cc82_idx_and_more.py b/executor/migrations/0046_secret_secret_executor_se_key_60cc82_idx_and_more.py index 879729d58..4b30a092d 100644 --- a/executor/migrations/0046_secret_secret_executor_se_key_60cc82_idx_and_more.py +++ b/executor/migrations/0046_secret_secret_executor_se_key_60cc82_idx_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.13 on 2025-03-05 09:24 +# Generated by Django 4.1.13 on 2025-03-05 11:35 from django.conf import settings from django.db import migrations, models @@ -27,7 +27,7 @@ class Migration(migrations.Migration): ('is_active', models.BooleanField(default=True)), ('description', models.TextField(blank=True)), ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.account')), - ('creator', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_secrets', to=settings.AUTH_USER_MODEL)), + ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_secrets', to=settings.AUTH_USER_MODEL)), ('last_updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_secrets', to=settings.AUTH_USER_MODEL)), ], ), diff --git a/executor/models.py b/executor/models.py index 7328b6bb6..c2ee6c1b1 100644 --- a/executor/models.py +++ b/executor/models.py @@ -679,7 +679,7 @@ class Secret(models.Model): key = models.CharField(max_length=255, help_text="Reference key for the secret") value = EncryptedTextField() account = models.ForeignKey('accounts.Account', on_delete=models.CASCADE) - creator = models.ForeignKey('accounts.User', on_delete=models.SET_NULL, null=True, related_name='created_secrets') + created_by = models.ForeignKey('accounts.User', on_delete=models.SET_NULL, null=True, related_name='created_secrets') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) last_updated_by = models.ForeignKey('accounts.User', on_delete=models.SET_NULL, null=True, related_name='updated_secrets') diff --git a/executor/secrets/views.py b/executor/secrets/views.py index f832079c5..6295ef6ad 100644 --- a/executor/secrets/views.py +++ b/executor/secrets/views.py @@ -10,14 +10,13 @@ from playbooks.utils.decorators import web_api from playbooks.utils.meta import get_meta from playbooks.utils.queryset import filter_page -from protos.base_pb2 import Message +from protos.base_pb2 import Message, Meta, Page from protos.secrets.api_pb2 import ( GetSecretsRequest, GetSecretsResponse, GetSecretRequest, GetSecretResponse, CreateSecretRequest, CreateSecretResponse, UpdateSecretRequest, UpdateSecretResponse, Secret as SecretProto, - UpdateSecretOp ) logger = logging.getLogger(__name__) @@ -37,27 +36,60 @@ def _secret_to_proto(secret: Secret) -> SecretProto: key=StringValue(value=secret.key), masked_value=StringValue(value=_mask_secret_value(secret.value)), description=StringValue(value=secret.description or ""), - creator=StringValue(value=secret.creator.email if secret.creator else ""), + created_by=StringValue(value=secret.created_by.email if secret.created_by else ""), last_updated_by=StringValue(value=secret.last_updated_by.email if secret.last_updated_by else ""), created_at=int(secret.created_at.replace(tzinfo=timezone.utc).timestamp()) if secret.created_at else 0, updated_at=int(secret.updated_at.replace(tzinfo=timezone.utc).timestamp()) if secret.updated_at else 0, is_active=secret.is_active ) +def _secret_to_proto_partial(secret: Secret) -> SecretProto: + """Convert a Secret model to a partial Secret proto (for list views)""" + return SecretProto( + id=StringValue(value=str(secret.id)), + key=StringValue(value=secret.key), + description=StringValue(value=secret.description or ""), + created_by=StringValue(value=secret.created_by.email if secret.created_by else ""), + created_at=int(secret.created_at.replace(tzinfo=timezone.utc).timestamp()) if secret.created_at else 0, + is_active=secret.is_active + ) + + @web_api(GetSecretsRequest) def secrets_list(request_message: GetSecretsRequest) -> Union[GetSecretsResponse, HttpResponse]: - """List all active secrets for the current account""" + """List secrets with optional filtering by IDs or key""" account: Account = get_request_account() - - # Get all active secrets for this account + meta: Meta = request_message.meta + show_inactive = meta.show_inactive + page: Page = meta.page + list_all = True + + # Base queryset qs = Secret.objects.filter(account=account, is_active=True) - - # Apply pagination + + # Filter by specific IDs if provided + if request_message.secret_ids: + qs = qs.filter(id__in=request_message.secret_ids) + list_all = False + # Filter by key if provided + if request_message.key: + qs = qs.filter(key__icontains=request_message.key.value.lower()) + list_all = False + # Otherwise filter by active status unless show_inactive is True + elif not show_inactive or not show_inactive.value: + qs = qs.filter(is_active=True) + total_count = qs.count() - page = request_message.meta.page - secrets = [_secret_to_proto(secret) for secret in filter_page(qs.order_by("-created_at"), page)] - + qs = qs.order_by('-created_at') + qs = filter_page(qs, page) + + # Use proto or proto_partial based on list_all flag + if list_all: + secrets = [_secret_to_proto_partial(secret) for secret in qs] + else: + secrets = [_secret_to_proto(secret) for secret in qs] + return GetSecretsResponse( meta=get_meta(page=page, total_count=total_count), success=BoolValue(value=True), @@ -108,7 +140,16 @@ def secret_create(request_message: CreateSecretRequest) -> Union[CreateSecretRes return CreateSecretResponse( meta=get_meta(), success=BoolValue(value=False), - message=Message(title="Invalid Request", description="Key, and value are required") + message=Message(title="Invalid Request", description="Key and value are required") + ) + + # Validate key format -> (single word, no spaces, only alphanumeric and underscores) + if not key.isalnum() or ' ' in key: + return CreateSecretResponse( + meta=get_meta(), + success=BoolValue(value=False), + message=Message(title="Invalid Key", + description="Key must be a single word containing only letters, numbers, and underscores") ) # Check if key already exists for this account @@ -126,7 +167,7 @@ def secret_create(request_message: CreateSecretRequest) -> Union[CreateSecretRes key=key, value=value, description=description, - creator=user, + created_by=user, last_updated_by=user, is_active=True ) @@ -219,4 +260,4 @@ def secret_update(request_message: UpdateSecretRequest) -> Union[UpdateSecretRes meta=get_meta(), success=BoolValue(value=False), message=Message(title="Error", description="Failed to update secret") - ) \ No newline at end of file + ) diff --git a/protos/secrets/api.proto b/protos/secrets/api.proto index 5385fa80b..db3d9e521 100644 --- a/protos/secrets/api.proto +++ b/protos/secrets/api.proto @@ -9,7 +9,7 @@ message Secret { google.protobuf.StringValue key = 2; google.protobuf.StringValue masked_value = 3; google.protobuf.StringValue description = 4; - google.protobuf.StringValue creator = 5; + google.protobuf.StringValue created_by = 5; google.protobuf.StringValue last_updated_by = 6; int64 created_at = 7; int64 updated_at = 8; @@ -18,6 +18,8 @@ message Secret { message GetSecretsRequest { Meta meta = 1; + repeated string secret_ids = 2; // Optional list of secret IDs to fetch + google.protobuf.StringValue key = 3; // Optional key to filter by (key_name) } message GetSecretsResponse { diff --git a/protos/secrets/api_pb2.py b/protos/secrets/api_pb2.py index 7a06ceaf3..a8bc26d84 100644 --- a/protos/secrets/api_pb2.py +++ b/protos/secrets/api_pb2.py @@ -15,7 +15,7 @@ from protos import base_pb2 as protos_dot_base__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18protos/secrets/api.proto\x12\x0eprotos.secrets\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x11protos/base.proto\"\xe5\x02\n\x06Secret\x12(\n\x02id\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12)\n\x03key\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x32\n\x0cmasked_value\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12-\n\x07\x63reator\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x35\n\x0flast_updated_by\x18\x06 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x12\n\ncreated_at\x18\x07 \x01(\x03\x12\x12\n\nupdated_at\x18\x08 \x01(\x03\x12\x11\n\tis_active\x18\t \x01(\x08\"/\n\x11GetSecretsRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\"\xa8\x01\n\x12GetSecretsResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12\'\n\x07secrets\x18\x04 \x03(\x0b\x32\x16.protos.secrets.Secret\"_\n\x10GetSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa6\x01\n\x11GetSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"\xbc\x01\n\x13\x43reateSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12)\n\x03key\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12+\n\x05value\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa9\x01\n\x14\x43reateSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"\x88\x04\n\x0eUpdateSecretOp\x12-\n\x02op\x18\x01 \x01(\x0e\x32!.protos.secrets.UpdateSecretOp.Op\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x44\n\rupdate_secret\x18\x03 \x01(\x0b\x32+.protos.secrets.UpdateSecretOp.UpdateSecretH\x00\x12Q\n\x14update_secret_status\x18\x04 \x01(\x0b\x32\x31.protos.secrets.UpdateSecretOp.UpdateSecretStatusH\x00\x1an\n\x0cUpdateSecret\x12\x31\n\x0b\x64\x65scription\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12+\n\x05value\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x1a\x43\n\x12UpdateSecretStatus\x12-\n\tis_active\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\">\n\x02Op\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x11\n\rUPDATE_SECRET\x10\x01\x12\x18\n\x14UPDATE_SECRET_STATUS\x10\x02\x42\x08\n\x06update\"l\n\x13UpdateSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12\x39\n\x11update_secret_ops\x18\x02 \x03(\x0b\x32\x1e.protos.secrets.UpdateSecretOp\"\xa9\x01\n\x14UpdateSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secretb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18protos/secrets/api.proto\x12\x0eprotos.secrets\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x11protos/base.proto\"\xe8\x02\n\x06Secret\x12(\n\x02id\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12)\n\x03key\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x32\n\x0cmasked_value\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x30\n\ncreated_by\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x35\n\x0flast_updated_by\x18\x06 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x12\n\ncreated_at\x18\x07 \x01(\x03\x12\x12\n\nupdated_at\x18\x08 \x01(\x03\x12\x11\n\tis_active\x18\t \x01(\x08\"n\n\x11GetSecretsRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12\x12\n\nsecret_ids\x18\x02 \x03(\t\x12)\n\x03key\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa8\x01\n\x12GetSecretsResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12\'\n\x07secrets\x18\x04 \x03(\x0b\x32\x16.protos.secrets.Secret\"_\n\x10GetSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa6\x01\n\x11GetSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"\xbc\x01\n\x13\x43reateSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12)\n\x03key\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12+\n\x05value\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa9\x01\n\x14\x43reateSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"\x88\x04\n\x0eUpdateSecretOp\x12-\n\x02op\x18\x01 \x01(\x0e\x32!.protos.secrets.UpdateSecretOp.Op\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x44\n\rupdate_secret\x18\x03 \x01(\x0b\x32+.protos.secrets.UpdateSecretOp.UpdateSecretH\x00\x12Q\n\x14update_secret_status\x18\x04 \x01(\x0b\x32\x31.protos.secrets.UpdateSecretOp.UpdateSecretStatusH\x00\x1an\n\x0cUpdateSecret\x12\x31\n\x0b\x64\x65scription\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12+\n\x05value\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x1a\x43\n\x12UpdateSecretStatus\x12-\n\tis_active\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\">\n\x02Op\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x11\n\rUPDATE_SECRET\x10\x01\x12\x18\n\x14UPDATE_SECRET_STATUS\x10\x02\x42\x08\n\x06update\"l\n\x13UpdateSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12\x39\n\x11update_secret_ops\x18\x02 \x03(\x0b\x32\x1e.protos.secrets.UpdateSecretOp\"\xa9\x01\n\x14UpdateSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secretb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'protos.secrets.api_pb2', globals()) @@ -23,29 +23,29 @@ DESCRIPTOR._options = None _SECRET._serialized_start=96 - _SECRET._serialized_end=453 - _GETSECRETSREQUEST._serialized_start=455 - _GETSECRETSREQUEST._serialized_end=502 - _GETSECRETSRESPONSE._serialized_start=505 - _GETSECRETSRESPONSE._serialized_end=673 - _GETSECRETREQUEST._serialized_start=675 - _GETSECRETREQUEST._serialized_end=770 - _GETSECRETRESPONSE._serialized_start=773 - _GETSECRETRESPONSE._serialized_end=939 - _CREATESECRETREQUEST._serialized_start=942 - _CREATESECRETREQUEST._serialized_end=1130 - _CREATESECRETRESPONSE._serialized_start=1133 - _CREATESECRETRESPONSE._serialized_end=1302 - _UPDATESECRETOP._serialized_start=1305 - _UPDATESECRETOP._serialized_end=1825 - _UPDATESECRETOP_UPDATESECRET._serialized_start=1572 - _UPDATESECRETOP_UPDATESECRET._serialized_end=1682 - _UPDATESECRETOP_UPDATESECRETSTATUS._serialized_start=1684 - _UPDATESECRETOP_UPDATESECRETSTATUS._serialized_end=1751 - _UPDATESECRETOP_OP._serialized_start=1753 - _UPDATESECRETOP_OP._serialized_end=1815 - _UPDATESECRETREQUEST._serialized_start=1827 - _UPDATESECRETREQUEST._serialized_end=1935 - _UPDATESECRETRESPONSE._serialized_start=1938 - _UPDATESECRETRESPONSE._serialized_end=2107 + _SECRET._serialized_end=456 + _GETSECRETSREQUEST._serialized_start=458 + _GETSECRETSREQUEST._serialized_end=568 + _GETSECRETSRESPONSE._serialized_start=571 + _GETSECRETSRESPONSE._serialized_end=739 + _GETSECRETREQUEST._serialized_start=741 + _GETSECRETREQUEST._serialized_end=836 + _GETSECRETRESPONSE._serialized_start=839 + _GETSECRETRESPONSE._serialized_end=1005 + _CREATESECRETREQUEST._serialized_start=1008 + _CREATESECRETREQUEST._serialized_end=1196 + _CREATESECRETRESPONSE._serialized_start=1199 + _CREATESECRETRESPONSE._serialized_end=1368 + _UPDATESECRETOP._serialized_start=1371 + _UPDATESECRETOP._serialized_end=1891 + _UPDATESECRETOP_UPDATESECRET._serialized_start=1638 + _UPDATESECRETOP_UPDATESECRET._serialized_end=1748 + _UPDATESECRETOP_UPDATESECRETSTATUS._serialized_start=1750 + _UPDATESECRETOP_UPDATESECRETSTATUS._serialized_end=1817 + _UPDATESECRETOP_OP._serialized_start=1819 + _UPDATESECRETOP_OP._serialized_end=1881 + _UPDATESECRETREQUEST._serialized_start=1893 + _UPDATESECRETREQUEST._serialized_end=2001 + _UPDATESECRETRESPONSE._serialized_start=2004 + _UPDATESECRETRESPONSE._serialized_end=2173 # @@protoc_insertion_point(module_scope) diff --git a/protos/secrets/api_pb2.pyi b/protos/secrets/api_pb2.pyi index 3e8844a25..3770aa9b0 100644 --- a/protos/secrets/api_pb2.pyi +++ b/protos/secrets/api_pb2.pyi @@ -28,7 +28,7 @@ class Secret(google.protobuf.message.Message): KEY_FIELD_NUMBER: builtins.int MASKED_VALUE_FIELD_NUMBER: builtins.int DESCRIPTION_FIELD_NUMBER: builtins.int - CREATOR_FIELD_NUMBER: builtins.int + CREATED_BY_FIELD_NUMBER: builtins.int LAST_UPDATED_BY_FIELD_NUMBER: builtins.int CREATED_AT_FIELD_NUMBER: builtins.int UPDATED_AT_FIELD_NUMBER: builtins.int @@ -42,7 +42,7 @@ class Secret(google.protobuf.message.Message): @property def description(self) -> google.protobuf.wrappers_pb2.StringValue: ... @property - def creator(self) -> google.protobuf.wrappers_pb2.StringValue: ... + def created_by(self) -> google.protobuf.wrappers_pb2.StringValue: ... @property def last_updated_by(self) -> google.protobuf.wrappers_pb2.StringValue: ... created_at: builtins.int @@ -55,14 +55,14 @@ class Secret(google.protobuf.message.Message): key: google.protobuf.wrappers_pb2.StringValue | None = ..., masked_value: google.protobuf.wrappers_pb2.StringValue | None = ..., description: google.protobuf.wrappers_pb2.StringValue | None = ..., - creator: google.protobuf.wrappers_pb2.StringValue | None = ..., + created_by: google.protobuf.wrappers_pb2.StringValue | None = ..., last_updated_by: google.protobuf.wrappers_pb2.StringValue | None = ..., created_at: builtins.int = ..., updated_at: builtins.int = ..., is_active: builtins.bool = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["creator", b"creator", "description", b"description", "id", b"id", "key", b"key", "last_updated_by", b"last_updated_by", "masked_value", b"masked_value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["created_at", b"created_at", "creator", b"creator", "description", b"description", "id", b"id", "is_active", b"is_active", "key", b"key", "last_updated_by", b"last_updated_by", "masked_value", b"masked_value", "updated_at", b"updated_at"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["created_by", b"created_by", "description", b"description", "id", b"id", "key", b"key", "last_updated_by", b"last_updated_by", "masked_value", b"masked_value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["created_at", b"created_at", "created_by", b"created_by", "description", b"description", "id", b"id", "is_active", b"is_active", "key", b"key", "last_updated_by", b"last_updated_by", "masked_value", b"masked_value", "updated_at", b"updated_at"]) -> None: ... global___Secret = Secret @@ -71,15 +71,25 @@ class GetSecretsRequest(google.protobuf.message.Message): DESCRIPTOR: google.protobuf.descriptor.Descriptor META_FIELD_NUMBER: builtins.int + SECRET_IDS_FIELD_NUMBER: builtins.int + KEY_FIELD_NUMBER: builtins.int @property def meta(self) -> protos.base_pb2.Meta: ... + @property + def secret_ids(self) -> google.protobuf.internal.containers.RepeatedScalarFieldContainer[builtins.str]: + """Optional list of secret IDs to fetch""" + @property + def key(self) -> google.protobuf.wrappers_pb2.StringValue: + """Optional key to filter by (key_name)""" def __init__( self, *, meta: protos.base_pb2.Meta | None = ..., + secret_ids: collections.abc.Iterable[builtins.str] | None = ..., + key: google.protobuf.wrappers_pb2.StringValue | None = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["meta", b"meta"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["meta", b"meta"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["key", b"key", "meta", b"meta"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["key", b"key", "meta", b"meta", "secret_ids", b"secret_ids"]) -> None: ... global___GetSecretsRequest = GetSecretsRequest diff --git a/web/src/store/features/secrets/api/getSecretsListApi.ts b/web/src/store/features/secrets/api/getSecretsListApi.ts index 4777b550f..a10bca02b 100644 --- a/web/src/store/features/secrets/api/getSecretsListApi.ts +++ b/web/src/store/features/secrets/api/getSecretsListApi.ts @@ -10,10 +10,12 @@ export const getSecretsListApi = apiSlice.injectEndpoints({ key?: string; } >({ - query: () => ({ + query: ({key}) => ({ url: SECRETS_LIST, method: "POST", - body: {}, + body: { + key + }, }), providesTags: ["Secrets"], }), From 26d01179eab90a4b4615fc29e80c8ac19f0401b0 Mon Sep 17 00:00:00 2001 From: droid-dhruv Date: Wed, 5 Mar 2025 23:10:19 +0530 Subject: [PATCH 14/16] fixes and clean up --- executor/secrets/__init__.py | 1 - executor/secrets/crud/secrets_update_processor.py | 9 ++------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/executor/secrets/__init__.py b/executor/secrets/__init__.py index eb0a76e96..e69de29bb 100644 --- a/executor/secrets/__init__.py +++ b/executor/secrets/__init__.py @@ -1 +0,0 @@ -# This file is intentionally left empty to make the directory a Python package \ No newline at end of file diff --git a/executor/secrets/crud/secrets_update_processor.py b/executor/secrets/crud/secrets_update_processor.py index 287a0d18f..7d6febef7 100644 --- a/executor/secrets/crud/secrets_update_processor.py +++ b/executor/secrets/crud/secrets_update_processor.py @@ -25,7 +25,6 @@ def update_secret(elem: Secret, update_op: UpdateSecretOp.UpdateSecret) -> Secre elem.value = update_op.value.value update_fields.append('value') - logger.info(f"Updating secret {elem.key} with fields: {update_fields}, {len(update_fields)}") if len(update_fields) > 1: # Only save if we have fields to update try: elem.save(update_fields=update_fields) @@ -41,14 +40,10 @@ def update_secret_status(elem: Secret, update_op: UpdateSecretOp.UpdateSecretSta is_active = update_op.is_active.value # Can't reactivate a secret that's been deactivated - if not elem.is_active and is_active: + if not elem.is_active: raise Exception(f"Secret {elem.key} cannot be reactivated") - # No change needed - if elem.is_active == is_active: - return elem - - elem.is_active = is_active + elem.is_active = False try: elem.save(update_fields=['is_active', 'updated_at']) except Exception as ex: From 57b9ab8fdb86a6155c2bb945e62c4d5cee9ba3ba Mon Sep 17 00:00:00 2001 From: droid-dhruv Date: Wed, 5 Mar 2025 23:45:18 +0530 Subject: [PATCH 15/16] clean up and removal of unneeded fields --- ...ret_executor_se_key_60cc82_idx_and_more.py | 5 +- executor/models.py | 1 - executor/secrets/secret_resolver.py | 7 ++- executor/secrets/views.py | 26 ++-------- protos/secrets/api.proto | 7 ++- protos/secrets/api_pb2.py | 52 +++++++++---------- protos/secrets/api_pb2.pyi | 8 +-- 7 files changed, 39 insertions(+), 67 deletions(-) diff --git a/executor/migrations/0046_secret_secret_executor_se_key_60cc82_idx_and_more.py b/executor/migrations/0046_secret_secret_executor_se_key_60cc82_idx_and_more.py index 4b30a092d..d95095267 100644 --- a/executor/migrations/0046_secret_secret_executor_se_key_60cc82_idx_and_more.py +++ b/executor/migrations/0046_secret_secret_executor_se_key_60cc82_idx_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 4.1.13 on 2025-03-05 11:35 +# Generated by Django 4.1.13 on 2025-03-05 18:05 from django.conf import settings from django.db import migrations, models @@ -10,8 +10,8 @@ class Migration(migrations.Migration): dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('accounts', '0003_accountuseroauth2sessioncodestore'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), ('executor', '0045_upgrade_step_relation_conditions'), ] @@ -28,7 +28,6 @@ class Migration(migrations.Migration): ('description', models.TextField(blank=True)), ('account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='accounts.account')), ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_secrets', to=settings.AUTH_USER_MODEL)), - ('last_updated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_secrets', to=settings.AUTH_USER_MODEL)), ], ), migrations.AddIndex( diff --git a/executor/models.py b/executor/models.py index c2ee6c1b1..8efa6a896 100644 --- a/executor/models.py +++ b/executor/models.py @@ -682,7 +682,6 @@ class Secret(models.Model): created_by = models.ForeignKey('accounts.User', on_delete=models.SET_NULL, null=True, related_name='created_secrets') created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) - last_updated_by = models.ForeignKey('accounts.User', on_delete=models.SET_NULL, null=True, related_name='updated_secrets') is_active = models.BooleanField(default=True) description = models.TextField(blank=True) diff --git a/executor/secrets/secret_resolver.py b/executor/secrets/secret_resolver.py index 7e9dde288..a360ca002 100644 --- a/executor/secrets/secret_resolver.py +++ b/executor/secrets/secret_resolver.py @@ -37,11 +37,10 @@ def resolve_secrets(cls, form_fields: [FormField], account_id: int, source_type_ Dict with secrets resolved """ try: - # Get fields that might contain secrets + # Collect all secret references string_fields = [ff.key_name.value for ff in form_fields if ff.data_type == LiteralType.STRING] string_array_fields = [ff.key_name.value for ff in form_fields if ff.data_type == LiteralType.STRING_ARRAY] - # Collect all secret references secret_refs = set() for field_name, value in source_type_task_def.items(): if field_name in string_fields: @@ -64,10 +63,10 @@ def resolve_secrets(cls, form_fields: [FormField], account_id: int, source_type_ logger.warning(f"No secrets found for given references") return source_type_task_def - # Create mapping of secret keys to values + # Create mapping secret_map = {s['key']: s['value'] for s in secrets} - # Replace secret references with values + # Final resolution resolved_def = source_type_task_def.copy() for field_name, value in source_type_task_def.items(): if field_name in string_fields: diff --git a/executor/secrets/views.py b/executor/secrets/views.py index 6295ef6ad..d65af2efa 100644 --- a/executor/secrets/views.py +++ b/executor/secrets/views.py @@ -18,12 +18,12 @@ UpdateSecretRequest, UpdateSecretResponse, Secret as SecretProto, ) +from executor.secrets.crud.secrets_update_processor import secrets_update_processor logger = logging.getLogger(__name__) def _mask_secret_value(value): - """Mask the secret value, showing only the first and last characters""" if not value or len(value) <= 8: return "••••••••" return value[:2] + "••••••" + value[-2:] @@ -37,7 +37,6 @@ def _secret_to_proto(secret: Secret) -> SecretProto: masked_value=StringValue(value=_mask_secret_value(secret.value)), description=StringValue(value=secret.description or ""), created_by=StringValue(value=secret.created_by.email if secret.created_by else ""), - last_updated_by=StringValue(value=secret.last_updated_by.email if secret.last_updated_by else ""), created_at=int(secret.created_at.replace(tzinfo=timezone.utc).timestamp()) if secret.created_at else 0, updated_at=int(secret.updated_at.replace(tzinfo=timezone.utc).timestamp()) if secret.updated_at else 0, is_active=secret.is_active @@ -143,7 +142,6 @@ def secret_create(request_message: CreateSecretRequest) -> Union[CreateSecretRes message=Message(title="Invalid Request", description="Key and value are required") ) - # Validate key format -> (single word, no spaces, only alphanumeric and underscores) if not key.isalnum() or ' ' in key: return CreateSecretResponse( meta=get_meta(), @@ -152,7 +150,6 @@ def secret_create(request_message: CreateSecretRequest) -> Union[CreateSecretRes description="Key must be a single word containing only letters, numbers, and underscores") ) - # Check if key already exists for this account if Secret.objects.filter(account=account, key=key, is_active=True).exists(): return CreateSecretResponse( meta=get_meta(), @@ -168,10 +165,9 @@ def secret_create(request_message: CreateSecretRequest) -> Union[CreateSecretRes value=value, description=description, created_by=user, - last_updated_by=user, is_active=True ) - + return CreateSecretResponse( meta=get_meta(), success=BoolValue(value=True), @@ -191,7 +187,7 @@ def secret_create(request_message: CreateSecretRequest) -> Union[CreateSecretRes def secret_update(request_message: UpdateSecretRequest) -> Union[UpdateSecretResponse, HttpResponse]: """Update a secret using operations""" account: Account = get_request_account() - user = get_request_user() + _user = get_request_user() update_secret_ops = request_message.update_secret_ops @@ -215,20 +211,8 @@ def secret_update(request_message: UpdateSecretRequest) -> Union[UpdateSecretRes try: secret = Secret.objects.get(id=secret_id, account=account, is_active=True) - - # Store the original user for later restoration - original_last_updated_by = secret.last_updated_by - - # Set the user who is making the update - secret.last_updated_by = user - secret.save(update_fields=['last_updated_by']) - try: - # Apply all update operations - from executor.secrets.crud.secrets_update_processor import secrets_update_processor secrets_update_processor.update(secret, update_secret_ops) - - # Get the updated secret updated_secret = Secret.objects.get(id=secret_id) return UpdateSecretResponse( @@ -238,10 +222,6 @@ def secret_update(request_message: UpdateSecretRequest) -> Union[UpdateSecretRes secret=_secret_to_proto(updated_secret) ) except Exception as e: - # Restore the original user if update fails - secret.last_updated_by = original_last_updated_by - secret.save(update_fields=['last_updated_by']) - logger.error(f"Error updating secret: {str(e)}") return UpdateSecretResponse( meta=get_meta(), diff --git a/protos/secrets/api.proto b/protos/secrets/api.proto index db3d9e521..07835a42c 100644 --- a/protos/secrets/api.proto +++ b/protos/secrets/api.proto @@ -10,10 +10,9 @@ message Secret { google.protobuf.StringValue masked_value = 3; google.protobuf.StringValue description = 4; google.protobuf.StringValue created_by = 5; - google.protobuf.StringValue last_updated_by = 6; - int64 created_at = 7; - int64 updated_at = 8; - bool is_active = 9; + int64 created_at = 6; + int64 updated_at = 7; + bool is_active = 8; } message GetSecretsRequest { diff --git a/protos/secrets/api_pb2.py b/protos/secrets/api_pb2.py index a8bc26d84..292789871 100644 --- a/protos/secrets/api_pb2.py +++ b/protos/secrets/api_pb2.py @@ -15,7 +15,7 @@ from protos import base_pb2 as protos_dot_base__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18protos/secrets/api.proto\x12\x0eprotos.secrets\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x11protos/base.proto\"\xe8\x02\n\x06Secret\x12(\n\x02id\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12)\n\x03key\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x32\n\x0cmasked_value\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x30\n\ncreated_by\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x35\n\x0flast_updated_by\x18\x06 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x12\n\ncreated_at\x18\x07 \x01(\x03\x12\x12\n\nupdated_at\x18\x08 \x01(\x03\x12\x11\n\tis_active\x18\t \x01(\x08\"n\n\x11GetSecretsRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12\x12\n\nsecret_ids\x18\x02 \x03(\t\x12)\n\x03key\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa8\x01\n\x12GetSecretsResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12\'\n\x07secrets\x18\x04 \x03(\x0b\x32\x16.protos.secrets.Secret\"_\n\x10GetSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa6\x01\n\x11GetSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"\xbc\x01\n\x13\x43reateSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12)\n\x03key\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12+\n\x05value\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa9\x01\n\x14\x43reateSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"\x88\x04\n\x0eUpdateSecretOp\x12-\n\x02op\x18\x01 \x01(\x0e\x32!.protos.secrets.UpdateSecretOp.Op\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x44\n\rupdate_secret\x18\x03 \x01(\x0b\x32+.protos.secrets.UpdateSecretOp.UpdateSecretH\x00\x12Q\n\x14update_secret_status\x18\x04 \x01(\x0b\x32\x31.protos.secrets.UpdateSecretOp.UpdateSecretStatusH\x00\x1an\n\x0cUpdateSecret\x12\x31\n\x0b\x64\x65scription\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12+\n\x05value\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x1a\x43\n\x12UpdateSecretStatus\x12-\n\tis_active\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\">\n\x02Op\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x11\n\rUPDATE_SECRET\x10\x01\x12\x18\n\x14UPDATE_SECRET_STATUS\x10\x02\x42\x08\n\x06update\"l\n\x13UpdateSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12\x39\n\x11update_secret_ops\x18\x02 \x03(\x0b\x32\x1e.protos.secrets.UpdateSecretOp\"\xa9\x01\n\x14UpdateSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secretb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x18protos/secrets/api.proto\x12\x0eprotos.secrets\x1a\x1egoogle/protobuf/wrappers.proto\x1a\x11protos/base.proto\"\xb1\x02\n\x06Secret\x12(\n\x02id\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12)\n\x03key\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x32\n\x0cmasked_value\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x30\n\ncreated_by\x18\x05 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x12\n\ncreated_at\x18\x06 \x01(\x03\x12\x12\n\nupdated_at\x18\x07 \x01(\x03\x12\x11\n\tis_active\x18\x08 \x01(\x08\"n\n\x11GetSecretsRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12\x12\n\nsecret_ids\x18\x02 \x03(\t\x12)\n\x03key\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa8\x01\n\x12GetSecretsResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12\'\n\x07secrets\x18\x04 \x03(\x0b\x32\x16.protos.secrets.Secret\"_\n\x10GetSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa6\x01\n\x11GetSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"\xbc\x01\n\x13\x43reateSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12)\n\x03key\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12+\n\x05value\x18\x03 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x31\n\x0b\x64\x65scription\x18\x04 \x01(\x0b\x32\x1c.google.protobuf.StringValue\"\xa9\x01\n\x14\x43reateSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secret\"\x88\x04\n\x0eUpdateSecretOp\x12-\n\x02op\x18\x01 \x01(\x0e\x32!.protos.secrets.UpdateSecretOp.Op\x12/\n\tsecret_id\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12\x44\n\rupdate_secret\x18\x03 \x01(\x0b\x32+.protos.secrets.UpdateSecretOp.UpdateSecretH\x00\x12Q\n\x14update_secret_status\x18\x04 \x01(\x0b\x32\x31.protos.secrets.UpdateSecretOp.UpdateSecretStatusH\x00\x1an\n\x0cUpdateSecret\x12\x31\n\x0b\x64\x65scription\x18\x01 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x12+\n\x05value\x18\x02 \x01(\x0b\x32\x1c.google.protobuf.StringValue\x1a\x43\n\x12UpdateSecretStatus\x12-\n\tis_active\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\">\n\x02Op\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x11\n\rUPDATE_SECRET\x10\x01\x12\x18\n\x14UPDATE_SECRET_STATUS\x10\x02\x42\x08\n\x06update\"l\n\x13UpdateSecretRequest\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12\x39\n\x11update_secret_ops\x18\x02 \x03(\x0b\x32\x1e.protos.secrets.UpdateSecretOp\"\xa9\x01\n\x14UpdateSecretResponse\x12\x1a\n\x04meta\x18\x01 \x01(\x0b\x32\x0c.protos.Meta\x12+\n\x07success\x18\x02 \x01(\x0b\x32\x1a.google.protobuf.BoolValue\x12 \n\x07message\x18\x03 \x01(\x0b\x32\x0f.protos.Message\x12&\n\x06secret\x18\x04 \x01(\x0b\x32\x16.protos.secrets.Secretb\x06proto3') _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'protos.secrets.api_pb2', globals()) @@ -23,29 +23,29 @@ DESCRIPTOR._options = None _SECRET._serialized_start=96 - _SECRET._serialized_end=456 - _GETSECRETSREQUEST._serialized_start=458 - _GETSECRETSREQUEST._serialized_end=568 - _GETSECRETSRESPONSE._serialized_start=571 - _GETSECRETSRESPONSE._serialized_end=739 - _GETSECRETREQUEST._serialized_start=741 - _GETSECRETREQUEST._serialized_end=836 - _GETSECRETRESPONSE._serialized_start=839 - _GETSECRETRESPONSE._serialized_end=1005 - _CREATESECRETREQUEST._serialized_start=1008 - _CREATESECRETREQUEST._serialized_end=1196 - _CREATESECRETRESPONSE._serialized_start=1199 - _CREATESECRETRESPONSE._serialized_end=1368 - _UPDATESECRETOP._serialized_start=1371 - _UPDATESECRETOP._serialized_end=1891 - _UPDATESECRETOP_UPDATESECRET._serialized_start=1638 - _UPDATESECRETOP_UPDATESECRET._serialized_end=1748 - _UPDATESECRETOP_UPDATESECRETSTATUS._serialized_start=1750 - _UPDATESECRETOP_UPDATESECRETSTATUS._serialized_end=1817 - _UPDATESECRETOP_OP._serialized_start=1819 - _UPDATESECRETOP_OP._serialized_end=1881 - _UPDATESECRETREQUEST._serialized_start=1893 - _UPDATESECRETREQUEST._serialized_end=2001 - _UPDATESECRETRESPONSE._serialized_start=2004 - _UPDATESECRETRESPONSE._serialized_end=2173 + _SECRET._serialized_end=401 + _GETSECRETSREQUEST._serialized_start=403 + _GETSECRETSREQUEST._serialized_end=513 + _GETSECRETSRESPONSE._serialized_start=516 + _GETSECRETSRESPONSE._serialized_end=684 + _GETSECRETREQUEST._serialized_start=686 + _GETSECRETREQUEST._serialized_end=781 + _GETSECRETRESPONSE._serialized_start=784 + _GETSECRETRESPONSE._serialized_end=950 + _CREATESECRETREQUEST._serialized_start=953 + _CREATESECRETREQUEST._serialized_end=1141 + _CREATESECRETRESPONSE._serialized_start=1144 + _CREATESECRETRESPONSE._serialized_end=1313 + _UPDATESECRETOP._serialized_start=1316 + _UPDATESECRETOP._serialized_end=1836 + _UPDATESECRETOP_UPDATESECRET._serialized_start=1583 + _UPDATESECRETOP_UPDATESECRET._serialized_end=1693 + _UPDATESECRETOP_UPDATESECRETSTATUS._serialized_start=1695 + _UPDATESECRETOP_UPDATESECRETSTATUS._serialized_end=1762 + _UPDATESECRETOP_OP._serialized_start=1764 + _UPDATESECRETOP_OP._serialized_end=1826 + _UPDATESECRETREQUEST._serialized_start=1838 + _UPDATESECRETREQUEST._serialized_end=1946 + _UPDATESECRETRESPONSE._serialized_start=1949 + _UPDATESECRETRESPONSE._serialized_end=2118 # @@protoc_insertion_point(module_scope) diff --git a/protos/secrets/api_pb2.pyi b/protos/secrets/api_pb2.pyi index 3770aa9b0..c10502c3e 100644 --- a/protos/secrets/api_pb2.pyi +++ b/protos/secrets/api_pb2.pyi @@ -29,7 +29,6 @@ class Secret(google.protobuf.message.Message): MASKED_VALUE_FIELD_NUMBER: builtins.int DESCRIPTION_FIELD_NUMBER: builtins.int CREATED_BY_FIELD_NUMBER: builtins.int - LAST_UPDATED_BY_FIELD_NUMBER: builtins.int CREATED_AT_FIELD_NUMBER: builtins.int UPDATED_AT_FIELD_NUMBER: builtins.int IS_ACTIVE_FIELD_NUMBER: builtins.int @@ -43,8 +42,6 @@ class Secret(google.protobuf.message.Message): def description(self) -> google.protobuf.wrappers_pb2.StringValue: ... @property def created_by(self) -> google.protobuf.wrappers_pb2.StringValue: ... - @property - def last_updated_by(self) -> google.protobuf.wrappers_pb2.StringValue: ... created_at: builtins.int updated_at: builtins.int is_active: builtins.bool @@ -56,13 +53,12 @@ class Secret(google.protobuf.message.Message): masked_value: google.protobuf.wrappers_pb2.StringValue | None = ..., description: google.protobuf.wrappers_pb2.StringValue | None = ..., created_by: google.protobuf.wrappers_pb2.StringValue | None = ..., - last_updated_by: google.protobuf.wrappers_pb2.StringValue | None = ..., created_at: builtins.int = ..., updated_at: builtins.int = ..., is_active: builtins.bool = ..., ) -> None: ... - def HasField(self, field_name: typing_extensions.Literal["created_by", b"created_by", "description", b"description", "id", b"id", "key", b"key", "last_updated_by", b"last_updated_by", "masked_value", b"masked_value"]) -> builtins.bool: ... - def ClearField(self, field_name: typing_extensions.Literal["created_at", b"created_at", "created_by", b"created_by", "description", b"description", "id", b"id", "is_active", b"is_active", "key", b"key", "last_updated_by", b"last_updated_by", "masked_value", b"masked_value", "updated_at", b"updated_at"]) -> None: ... + def HasField(self, field_name: typing_extensions.Literal["created_by", b"created_by", "description", b"description", "id", b"id", "key", b"key", "masked_value", b"masked_value"]) -> builtins.bool: ... + def ClearField(self, field_name: typing_extensions.Literal["created_at", b"created_at", "created_by", b"created_by", "description", b"description", "id", b"id", "is_active", b"is_active", "key", b"key", "masked_value", b"masked_value", "updated_at", b"updated_at"]) -> None: ... global___Secret = Secret From b19a4e837153c934042028c38373eaf946b22b66 Mon Sep 17 00:00:00 2001 From: droid-dhruv Date: Wed, 12 Mar 2025 14:42:53 +0530 Subject: [PATCH 16/16] added crud manager for secrets and stricter checks for ownership --- ..._unique_active_key_per_account_and_more.py | 29 ++ executor/models.py | 51 +++- executor/secrets/crud/secrets_crud_manager.py | 250 ++++++++++++++++++ .../secrets/crud/secrets_update_processor.py | 55 ---- executor/secrets/secret_resolver.py | 19 +- executor/secrets/views.py | 201 ++++++-------- 6 files changed, 413 insertions(+), 192 deletions(-) create mode 100644 executor/migrations/0047_remove_secret_unique_active_key_per_account_and_more.py create mode 100644 executor/secrets/crud/secrets_crud_manager.py delete mode 100644 executor/secrets/crud/secrets_update_processor.py diff --git a/executor/migrations/0047_remove_secret_unique_active_key_per_account_and_more.py b/executor/migrations/0047_remove_secret_unique_active_key_per_account_and_more.py new file mode 100644 index 000000000..77168683d --- /dev/null +++ b/executor/migrations/0047_remove_secret_unique_active_key_per_account_and_more.py @@ -0,0 +1,29 @@ +# Generated by Django 4.1.13 on 2025-03-12 08:16 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("executor", "0046_secret_secret_executor_se_key_60cc82_idx_and_more"), + ] + + operations = [ + migrations.RemoveConstraint( + model_name="secret", + name="unique_active_key_per_account", + ), + migrations.AddIndex( + model_name="secret", + index=models.Index( + fields=["account", "created_by"], name="executor_se_account_02f4c3_idx" + ), + ), + migrations.AddConstraint( + model_name="secret", + constraint=models.UniqueConstraint( + fields=("key", "account"), name="unique_key_per_account" + ), + ), + ] diff --git a/executor/models.py b/executor/models.py index 8efa6a896..02055f3cf 100644 --- a/executor/models.py +++ b/executor/models.py @@ -21,10 +21,11 @@ PlaybookStepExecutionLog as PlaybookStepExecutionLogProto, PlaybookExecution as PlaybookExecutionProto, \ PlaybookStepRelation as PlaybookStepRelationProto, \ PlaybookStepRelationExecutionLog as PlaybookStepRelationExecutionLogProto, PlaybookStepResultCondition -from utils.model_utils import generate_choices +from protos.secrets.api_pb2 import Secret as SecretProto from accounts.models import Account from utils.proto_utils import dict_to_proto +from utils.model_utils import generate_choices from encrypted_model_fields.fields import EncryptedTextField import uuid @@ -688,11 +689,51 @@ class Secret(models.Model): class Meta: constraints = [ models.UniqueConstraint( - fields=['key', 'account', 'is_active'], - condition=models.Q(is_active=True), - name='unique_active_key_per_account' + fields=['key', 'account'], + name='unique_key_per_account' ) ] indexes = [ models.Index(fields=['key', 'account', 'is_active']), - ] \ No newline at end of file + models.Index(fields=['account', 'created_by']), + ] + + def deactivate(self): + """ + Deactivate a secret by setting is_active to False and modifying the key + to prevent conflicts with future secrets using the same key + """ + if not self.is_active: + return False + + self.is_active = False + # Generate a unique suffix for the key + unique_suffix = str(uuid.uuid4()) + self.key = f"{self.key}_inactive_{unique_suffix}" + self.save(update_fields=['is_active', 'key', 'updated_at']) + return True + + @staticmethod + def _mask_secret_value(value): + """Mask secret value for display purposes""" + if not value or len(value) <= 8: + return "••••••••" + return value[:2] + "••••••" + value[-2:] + + def to_proto(self, include_masked_value: bool = True) -> 'SecretProto': + """Convert this Secret model to a Secret proto with full details""" + + proto = SecretProto( + id=StringValue(value=str(self.id)), + key=StringValue(value=self.key), + description=StringValue(value=self.description or ""), + created_by=StringValue(value=self.created_by.email if self.created_by else ""), + created_at=int(self.created_at.replace(tzinfo=timezone.utc).timestamp()) if self.created_at else 0, + updated_at=int(self.updated_at.replace(tzinfo=timezone.utc).timestamp()) if self.updated_at else 0, + is_active=self.is_active + ) + + if include_masked_value: + proto.masked_value.value = self._mask_secret_value(self.value) + + return proto \ No newline at end of file diff --git a/executor/secrets/crud/secrets_crud_manager.py b/executor/secrets/crud/secrets_crud_manager.py new file mode 100644 index 000000000..a341ed808 --- /dev/null +++ b/executor/secrets/crud/secrets_crud_manager.py @@ -0,0 +1,250 @@ +import logging +from typing import List, Optional, Union +from datetime import timezone +import uuid + +from django.db.models import QuerySet +from google.protobuf.wrappers_pb2 import BoolValue, StringValue + +from executor.models import Secret +from protos.secrets.api_pb2 import UpdateSecretOp, Secret as SecretProto +from protos.base_pb2 import Message, Meta, Page +from utils.update_processor_mixin import UpdateProcessorMixin + +logger = logging.getLogger(__name__) + + +class SecretsCrudManager(UpdateProcessorMixin): + """ + Centralized manager for all CRUD operations on Secrets + """ + update_op_cls = UpdateSecretOp + + @staticmethod + def get_by_id(secret_id: str, account_id: int, user_id: int = None) -> Optional[Secret]: + """ + Get a secret by ID + + Args: + secret_id: The ID of the secret to retrieve + account_id: The account ID the secret belongs to + user_id: If provided, only return secrets created by this user + + Returns: + Secret object if found, None otherwise + """ + try: + query = { + 'id': secret_id, + 'account_id': account_id, + 'is_active': True + } + + # Add user filter if provided + if user_id: + query['created_by_id'] = user_id + + return Secret.objects.get(**query) + except Secret.DoesNotExist: + return None + + @staticmethod + def list_secrets(account_id: int, user_id: int = None, secret_ids: List[str] = None, + key_filter: str = None, show_inactive: bool = False, + page: Page = None) -> QuerySet: + """ + List secrets with optional filtering + + Args: + account_id: The account ID to filter secrets by + user_id: If provided, only return secrets created by this user + secret_ids: Optional list of secret IDs to filter by + key_filter: Optional key substring to filter by + show_inactive: Whether to include inactive secrets + page: Pagination information + + Returns: + QuerySet of Secret objects + """ + # Base queryset + qs = Secret.objects.filter(account_id=account_id) + + # Filtering as per options + if user_id: + qs = qs.filter(created_by_id=user_id) + + if secret_ids: + qs = qs.filter(id__in=secret_ids) + + elif key_filter: + qs = qs.filter(key__icontains=key_filter.lower()) + + elif not show_inactive: + qs = qs.filter(is_active=True) + + return qs.order_by('-created_at') + + @staticmethod + def create_secret(account_id: int, key: str, value: str, + description: str, user) -> Union[Secret, str]: + """ + Create a new secret + + Args: + account_id: The account ID to create the secret under + key: The key for the secret + value: The value of the secret + description: Optional description for the secret + user: The user creating the secret + + Returns: + Secret object if successful, error message string if failed + """ + # Validate required fields + if not key or not value: + return "Key and value are required" + + if not key.isalnum() and not '_' in key: + return "Key must contain only letters, numbers, and underscores" + + if Secret.objects.filter(account_id=account_id, key=key, is_active=True).exists(): + return f"A secret with key '{key}' already exists" + + # Create the secret + try: + secret = Secret.objects.create( + account_id=account_id, + key=key, + value=value, + description=description, + created_by=user, + is_active=True + ) + return secret + except Exception as ex: + logger.exception(f"Error creating secret with key {key}") + return f"Failed to create secret: {str(ex)}" + + @staticmethod + def verify_ownership(secret: Secret, account_id: int, user_id: int) -> bool: + """ + Verify that the secret belongs to the specified account and user + + Args: + secret: The secret to verify + account_id: The account ID to check against + user_id: The user ID to check against + + Returns: + True if the secret belongs to the account and user, False otherwise + """ + return (secret.account_id == account_id and + secret.created_by_id == user_id and + secret.is_active) + + @staticmethod + def update_secret(elem: Secret, update_op: UpdateSecretOp.UpdateSecret, account_id: int, user_id: int) -> Secret: + """ + Update a secret's description and/or value + + Args: + elem: The secret to update + update_op: The update operation to apply + account_id: If provided, verify the secret belongs to this account + user_id: If provided, verify the secret was created by this user + + Returns: + The updated secret + + Raises: + Exception: If the update fails or the user doesn't have permission + """ + # Verify ownership if account_id and user_id are provided + if not SecretsCrudManager.verify_ownership(elem, account_id, user_id): + raise Exception(f"You don't have permission to update secret {elem.key}") + + update_fields = ['updated_at'] + + # Update description if provided and has a value + if update_op.HasField('description') and update_op.description.value: + elem.description = update_op.description.value + update_fields.append('description') + + # Update value if provided and has a value + if update_op.HasField('value') and update_op.value.value: + elem.value = update_op.value.value + update_fields.append('value') + + if len(update_fields) > 1: # Only save if we have fields to update + try: + elem.save(update_fields=update_fields) + except Exception as ex: + logger.exception(f"Error occurred updating secret {elem.key}") + raise Exception(f"Error updating secret: {str(ex)}") + + return elem + + @staticmethod + def update_secret_status(elem: Secret, update_op: UpdateSecretOp.UpdateSecretStatus, account_id: int, user_id: int) -> Secret: + """ + Update a secret's active status (soft delete) + + When deactivating a secret, the key is modified to prevent conflicts + with future secrets using the same key + + Args: + elem: The secret to update + update_op: The update operation to apply + account_id: If provided, verify the secret belongs to this account + user_id: If provided, verify the secret was created by this user + + Returns: + The updated secret + + Raises: + Exception: If the update fails or the user doesn't have permission + """ + # Verify ownership if account_id and user_id are provided + if not SecretsCrudManager.verify_ownership(elem, account_id, user_id): + raise Exception(f"You don't have permission to deactivate secret {elem.key}") + + is_active = update_op.is_active.value + + # We only support deactivation, not reactivation + if is_active or not elem.is_active: + raise Exception(f"Secret {elem.key} cannot be reactivated once deactivated") + + # Use the deactivate method to handle the key modification + if not elem.deactivate(): + raise Exception(f"Secret {elem.key} is already inactive") + + return elem + + def update(self, elem: Secret, update_ops, account_id: int, user_id: int): + """ + Apply a list of update operations to a secret + + Args: + elem: The secret to update + update_ops: The list of update operations to apply + account_id: If provided, verify the secret belongs to this account + user_id: If provided, verify the secret was created by this user + + Returns: + The updated secret + + Raises: + Exception: If any update fails or the user doesn't have permission + """ + if not self.verify_ownership(elem, account_id, user_id): + raise Exception(f"You don't have permission to update secret {elem.key}") + # Apply each update operation + for update_op in update_ops: + if update_op.HasField('update_secret'): + elem = self.update_secret(elem, update_op.update_secret, account_id, user_id) + elif update_op.HasField('update_secret_status'): + elem = self.update_secret_status(elem, update_op.update_secret_status, account_id, user_id) + return elem + + +secrets_crud_manager = SecretsCrudManager() diff --git a/executor/secrets/crud/secrets_update_processor.py b/executor/secrets/crud/secrets_update_processor.py deleted file mode 100644 index 7d6febef7..000000000 --- a/executor/secrets/crud/secrets_update_processor.py +++ /dev/null @@ -1,55 +0,0 @@ -import logging - -from executor.models import Secret -from protos.secrets.api_pb2 import UpdateSecretOp -from utils.update_processor_mixin import UpdateProcessorMixin - -logger = logging.getLogger(__name__) - - -class SecretsUpdateProcessor(UpdateProcessorMixin): - update_op_cls = UpdateSecretOp - - @staticmethod - def update_secret(elem: Secret, update_op: UpdateSecretOp.UpdateSecret) -> Secret: - """Update a secret's description and/or value""" - update_fields = ['updated_at'] - - # Update description if provided and has a value - if hasattr(update_op, 'description') and update_op.description.value: - elem.description = update_op.description.value - update_fields.append('description') - - # Update value if provided and has a value - if hasattr(update_op, 'value') and update_op.value.value: - elem.value = update_op.value.value - update_fields.append('value') - - if len(update_fields) > 1: # Only save if we have fields to update - try: - elem.save(update_fields=update_fields) - except Exception as ex: - logger.exception(f"Error occurred updating secret {elem.key}") - raise Exception(f"Error updating secret: {str(ex)}") - - return elem - - @staticmethod - def update_secret_status(elem: Secret, update_op: UpdateSecretOp.UpdateSecretStatus) -> Secret: - """Update a secret's active status (soft delete)""" - is_active = update_op.is_active.value - - # Can't reactivate a secret that's been deactivated - if not elem.is_active: - raise Exception(f"Secret {elem.key} cannot be reactivated") - - elem.is_active = False - try: - elem.save(update_fields=['is_active', 'updated_at']) - except Exception as ex: - logger.exception(f"Error occurred updating secret status for {elem.key}") - raise Exception(f"Error updating secret status: {str(ex)}") - return elem - - -secrets_update_processor = SecretsUpdateProcessor() diff --git a/executor/secrets/secret_resolver.py b/executor/secrets/secret_resolver.py index a360ca002..d6b68a939 100644 --- a/executor/secrets/secret_resolver.py +++ b/executor/secrets/secret_resolver.py @@ -4,6 +4,7 @@ from protos.ui_definition_pb2 import FormField from protos.literal_pb2 import LiteralType from executor.models import Secret +from executor.secrets.crud.secrets_crud_manager import secrets_crud_manager logger = logging.getLogger(__name__) @@ -52,19 +53,23 @@ def resolve_secrets(cls, form_fields: [FormField], account_id: int, source_type_ if not secret_refs: return source_type_task_def - # Get referenced secrets from DB - secrets = Secret.objects.filter( + # Get referenced secrets from DB using the CRUD manager's list method + # Note: We don't filter by user here since secrets might be created by different users + # but need to be accessible for task execution + secrets_qs = secrets_crud_manager.list_secrets( account_id=account_id, - is_active=True, - key__in=secret_refs - ).values('key', 'value') + user_id=None, # Don't filter by user for task execution + secret_ids=None, + key_filter=None, + show_inactive=False + ).filter(key__in=secret_refs).values('key', 'value') - if not secrets: + if not secrets_qs: logger.warning(f"No secrets found for given references") return source_type_task_def # Create mapping - secret_map = {s['key']: s['value'] for s in secrets} + secret_map = {s['key']: s['value'] for s in secrets_qs} # Final resolution resolved_def = source_type_task_def.copy() diff --git a/executor/secrets/views.py b/executor/secrets/views.py index d65af2efa..ffcd9785d 100644 --- a/executor/secrets/views.py +++ b/executor/secrets/views.py @@ -1,7 +1,5 @@ import logging from typing import Union -from datetime import timezone - from django.http import HttpResponse from google.protobuf.wrappers_pb2 import BoolValue, StringValue @@ -16,79 +14,42 @@ GetSecretRequest, GetSecretResponse, CreateSecretRequest, CreateSecretResponse, UpdateSecretRequest, UpdateSecretResponse, - Secret as SecretProto, ) -from executor.secrets.crud.secrets_update_processor import secrets_update_processor +from executor.secrets.crud.secrets_crud_manager import secrets_crud_manager logger = logging.getLogger(__name__) - -def _mask_secret_value(value): - if not value or len(value) <= 8: - return "••••••••" - return value[:2] + "••••••" + value[-2:] - - -def _secret_to_proto(secret: Secret) -> SecretProto: - """Convert a Secret model to a Secret proto""" - return SecretProto( - id=StringValue(value=str(secret.id)), - key=StringValue(value=secret.key), - masked_value=StringValue(value=_mask_secret_value(secret.value)), - description=StringValue(value=secret.description or ""), - created_by=StringValue(value=secret.created_by.email if secret.created_by else ""), - created_at=int(secret.created_at.replace(tzinfo=timezone.utc).timestamp()) if secret.created_at else 0, - updated_at=int(secret.updated_at.replace(tzinfo=timezone.utc).timestamp()) if secret.updated_at else 0, - is_active=secret.is_active - ) - -def _secret_to_proto_partial(secret: Secret) -> SecretProto: - """Convert a Secret model to a partial Secret proto (for list views)""" - return SecretProto( - id=StringValue(value=str(secret.id)), - key=StringValue(value=secret.key), - description=StringValue(value=secret.description or ""), - created_by=StringValue(value=secret.created_by.email if secret.created_by else ""), - created_at=int(secret.created_at.replace(tzinfo=timezone.utc).timestamp()) if secret.created_at else 0, - is_active=secret.is_active - ) - - - @web_api(GetSecretsRequest) def secrets_list(request_message: GetSecretsRequest) -> Union[GetSecretsResponse, HttpResponse]: """List secrets with optional filtering by IDs or key""" account: Account = get_request_account() + user = get_request_user() meta: Meta = request_message.meta - show_inactive = meta.show_inactive + show_inactive = meta.show_inactive if meta.show_inactive else False page: Page = meta.page - list_all = True - - # Base queryset - qs = Secret.objects.filter(account=account, is_active=True) - - # Filter by specific IDs if provided - if request_message.secret_ids: - qs = qs.filter(id__in=request_message.secret_ids) - list_all = False - # Filter by key if provided - if request_message.key: - qs = qs.filter(key__icontains=request_message.key.value.lower()) - list_all = False - # Otherwise filter by active status unless show_inactive is True - elif not show_inactive or not show_inactive.value: - qs = qs.filter(is_active=True) + + # Get secret IDs if provided + secret_ids = request_message.secret_ids if request_message.secret_ids else None + + # Get key filter if provided + key_filter = request_message.key.value if request_message.key else None + + # Determine if we should show inactive secrets + show_inactive_value = show_inactive.value if show_inactive else False + + # Use the CRUD manager to get the queryset, including user filtering + qs = secrets_crud_manager.list_secrets( + account_id=account.id, + secret_ids=secret_ids, + key_filter=key_filter, + show_inactive=show_inactive_value + ) total_count = qs.count() - qs = qs.order_by('-created_at') qs = filter_page(qs, page) - # Use proto or proto_partial based on list_all flag - if list_all: - secrets = [_secret_to_proto_partial(secret) for secret in qs] - else: - secrets = [_secret_to_proto(secret) for secret in qs] - + # Use appropriate proto conversion based on list_all flag + secrets = [secret.to_proto(include_masked_value=False) for secret in qs] return GetSecretsResponse( meta=get_meta(page=page, total_count=total_count), success=BoolValue(value=True), @@ -100,27 +61,37 @@ def secrets_list(request_message: GetSecretsRequest) -> Union[GetSecretsResponse def secret_get(request_message: GetSecretRequest) -> Union[GetSecretResponse, HttpResponse]: """Get a specific secret by ID""" account: Account = get_request_account() + user = get_request_user() secret_id = request_message.secret_id.value - + if not secret_id: return GetSecretResponse( meta=get_meta(), success=BoolValue(value=False), message=Message(title="Invalid Request", description="Secret ID is required") ) - - try: - secret = Secret.objects.get(id=secret_id, account=account, is_active=True) - return GetSecretResponse( - meta=get_meta(), - success=BoolValue(value=True), - secret=_secret_to_proto(secret) - ) - except Secret.DoesNotExist: + + # Use the CRUD manager to get the secret + secret = secrets_crud_manager.get_by_id(secret_id, account.id) + if secret: + if secrets_crud_manager.verify_ownership(secret, account.id, user.id): + return GetSecretResponse( + meta=get_meta(), + success=BoolValue(value=True), + secret=secret.to_proto() + ) + else: + # Do not show even masked value if user wasn't the creator + return GetSecretResponse( + meta=get_meta(), + success=BoolValue(value=True), + secret=secret.to_proto(include_masked_value=False) + ) + else: return GetSecretResponse( meta=get_meta(), success=BoolValue(value=False), - message=Message(title="Not Found", description="Secret not found") + message=Message(title="Not Found", description="Secret not found or you don't have permission to view it") ) @@ -134,60 +105,37 @@ def secret_create(request_message: CreateSecretRequest) -> Union[CreateSecretRes value = request_message.value.value description = request_message.description.value - # Validate required fields - if not key or not value: - return CreateSecretResponse( - meta=get_meta(), - success=BoolValue(value=False), - message=Message(title="Invalid Request", description="Key and value are required") - ) + # Use the CRUD manager to create the secret + result = secrets_crud_manager.create_secret( + account_id=account.id, + key=key, + value=value, + description=description, + user=user + ) - if not key.isalnum() or ' ' in key: + # If result is a string, it's an error message + if isinstance(result, str): return CreateSecretResponse( meta=get_meta(), success=BoolValue(value=False), - message=Message(title="Invalid Key", - description="Key must be a single word containing only letters, numbers, and underscores") + message=Message(title="Error", description=result) ) - if Secret.objects.filter(account=account, key=key, is_active=True).exists(): - return CreateSecretResponse( - meta=get_meta(), - success=BoolValue(value=False), - message=Message(title="Duplicate Key", description=f"A secret with key '{key}' already exists") - ) - - # Create the secret - try: - secret = Secret.objects.create( - account=account, - key=key, - value=value, - description=description, - created_by=user, - is_active=True - ) - - return CreateSecretResponse( - meta=get_meta(), - success=BoolValue(value=True), - message=Message(title="Success", description="Secret created successfully"), - secret=_secret_to_proto(secret) - ) - except Exception as e: - logger.error(f"Error creating secret: {str(e)}") - return CreateSecretResponse( - meta=get_meta(), - success=BoolValue(value=False), - message=Message(title="Error", description="Failed to create secret") - ) + # Otherwise, it's a Secret object + return CreateSecretResponse( + meta=get_meta(), + success=BoolValue(value=True), + message=Message(title="Success", description="Secret created successfully"), + secret=result.to_proto() + ) @web_api(UpdateSecretRequest) def secret_update(request_message: UpdateSecretRequest) -> Union[UpdateSecretResponse, HttpResponse]: """Update a secret using operations""" account: Account = get_request_account() - _user = get_request_user() + user = get_request_user() update_secret_ops = request_message.update_secret_ops @@ -210,16 +158,25 @@ def secret_update(request_message: UpdateSecretRequest) -> Union[UpdateSecretRes secret_id = list(secret_ids)[0] try: - secret = Secret.objects.get(id=secret_id, account=account, is_active=True) - try: - secrets_update_processor.update(secret, update_secret_ops) - updated_secret = Secret.objects.get(id=secret_id) + # Use the CRUD manager to get the secret, including user filtering + secret = secrets_crud_manager.get_by_id(secret_id, account.id, user.id) + + if not secret: + return UpdateSecretResponse( + meta=get_meta(), + success=BoolValue(value=False), + message=Message(title="Not Found", description="Secret not found or you don't have permission to update it") + ) + try: + # Use the CRUD manager to update the secret, passing account and user IDs for permission check + updated_secret = secrets_crud_manager.update(secret, update_secret_ops, account_id=account.id, user_id=user.id) + # Get the updated secret return UpdateSecretResponse( meta=get_meta(), success=BoolValue(value=True), message=Message(title="Success", description="Secret updated successfully"), - secret=_secret_to_proto(updated_secret) + secret=updated_secret.to_proto() ) except Exception as e: logger.error(f"Error updating secret: {str(e)}") @@ -228,12 +185,6 @@ def secret_update(request_message: UpdateSecretRequest) -> Union[UpdateSecretRes success=BoolValue(value=False), message=Message(title="Error", description=str(e)) ) - except Secret.DoesNotExist: - return UpdateSecretResponse( - meta=get_meta(), - success=BoolValue(value=False), - message=Message(title="Not Found", description="Secret not found") - ) except Exception as e: logger.error(f"Error updating secret: {str(e)}") return UpdateSecretResponse(