diff --git a/.vscode/settings.json b/.vscode/settings.json index b3af7d528..c63be9001 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -32,5 +32,7 @@ "[typescriptreact]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, - "cSpell.words": ["organisation"] // Don't run prettier for files listed in .gitignore + "[typescript]": { + "editor.defaultFormatter": "esbenp.prettier-vscode" + } // Don't run prettier for files listed in .gitignore } diff --git a/backend/api/auth.py b/backend/api/auth.py index 7439c356e..dfa5c7c83 100644 --- a/backend/api/auth.py +++ b/backend/api/auth.py @@ -6,7 +6,7 @@ get_token_type, token_is_expired_or_deleted, ) -from api.models import Environment +from api.models import DynamicSecret, Environment, Secret from api.utils.access.permissions import ( service_account_can_access_environment, user_can_access_environment, @@ -44,34 +44,59 @@ def authenticate(self, request): if token_is_expired_or_deleted(auth_token): raise exceptions.AuthenticationFailed("Token expired or deleted") - env_id = request.headers.get("environment") + env = None - # Try resolving env from header - if env_id: + # Try resolving secret_id from header OR query params (supports Secret or DynamicSecret) + secret_id = request.headers.get("secret_id") or request.GET.get("secret_id") + if secret_id: + found = False try: - env = Environment.objects.get(id=env_id) - except Environment.DoesNotExist: - raise exceptions.AuthenticationFailed("Environment not found") + secret = Secret.objects.get(id=secret_id) + env = secret.environment + found = True + except Secret.DoesNotExist: + pass + if not found: + try: + dyn_secret = DynamicSecret.objects.get(id=secret_id) + env = dyn_secret.environment + found = True + except DynamicSecret.DoesNotExist: + pass + if not found: + raise exceptions.NotFound("Secret not found") + + # If env is still None, try resolving from header or query params + if env is None: + env_id = request.headers.get("environment") + # Try resolving env from header + if env_id: + try: + env = Environment.objects.get(id=env_id) + except Environment.DoesNotExist: + raise exceptions.AuthenticationFailed("Environment not found") - # Try resolving env from query params - else: - try: - app_id = request.GET.get("app_id") - env_name = request.GET.get("env") - if not app_id: - raise exceptions.AuthenticationFailed("Missing app_id parameter") - if not env_name: - raise exceptions.AuthenticationFailed("Missing env parameter") - env = Environment.objects.get(app_id=app_id, name__iexact=env_name) - except Environment.DoesNotExist: - # Check if the app exists to give a more specific error - App = apps.get_model("api", "App") - if not App.objects.filter(id=app_id).exists(): - raise exceptions.NotFound(f"App with ID {app_id} not found") - else: - raise exceptions.NotFound( - f"Environment '{env_name}' not found in App {app_id}" - ) + # Try resolving env from query params + else: + try: + app_id = request.GET.get("app_id") + env_name = request.GET.get("env") + if not app_id: + raise exceptions.AuthenticationFailed( + "Missing app_id parameter" + ) + if not env_name: + raise exceptions.AuthenticationFailed("Missing env parameter") + env = Environment.objects.get(app_id=app_id, name__iexact=env_name) + except Environment.DoesNotExist: + # Check if the app exists to give a more specific error + App = apps.get_model("api", "App") + if not App.objects.filter(id=app_id).exists(): + raise exceptions.NotFound(f"App with ID {app_id} not found") + else: + raise exceptions.NotFound( + f"Environment '{env_name}' not found in App {app_id}" + ) auth["environment"] = env diff --git a/backend/api/migrations/0106_dynamicsecret_dynamicsecretlease_and_more.py b/backend/api/migrations/0106_dynamicsecret_dynamicsecretlease_and_more.py new file mode 100644 index 000000000..afa21edf2 --- /dev/null +++ b/backend/api/migrations/0106_dynamicsecret_dynamicsecretlease_and_more.py @@ -0,0 +1,59 @@ +# Generated by Django 4.2.22 on 2025-08-20 08:11 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0105_environmentkey_unique_envkey_user_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='DynamicSecret', + fields=[ + ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.TextField()), + ('description', models.TextField(blank=True)), + ('path', models.TextField(default='/')), + ('default_ttl', models.DurationField(help_text='Default TTL for leases (must be <= max_ttl).')), + ('max_ttl', models.DurationField(help_text='Maximum allowed TTL for leases.')), + ('provider', models.CharField(choices=[('aws', 'AWS')], help_text='Which provider this secret is associated with.', max_length=50)), + ('config', models.JSONField()), + ('created_at', models.DateTimeField(auto_now_add=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('deleted_at', models.DateTimeField(blank=True, null=True)), + ('authentication', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.providercredentials')), + ('environment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.environment')), + ('folder', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.secretfolder')), + ], + ), + migrations.CreateModel( + name='DynamicSecretLease', + fields=[ + ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.TextField()), + ('description', models.TextField(blank=True)), + ('ttl', models.DurationField()), + ('status', models.CharField(choices=[('active', 'Active'), ('renewed', 'Renewed'), ('revoked', 'Revoked'), ('expired', 'Expired')], default='active', help_text='Current status of the lease', max_length=50)), + ('created_at', models.DateTimeField(auto_now_add=True, null=True)), + ('renewed_at', models.DateTimeField(null=True)), + ('expires_at', models.DateTimeField(null=True)), + ('revoked_at', models.DateTimeField(null=True)), + ('deleted_at', models.DateTimeField(null=True)), + ('organisation_member', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.organisationmember')), + ('secret', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leases', to='api.dynamicsecret')), + ('service_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.serviceaccount')), + ], + ), + migrations.CreateModel( + name='DynamicSecretLeaseEvent', + fields=[ + ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('lease', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.dynamicsecretlease')), + ], + ), + ] diff --git a/backend/api/migrations/0107_dynamicsecret_key_map_dynamicsecretlease_credentials_and_more.py b/backend/api/migrations/0107_dynamicsecret_key_map_dynamicsecretlease_credentials_and_more.py new file mode 100644 index 000000000..f2647489a --- /dev/null +++ b/backend/api/migrations/0107_dynamicsecret_key_map_dynamicsecretlease_credentials_and_more.py @@ -0,0 +1,76 @@ +# Generated by Django 4.2.22 on 2025-08-26 09:16 + +from django.db import migrations, models +import django.db.models.deletion +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0106_dynamicsecret_dynamicsecretlease_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='dynamicsecret', + name='key_map', + field=models.JSONField(default=list, help_text="Provider-agnostic mapping of keys: [{'id': '', 'name': ''}, ...]"), + ), + migrations.AddField( + model_name='dynamicsecretlease', + name='credentials', + field=models.JSONField(default=list), + ), + migrations.AddField( + model_name='dynamicsecretleaseevent', + name='created_at', + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name='dynamicsecretleaseevent', + name='event_type', + field=models.CharField(choices=[('created', 'Created'), ('active', 'Active'), ('renewed', 'Renewed'), ('revoked', 'Revoked'), ('expired', 'Expired')], default='created', max_length=50), + ), + migrations.AddField( + model_name='dynamicsecretleaseevent', + name='ip_address', + field=models.GenericIPAddressField(blank=True, null=True), + ), + migrations.AddField( + model_name='dynamicsecretleaseevent', + name='metadata', + field=models.JSONField(blank=True, default=dict), + ), + migrations.AddField( + model_name='dynamicsecretleaseevent', + name='organisation_member', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lease_events', to='api.organisationmember'), + ), + migrations.AddField( + model_name='dynamicsecretleaseevent', + name='service_account', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lease_events', to='api.serviceaccount'), + ), + migrations.AddField( + model_name='dynamicsecretleaseevent', + name='user_agent', + field=models.TextField(blank=True, null=True), + ), + migrations.AlterField( + model_name='dynamicsecretlease', + name='status', + field=models.CharField(choices=[('created', 'Created'), ('active', 'Active'), ('renewed', 'Renewed'), ('revoked', 'Revoked'), ('expired', 'Expired')], default='active', help_text='Current status of the lease', max_length=50), + ), + migrations.AlterField( + model_name='dynamicsecretleaseevent', + name='id', + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name='dynamicsecretleaseevent', + name='lease', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='events', to='api.dynamicsecretlease'), + ), + ] diff --git a/backend/api/migrations/0108_dynamicsecretlease_cleanup_job_id_and_more.py b/backend/api/migrations/0108_dynamicsecretlease_cleanup_job_id_and_more.py new file mode 100644 index 000000000..76b0c149e --- /dev/null +++ b/backend/api/migrations/0108_dynamicsecretlease_cleanup_job_id_and_more.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.22 on 2025-08-28 08:49 + +from django.db import migrations, models +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0107_dynamicsecret_key_map_dynamicsecretlease_credentials_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='dynamicsecretlease', + name='cleanup_job_id', + field=models.TextField(default=uuid.uuid4), + ), + migrations.AlterField( + model_name='dynamicsecretlease', + name='credentials', + field=models.JSONField(default=dict), + ), + ] diff --git a/backend/api/migrations/0109_alter_dynamicsecret_key_map.py b/backend/api/migrations/0109_alter_dynamicsecret_key_map.py new file mode 100644 index 000000000..4b5c92372 --- /dev/null +++ b/backend/api/migrations/0109_alter_dynamicsecret_key_map.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.22 on 2025-09-12 05:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0108_dynamicsecretlease_cleanup_job_id_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='dynamicsecret', + name='key_map', + field=models.JSONField(default=list, help_text="Provider-agnostic mapping of keys: [{'id': '', 'key_name': '', 'key_digest': ''}, ...]"), + ), + ] diff --git a/backend/api/models.py b/backend/api/models.py index e71b18ac8..e26cc1740 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -17,7 +17,7 @@ can_add_environment, can_add_service_token, ) - +from django.core.exceptions import ValidationError CLOUD_HOSTED = settings.APP_HOST == "cloud" @@ -609,12 +609,12 @@ class Meta: ] def delete(self, *args, **kwargs): - env = self.environment - super().delete(*args, **kwargs) - # Update the 'updated_at' timestamp of the associated Environment - if env: - env.updated_at = timezone.now() - env.save() + env = self.environment + super().delete(*args, **kwargs) + # Update the 'updated_at' timestamp of the associated Environment + if env: + env.updated_at = timezone.now() + env.save() class SecretTag(models.Model): @@ -651,15 +651,185 @@ def save(self, *args, **kwargs): self.environment.updated_at = timezone.now() self.environment.save() + def delete(self, *args, **kwargs): + env = self.environment + super().delete(*args, **kwargs) + # Update the 'updated_at' timestamp of the associated Environment + if env: + env.updated_at = timezone.now() + env.save() + + +class DynamicSecret(models.Model): + + PROVIDER_CHOICES = [("aws", "AWS")] + + id = models.TextField(default=uuid4, primary_key=True, editable=False) + name = models.TextField() + description = models.TextField(blank=True) + environment = models.ForeignKey(Environment, on_delete=models.CASCADE) + folder = models.ForeignKey(SecretFolder, on_delete=models.CASCADE, null=True) + path = models.TextField(default="/") + default_ttl = models.DurationField( + help_text="Default TTL for leases (must be <= max_ttl)." + ) + max_ttl = models.DurationField(help_text="Maximum allowed TTL for leases.") + authentication = models.ForeignKey( + ProviderCredentials, on_delete=models.SET_NULL, null=True + ) + provider = models.CharField( + max_length=50, + choices=PROVIDER_CHOICES, + help_text="Which provider this secret is associated with.", + ) + config = models.JSONField() + key_map = models.JSONField( + help_text="Provider-agnostic mapping of keys: " + "[{'id': '', 'key_name': '', 'key_digest': ''}, ...]", + default=list, + ) + created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True) + updated_at = models.DateTimeField(auto_now=True) + deleted_at = models.DateTimeField(blank=True, null=True) + + def save(self, *args, **kwargs): + # Call the "real" save() method to save the Secret + super().save(*args, **kwargs) + + # Update the 'updated_at' timestamp of the associated Environment + if self.environment: + self.environment.updated_at = timezone.now() + self.environment.save() def delete(self, *args, **kwargs): - env = self.environment - super().delete(*args, **kwargs) - # Update the 'updated_at' timestamp of the associated Environment - if env: - env.updated_at = timezone.now() - env.save() + # Soft delete the object by setting the 'deleted_at' field. + self.updated_at = timezone.now() + self.deleted_at = timezone.now() + self.save() + + # Revoke all active leases + from ee.integrations.secrets.dynamic.utils import schedule_lease_revocation + + for lease in self.leases.filter(status=DynamicSecretLease.ACTIVE): + schedule_lease_revocation(lease, True) + + # Update the 'updated_at' timestamp of the associated Environment + env = self.environment + if env: + env.updated_at = timezone.now() + env.save() + + +class DynamicSecretLease(models.Model): + + CREATED = "created" + ACTIVE = "active" + RENEWED = "renewed" + REVOKED = "revoked" + EXPIRED = "expired" + + STATUS_OPTIONS = [ + (CREATED, "Created"), + (ACTIVE, "Active"), + (RENEWED, "Renewed"), + (REVOKED, "Revoked"), + (EXPIRED, "Expired"), + ] + + id = models.TextField(default=uuid4, primary_key=True, editable=False) + name = models.TextField() + description = models.TextField(blank=True) + secret = models.ForeignKey( + DynamicSecret, on_delete=models.CASCADE, related_name="leases" + ) + organisation_member = models.ForeignKey( + OrganisationMember, null=True, blank=True, on_delete=models.CASCADE + ) + service_account = models.ForeignKey( + ServiceAccount, null=True, blank=True, on_delete=models.CASCADE + ) + ttl = models.DurationField() + status = models.CharField( + max_length=50, + choices=STATUS_OPTIONS, + default=ACTIVE, + help_text="Current status of the lease", + ) + credentials = models.JSONField( + default=dict, + ) + created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True) + renewed_at = models.DateTimeField(null=True) + expires_at = models.DateTimeField(null=True) + revoked_at = models.DateTimeField(null=True) + deleted_at = models.DateTimeField(null=True) + cleanup_job_id = models.TextField(default=uuid4) + + def clean(self): + """ + Ensure only one of organisation_member or service_account is set. + """ + if not (self.organisation_member or self.service_account): + raise ValidationError( + "Must set either organisation_member or service_account" + ) + if self.organisation_member and self.service_account: + raise ValidationError( + "Only one of organisation_member or service_account may be set" + ) + + def get_account(self): + """ + Return whichever account is associated with this lease. + """ + return self.organisation_member or self.service_account + + +class DynamicSecretLeaseEvent(models.Model): + + EVENT_TYPES = DynamicSecretLease.STATUS_OPTIONS + + id = models.BigAutoField(primary_key=True) + lease = models.ForeignKey( + DynamicSecretLease, on_delete=models.CASCADE, related_name="events" + ) + event_type = models.CharField( + max_length=50, choices=EVENT_TYPES, default=DynamicSecretLease.CREATED + ) + organisation_member = models.ForeignKey( + OrganisationMember, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="lease_events", + ) + service_account = models.ForeignKey( + ServiceAccount, + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="lease_events", + ) + ip_address = models.GenericIPAddressField(null=True, blank=True) + user_agent = models.TextField(blank=True, null=True) + metadata = models.JSONField(default=dict, blank=True) + created_at = models.DateTimeField(auto_now_add=True) + + def clean(self): + """ + Ensure only one of organisation_member or service_account is set. + """ + if not (self.organisation_member or self.service_account): + raise ValidationError( + "Must set either organisation_member or service_account" + ) + if self.organisation_member and self.service_account: + raise ValidationError( + "Only one of organisation_member or service_account may be set" + ) + def get_actor(self): + return self.organisation_member or self.service_account class SecretEvent(models.Model): diff --git a/backend/api/serializers.py b/backend/api/serializers.py index 7c8fd5d90..2bc1aae87 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -13,7 +13,9 @@ EnvironmentKey, Lockbox, Organisation, + OrganisationMember, Secret, + ServiceAccount, ServiceToken, UserToken, PersonalSecret, @@ -56,6 +58,59 @@ def create(self, validated_data): return Organisation(**validated_data) +class OrganisationMemberSerializer(serializers.ModelSerializer): + + username = serializers.CharField(source="user.username", read_only=True) + full_name = serializers.SerializerMethodField() + email = serializers.EmailField(source="user.email", read_only=True) + role = serializers.SerializerMethodField() + + class Meta: + model = OrganisationMember + fields = [ + "id", + "username", + "full_name", + "email", + "role", + ] + read_only_fields = fields + + def get_full_name(self, obj): + social_acc = obj.user.socialaccount_set.first() + if social_acc: + return social_acc.extra_data.get("name") + return None + + def get_role(self, obj): + r = getattr(obj, "role", None) + if not r: + return None + return {"id": r.id, "name": r.name} + + +class ServiceAccountSerializer(serializers.ModelSerializer): + + role = serializers.SerializerMethodField() + + class Meta: + model = ServiceAccount + fields = [ + "id", + "name", + "role", + ] + read_only_fields = fields + + def get_role(self, obj): + if not obj.role: + return None + return { + "id": obj.role.id, + "name": obj.role.name, + } + + class PersonalSecretSerializer(serializers.ModelSerializer): value = serializers.SerializerMethodField() @@ -78,6 +133,7 @@ class SecretSerializer(serializers.ModelSerializer): comment = serializers.SerializerMethodField() tags = serializers.SerializerMethodField() override = serializers.SerializerMethodField() + type = serializers.SerializerMethodField() class Meta: model = Secret @@ -127,6 +183,9 @@ def get_override(self, obj): return None return None + def get_type(self, obj): + return "static" + class EnvironmentSerializer(serializers.ModelSerializer): class Meta: diff --git a/backend/api/utils/access/roles.py b/backend/api/utils/access/roles.py index b3723fd16..6eb243191 100644 --- a/backend/api/utils/access/roles.py +++ b/backend/api/utils/access/roles.py @@ -19,6 +19,7 @@ "app_permissions": { "Environments": ["create", "read", "update", "delete"], "Secrets": ["create", "read", "update", "delete"], + "DynamicSecretLeases": ["create", "read", "update", "delete"], "Lockbox": ["create", "read", "update", "delete"], "Logs": ["create", "read", "update", "delete"], "Tokens": ["create", "read", "update", "delete"], @@ -49,6 +50,7 @@ "app_permissions": { "Environments": ["create", "read", "update", "delete"], "Secrets": ["create", "read", "update", "delete"], + "DynamicSecretLeases": ["create", "read", "update", "delete"], "Lockbox": ["create", "read", "update", "delete"], "Logs": ["create", "read", "update", "delete"], "Tokens": ["create", "read", "update", "delete"], @@ -78,6 +80,7 @@ "app_permissions": { "Environments": ["read", "create", "update"], "Secrets": ["create", "read", "update", "delete"], + "DynamicSecretLeases": ["create", "read", "update", "delete"], "Lockbox": ["create", "read", "update", "delete"], "Logs": ["create", "read", "update", "delete"], "Tokens": ["create", "read", "update", "delete"], @@ -111,6 +114,7 @@ "app_permissions": { "Environments": ["read", "create", "update"], "Secrets": ["create", "read", "update", "delete"], + "DynamicSecretLeases": ["create", "read"], "Lockbox": ["create", "read", "update", "delete"], "Logs": ["read"], "Tokens": ["read", "create"], @@ -140,6 +144,7 @@ "app_permissions": { "Environments": ["read", "create", "update", "delete"], "Secrets": ["create", "read", "update", "delete"], + "DynamicSecretLeases": ["create", "read"], "Lockbox": [], "Logs": [], "Tokens": [], diff --git a/backend/api/utils/secrets.py b/backend/api/utils/secrets.py index b460059c1..0ca1556e0 100644 --- a/backend/api/utils/secrets.py +++ b/backend/api/utils/secrets.py @@ -144,7 +144,8 @@ def normalize_path_string(path): def check_for_duplicates_blind(secrets, environment): """ - Checks if a list of secrets contains any duplicates internally or in the target env + path by checking each secret's key_digest + Checks if a list of secrets contains any duplicates internally or in the target env + path by checking each secret's key_digest. + Also checks key_map key_digest for DynamicSecret objects at the environment and path. Args: secrets (List[Dict]): The list of encrypted secrets to check for duplicates. @@ -154,38 +155,59 @@ def check_for_duplicates_blind(secrets, environment): bool: True if a duplicate is found, False otherwise. """ Secret = apps.get_model("api", "Secret") + DynamicSecret = apps.get_model("api", "DynamicSecret") processed_secrets = set() # Set to store processed secrets + # --- Collect digests from input secrets --- for secret in secrets: if "keyDigest" in secret: try: path = normalize_path_string(secret["path"]) except: path = "/" + processed_secrets.add((path, secret["keyDigest"])) - # Check if the secret is already processed - if (path, secret["keyDigest"]) in processed_secrets: - return True # Found a duplicate within the list - - # Check if the secret already exists in the database + # --- Check static secrets in DB --- + for secret in secrets: + if "keyDigest" in secret: + try: + path = normalize_path_string(secret["path"]) + except: + path = "/" query_duplicates = Secret.objects.filter( environment=environment, path=path, key_digest=secret["keyDigest"], deleted_at=None, ) - if "id" in secret: query_duplicates = query_duplicates.exclude(id=secret["id"]) - if query_duplicates.exists(): - return True # Found a duplicate in the database - - # Add the processed secret to the set - processed_secrets.add((path, secret["keyDigest"])) + return True - return False # No duplicates found + # --- Check dynamic secrets key_map in DB --- + for secret in secrets: + try: + path = normalize_path_string(secret.get("path", "/")) + except: + path = "/" + dynamic_secrets_qs = DynamicSecret.objects.filter( + environment=environment, + path=path, + deleted_at=None, + ) + exclude_id = secret.get("dynamic_secret_id") + if exclude_id: + dynamic_secrets_qs = dynamic_secrets_qs.exclude(id=exclude_id) + for dyn_secret in dynamic_secrets_qs: + key_map = dyn_secret.key_map or [] + for entry in key_map: + key_digest = entry.get("key_digest") + if key_digest and (path, key_digest) in processed_secrets: + return True + + return False def decompose_path_and_key(composed_key): diff --git a/backend/api/views/secrets.py b/backend/api/views/secrets.py index ff0f6f075..62d948076 100644 --- a/backend/api/views/secrets.py +++ b/backend/api/views/secrets.py @@ -1,5 +1,7 @@ from api.auth import PhaseTokenAuthentication from api.models import ( + DynamicSecret, + DynamicSecretLease, Environment, PersonalSecret, Secret, @@ -26,10 +28,19 @@ METHOD_TO_ACTION, get_resolver_request_meta, ) - +import logging import json from api.content_negotiation import CamelCaseContentNegotiation from api.utils.access.middleware import IsIPAllowed +from ee.integrations.secrets.dynamic.exceptions import ( + DynamicSecretError, + PlanRestrictionError, + TTLExceededError, +) +from ee.integrations.secrets.dynamic.serializers import DynamicSecretSerializer +from ee.integrations.secrets.dynamic.utils import ( + create_dynamic_secret_lease, +) from rest_framework.views import APIView from rest_framework.permissions import IsAuthenticated from rest_framework.exceptions import PermissionDenied @@ -42,6 +53,8 @@ ) from rest_framework.renderers import JSONRenderer +logger = logging.getLogger(__name__) + class E2EESecretsView(APIView): authentication_classes = [PhaseTokenAuthentication] @@ -138,7 +151,129 @@ def get(self, request, *args, **kwargs): secrets, many=True, context={"org_member": request.auth["org_member"]} ) - return Response(serializer.data, status=status.HTTP_200_OK) + include_dynamic_secrets = ( + # treat presence (any value) of either header as True unless explicitly "false" + ( + "dynamic" in request.headers + and request.headers.get("dynamic", "false").lower() != "false" + ) + or ( + "include_dynamic" in request.headers + and request.headers.get("include_dynamic", "false").lower() != "false" + ) + ) + + dynamic_secrets_data = [] + if include_dynamic_secrets: + dynamic_secrets_filter = { + "environment": env, + "deleted_at": None, + } + try: + path = request.headers.get("path") + if path: + path = normalize_path_string(path) + dynamic_secrets_filter["path"] = path + except Exception: + pass + + dynamic_secrets_qs = DynamicSecret.objects.filter(**dynamic_secrets_filter) + + # If lease header is present, generate a lease per secret + include_lease = ( + "lease" in request.headers + and request.headers.get("lease", "false").lower() != "false" + ) + + # Get optional lease_ttl header for custom TTL + lease_ttl = request.headers.get("lease_ttl") + if lease_ttl: + try: + lease_ttl = int(lease_ttl) + except ValueError: + return Response( + {"error": "lease_ttl must be a valid integer (seconds)"}, + status=400, + ) + + service_account = None + if request.auth.get("service_account_token") is not None: + service_account = request.auth["service_account_token"].service_account + + if include_lease: + leases_by_secret_id = {} + failed_leases = [] + for ds in dynamic_secrets_qs: + try: + lease, _ = create_dynamic_secret_lease( + ds, + ttl=lease_ttl, # Pass the TTL if provided + organisation_member=request.auth.get("org_member"), + service_account=service_account, + request=request, + ) + leases_by_secret_id[ds.id] = str(lease.id) + except PlanRestrictionError as e: + return Response({"error": str(e)}, status=403) + except DynamicSecretError as e: + failed_leases.append( + { + "secret_id": str(ds.id), + "secret_name": ds.name, + "error": str(e), + } + ) + except Exception as e: + logger.exception( + "Unexpected error creating lease for dynamic secret %s", + ds.id, + ) + failed_leases.append( + { + "secret_id": str(ds.id), + "secret_name": ds.name, + "error": "Internal error occurred", + } + ) + + # If any leases failed to create, return error response + if failed_leases: + return Response( + { + "error": "One or more dynamic secret leases could not be created", + "failed_leases": failed_leases, + "successful_leases": len(leases_by_secret_id), + }, + status=400, + ) + + # Serialize each secret with its lease_id in context + dynamic_secrets_data = [ + DynamicSecretSerializer( + ds, + context={ + "sse": False, + "with_credentials": True, + "lease_id": leases_by_secret_id.get(ds.id), + }, + ).data + for ds in dynamic_secrets_qs + ] + else: + # Serialize without lease + dynamic_secrets_data = DynamicSecretSerializer( + dynamic_secrets_qs, many=True, context={"sse": False} + ).data + + response_data = serializer.data + + if include_dynamic_secrets: + response_data.extend(dynamic_secrets_data) + + return Response( + response_data, + status=status.HTTP_200_OK, + ) def post(self, request, *args, **kwargs): @@ -451,7 +586,137 @@ def get(self, request, *args, **kwargs): }, ) - return Response(serializer.data, status=status.HTTP_200_OK) + include_dynamic_secrets = ( + # treat presence (any value) of either param as True unless explicitly "false" + ( + "dynamic" in request.GET + and request.GET.get("dynamic", "false").lower() != "false" + ) + or ( + "include_dynamic" in request.GET + and request.GET.get("include_dynamic", "false").lower() != "false" + ) + ) + + dynamic_secrets_data = [] + if include_dynamic_secrets: + dynamic_secrets_filter = { + "environment": env, + "deleted_at": None, + } + try: + path = request.GET.get("path") + if path: + path = normalize_path_string(path) + dynamic_secrets_filter["path"] = path + except Exception: + pass + + dynamic_secrets_qs = DynamicSecret.objects.filter(**dynamic_secrets_filter) + + # If ?lease is present, generate a lease per secret + include_lease = ( + "lease" in request.GET + and request.GET.get("lease", "false").lower() != "false" + ) + + # Get optional lease_ttl parameter for custom TTL + lease_ttl = request.GET.get("lease_ttl") + if lease_ttl: + try: + lease_ttl = int(lease_ttl) + except ValueError: + return Response( + {"error": "lease_ttl must be a valid integer (seconds)"}, + status=400, + ) + + service_account = None + if request.auth.get("service_account_token") is not None: + service_account = request.auth["service_account_token"].service_account + + if include_lease: + leases_by_secret_id = {} + failed_leases = [] + for ds in dynamic_secrets_qs: + try: + lease, _ = create_dynamic_secret_lease( + ds, + ttl=lease_ttl, + organisation_member=request.auth.get("org_member"), + service_account=service_account, + request=request, + ) + leases_by_secret_id[ds.id] = str(lease.id) + except PlanRestrictionError as e: + return Response({"error": str(e)}, status=403) + except (TTLExceededError,) as e: + failed_leases.append( + { + "secret_id": str(ds.id), + "secret_name": ds.name, + "error": str(e), + } + ) + except DynamicSecretError as e: + failed_leases.append( + { + "secret_id": str(ds.id), + "secret_name": ds.name, + "error": str(e), + } + ) + except Exception as e: + logger.exception( + "Unexpected error creating lease for dynamic secret %s", + ds.id, + ) + failed_leases.append( + { + "secret_id": str(ds.id), + "secret_name": ds.name, + "error": "Internal error occurred", + } + ) + + # If any leases failed to create, return error response + if failed_leases: + return Response( + { + "error": "One or more dynamic secret leases could not be created", + "failed_leases": failed_leases, + "successful_leases": len(leases_by_secret_id), + }, + status=400, + ) + + # Serialize each secret with its lease_id in context + dynamic_secrets_data = [ + DynamicSecretSerializer( + ds, + context={ + "sse": True, + "with_credentials": True, + "lease_id": leases_by_secret_id.get(ds.id), + }, + ).data + for ds in dynamic_secrets_qs + ] + else: + # Serialize without lease + dynamic_secrets_data = DynamicSecretSerializer( + dynamic_secrets_qs, many=True, context={"sse": True} + ).data + + response_data = serializer.data + + if include_dynamic_secrets: + response_data.extend(dynamic_secrets_data) + + return Response( + response_data, + status=status.HTTP_200_OK, + ) def post(self, request, *args, **kwargs): diff --git a/backend/backend/graphene/types.py b/backend/backend/graphene/types.py index ebf3d3c82..9482d166c 100644 --- a/backend/backend/graphene/types.py +++ b/backend/backend/graphene/types.py @@ -4,6 +4,8 @@ user_can_access_environment, user_has_permission, ) +from ee.integrations.secrets.dynamic.graphene.queries import resolve_dynamic_secrets +from ee.integrations.secrets.dynamic.graphene.types import DynamicSecretType from backend.quotas import PLAN_CONFIG import graphene from enum import Enum @@ -12,6 +14,7 @@ from api.models import ( ActivatedPhaseLicense, CustomUser, + DynamicSecret, Environment, EnvironmentKey, EnvironmentSync, @@ -389,7 +392,10 @@ def resolve_folder_count(self, info): return SecretFolder.objects.filter(folder=self).count() def resolve_secret_count(self, info): - return Secret.objects.filter(folder=self).count() + return ( + Secret.objects.filter(folder=self).count() + + DynamicSecret.objects.filter(folder=self, deleted_at=None).count() + ) class SecretEventType(DjangoObjectType): @@ -500,6 +506,9 @@ class EnvironmentType(DjangoObjectType): secrets = graphene.NonNull( graphene.List(SecretType), path=graphene.String(required=False) ) + dynamic_secrets = graphene.NonNull( + graphene.List(DynamicSecretType), path=graphene.String(required=False) + ) folder_count = graphene.Int() secret_count = graphene.Int() members = graphene.NonNull(graphene.List(OrganisationMemberType)) @@ -542,6 +551,10 @@ def resolve_secrets(self, info, path=None): return Secret.objects.filter(**filter).order_by("-created_at") + def resolve_dynamic_secrets(self, info, path=None): + # Reuse the existing resolver from queries.py + return resolve_dynamic_secrets(root=None, info=info, env_id=self.id, path=path) + def resolve_folders(self, info, path=None): if not user_can_access_environment(info.context.user.userId, self.id): raise GraphQLError("You don't have access to this environment") @@ -557,7 +570,10 @@ def resolve_folder_count(self, info): return SecretFolder.objects.filter(environment=self).count() def resolve_secret_count(self, info): - return Secret.objects.filter(environment=self, deleted_at=None).count() + return ( + Secret.objects.filter(environment=self, deleted_at=None).count() + + DynamicSecret.objects.filter(environment=self, deleted_at=None).count() + ) def resolve_wrapped_seed(self, info): org_member = OrganisationMember.objects.get( diff --git a/backend/backend/schema.py b/backend/backend/schema.py index 66a34f6f1..1496c7eef 100644 --- a/backend/backend/schema.py +++ b/backend/backend/schema.py @@ -5,6 +5,24 @@ from api.utils.syncing.gitlab.main import GitLabGroupType, GitLabProjectType from api.utils.syncing.railway.main import RailwayProjectType from api.utils.syncing.render.main import RenderEnvGroupType, RenderServiceType +from ee.integrations.secrets.dynamic.graphene.mutations import ( + DeleteDynamicSecretMutation, + LeaseDynamicSecret, + RenewLeaseMutation, + RevokeLeaseMutation, +) +from ee.integrations.secrets.dynamic.graphene.types import ( + DynamicSecretProviderType, + DynamicSecretType, +) +from ee.integrations.secrets.dynamic.aws.graphene.mutations import ( + CreateAWSDynamicSecretMutation, + UpdateAWSDynamicSecretMutation, +) +from ee.integrations.secrets.dynamic.graphene.queries import ( + resolve_dynamic_secret_providers, + resolve_dynamic_secrets, +) from backend.graphene.mutations.service_accounts import ( CreateServiceAccountMutation, CreateServiceAccountTokenMutation, @@ -33,7 +51,7 @@ StripeSubscriptionDetails, resolve_stripe_checkout_details, resolve_stripe_subscription_details, - resolve_stripe_customer_portal_url + resolve_stripe_customer_portal_url, ) from ee.billing.graphene.mutations.stripe import ( CancelSubscriptionMutation, @@ -389,7 +407,20 @@ class Query(graphene.ObjectType): StripeSubscriptionDetails, organisation_id=graphene.ID() ) - stripe_customer_portal_url = graphene.String(organisation_id=graphene.ID(required=True)) + stripe_customer_portal_url = graphene.String( + organisation_id=graphene.ID(required=True) + ) + + # Dynamic secrets + dynamic_secret_providers = graphene.List(DynamicSecretProviderType) + dynamic_secrets = graphene.List( + DynamicSecretType, + secret_id=graphene.ID(required=False), + app_id=graphene.ID(required=False), + env_id=graphene.ID(required=False), + path=graphene.String(required=False), + org_id=graphene.ID(), + ) # -------------------------------------------------------------------- @@ -437,6 +468,9 @@ class Query(graphene.ObjectType): resolve_validate_aws_assume_role_credentials ) + resolve_dynamic_secret_providers = resolve_dynamic_secret_providers + resolve_dynamic_secrets = resolve_dynamic_secrets + def resolve_organisations(root, info): memberships = OrganisationMember.objects.filter( user=info.context.user, deleted_at=None @@ -1015,5 +1049,13 @@ class Mutation(graphene.ObjectType): create_setup_intent = CreateSetupIntentMutation.Field() set_default_payment_method = SetDefaultPaymentMethodMutation.Field() + # Dynamic Secrets + create_aws_dynamic_secret = CreateAWSDynamicSecretMutation.Field() + update_aws_dynamic_secret = UpdateAWSDynamicSecretMutation.Field() + delete_dynamic_secret = DeleteDynamicSecretMutation.Field() + create_dynamic_secret_lease = LeaseDynamicSecret.Field() + renew_dynamic_secret_lease = RenewLeaseMutation.Field() + revoke_dynamic_secret_lease = RevokeLeaseMutation.Field() + schema = graphene.Schema(query=Query, mutation=Mutation) diff --git a/backend/backend/settings.py b/backend/backend/settings.py index fa5b2315f..301c1c3d9 100644 --- a/backend/backend/settings.py +++ b/backend/backend/settings.py @@ -95,6 +95,7 @@ def get_version(): "allauth.socialaccount.providers.gitlab", "allauth.socialaccount.providers.microsoft", "api.config.APIConfig", + # "ee", "logs", "graphene_django", "django_rq", diff --git a/backend/backend/urls.py b/backend/backend/urls.py index eaac0f175..26f2c9087 100644 --- a/backend/backend/urls.py +++ b/backend/backend/urls.py @@ -14,7 +14,6 @@ ) from api.views.kms import kms - CLOUD_HOSTED = settings.APP_HOST == "cloud" urlpatterns = [ @@ -32,6 +31,10 @@ path("secrets/tokens/", secrets_tokens), path("oauth/github/callback", github_integration_callback), path("lockbox/", LockboxView.as_view()), + path( + "public/v1/secrets/dynamic/", + include("ee.integrations.secrets.dynamic.rest.urls"), + ), ] if CLOUD_HOSTED: diff --git a/backend/ee/__init__.py b/backend/ee/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/ee/integrations/__init__.py b/backend/ee/integrations/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/ee/integrations/secrets/__init__.py b/backend/ee/integrations/secrets/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/ee/integrations/secrets/dynamic/__init__.py b/backend/ee/integrations/secrets/dynamic/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/ee/integrations/secrets/dynamic/aws/graphene/mutations.py b/backend/ee/integrations/secrets/dynamic/aws/graphene/mutations.py new file mode 100644 index 000000000..ccc510be6 --- /dev/null +++ b/backend/ee/integrations/secrets/dynamic/aws/graphene/mutations.py @@ -0,0 +1,235 @@ +from api.utils.access.permissions import ( + user_can_access_environment, + user_has_permission, + user_is_org_member, +) +from api.utils.secrets import create_environment_folder_structure +from ee.integrations.secrets.dynamic.utils import ( + create_dynamic_secret, + validate_key_map, +) + +from ee.integrations.secrets.dynamic.graphene.types import ( + DynamicSecretType, + KeyMapInput, +) + +from datetime import timedelta +import graphene +from graphql import GraphQLError +from django.core.exceptions import ValidationError +from api.models import ( + DynamicSecret, + Organisation, + Environment, + OrganisationMember, + ProviderCredentials, +) +from graphene.types.generic import GenericScalar + + +class AWSConfigInput(graphene.InputObjectType): + username_template = graphene.String(required=True) + iam_path = graphene.String(required=False, default_value="/") + permission_boundary_arn = graphene.String(required=False) + groups = graphene.String(required=False) # comma-separated + policy_arns = graphene.String(required=False) # comma-separated + policy_document = GenericScalar(required=False) + + +class CreateAWSDynamicSecretMutation(graphene.Mutation): + class Arguments: + organisation_id = graphene.ID(required=True) + environment_id = graphene.ID(required=True) + name = graphene.String(required=True) + description = graphene.String(required=False) + path = graphene.String(required=False) + default_ttl = graphene.Int() # seconds + max_ttl = graphene.Int() # seconds + authentication_id = graphene.ID(required=False) + config = AWSConfigInput(required=True) + key_map = graphene.List(KeyMapInput, required=True) + + dynamic_secret = graphene.Field(DynamicSecretType) + + @classmethod + def mutate( + cls, + root, + info, + organisation_id, + environment_id, + name, + config, + key_map, + description="", + path="/", + default_ttl=None, + max_ttl=None, + authentication_id=None, + ): + user = info.context.user + + # --- permission checks --- + if not user_is_org_member(user.userId, organisation_id): + raise GraphQLError("You don't have access to this organisation") + + org = Organisation.objects.get(id=organisation_id) + + if not user_has_permission(user, "create", "Secrets", org, True): + raise GraphQLError("You don't have permission to create Dynamic Secrets") + + if not user_can_access_environment(user.userId, environment_id): + raise GraphQLError("You don't have access to this environment") + + env = Environment.objects.get(id=environment_id) + + if not env.app.sse_enabled: + raise GraphQLError("SSE is not enabled!") + + authentication = None + if authentication_id: + try: + authentication = ProviderCredentials.objects.get( + id=authentication_id, organisation=org + ) + except ProviderCredentials.DoesNotExist: + raise GraphQLError("Invalid authentication credentials") + + try: + validated_key_map = validate_key_map(key_map, "aws", env, path) + key_map = validated_key_map + except ValidationError as e: + message = f"Error creating secret: {e.messages[0]}" + raise GraphQLError(message) + + # --- create secret --- + try: + dynamic_secret = create_dynamic_secret( + environment=Environment.objects.get(id=environment_id), + path=path, + name=name, + description=description, + default_ttl=timedelta(seconds=default_ttl), + max_ttl=timedelta(seconds=max_ttl), + authentication=authentication, + provider="aws", + config=config, + key_map=key_map, + ) + except ValidationError as e: + raise GraphQLError(e.message) + + return CreateAWSDynamicSecretMutation(dynamic_secret=dynamic_secret) + + +class UpdateAWSDynamicSecretMutation(graphene.Mutation): + class Arguments: + dynamic_secret_id = graphene.ID(required=True) + organisation_id = graphene.ID(required=True) + name = graphene.String(required=True) + description = graphene.String(required=False) + path = graphene.String(required=False) + default_ttl = graphene.Int() # seconds + max_ttl = graphene.Int() # seconds + authentication_id = graphene.ID(required=False) + config = AWSConfigInput(required=True) + key_map = graphene.List(KeyMapInput, required=True) + + dynamic_secret = graphene.Field(DynamicSecretType) + + @classmethod + def mutate( + cls, + root, + info, + dynamic_secret_id, + organisation_id, + name, + config, + key_map, + description="", + path=None, + default_ttl=None, + max_ttl=None, + authentication_id=None, + ): + user = info.context.user + + # --- permission checks --- + if not user_is_org_member(user.userId, organisation_id): + raise GraphQLError("You don't have access to this organisation") + + org = Organisation.objects.get(id=organisation_id) + + dynamic_secret = DynamicSecret.objects.get(id=dynamic_secret_id) + + env = Environment.objects.get(id=dynamic_secret.environment.id) + + if not user_has_permission(user, "update", "Secrets", org, True): + raise GraphQLError("You don't have permission to update Dynamic Secrets") + + if not user_can_access_environment(user.userId, dynamic_secret.environment.id): + raise GraphQLError("You don't have access to this environment") + + if not env.app.sse_enabled: + raise GraphQLError("SSE is not enabled!") + + folder = None + if path is not None and path != "/": + folder = create_environment_folder_structure( + path, dynamic_secret.environment.id + ) + + authentication = None + if authentication_id: + try: + authentication = ProviderCredentials.objects.get( + id=authentication_id, organisation=org + ) + except ProviderCredentials.DoesNotExist: + raise GraphQLError("Invalid authentication credentials") + + # --- ensure name is unique in this environment and path --- + if ( + DynamicSecret.objects.filter( + environment=dynamic_secret.environment, + path=path, + name=name, + deleted_at=None, + ) + .exclude(id=dynamic_secret_id) + .exists() + ): + raise GraphQLError( + f"A dynamic secret with name '{name}' already exists at this path." + ) + + # --- update secret fields --- + dynamic_secret.name = name + dynamic_secret.description = description + if folder is not None: + dynamic_secret.folder = folder + dynamic_secret.default_ttl = ( + timedelta(seconds=default_ttl) if default_ttl else None + ) + dynamic_secret.max_ttl = timedelta(seconds=max_ttl) if max_ttl else None + dynamic_secret.authentication = authentication + dynamic_secret.config = config + + try: + validated_key_map = validate_key_map( + key_map, + dynamic_secret.provider, + dynamic_secret.environment, + path, + dynamic_secret.id, + ) + dynamic_secret.key_map = validated_key_map + except ValidationError as e: + message = f"Error updating secret: {e.messages[0]}" + raise GraphQLError(message) + + dynamic_secret.save() + + return UpdateAWSDynamicSecretMutation(dynamic_secret=dynamic_secret) diff --git a/backend/ee/integrations/secrets/dynamic/aws/graphene/types.py b/backend/ee/integrations/secrets/dynamic/aws/graphene/types.py new file mode 100644 index 000000000..69d076910 --- /dev/null +++ b/backend/ee/integrations/secrets/dynamic/aws/graphene/types.py @@ -0,0 +1,18 @@ +import graphene + +from graphene.types.generic import GenericScalar # JSON-safe scalar + + +class AWSConfigType(graphene.ObjectType): + username_template = graphene.String(required=True) + iam_path = graphene.String(required=False, default_value="/") + permission_boundary_arn = graphene.String(required=False) + groups = graphene.String(required=False) + policy_arns = graphene.String(required=False) + policy_document = GenericScalar(required=False) # JSON object + + +class AwsCredentialsType(graphene.ObjectType): + access_key_id = graphene.String() + secret_access_key = graphene.String() + username = graphene.String() diff --git a/backend/ee/integrations/secrets/dynamic/aws/utils.py b/backend/ee/integrations/secrets/dynamic/aws/utils.py new file mode 100644 index 000000000..a460809ed --- /dev/null +++ b/backend/ee/integrations/secrets/dynamic/aws/utils.py @@ -0,0 +1,790 @@ +from django.utils import timezone +from django.core.exceptions import ValidationError + +from api.models import DynamicSecret, DynamicSecretLeaseEvent +from api.utils.crypto import decrypt_asymmetric, encrypt_asymmetric, get_server_keypair +from api.utils.syncing.aws.auth import get_aws_sts_session +from api.utils.secrets import get_environment_keys + +from api.utils.rest import get_resolver_request_meta +from ee.integrations.secrets.dynamic.exceptions import LeaseAlreadyRevokedError +from backend.utils.secrets import get_secret +from ee.integrations.secrets.dynamic.providers import DynamicSecretProviders +import logging +from datetime import datetime, timedelta +import boto3 +from botocore.exceptions import ClientError, BotoCoreError +from api.models import DynamicSecret, DynamicSecretLease +import string +import random +import re +import django_rq + + +logger = logging.getLogger(__name__) + + +def generate_random_string(length=8): + """Generate random alphanumeric suffix.""" + return "".join(random.choices(string.ascii_lowercase + string.digits, k=length)) + + +def render_username_template(template: str) -> str: + """ + Render a username template by replacing {{ random }} placeholders. + + Examples: + 'prefix-{{ random }}' -> 'prefix-x8f2k9' + '{{ random }}-suffix' -> 'q1z9xk-suffix' + 'plain-username' -> 'plain-username' + """ + + def replace_placeholder(match): + return generate_random_string( + random.randint(6, 18) + ) # random length between 6–18 + + return re.sub(r"\{\{\s*random\s*\}\}", replace_placeholder, template) + + +def get_aws_access_key_credentials(provider_credentials): + """ + Get AWS integration Access Key credentials from ProviderCredentials object. + + Args: + provider_credentials: ProviderCredentials object with access key credentials + + Returns: + dict: Decrypted credentials containing access_key_id, secret_access_key, and region + """ + pk, sk = get_server_keypair() + + access_key_id = decrypt_asymmetric( + provider_credentials.credentials["access_key_id"], sk.hex(), pk.hex() + ) + secret_access_key = decrypt_asymmetric( + provider_credentials.credentials["secret_access_key"], + sk.hex(), + pk.hex(), + ) + region = decrypt_asymmetric( + provider_credentials.credentials["region"], sk.hex(), pk.hex() + ) + + return { + "access_key_id": access_key_id, + "secret_access_key": secret_access_key, + "region": region, + } + + +def get_aws_assume_role_credentials(provider_credentials): + """ + Get AWS integratio Assume Role credentials from ProviderCredentials object. + + Args: + provider_credentials: ProviderCredentials object with assume role credentials + + Returns: + dict: Decrypted credentials containing role_arn, region, and external_id + """ + pk, sk = get_server_keypair() + + role_arn = decrypt_asymmetric( + provider_credentials.credentials["role_arn"], sk.hex(), pk.hex() + ) + + region = None + if "region" in provider_credentials.credentials: + region = decrypt_asymmetric( + provider_credentials.credentials["region"], sk.hex(), pk.hex() + ) + + external_id = None + if "external_id" in provider_credentials.credentials: + external_id = decrypt_asymmetric( + provider_credentials.credentials["external_id"], sk.hex(), pk.hex() + ) + + return { + "role_arn": role_arn, + "region": region, + "external_id": external_id, + } + + +def build_dynamic_secret_config(provider: str, user_config: dict) -> dict: + """ + Merge user-supplied config with provider defaults for key_map. + - All non-key_map fields are preserved exactly as user provided. + - key_map is filled with defaults where user did not specify. + """ + # --- find provider definition --- + provider_def = None + for prov in DynamicSecretProviders.__dict__.values(): + if isinstance(prov, dict) and prov.get("id") == provider: + provider_def = prov + break + if not provider_def: + raise ValidationError(f"Unsupported provider: {provider}") + + # --- start with user config --- + merged_config = dict(user_config or {}) + + # --- build key_map --- + key_map = merged_config.get("key_map", {}) + merged_key_map = {} + for cred in provider_def.get("credentials", []): + cid = cred["id"] + default_key = cred.get("default_key_name") + merged_key_map[cid] = key_map.get(cid, default_key) + + merged_config["key_map"] = merged_key_map + return merged_config + + +def create_temporary_user(user_config, iam_client): + """ + Create a temporary IAM user with specified configuration. + + Args: + user_config (dict): Configuration for user creation + - username_template (str): Base name for user + - groups (list of str, optional): Comma separated list of groups to add user to + - policy_arns (list of str, optional): Policy ARN to attach + - iam_user_path (str, optional): User path + - permission_boundary_arn (str, optional): Permission boundary ARN + - ttl_seconds (int): Time to live in seconds + + Returns: + tuple: (user_result_dict, meta_dict) User creation result with username and metadata + """ + # Initialize metadata tracking + meta = { + "action": "create", + "provider": "aws", + "attached_policies": [], + "added_groups": [], + "tags_applied": [], + } + + try: + # Generate unique username + base_template = user_config.get( + "username_template", "phase-dynamic-{{ random }}" + ) + username = render_username_template(base_template) + meta["username"] = username + + # Prepare user creation parameters + path = user_config.get("iam_user_path", "/") or "/" + + # ensure leading slash + if not path.startswith("/"): + path = "/" + path + + # ensure trailing slash + if not path.endswith("/"): + path = path + "/" + + create_params = { + "UserName": username, + "Path": path, + } + meta["iam_path"] = path + + # Add permission boundary if specified + if user_config.get("permission_boundary_arn"): + create_params["PermissionsBoundary"] = user_config[ + "permission_boundary_arn" + ] + meta["permission_boundary_arn"] = user_config["permission_boundary_arn"] + + # Create IAM user + response = iam_client.create_user(**create_params) + logger.info(f"Created IAM user {username}") + meta["user_created"] = True + meta["user_arn"] = response["User"]["Arn"] + + # Add tags to track the user + creation_time = datetime.utcnow() + expiry_time = creation_time + timedelta( + seconds=user_config.get("ttl_seconds", 60) + ) + + tags = [ + {"Key": "CreatedBy", "Value": "phase-dynamic-secrets"}, + {"Key": "CreationTime", "Value": creation_time.isoformat()}, + {"Key": "ExpiryTime", "Value": expiry_time.isoformat()}, + {"Key": "TTL", "Value": str(user_config.get("ttl_seconds", 60))}, + ] + iam_client.tag_user( + UserName=username, + Tags=tags, + ) + logger.info(f"Tagged IAM user {username}") + meta["tags_applied"] = [tag["Key"] for tag in tags] + + # Attach policies if specified + policy_arns = user_config.get("policy_arns") + if policy_arns: + # Support both comma-separated string and list + if isinstance(policy_arns, str): + policy_arns = [p.strip() for p in policy_arns.split(",") if p.strip()] + for policy_arn in policy_arns: + try: + iam_client.attach_user_policy( + UserName=username, PolicyArn=policy_arn + ) + logger.info(f"Attached policy {policy_arn} to user {username}") + meta["attached_policies"].append(policy_arn) + except ClientError as e: + logger.error( + f"Failed to attach policy {policy_arn} to user {username}: {str(e)}" + ) + meta["failed_policy_attachments"] = meta.get( + "failed_policy_attachments", [] + ) + meta["failed_policy_attachments"].append( + {"policy_arn": policy_arn, "error": str(e)} + ) + raise + + # Add user to specified groups + groups = user_config.get("groups") + if groups: + # If groups is a string (legacy), split by comma + if isinstance(groups, str): + groups = [g.strip() for g in groups.split(",") if g.strip()] + for group_identifier in groups: + try: + # Handle both group names and ARNs + if group_identifier.startswith("arn:aws:iam::"): + # Extract group name from ARN + # ARN format: arn:aws:iam::account-id:group/group-name + group_name = group_identifier.split("/")[-1] + else: + # Assume it's already a group name + group_name = group_identifier + + iam_client.add_user_to_group( + GroupName=group_name, UserName=username + ) + logger.info( + f"Added user {username} to group {group_name} (from: {group_identifier})" + ) + meta["added_groups"].append( + {"group_name": group_name, "group_identifier": group_identifier} + ) + except ClientError as e: + logger.error( + f"Failed to add user {username} to group {group_identifier}: {str(e)}" + ) + meta["failed_group_additions"] = meta.get( + "failed_group_additions", [] + ) + meta["failed_group_additions"].append( + {"group_identifier": group_identifier, "error": str(e)} + ) + raise + + logger.info(f"Successfully created temporary IAM user: {username}") + meta["outcome"] = "success" + + user_result = { + "username": username, + "arn": response["User"]["Arn"], + "creation_time": creation_time.isoformat(), + "expiry_time": expiry_time.isoformat(), + } + + return user_result, meta + + except ClientError as e: + logger.error(f"AWS client error creating user: {str(e)}") + meta["outcome"] = "error" + meta["error"] = str(e) + raise + except Exception as e: + logger.error(f"Unexpected error creating user: {str(e)}") + meta["outcome"] = "error" + meta["error"] = str(e) + raise + + +def create_access_key(username, iam_client): + """ + Create access key for IAM user. + + Args: + username (str): IAM username + + Returns: + tuple: (access_key_dict, meta_dict) Access key details and metadata + """ + meta = { + "action": "create_access_key", + "username": username, + } + + try: + response = iam_client.create_access_key(UserName=username) + access_key = response["AccessKey"] + + logger.info(f"Successfully created access key for user: {username}") + meta["access_key_id"] = access_key["AccessKeyId"] + meta["outcome"] = "success" + + access_key_result = { + "access_key_id": access_key["AccessKeyId"], + "secret_access_key": access_key["SecretAccessKey"], + "status": access_key["Status"], + } + + return access_key_result, meta + + except ClientError as e: + logger.error(f"AWS client error creating access key: {str(e)}") + meta["outcome"] = "error" + meta["error"] = str(e) + raise + except Exception as e: + logger.error(f"Unexpected error creating access key: {str(e)}") + meta["outcome"] = "error" + meta["error"] = str(e) + raise + + +def get_sts_client(region="us-east-1"): + + aws_access_key_id = get_secret("AWS_INTEGRATION_ACCESS_KEY_ID") + aws_secret_access_key = get_secret("AWS_INTEGRATION_SECRET_ACCESS_KEY") + + # If explicit integration credentials are passed, use them. + # Otherwise, rely on the instance/machine role auth provider chain. + if aws_access_key_id and aws_secret_access_key: + sts_client = boto3.client( + "sts", + region_name=region, + aws_access_key_id=aws_access_key_id, + aws_secret_access_key=aws_secret_access_key, + ) + else: + sts_client = boto3.client("sts", region_name=region) + + return sts_client + + +import boto3 + + +def get_iam_client(secret: DynamicSecret) -> tuple[boto3.client, dict]: + """ + Construct an IAM client using the given DynamicSecret's authentication config. + Returns (iam_client, aws_credentials). + """ + sts_client = get_sts_client() + + # Determine authentication method + has_role_arn = "role_arn" in secret.authentication.credentials + aws_credentials = {} + + if has_role_arn: + integration_credentials = get_aws_assume_role_credentials(secret.authentication) + role_arn = integration_credentials.get("role_arn") + external_id = integration_credentials.get("external_id") + + assume_params = { + "RoleArn": role_arn, + "RoleSessionName": "phase-dynamic-secret-session", + } + if external_id: + assume_params["ExternalId"] = external_id + + assumed_role = sts_client.assume_role(**assume_params) + aws_credentials = assumed_role["Credentials"] + else: + integration_credentials = get_aws_access_key_credentials(secret.authentication) + aws_credentials["AccessKeyId"] = integration_credentials.get("access_key_id") + aws_credentials["SecretAccessKey"] = integration_credentials.get( + "secret_access_key" + ) + + region = integration_credentials.get("region") + + # Construct IAM client kwargs + iam_client_kwargs = { + "service_name": "iam", + "region_name": region, + "aws_access_key_id": aws_credentials["AccessKeyId"], + "aws_secret_access_key": aws_credentials["SecretAccessKey"], + } + if "SessionToken" in aws_credentials: + iam_client_kwargs["aws_session_token"] = aws_credentials["SessionToken"] + + return boto3.client(**iam_client_kwargs), aws_credentials + + +def create_aws_dynamic_secret_lease( + *, + secret: DynamicSecret, + lease_name, + organisation_member=None, + service_account=None, + ttl_seconds, +) -> dict: + """ + Create a new lease for dynamic AWS credentials. + """ + # Initialize combined metadata + combined_meta = { + "action": "create_lease", + "provider": "aws", + "ttl_seconds": ttl_seconds, + } + + lease_config = secret.config + + if not organisation_member and not service_account: + raise ValidationError("Must set either organisation_member or service_account") + if organisation_member and service_account: + raise ValidationError( + "Only one of organisation_member or service_account may be set" + ) + + iam_client, _ = get_iam_client(secret) + created_username = None # Track if we created a user for cleanup + + # Build config for user creation + user_config = { + "username_template": lease_config.get("username_template"), + "groups": lease_config.get("groups"), + "policy_arns": lease_config.get("policy_arns"), + "iam_user_path": lease_config.get("iam_path", "/"), + "permission_boundary_arn": lease_config.get("permission_boundary_arn"), + "ttl_seconds": ttl_seconds, + } + + try: + user_result, user_meta = create_temporary_user(user_config, iam_client) + combined_meta["user_creation"] = user_meta + username = user_result["username"] + created_username = username # Mark that we created a user + + # Create access key for the user + access_key_result, access_key_meta = create_access_key(username, iam_client) + combined_meta["access_key_creation"] = access_key_meta + + lease_data = { + "username": username, + "user_arn": user_result["arn"], + "access_key_id": access_key_result["access_key_id"], + "secret_access_key": access_key_result["secret_access_key"], + "creation_time": user_result["creation_time"], + "expiry_time": user_result["expiry_time"], + "ttl_seconds": user_config.get("ttl_seconds", 60), + } + + env_pubkey, _ = get_environment_keys(secret.environment.id) + + encrypted_credentials = { + "access_key_id": encrypt_asymmetric( + access_key_result["access_key_id"], env_pubkey + ), + "secret_access_key": encrypt_asymmetric( + access_key_result["secret_access_key"], env_pubkey + ), + "username": encrypt_asymmetric(username, env_pubkey), + } + + lease = DynamicSecretLease.objects.create( + secret=secret, + name=lease_name, + organisation_member=organisation_member, + service_account=service_account, + ttl=timedelta(seconds=ttl_seconds), + expires_at=timezone.now() + timedelta(seconds=ttl_seconds), + credentials=encrypted_credentials, + ) + combined_meta["lease_id"] = str(lease.id) + combined_meta["outcome"] = "success" + + # --- Schedule revocation --- + from ee.integrations.secrets.dynamic.utils import schedule_lease_revocation + + schedule_lease_revocation(lease) + combined_meta["revocation_scheduled"] = True + + return lease, lease_data, combined_meta + + except Exception as e: + # If we created a user but something failed later, clean it up + if created_username: + logger.warning( + f"Lease creation failed after user creation, cleaning up user {created_username}" + ) + combined_meta["cleanup_attempted"] = True + try: + cleanup_failed_user_creation(created_username, iam_client) + combined_meta["cleanup_successful"] = True + logger.info( + f"Successfully cleaned up user {created_username} after failed lease creation" + ) + except Exception as cleanup_error: + combined_meta["cleanup_error"] = str(cleanup_error) + logger.error( + f"Failed to cleanup user {created_username}: {cleanup_error}" + ) + # Don't suppress the original error + + # Re-raise the original exception + raise + + +def cleanup_failed_user_creation(username: str, iam_client): + """ + Clean up a partially created IAM user by removing all attached resources. + This is called when lease creation fails after user creation. + """ + try: + # Delete all access keys + try: + access_keys = iam_client.list_access_keys(UserName=username) + for key in access_keys["AccessKeyMetadata"]: + iam_client.delete_access_key( + UserName=username, AccessKeyId=key["AccessKeyId"] + ) + logger.info( + f"Cleaned up access key {key['AccessKeyId']} for user {username}" + ) + except ClientError: + pass # User might not have access keys yet + + # Detach all managed policies + try: + attached_policies = iam_client.list_attached_user_policies( + UserName=username + ) + for policy in attached_policies["AttachedPolicies"]: + iam_client.detach_user_policy( + UserName=username, PolicyArn=policy["PolicyArn"] + ) + logger.info( + f"Cleaned up attached policy {policy['PolicyArn']} for user {username}" + ) + except ClientError: + pass + + # Delete all inline policies + try: + inline_policies = iam_client.list_user_policies(UserName=username) + for policy_name in inline_policies["PolicyNames"]: + iam_client.delete_user_policy(UserName=username, PolicyName=policy_name) + logger.info( + f"Cleaned up inline policy {policy_name} for user {username}" + ) + except ClientError: + pass + + # Remove user from all groups + try: + groups_resp = iam_client.list_groups_for_user(UserName=username) + for group in groups_resp["Groups"]: + iam_client.remove_user_from_group( + UserName=username, GroupName=group["GroupName"] + ) + logger.info( + f"Cleaned up group membership {group['GroupName']} for user {username}" + ) + except ClientError: + pass + + # Finally, delete the user + iam_client.delete_user(UserName=username) + logger.info(f"Cleaned up IAM user {username}") + + except iam_client.exceptions.NoSuchEntityException: + logger.info(f"User {username} already deleted during cleanup") + except ClientError as e: + logger.error(f"Error during cleanup of user {username}: {e}") + raise + except Exception as e: + logger.error(f"Unexpected error during cleanup of user {username}: {e}") + raise + + +def revoke_aws_dynamic_secret_lease( + lease_id, + manual=False, + request=None, + organisation_member=None, + service_account=None, +): + """ + Delete IAM user and all associated credentials. + + Args: + lease_id (DynamicSecretLease): id for the lease containing IAM username to delete + manual (boolean): Whether this is a manual revoke or automatically scheduled expiry. Defaults to false + + Returns: + bool: True if successful, False otherwise + """ + lease = DynamicSecretLease.objects.get(id=lease_id) + + if lease.revoked_at is not None: + logger.info(f"Lease {lease.id} already revoked at {lease.revoked_at}") + raise LeaseAlreadyRevokedError( + f"Lease has already been revoked at {lease.revoked_at}" + ) + + logger.info(f"Revoking lease {lease.id} (manual={manual})") + + iam_client, _ = get_iam_client(lease.secret) + + meta = { + "action": "revoke", + "provider": "aws", + "source": "manual" if manual else "scheduled", + "deleted_access_keys": [], + "detached_policies": [], + "deleted_inline_policies": [], + "removed_groups": [], + } + + # Add request metadata if available + ip_address = user_agent = None + if request is not None: + try: + ip_address, user_agent = get_resolver_request_meta(request) + + logger.info(f"Created revoke event for lease {lease.id}") + except Exception: + logger.error("Failed to read request meta for lease event", exc_info=True) + pass + + try: + # Decrypt username + env_pubkey, env_privkey = get_environment_keys(lease.secret.environment.id) + encrypted_username = lease.credentials.get("username") + username = decrypt_asymmetric(encrypted_username, env_privkey, env_pubkey) + + # List and delete all access keys for the user + access_keys = iam_client.list_access_keys(UserName=username) + for key in access_keys["AccessKeyMetadata"]: + iam_client.delete_access_key( + UserName=username, AccessKeyId=key["AccessKeyId"] + ) + logger.info(f"Deleted access key for user") + meta["deleted_access_keys"].append(key["AccessKeyId"]) + + # Detach all managed policies + attached_policies = iam_client.list_attached_user_policies(UserName=username) + for policy in attached_policies["AttachedPolicies"]: + iam_client.detach_user_policy( + UserName=username, PolicyArn=policy["PolicyArn"] + ) + logger.info(f"Detached policy from user") + meta["detached_policies"].append(policy["PolicyArn"]) + + # Delete all inline policies + inline_policies = iam_client.list_user_policies(UserName=username) + for policy_name in inline_policies["PolicyNames"]: + iam_client.delete_user_policy(UserName=username, PolicyName=policy_name) + logger.info(f"Deleted inline policy from user") + meta["deleted_inline_policies"].append(policy_name) + + # Remove user from all groups + groups_resp = iam_client.list_groups_for_user(UserName=username) + for group in groups_resp["Groups"]: + iam_client.remove_user_from_group( + UserName=username, GroupName=group["GroupName"] + ) + logger.info(f"Removed user {username} from group {group['GroupName']}") + # Store the ARN in metadata for better audit trail + meta["removed_groups"].append(group.get("Arn", group["GroupName"])) + + # Finally, delete the user + iam_client.delete_user(UserName=username) + logger.info(f"Successfully deleted IAM user") + + if manual: + lease.status = DynamicSecretLease.REVOKED + else: + lease.status = DynamicSecretLease.EXPIRED + lease.credentials = {} + lease.revoked_at = timezone.now() + lease.save() + + DynamicSecretLeaseEvent.objects.create( + lease=lease, + event_type=DynamicSecretLease.REVOKED, + organisation_member=organisation_member, + service_account=service_account, + ip_address=ip_address, + user_agent=user_agent, + metadata=meta, + ) + + return True + + except iam_client.exceptions.NoSuchEntityException: + logger.warning( + f"User {meta.get('username', '')} does not exist, treating as already revoked" + ) + # Emit event to reflect attempted revoke on missing user + meta["outcome"] = "user_absent" + if manual: + lease.status = DynamicSecretLease.REVOKED + else: + lease.status = DynamicSecretLease.EXPIRED + lease.credentials = {} + lease.revoked_at = timezone.now() + lease.save() + try: + DynamicSecretLeaseEvent.objects.create( + lease=lease, + event_type=DynamicSecretLease.REVOKED, + organisation_member=organisation_member, + service_account=service_account, + ip_address=ip_address, + user_agent=user_agent, + metadata=meta, + ) + except Exception as e: + logger.warning(f"Failed to create revoke event for lease {lease.id}: {e}") + return True + except ClientError as e: + logger.error(f"AWS client error deleting user: {str(e)}") + meta["outcome"] = "error" + meta["error"] = str(e) + try: + DynamicSecretLeaseEvent.objects.create( + lease=lease, + event_type=DynamicSecretLease.REVOKED, + organisation_member=organisation_member, + service_account=service_account, + ip_address=ip_address, + user_agent=user_agent, + metadata=meta, + ) + except Exception: + pass + raise ValidationError(f"AWS client error deleting user: {str(e)}") + except Exception as e: + logger.error(f"Unexpected error deleting user: {str(e)}") + meta["outcome"] = "error" + meta["error"] = str(e) + try: + DynamicSecretLeaseEvent.objects.create( + lease=lease, + event_type=DynamicSecretLease.REVOKED, + organisation_member=organisation_member, + service_account=service_account, + ip_address=ip_address, + user_agent=user_agent, + metadata=meta, + ) + except Exception: + pass + raise Exception(f"Unexpected error deleting user: {str(e)}") diff --git a/backend/ee/integrations/secrets/dynamic/exceptions.py b/backend/ee/integrations/secrets/dynamic/exceptions.py new file mode 100644 index 000000000..ab7cf3ba2 --- /dev/null +++ b/backend/ee/integrations/secrets/dynamic/exceptions.py @@ -0,0 +1,34 @@ +class DynamicSecretError(Exception): + """Base exception for dynamic secret operations""" + + pass + + +class PlanRestrictionError(DynamicSecretError): + """Raised when operation is restricted by organization plan""" + + pass + + +class LeaseRenewalError(DynamicSecretError): + """Raised when lease renewal fails due to business logic constraints""" + + pass + + +class LeaseExpiredError(DynamicSecretError): + """Raised when attempting to renew an expired lease""" + + pass + + +class TTLExceededError(DynamicSecretError): + """Raised when TTL exceeds maximum allowed values""" + + pass + + +class LeaseAlreadyRevokedError(DynamicSecretError): + """Raised when attempting to operate on an already revoked lease""" + + pass diff --git a/backend/ee/integrations/secrets/dynamic/graphene/mutations.py b/backend/ee/integrations/secrets/dynamic/graphene/mutations.py new file mode 100644 index 000000000..93b60f8a4 --- /dev/null +++ b/backend/ee/integrations/secrets/dynamic/graphene/mutations.py @@ -0,0 +1,201 @@ +from datetime import timedelta +from api.utils.access.permissions import ( + user_can_access_environment, + user_has_permission, + user_is_org_member, +) +from ee.integrations.secrets.dynamic.utils import ( + create_dynamic_secret_lease, + renew_dynamic_secret_lease, +) +from ee.integrations.secrets.dynamic.aws.graphene.types import ( + AwsCredentialsType, +) +from ee.integrations.secrets.dynamic.aws.utils import ( + create_aws_dynamic_secret_lease, + revoke_aws_dynamic_secret_lease, +) +from ee.integrations.secrets.dynamic.graphene.types import DynamicSecretLeaseType +import graphene +from graphql import GraphQLError +from api.models import ( + DynamicSecret, + DynamicSecretLease, + OrganisationMember, +) +from django.utils import timezone +from django.core.exceptions import ValidationError +import logging + +logger = logging.getLogger(__name__) + + +class DeleteDynamicSecretMutation(graphene.Mutation): + class Arguments: + secret_id = graphene.ID(required=True) + + ok = graphene.Boolean() + + @classmethod + def mutate( + cls, + root, + info, + secret_id, + ): + user = info.context.user + + secret = DynamicSecret.objects.get(id=secret_id, deleted_at=None) + org = secret.environment.app.organisation + + # --- permission checks --- + if not user_has_permission(user, "delete", "Secrets", org, True): + raise GraphQLError( + "You don't have permission to delete secrets in this organisation" + ) + + secret.delete() + + return DeleteDynamicSecretMutation(ok=True) + + +class LeaseDynamicSecret(graphene.Mutation): + class Arguments: + secret_id = graphene.ID(required=True) + name = graphene.String(required=False) + ttl = graphene.Int() # seconds + + lease = graphene.Field(DynamicSecretLeaseType) + + @classmethod + def mutate( + cls, + root, + info, + secret_id, + name=None, + ttl=3600, + ): + user = info.context.user + + secret = DynamicSecret.objects.get(id=secret_id, deleted_at=None) + org = secret.environment.app.organisation + + # --- permission checks --- + if not user_is_org_member(user.userId, org.id): + raise GraphQLError("You don't have access to this organisation") + + if not user_has_permission(user, "create", "Secrets", org, True): + raise GraphQLError("You don't have permission to create Dynamic Secrets") + + if not user_can_access_environment(user.userId, secret.environment.id): + raise GraphQLError("You don't have access to this environment") + + org_member = OrganisationMember.objects.get(organisation=org, user=user) + + # create lease + lease_name = secret.name if name is None else name + lease, lease_data = create_dynamic_secret_lease( + secret, lease_name, ttl, org_member, request=info.context + ) + lease._credentials = AwsCredentialsType( + access_key_id=lease_data["access_key_id"], + secret_access_key=lease_data["secret_access_key"], + username=lease_data["username"], + ) + return LeaseDynamicSecret(lease=lease) + + +class RenewLeaseMutation(graphene.Mutation): + class Arguments: + lease_id = graphene.ID(required=True) + ttl = graphene.Int() # seconds + + lease = graphene.Field(DynamicSecretLeaseType) + + @classmethod + def mutate( + cls, + root, + info, + lease_id, + ttl=3600, + ): + + user = info.context.user + + lease = DynamicSecretLease.objects.get(id=lease_id) + org = lease.secret.environment.app.organisation + org_member = OrganisationMember.objects.get(organisation=org, user=user) + + # --- permission checks --- + if not user_is_org_member(user.userId, org.id): + raise GraphQLError("You don't have access to this organisation") + + if ( + lease.organisation_member is None + or lease.organisation_member.id != org_member.id + ) and not user_has_permission( + info.context.user, "update", "DynamicSecretLeases", org, True + ): + raise GraphQLError( + "You cannot renew this lease as it wasn't created by you" + ) + + if not user_can_access_environment(user.userId, lease.secret.environment.id): + raise GraphQLError("You don't have access to this environment") + + lease = renew_dynamic_secret_lease( + lease, ttl, request=info.context, organisation_member=org_member + ) + + return RenewLeaseMutation(lease=lease) + + +class RevokeLeaseMutation(graphene.Mutation): + class Arguments: + lease_id = graphene.ID(required=True) + + lease = graphene.Field(DynamicSecretLeaseType) + + @classmethod + def mutate( + cls, + root, + info, + lease_id, + ): + + user = info.context.user + + lease = DynamicSecretLease.objects.get(id=lease_id) + org = lease.secret.environment.app.organisation + org_member = OrganisationMember.objects.get(organisation=org, user=user) + + # --- permission checks --- + if not user_is_org_member(user.userId, org.id): + raise GraphQLError("You don't have access to this organisation") + + if ( + lease.organisation_member is None + or lease.organisation_member.id != org_member.id + ) and not user_has_permission( + info.context.user, "delete", "DynamicSecretLeases", org, True + ): + raise GraphQLError( + "You cannot revoke this lease as it wasn't created by you" + ) + + if not user_can_access_environment(user.userId, lease.secret.environment.id): + raise GraphQLError("You don't have access to this environment") + + else: + if lease.secret.provider == "aws": + revoke_aws_dynamic_secret_lease( + lease.id, + organisation_member=org_member, + manual=True, + request=info.context, + ) + + return RevokeLeaseMutation(lease=lease) diff --git a/backend/ee/integrations/secrets/dynamic/graphene/queries.py b/backend/ee/integrations/secrets/dynamic/graphene/queries.py new file mode 100644 index 000000000..bcbbb9c21 --- /dev/null +++ b/backend/ee/integrations/secrets/dynamic/graphene/queries.py @@ -0,0 +1,87 @@ +from ee.integrations.secrets.dynamic.graphene.types import DynamicSecretProviderType +from ee.integrations.secrets.dynamic.providers import DynamicSecretProviders +from graphql import GraphQLError +from api.models import DynamicSecret, App, Environment, Organisation +from api.utils.access.permissions import ( + user_has_permission, + user_can_access_app, + user_can_access_environment, +) + + +def resolve_dynamic_secret_providers(self, info): + providers = [ + DynamicSecretProviderType( + id=provider["id"], + name=provider["name"], + credentials=provider["credentials"], + config_map=provider["config_map"], + ) + for provider in DynamicSecretProviders.__dict__.values() + if isinstance(provider, dict) + ] + return providers + + +def resolve_dynamic_secrets( + root, info, secret_id=None, app_id=None, env_id=None, path=None, org_id=None +): + user = info.context.user + filters = {"deleted_at": None} + + if secret_id: + filters["id"] = secret_id + + elif path is not None: + filters["path"] = path + org = None + + # Figure out which org to use + if app_id: + app = App.objects.get(id=app_id) + org = app.organisation + elif env_id: + env = Environment.objects.get(id=env_id) + org = env.app.organisation + elif org_id: + org = Organisation.objects.get(id=org_id) + else: + raise GraphQLError( + "You must provide an app ID, an environment ID, or an organisation ID" + ) + + # Permission check (common to all cases) + if not user_has_permission(user, "read", "Secrets", org, True): + return [] + + # Build filters + access checks + if app_id and env_id: + if not user_can_access_app(user.userId, app_id): + raise GraphQLError("You don't have access to this app") + if not user_can_access_environment(user.userId, env_id): + raise GraphQLError("You don't have access to this environment") + + filters.update({"environment__app__id": app_id, "environment_id": env_id}) + return DynamicSecret.objects.filter(**filters) + + if app_id: + if not user_can_access_app(user.userId, app_id): + raise GraphQLError("You don't have access to this app") + + filters.update({"environment__app__id": app_id}) + return DynamicSecret.objects.filter(**filters) + + if env_id: + if not user_can_access_environment(user.userId, env_id): + raise GraphQLError("You don't have access to this environment") + + filters.update({"environment_id": env_id}) + return DynamicSecret.objects.filter(**filters) + + if org_id: + filters.update({"environment__app__organisation_id": org_id}) + return [ + ds + for ds in DynamicSecret.objects.filter(**filters) + if user_can_access_app(user.userId, ds.environment.app.id) + ] diff --git a/backend/ee/integrations/secrets/dynamic/graphene/types.py b/backend/ee/integrations/secrets/dynamic/graphene/types.py new file mode 100644 index 000000000..2a5ca0770 --- /dev/null +++ b/backend/ee/integrations/secrets/dynamic/graphene/types.py @@ -0,0 +1,138 @@ +from api.models import ( + DynamicSecret, + DynamicSecretLease, + DynamicSecretLeaseEvent, + OrganisationMember, +) +from api.utils.access.permissions import user_has_permission +import graphene +from graphene_django import DjangoObjectType +from graphene.types.generic import GenericScalar +from ee.integrations.secrets.dynamic.aws.graphene.types import ( + AWSConfigType, + AwsCredentialsType, +) + + +class KeyMap(graphene.ObjectType): + id = graphene.String() + key_name = graphene.String() + masked = graphene.Boolean() + + +class KeyMapInput(graphene.InputObjectType): + id = graphene.String(required=True) + key_name = graphene.String(required=True) + + +class DynamicSecretProviderType(graphene.ObjectType): + id = graphene.String(required=True) + name = graphene.String(required=True) + credentials = GenericScalar(required=True) + config_map = GenericScalar(required=True) + + +class DynamicSecretConfigUnion(graphene.Union): + class Meta: + types = (AWSConfigType,) + + +class LeaseCredentialsUnion(graphene.Union): + class Meta: + types = (AwsCredentialsType,) + + +class DynamicSecretType(DjangoObjectType): + # Expose JSON config safely + config = graphene.Field(DynamicSecretConfigUnion) + key_map = graphene.List(KeyMap) + + # Convenience fields for TTLs + default_ttl_seconds = graphene.Int() + max_ttl_seconds = graphene.Int() + + class Meta: + model = DynamicSecret + fields = ( + "id", + "name", + "description", + "environment", + "folder", + "path", + "authentication", + "provider", + "config", + "key_map", + "leases", + "created_at", + "updated_at", + "deleted_at", + ) + + def resolve_config(self, info): + if self.provider == "aws": + return AWSConfigType(**self.config) + return None + + def resolve_default_ttl_seconds(self, info): + return int(self.default_ttl.total_seconds()) if self.default_ttl else None + + def resolve_max_ttl_seconds(self, info): + return int(self.max_ttl.total_seconds()) if self.max_ttl else None + + def resolve_leases(self, info): + filter = {} + if not user_has_permission( + info.context.user, + "read", + "DynamicSecretLeases", + self.environment.app.organisation, + True, + ): + filter["organisation_member"] = OrganisationMember.objects.get( + organisation=self.environment.app.organisation, user=info.context.user + ) + return self.leases.filter(**filter).order_by("-created_at") + + +class DynamicSecretLeaseEventType(DjangoObjectType): + class Meta: + model = DynamicSecretLeaseEvent + fields = "__all__" + + def resolve_ttl(self, info): + return int(self.ttl.total_seconds()) if self.ttl else None + + +class DynamicSecretLeaseType(DjangoObjectType): + credentials = graphene.Field(LeaseCredentialsUnion) + events = graphene.List(DynamicSecretLeaseEventType) + ttl = graphene.Int() # Add this to convert timedelta to seconds + + class Meta: + model = DynamicSecretLease + fields = ( + "id", + "secret", + "name", + "organisation_member", + "service_account", + "ttl", + "status", + "expires_at", + "credentials", + "events", + "created_at", + "revoked_at", + "deleted_at", + ) + + def resolve_credentials(self, info): + return getattr(self, "_credentials", None) + + def resolve_events(self, info): + return self.events.all().order_by("created_at") + + def resolve_ttl(self, info): + return int(self.ttl.total_seconds()) if self.ttl else None diff --git a/backend/ee/integrations/secrets/dynamic/providers.py b/backend/ee/integrations/secrets/dynamic/providers.py new file mode 100644 index 000000000..2ed990498 --- /dev/null +++ b/backend/ee/integrations/secrets/dynamic/providers.py @@ -0,0 +1,72 @@ +class DynamicSecretProviders: + AWS = { + "id": "aws", + "name": "AWS IAM", + "credentials": [ + { + "id": "username", + "type": "string", + "default_key_name": "AWS_IAM_USERNAME", + "masked": False, + }, + { + "id": "access_key_id", + "type": "string", + "default_key_name": "AWS_ACCESS_KEY_ID", + "masked": False, + }, + { + "id": "secret_access_key", + "type": "string", + "default_key_name": "AWS_SECRET_ACCESS_KEY", + "masked": True, + }, + ], + "config_map": [ + { + "id": "username_template", + "label": "IAM Username template", + "input_type": "string", + "required": True, + "default": "{{ random }}", + "help_text": "A template for usernames created per credential", + }, + { + "id": "iam_path", + "label": "AWS IAM Path", + "input_type": "string", + "required": False, + "default": "/", + "help_text": "Optional IAM user path. Defaults to '/'.", + }, + { + "id": "policy_arns", + "label": "AWS Policy ARNs", + "input_type": "list", + "required": False, + "help_text": "Generated users will be attached to the specified policy ARNs.", + }, + { + "id": "groups", + "label": "AWS IAM Groups", + "input_type": "list", # accept comma-separated or array + "required": False, + "help_text": "Generated users will be attached to the specified IAM groups.", + }, + { + "id": "permission_boundary_arn", + "label": "IAM User Permission Boundary ARN", + "input_type": "string", + "required": False, + "help_text": "ARN attached to the generated user for AWS Permission Boundary.", + }, + ], + } + + @classmethod + def get_service_choices(cls): + return [ + (provider["id"], provider["name"]) + for provider in cls.__dict__.values() + if isinstance(provider, dict) + ] diff --git a/backend/ee/integrations/secrets/dynamic/rest/__init__.py b/backend/ee/integrations/secrets/dynamic/rest/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/backend/ee/integrations/secrets/dynamic/rest/urls.py b/backend/ee/integrations/secrets/dynamic/rest/urls.py new file mode 100644 index 000000000..d5f612c35 --- /dev/null +++ b/backend/ee/integrations/secrets/dynamic/rest/urls.py @@ -0,0 +1,11 @@ +from django.urls import path + +from ee.integrations.secrets.dynamic.rest.views import ( + DynamicSecretLeaseView, + DynamicSecretsView, +) + +urlpatterns = [ + path("", DynamicSecretsView.as_view(), name="dynamic-secrets"), + path("leases/", DynamicSecretLeaseView.as_view(), name="dynamic-secret-lease"), +] diff --git a/backend/ee/integrations/secrets/dynamic/rest/views.py b/backend/ee/integrations/secrets/dynamic/rest/views.py new file mode 100644 index 000000000..b8379de47 --- /dev/null +++ b/backend/ee/integrations/secrets/dynamic/rest/views.py @@ -0,0 +1,381 @@ +from api.auth import PhaseTokenAuthentication +from api.models import ( + DynamicSecret, + DynamicSecretLease, +) +from api.utils.secrets import ( + normalize_path_string, +) +from api.utils.access.permissions import ( + user_has_permission, +) +from ee.integrations.secrets.dynamic.serializers import ( + DynamicSecretLeaseSerializer, + DynamicSecretSerializer, +) +from api.utils.rest import ( + METHOD_TO_ACTION, +) + +from api.utils.access.middleware import IsIPAllowed +from ee.integrations.secrets.dynamic.aws.utils import ( + revoke_aws_dynamic_secret_lease, +) +from ee.integrations.secrets.dynamic.exceptions import ( + DynamicSecretError, + PlanRestrictionError, + LeaseRenewalError, + LeaseExpiredError, + TTLExceededError, + LeaseAlreadyRevokedError, +) +from ee.integrations.secrets.dynamic.utils import ( + create_dynamic_secret_lease, + renew_dynamic_secret_lease, +) +from rest_framework.views import APIView +from rest_framework.permissions import IsAuthenticated +from rest_framework.exceptions import PermissionDenied +from rest_framework.response import Response +from rest_framework import status +from djangorestframework_camel_case.render import ( + CamelCaseJSONRenderer, +) +from rest_framework.exceptions import PermissionDenied, NotFound +import logging + +logger = logging.getLogger(__name__) + + +class DynamicSecretsView(APIView): + authentication_classes = [PhaseTokenAuthentication] + permission_classes = [IsAuthenticated, IsIPAllowed] + renderer_classes = [ + CamelCaseJSONRenderer, + ] + + def initial(self, request, *args, **kwargs): + super().initial(request, *args, **kwargs) + + # Determine the action based on the request method + action = METHOD_TO_ACTION.get(request.method) + if not action: + raise PermissionDenied(f"Unsupported HTTP method: {request.method}") + + # Perform permission check + account = None + if request.auth["auth_type"] == "User": + account = request.auth["org_member"].user + elif request.auth["auth_type"] == "ServiceAccount": + account = request.auth["service_account"] + + if account is not None: + env = request.auth["environment"] + organisation = env.app.organisation + + if not user_has_permission( + account, + action, + "Secrets", + organisation, + True, + request.auth.get("service_account") is not None, + ): + raise PermissionDenied( + f"You don't have permission to {action} secrets in this environment." + ) + + def get(self, request, *args, **kwargs): + env = request.auth["environment"] + + # Check if SSE is enabled for this environment + if not env.app.sse_enabled: + return Response({"error": "SSE is not enabled for this App"}, status=400) + + dynamic_secrets_filter = { + "environment": env, + "deleted_at": None, + } + + try: + path = request.GET.get("path") + if path: + path = normalize_path_string(path) + dynamic_secrets_filter["path"] = path + except: + pass + + # Filter by secret id + secret_id = request.GET.get("id") + if secret_id: + dynamic_secrets_filter["id"] = secret_id + + # Filter by secret name + secret_name = request.GET.get("name") + if secret_name: + dynamic_secrets_filter["name"] = secret_name + + dynamic_secrets = DynamicSecret.objects.filter(**dynamic_secrets_filter) + + if not dynamic_secrets.exists(): + return Response({"error": "No dynamic secrets found"}, status=404) + + # If lease param is present, generate a lease per secret + include_lease = ( + "lease" in request.GET + and request.GET.get("lease", "false").lower() != "false" + ) + + # Get optional TTL parameter for lease creation + lease_ttl = request.GET.get("ttl") + if lease_ttl: + try: + lease_ttl = int(lease_ttl) + except ValueError: + return Response( + {"error": "ttl must be a valid integer (seconds)"}, status=400 + ) + + # 2. Create leases for each secret + service_account = org_member = None + + if request.auth["auth_type"] == "User": + org_member = request.auth["org_member"] + elif request.auth["auth_type"] == "ServiceAccount": + service_account = request.auth["service_account"] + + if include_lease: + leases_by_secret_id = {} + for ds in dynamic_secrets: + try: + lease, _ = create_dynamic_secret_lease( + ds, + ttl=lease_ttl, + organisation_member=org_member, + service_account=service_account, + request=request, + ) + leases_by_secret_id[ds.id] = str(lease.id) + except PlanRestrictionError as e: + return Response({"error": str(e)}, status=403) + except (TTLExceededError, LeaseRenewalError) as e: + return Response({"error": str(e)}, status=400) + except DynamicSecretError as e: + return Response({"error": str(e)}, status=400) + except Exception as e: + logger.exception( + "Unexpected error creating lease for dynamic secret %s", ds.id + ) + return Response( + { + "error": "An internal error occurred while creating the lease" + }, + status=500, + ) + + # Serialize each secret with its lease_id in context + dynamic_secrets_data = [ + DynamicSecretSerializer( + ds, + context={ + "sse": True, + "with_credentials": True, + "lease_id": leases_by_secret_id.get(ds.id), + }, + ).data + for ds in dynamic_secrets + ] + else: + # Serialize without lease + dynamic_secrets_data = DynamicSecretSerializer( + dynamic_secrets, many=True, context={"sse": True} + ).data + + return Response( + dynamic_secrets_data, + status=status.HTTP_200_OK, + ) + + +class DynamicSecretLeaseView(APIView): + authentication_classes = [PhaseTokenAuthentication] + permission_classes = [IsAuthenticated, IsIPAllowed] + renderer_classes = [CamelCaseJSONRenderer] + + def _get_account_and_org(self, request): + account = None + if request.auth["auth_type"] == "User": + account = request.auth["org_member"].user + elif request.auth["auth_type"] == "ServiceAccount": + account = request.auth["service_account"] + env = request.auth["environment"] if account is not None else None + organisation = env.app.organisation if env is not None else None + return account, organisation + + def _get_lease_or_404(self, lease_id: str) -> DynamicSecretLease: + try: + return DynamicSecretLease.objects.get(id=lease_id) + except DynamicSecretLease.DoesNotExist: + raise NotFound("Lease not found") + + def _assert_can_act_on_lease(self, request, lease: DynamicSecretLease, action: str): + # action: "update" for renew, "delete" for revoke + account, organisation = self._get_account_and_org(request) + lease_holder = lease.organisation_member or lease.service_account + if ( + lease_holder + and hasattr(lease_holder, "id") + and lease_holder.id == getattr(account, "id", None) + ): + return + if not user_has_permission( + account, + action, + "DynamicSecretLeases", + organisation, + True, + request.auth.get("service_account") is not None, + ): + raise PermissionDenied( + f"You don't have permission to {action} leases for other accounts." + ) + + # List leases + def get(self, request, *args, **kwargs): + secret_id = request.query_params.get("secret_id") + if not secret_id: + return Response({"error": "secret_id is required"}, status=400) + + account, organisation = self._get_account_and_org(request) + + if not user_has_permission( + account, + "read", + "Secrets", + organisation, + True, + request.auth.get("service_account") is not None, + ): + raise PermissionDenied( + f"You don't have permission to read secrets in this environment." + ) + + try: + secret = DynamicSecret.objects.get(id=secret_id) + except DynamicSecret.DoesNotExist: + return Response({"error": "Secret not found"}, status=404) + + leases_filter = {"secret": secret} + + # only return own leases if no permission to view all leases + if not user_has_permission( + account, + "read", + "DynamicSecretLeases", + organisation, + True, + request.auth.get("service_account") is not None, + ): + if request.auth["org_member"] is not None: + leases_filter["organisation_member"] = request.auth["org_member"] + elif request.auth["service_account"] is not None: + leases_filter["service_account"] = request.auth["service_account"] + + leases = DynamicSecretLease.objects.filter(**leases_filter).order_by( + "-created_at" + ) + serializer = DynamicSecretLeaseSerializer( + leases, many=True, context={"sse": False} + ) + return Response(serializer.data, status=status.HTTP_200_OK) + + # Renew + def put(self, request, *args, **kwargs): + lease_id = request.data.get("lease_id") + ttl = request.data.get("ttl", 3600) + if not lease_id: + return Response({"error": "lease_id is required"}, status=400) + + lease = self._get_lease_or_404(lease_id) + self._assert_can_act_on_lease(request, lease, action="update") + + org_member = service_account = None + if request.auth["auth_type"] == "User": + org_member = request.auth["org_member"] + elif request.auth["auth_type"] == "ServiceAccount": + service_account = request.auth["service_account"] + + try: + lease = renew_dynamic_secret_lease( + lease, + ttl, + request=request, + organisation_member=org_member, + service_account=service_account, + ) + except PlanRestrictionError as e: + return Response({"error": str(e)}, status=403) + except (LeaseRenewalError, TTLExceededError, LeaseExpiredError) as e: + return Response({"error": str(e)}, status=400) + except DynamicSecretError as e: + # Catch any other dynamic secret errors + return Response({"error": str(e)}, status=400) + except Exception as e: + logger.exception("Unexpected error renewing lease (lease_id=%s)", lease_id) + return Response( + {"error": "An internal error occurred while renewing the lease"}, + status=500, + ) + + return Response( + { + "message": f"Lease renewed successfully. Updated expiry: {lease.expires_at}" + }, + status=status.HTTP_200_OK, + ) + + # Revoke + def delete(self, request, *args, **kwargs): + lease_id = request.data.get("lease_id") or request.query_params.get("lease_id") + if not lease_id: + return Response({"error": "lease_id is required"}, status=400) + + lease = self._get_lease_or_404(lease_id) + self._assert_can_act_on_lease(request, lease, action="delete") + + org_member = service_account = None + if request.auth["auth_type"] == "User": + org_member = request.auth["org_member"] + elif request.auth["auth_type"] == "ServiceAccount": + service_account = request.auth["service_account"] + + try: + if lease.secret.provider == "aws": + revoke_aws_dynamic_secret_lease( + lease.id, + manual=True, + request=request, + organisation_member=org_member, + service_account=service_account, + ) + + return Response( + {"message": "Lease revoked successfully"}, status=status.HTTP_200_OK + ) + + except LeaseAlreadyRevokedError as e: + # If lease is already revoked, still return success with message + return Response( + {"message": "Lease was already revoked"}, status=status.HTTP_200_OK + ) + except PlanRestrictionError as e: + return Response({"error": str(e)}, status=403) + except DynamicSecretError as e: + # Catch any other dynamic secret errors + return Response({"error": str(e)}, status=400) + except Exception as e: + logger.exception("Unexpected error revoking lease (lease_id=%s)", lease_id) + return Response( + {"error": "An internal error occurred while revoking the lease"}, + status=500, + ) diff --git a/backend/ee/integrations/secrets/dynamic/serializers.py b/backend/ee/integrations/secrets/dynamic/serializers.py new file mode 100644 index 000000000..e1a62ca4f --- /dev/null +++ b/backend/ee/integrations/secrets/dynamic/serializers.py @@ -0,0 +1,151 @@ +from api.models import DynamicSecretLease, DynamicSecret +from api.utils.crypto import decrypt_asymmetric +from api.utils.secrets import get_environment_keys +from api.serializers import ( + OrganisationMemberSerializer, + ServiceAccountSerializer, +) +from rest_framework import serializers + + +class DynamicSecretLeaseSerializer(serializers.ModelSerializer): + credentials = serializers.SerializerMethodField() + owner = serializers.SerializerMethodField() + + class Meta: + model = DynamicSecretLease + fields = [ + "id", + "name", + "description", + "secret", + "ttl", + "status", + "owner", + "credentials", + "created_at", + "renewed_at", + "expires_at", + "revoked_at", + "deleted_at", + ] + + def get_credentials(self, obj): + with_credentials = self.context.get("with_credentials", False) + if not with_credentials: + return [] + sse = self.context.get("sse") + credentials = obj.credentials or {} + key_map = obj.secret.key_map if obj.secret else [] + result = [] + if sse: + env_pubkey, env_privkey = get_environment_keys(obj.secret.environment.id) + for entry in key_map: + key_id = entry.get("id") + key_name_encrypted = entry.get("key_name") + cred_encrypted = credentials.get(key_id) + if key_name_encrypted and cred_encrypted: + key_name = decrypt_asymmetric( + key_name_encrypted, env_privkey, env_pubkey + ) + value = decrypt_asymmetric(cred_encrypted, env_privkey, env_pubkey) + result.append({"key": key_name, "value": value}) + else: + for entry in key_map: + key_id = entry.get("id") + key_name = entry.get("key_name") + value = credentials.get(key_id) + if key_name and value: + result.append({"key": key_name, "value": value}) + return result + + def get_owner(self, obj): + """ + Return the lease owner serialized as either an OrganisationMember or ServiceAccount. + Shape: + { + "type": "organisation_member" | "service_account", + "data": + } + """ + # Prefer explicit organisation member if present + org_member = getattr(obj, "organisation_member", None) + if org_member: + return { + "type": "organisation_member", + "data": OrganisationMemberSerializer( + org_member, context=self.context + ).data, + } + service_account = getattr(obj, "service_account", None) + if service_account: + return { + "type": "service_account", + "data": ServiceAccountSerializer( + service_account, context=self.context + ).data, + } + return None + + +class DynamicSecretSerializer(serializers.ModelSerializer): + lease = serializers.SerializerMethodField() + key_map = serializers.SerializerMethodField() + type = serializers.SerializerMethodField() + + class Meta: + model = DynamicSecret + fields = [ + "id", + "name", + "type", + "description", + "environment", + "folder", + "path", + "default_ttl", + "max_ttl", + "provider", + "key_map", + "created_at", + "updated_at", + "deleted_at", + "lease", + ] + + def get_key_map(self, obj): + sse = self.context.get("sse") + entries = obj.key_map or [] + if not sse: + return entries + + # Decrypt key_name for each entry using environment keys + env_pubkey, env_privkey = get_environment_keys(obj.environment.id) + decrypted = [] + for entry in entries: + # keep other fields (e.g., id, key_digest) intact + out = dict(entry) if isinstance(entry, dict) else {} + key_name_encrypted = out.get("key_name") + if key_name_encrypted: + try: + out["key_name"] = decrypt_asymmetric( + key_name_encrypted, env_privkey, env_pubkey + ) + except Exception: + # If decryption fails, fall back to stored value + pass + decrypted.append(out) + return decrypted + + def get_lease(self, obj): + lease_id = self.context.get("lease_id") + if not lease_id: + return None + try: + lease = obj.leases.get(id=lease_id) + except (obj.leases.model.DoesNotExist, AttributeError): + return None + return DynamicSecretLeaseSerializer(lease, context=self.context).data + + def get_type(self, obj): + return "dynamic" diff --git a/backend/ee/integrations/secrets/dynamic/utils.py b/backend/ee/integrations/secrets/dynamic/utils.py new file mode 100644 index 000000000..127b3ce6f --- /dev/null +++ b/backend/ee/integrations/secrets/dynamic/utils.py @@ -0,0 +1,359 @@ +from datetime import timedelta + +from api.utils.secrets import ( + check_for_duplicates_blind, + compute_key_digest, + create_environment_folder_structure, + get_environment_keys, +) +from api.utils.crypto import decrypt_asymmetric +from api.models import DynamicSecretLease, DynamicSecretLeaseEvent +from api.utils.rest import get_resolver_request_meta +from ee.integrations.secrets.dynamic.exceptions import ( + LeaseAlreadyRevokedError, + LeaseExpiredError, + LeaseRenewalError, + PlanRestrictionError, + TTLExceededError, +) +from ee.integrations.secrets.dynamic.aws.utils import ( + create_aws_dynamic_secret_lease, +) +from ee.integrations.secrets.dynamic.providers import DynamicSecretProviders +from uuid import uuid4 +from django.core.exceptions import ValidationError +from graphql import GraphQLError +from django.utils import timezone +import django_rq +from rq.job import Job +import logging +from django.apps import apps + +logger = logging.getLogger(__name__) + +DynamicSecret = apps.get_model("api", "DynamicSecret") + + +def validate_key_map(key_map, provider, environment, path, dynamic_secret_id=None): + provider_def = None + for prov in DynamicSecretProviders.__dict__.values(): + if isinstance(prov, dict) and prov.get("id") == provider: + provider_def = prov + break + if not provider_def: + raise ValidationError(f"Unsupported provider: {provider}") + + valid_creds = {c["id"]: c for c in provider_def.get("credentials", [])} + validated_key_map = [] + + env_pubkey, env_privkey = get_environment_keys(environment.id) + + for entry in key_map: + decrypted_key_name = decrypt_asymmetric( + entry["key_name"], env_privkey, env_pubkey + ) + entry["dynamic_secret_id"] = dynamic_secret_id + entry["path"] = path + digest = compute_key_digest(decrypted_key_name, environment.id) + entry["keyDigest"] = digest + + if check_for_duplicates_blind(key_map, environment): + raise ValidationError("One or more secrets keys already exist ") + + for entry in key_map: + if not isinstance(entry, dict): + raise ValidationError(f"Invalid key_map entry (must be dict): {entry}") + + key_id = entry.get("id") + key_name = entry.get("key_name") + key_digest = entry.get("keyDigest") + + if key_id not in valid_creds: + raise ValidationError( + f"Invalid key id '{key_id}' for provider '{provider}'" + ) + + # fallback to provider default_key_name + if not key_name: + key_name = valid_creds[key_id].get("default_key_name") + + if not key_name: + raise ValidationError( + f"No key name provided for key id '{key_id}', and no default defined" + ) + + # Get masked property from provider definition + masked = valid_creds[key_id].get("masked", True) # default to masked + + validated_key_map.append( + { + "id": key_id, + "key_name": key_name, + "key_digest": key_digest, + "masked": masked, + } + ) + + return validated_key_map + + +def create_dynamic_secret( + *, + environment, + path, + name: str, + description="", + default_ttl, + max_ttl, + authentication=None, + provider: str, + config: dict, + key_map: list, +) -> DynamicSecret: + """ + Create a DynamicSecret with validated provider config. + Used by both GraphQL resolvers and REST API. + """ + + Organisation = apps.get_model("api", "Organisation") + + org = environment.app.organisation + if not org.plan == Organisation.ENTERPRISE_PLAN: + raise Exception("Dynamic secrets are only available on the Enterprise plan.") + + # --- ensure name is unique in this environment and path --- + if DynamicSecret.objects.filter( + environment=environment, + path=path, + name=name, + deleted_at=None, + ).exists(): + raise ValidationError( + f"A dynamic secret with name '{name}' already exists at this path." + ) + + # --- validate provider --- + provider_def = None + for prov in DynamicSecretProviders.__dict__.values(): + if isinstance(prov, dict) and prov.get("id") == provider: + provider_def = prov + break + if not provider_def: + raise ValidationError(f"Unsupported provider: {provider}") + + folder = None + if path and path != "/": + folder = create_environment_folder_structure(path, environment.id) + + # --- validate required config fields --- + validated_config = {} + for field in provider_def.get("config_map", []): + fid = field["id"] + required = field.get("required", False) + default = field.get("default") + + if fid in config: + validated_config[fid] = config[fid] + elif required and default is None: + raise ValidationError(f"Missing required config field: {fid}") + elif default is not None: + validated_config[fid] = default + + # --- validate key_map --- + validated_key_map = validate_key_map(key_map, provider, environment, path) + + # --- construct DynamicSecret --- + dynamic_secret = DynamicSecret.objects.create( + id=uuid4(), + environment=environment, + folder=folder, + path=path, + name=name, + description=description, + default_ttl=default_ttl, + max_ttl=max_ttl, + authentication=authentication, + provider=provider, + config=validated_config, + key_map=validated_key_map, + ) + + # Update environment timestamp + environment.updated_at = timezone.now() + environment.save(update_fields=["updated_at"]) + + return dynamic_secret + + +def create_dynamic_secret_lease( + secret, + lease_name=None, + ttl=None, + organisation_member=None, + service_account=None, + request=None, +): + + Organisation = apps.get_model("api", "Organisation") + org = secret.environment.app.organisation + if not org.plan == Organisation.ENTERPRISE_PLAN: + raise Exception("Dynamic secrets are only available on the Enterprise plan.") + + try: + lease_name = lease_name or secret.name + ttl = ttl or int(secret.default_ttl.total_seconds()) + if secret.provider == "aws": + lease, lease_data, meta = create_aws_dynamic_secret_lease( + secret=secret, + lease_name=lease_name, + organisation_member=organisation_member, + service_account=service_account, + ttl_seconds=ttl, + ) + + # Record creation event with request metadata (if available) + ip_address, user_agent = (None, None) + if request is not None: + try: + ip_address, user_agent = get_resolver_request_meta(request) + except Exception: + logger.debug( + "Failed to read request meta for lease event", exc_info=True + ) + + DynamicSecretLeaseEvent.objects.create( + lease=lease, + event_type=DynamicSecretLease.CREATED, + organisation_member=( + organisation_member if organisation_member else None + ), + service_account=service_account if service_account else None, + ip_address=ip_address, + user_agent=user_agent, + metadata=meta, + ) + + return lease, lease_data + + except ValidationError as e: + logger.error(f"Error creating dynamic secret lease: {e}") + raise GraphQLError(e.message) + + +def renew_dynamic_secret_lease( + lease, + ttl, + request=None, + organisation_member=None, + service_account=None, +): + + Organisation = apps.get_model("api", "Organisation") + org = lease.secret.environment.app.organisation + if not org.plan == Organisation.ENTERPRISE_PLAN: + raise PlanRestrictionError( + "Dynamic secrets are only available on the Enterprise plan." + ) + + # Check if adding this renewal would exceed max TTL + current_ttl_seconds = lease.ttl.total_seconds() + new_total_ttl = current_ttl_seconds + ttl + max_ttl_seconds = lease.secret.max_ttl.total_seconds() + + if new_total_ttl > max_ttl_seconds: + remaining_seconds = max_ttl_seconds - current_ttl_seconds + raise LeaseRenewalError( + f"The renewal TTL would exceed the maximum TTL for this dynamic secret. " + f"Maximum remaining renewal time: {int(remaining_seconds)} seconds" + ) + + if timedelta(seconds=ttl) > lease.secret.max_ttl: + raise TTLExceededError( + "The specified TTL exceeds the maximum TTL for this dynamic secret." + ) + + if lease.expires_at <= timezone.now(): + raise LeaseExpiredError("This lease has expired and cannot be renewed") + + else: + lease.expires_at = lease.expires_at + timedelta(seconds=ttl) + # Add the renewal TTL to the existing lease TTL + lease.ttl = lease.ttl + timedelta(seconds=ttl) + lease.updated_at = timezone.now() + + # --- reschedule cleanup job --- + scheduler = django_rq.get_scheduler("scheduled-jobs") + + # cancel the old job if it exists + if lease.cleanup_job_id: + try: + old_job = Job.fetch(lease.cleanup_job_id, connection=scheduler.connection) + old_job.cancel() + except Exception as e: + logger.info(f"Failed to delete job: {e}") + pass + + lease.save() + + # enqueue a new revocation job + schedule_lease_revocation(lease) + + # record renewal event + ip_address, user_agent = (None, None) + if request is not None: + try: + ip_address, user_agent = get_resolver_request_meta(request) + except Exception: + logger.debug( + "Failed to read request meta for lease renewal event", exc_info=True + ) + + try: + DynamicSecretLeaseEvent.objects.create( + lease=lease, + event_type=DynamicSecretLease.RENEWED, + organisation_member=organisation_member, + service_account=service_account, + ip_address=ip_address, + user_agent=user_agent, + metadata={"action": "renew", "ttl": ttl}, + ) + except Exception as e: + logger.warning(f"Failed to create renewal event for lease {lease.id}: {e}") + + return lease + + +def schedule_lease_revocation(lease, immediate=False): + """ + Schedule a job to revoke the lease at its expiry time. + """ + + # --- Schedule revocation --- + + if lease.revoked_at is not None: + raise LeaseAlreadyRevokedError( + f"Lease has already been revoked at {lease.revoked_at}" + ) + + scheduled_revoke_time = lease.expires_at + if immediate: + scheduled_revoke_time = timezone.now() + + scheduler = django_rq.get_scheduler("scheduled-jobs") + + if lease.secret.provider == "aws": + from ee.integrations.secrets.dynamic.aws.utils import ( + revoke_aws_dynamic_secret_lease, + ) + + revoke_job = revoke_aws_dynamic_secret_lease + + job = scheduler.enqueue_at( + scheduled_revoke_time, + revoke_job, + lease.id, + ) + + lease.cleanup_job_id = job.id + lease.save() diff --git a/backend/version.txt b/backend/version.txt index 194d155c1..6e380ebf9 100644 --- a/backend/version.txt +++ b/backend/version.txt @@ -1 +1 @@ -v2.51.0 +v2.52.0 diff --git a/frontend/apollo/client.ts b/frontend/apollo/client.ts index fb5c426f5..78fd6491e 100644 --- a/frontend/apollo/client.ts +++ b/frontend/apollo/client.ts @@ -51,7 +51,13 @@ const errorLink = onError(({ graphQLErrors, networkError }) => { export const graphQlClient = new ApolloClient({ link: from([errorLink, httpLink]), - cache: new InMemoryCache(), + cache: new InMemoryCache({ + typePolicies: { + KeyMap: { + keyFields: ["id", "keyName"], // composite key + }, + }, +}), defaultOptions: { watchQuery: { skipPollAttempt: () => document.hidden, diff --git a/frontend/apollo/gql.ts b/frontend/apollo/gql.ts index 601d00997..b4c16e884 100644 --- a/frontend/apollo/gql.ts +++ b/frontend/apollo/gql.ts @@ -53,6 +53,12 @@ const documents = { "mutation LogSecretReads($ids: [ID]!) {\n readSecret(ids: $ids) {\n ok\n }\n}": types.LogSecretReadsDocument, "mutation RemovePersonalSecret($secretId: ID!) {\n removeOverride(secretId: $secretId) {\n ok\n }\n}": types.RemovePersonalSecretDocument, "mutation RenameEnv($environmentId: ID!, $name: String!) {\n renameEnvironment(environmentId: $environmentId, name: $name) {\n environment {\n id\n name\n updatedAt\n }\n }\n}": types.RenameEnvDocument, + "mutation CreateNewAWSDynamicSecret($organisationId: ID!, $environmentId: ID!, $path: String, $name: String!, $description: String, $defaultTtl: Int, $maxTtl: Int, $authenticationId: ID, $config: AWSConfigInput!, $keyMap: [KeyMapInput]!) {\n createAwsDynamicSecret(\n organisationId: $organisationId\n environmentId: $environmentId\n path: $path\n name: $name\n description: $description\n defaultTtl: $defaultTtl\n maxTtl: $maxTtl\n authenticationId: $authenticationId\n config: $config\n keyMap: $keyMap\n ) {\n dynamicSecret {\n id\n name\n description\n provider\n createdAt\n updatedAt\n }\n }\n}": types.CreateNewAwsDynamicSecretDocument, + "mutation CreateDynamicSecretLease($secretId: ID!, $ttl: Int!, $name: String!) {\n createDynamicSecretLease(secretId: $secretId, ttl: $ttl, name: $name) {\n lease {\n id\n name\n credentials {\n ... on AwsCredentialsType {\n accessKeyId\n secretAccessKey\n username\n }\n }\n expiresAt\n }\n }\n}": types.CreateDynamicSecretLeaseDocument, + "mutation DeleteDynamicSecretOP($secretId: ID!) {\n deleteDynamicSecret(secretId: $secretId) {\n ok\n }\n}": types.DeleteDynamicSecretOpDocument, + "mutation RenewDynamicSecretLeaseOP($leaseId: ID!, $ttl: Int!) {\n renewDynamicSecretLease(leaseId: $leaseId, ttl: $ttl) {\n lease {\n id\n name\n expiresAt\n status\n }\n }\n}": types.RenewDynamicSecretLeaseOpDocument, + "mutation RevokeDynamicSecretLeaseOP($leaseId: ID!) {\n revokeDynamicSecretLease(leaseId: $leaseId) {\n lease {\n id\n name\n expiresAt\n revokedAt\n status\n }\n }\n}": types.RevokeDynamicSecretLeaseOpDocument, + "mutation UpdateDynamicSecret($dynamicSecretId: ID!, $organisationId: ID!, $path: String, $name: String!, $description: String, $defaultTtl: Int, $maxTtl: Int, $authenticationId: ID, $config: AWSConfigInput!, $keyMap: [KeyMapInput]!) {\n updateAwsDynamicSecret(\n organisationId: $organisationId\n dynamicSecretId: $dynamicSecretId\n path: $path\n name: $name\n description: $description\n defaultTtl: $defaultTtl\n maxTtl: $maxTtl\n authenticationId: $authenticationId\n config: $config\n keyMap: $keyMap\n ) {\n dynamicSecret {\n id\n name\n description\n provider\n createdAt\n updatedAt\n }\n }\n}": types.UpdateDynamicSecretDocument, "mutation CreateSharedSecret($input: LockboxInput!) {\n createLockbox(input: $input) {\n lockbox {\n id\n allowedViews\n expiresAt\n }\n }\n}": types.CreateSharedSecretDocument, "mutation SwapEnvOrder($environment1Id: ID!, $environment2Id: ID!) {\n swapEnvironmentOrder(\n environment1Id: $environment1Id\n environment2Id: $environment2Id\n ) {\n ok\n }\n}": types.SwapEnvOrderDocument, "mutation AcceptOrganisationInvite($orgId: ID!, $identityKey: String!, $wrappedKeyring: String!, $wrappedRecovery: String!, $inviteId: ID!) {\n createOrganisationMember(\n orgId: $orgId\n identityKey: $identityKey\n wrappedKeyring: $wrappedKeyring\n wrappedRecovery: $wrappedRecovery\n inviteId: $inviteId\n ) {\n orgMember {\n id\n email\n createdAt\n role {\n name\n }\n }\n }\n}": types.AcceptOrganisationInviteDocument, @@ -111,8 +117,11 @@ const documents = { "query GetOrganisationPlan($organisationId: ID!) {\n organisationPlan(organisationId: $organisationId) {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n seatsUsed {\n users\n serviceAccounts\n total\n }\n seatLimit\n appCount\n }\n}": types.GetOrganisationPlanDocument, "query GetRoles($orgId: ID!) {\n roles(orgId: $orgId) {\n id\n name\n description\n color\n permissions\n isDefault\n }\n}": types.GetRolesDocument, "query VerifyInvite($inviteId: ID!) {\n validateInvite(inviteId: $inviteId) {\n id\n organisation {\n id\n name\n }\n inviteeEmail\n invitedBy {\n fullName\n email\n }\n apps {\n id\n name\n }\n }\n}": types.VerifyInviteDocument, + "query GetDynamicSecrets($orgId: ID!, $appId: ID, $envId: ID, $path: String) {\n dynamicSecrets(orgId: $orgId, appId: $appId, envId: $envId, path: $path) {\n id\n name\n environment {\n id\n name\n index\n app {\n id\n name\n }\n }\n path\n description\n provider\n config {\n ... on AWSConfigType {\n usernameTemplate\n iamPath\n }\n }\n keyMap {\n id\n keyName\n masked\n }\n defaultTtlSeconds\n maxTtlSeconds\n authentication {\n id\n name\n }\n createdAt\n }\n}": types.GetDynamicSecretsDocument, + "query GetDynamicSecretProviders {\n dynamicSecretProviders {\n id\n name\n credentials\n configMap\n }\n}": types.GetDynamicSecretProvidersDocument, + "query GetDynamicSecretLeases($secretId: ID!, $orgId: ID!) {\n dynamicSecrets(secretId: $secretId, orgId: $orgId) {\n id\n leases {\n id\n name\n ttl\n createdAt\n expiresAt\n revokedAt\n status\n organisationMember {\n id\n fullName\n email\n avatarUrl\n self\n }\n serviceAccount {\n id\n name\n }\n events {\n id\n eventType\n createdAt\n metadata\n ipAddress\n userAgent\n organisationMember {\n id\n fullName\n email\n avatarUrl\n self\n }\n serviceAccount {\n id\n name\n }\n }\n }\n }\n}": types.GetDynamicSecretLeasesDocument, "query GetAppEnvironments($appId: ID!, $memberId: ID, $memberType: MemberType) {\n appEnvironments(\n appId: $appId\n environmentId: null\n memberId: $memberId\n memberType: $memberType\n ) {\n id\n name\n envType\n identityKey\n wrappedSeed\n wrappedSalt\n createdAt\n app {\n name\n id\n }\n secretCount\n folderCount\n index\n members {\n email\n fullName\n avatarUrl\n }\n }\n sseEnabled(appId: $appId)\n serverPublicKey\n}": types.GetAppEnvironmentsDocument, - "query GetAppSecrets($appId: ID!, $memberId: ID, $memberType: MemberType, $path: String) {\n appEnvironments(\n appId: $appId\n environmentId: null\n memberId: $memberId\n memberType: $memberType\n ) {\n id\n name\n envType\n identityKey\n wrappedSeed\n wrappedSalt\n createdAt\n app {\n name\n id\n }\n secretCount\n folderCount\n index\n members {\n email\n fullName\n avatarUrl\n }\n folders {\n id\n name\n path\n }\n secrets(path: $path) {\n id\n key\n value\n comment\n path\n }\n }\n sseEnabled(appId: $appId)\n serverPublicKey\n}": types.GetAppSecretsDocument, + "query GetAppSecrets($appId: ID!, $memberId: ID, $memberType: MemberType, $path: String) {\n appEnvironments(\n appId: $appId\n environmentId: null\n memberId: $memberId\n memberType: $memberType\n ) {\n id\n name\n envType\n identityKey\n wrappedSeed\n wrappedSalt\n createdAt\n app {\n name\n id\n }\n secretCount\n folderCount\n index\n members {\n email\n fullName\n avatarUrl\n }\n folders {\n id\n name\n path\n }\n secrets(path: $path) {\n id\n key\n value\n comment\n path\n }\n dynamicSecrets(path: $path) {\n id\n name\n path\n description\n provider\n keyMap {\n id\n keyName\n }\n }\n }\n sseEnabled(appId: $appId)\n serverPublicKey\n}": types.GetAppSecretsDocument, "query GetAppSecretsLogs($appId: ID!, $start: BigInt, $end: BigInt, $eventTypes: [String], $memberId: ID, $memberType: MemberType, $environmentId: ID) {\n secretLogs(\n appId: $appId\n start: $start\n end: $end\n eventTypes: $eventTypes\n memberId: $memberId\n memberType: $memberType\n environmentId: $environmentId\n ) {\n logs {\n id\n path\n key\n value\n tags {\n id\n name\n color\n }\n version\n comment\n timestamp\n ipAddress\n userAgent\n user {\n email\n username\n fullName\n avatarUrl\n }\n serviceToken {\n id\n name\n }\n serviceAccount {\n id\n name\n deletedAt\n }\n serviceAccountToken {\n id\n name\n deletedAt\n }\n eventType\n environment {\n id\n envType\n name\n }\n secret {\n id\n path\n }\n }\n count\n }\n environmentKeys(appId: $appId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n environment {\n id\n }\n }\n}": types.GetAppSecretsLogsDocument, "query GetEnvironmentKey($envId: ID!, $appId: ID!) {\n environmentKeys(environmentId: $envId, appId: $appId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n}": types.GetEnvironmentKeyDocument, "query GetEnvironmentTokens($envId: ID!) {\n environmentTokens(environmentId: $envId) {\n id\n name\n wrappedKeyShare\n createdAt\n }\n}": types.GetEnvironmentTokensDocument, @@ -121,7 +130,7 @@ const documents = { "query GetSecretHistory($appId: ID!, $envId: ID!, $id: ID!) {\n secrets(envId: $envId, id: $id) {\n id\n history {\n id\n key\n value\n path\n tags {\n id\n name\n color\n }\n version\n comment\n timestamp\n ipAddress\n userAgent\n user {\n email\n username\n fullName\n avatarUrl\n }\n serviceToken {\n id\n name\n }\n serviceAccount {\n id\n name\n deletedAt\n }\n eventType\n }\n }\n environmentKeys(appId: $appId, environmentId: $envId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n}": types.GetSecretHistoryDocument, "query GetEnvSecretsKV($envId: ID!) {\n folders(envId: $envId, path: \"/\") {\n id\n name\n }\n secrets(envId: $envId, path: \"/\") {\n id\n key\n value\n comment\n path\n }\n environmentKeys(environmentId: $envId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n}": types.GetEnvSecretsKvDocument, "query GetSecretTags($orgId: ID!) {\n secretTags(orgId: $orgId) {\n id\n name\n color\n }\n}": types.GetSecretTagsDocument, - "query GetSecrets($appId: ID!, $envId: ID!, $path: String) {\n secrets(envId: $envId, path: $path) {\n id\n key\n value\n path\n tags {\n id\n name\n color\n }\n comment\n createdAt\n updatedAt\n override {\n value\n isActive\n }\n environment {\n id\n app {\n id\n }\n }\n }\n folders(envId: $envId, path: $path) {\n id\n name\n path\n createdAt\n folderCount\n secretCount\n }\n appEnvironments(appId: $appId, environmentId: $envId) {\n id\n name\n envType\n identityKey\n app {\n name\n }\n }\n environmentKeys(appId: $appId, environmentId: $envId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n envSyncs(envId: $envId) {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n options\n isActive\n status\n lastSync\n createdAt\n }\n}": types.GetSecretsDocument, + "query GetSecrets($appId: ID!, $envId: ID!, $path: String) {\n secrets(envId: $envId, path: $path) {\n id\n key\n value\n path\n tags {\n id\n name\n color\n }\n comment\n createdAt\n updatedAt\n override {\n value\n isActive\n }\n environment {\n id\n app {\n id\n }\n }\n }\n folders(envId: $envId, path: $path) {\n id\n name\n path\n createdAt\n folderCount\n secretCount\n }\n appEnvironments(appId: $appId, environmentId: $envId) {\n id\n name\n envType\n identityKey\n app {\n id\n name\n sseEnabled\n }\n }\n environmentKeys(appId: $appId, environmentId: $envId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n envSyncs(envId: $envId) {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n options\n isActive\n status\n lastSync\n createdAt\n }\n dynamicSecrets(envId: $envId, path: $path) {\n id\n name\n path\n description\n provider\n keyMap {\n id\n keyName\n masked\n }\n config {\n ... on AWSConfigType {\n usernameTemplate\n groups\n iamPath\n permissionBoundaryArn\n policyArns\n policyDocument\n }\n }\n defaultTtlSeconds\n maxTtlSeconds\n authentication {\n id\n name\n }\n createdAt\n }\n}": types.GetSecretsDocument, "query GetServiceTokens($appId: ID!) {\n serviceTokens(appId: $appId) {\n id\n name\n createdAt\n createdBy {\n fullName\n avatarUrl\n self\n }\n expiresAt\n keys {\n id\n identityKey\n }\n }\n}": types.GetServiceTokensDocument, "query GetServiceAccountDetail($orgId: ID!, $id: ID) {\n serviceAccounts(orgId: $orgId, serviceAccountId: $id) {\n id\n name\n identityKey\n role {\n id\n name\n description\n color\n permissions\n }\n createdAt\n handlers {\n id\n wrappedKeyring\n wrappedRecovery\n user {\n self\n }\n }\n appMemberships {\n id\n name\n environments {\n id\n name\n }\n sseEnabled\n }\n networkPolicies {\n id\n name\n allowedIps\n isGlobal\n }\n }\n}": types.GetServiceAccountDetailDocument, "query GetServiceAccountHandlers($orgId: ID!) {\n serviceAccountHandlers(orgId: $orgId) {\n id\n email\n role {\n name\n permissions\n }\n identityKey\n self\n }\n}": types.GetServiceAccountHandlersDocument, @@ -323,6 +332,30 @@ export function graphql(source: "mutation RemovePersonalSecret($secretId: ID!) { * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "mutation RenameEnv($environmentId: ID!, $name: String!) {\n renameEnvironment(environmentId: $environmentId, name: $name) {\n environment {\n id\n name\n updatedAt\n }\n }\n}"): (typeof documents)["mutation RenameEnv($environmentId: ID!, $name: String!) {\n renameEnvironment(environmentId: $environmentId, name: $name) {\n environment {\n id\n name\n updatedAt\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation CreateNewAWSDynamicSecret($organisationId: ID!, $environmentId: ID!, $path: String, $name: String!, $description: String, $defaultTtl: Int, $maxTtl: Int, $authenticationId: ID, $config: AWSConfigInput!, $keyMap: [KeyMapInput]!) {\n createAwsDynamicSecret(\n organisationId: $organisationId\n environmentId: $environmentId\n path: $path\n name: $name\n description: $description\n defaultTtl: $defaultTtl\n maxTtl: $maxTtl\n authenticationId: $authenticationId\n config: $config\n keyMap: $keyMap\n ) {\n dynamicSecret {\n id\n name\n description\n provider\n createdAt\n updatedAt\n }\n }\n}"): (typeof documents)["mutation CreateNewAWSDynamicSecret($organisationId: ID!, $environmentId: ID!, $path: String, $name: String!, $description: String, $defaultTtl: Int, $maxTtl: Int, $authenticationId: ID, $config: AWSConfigInput!, $keyMap: [KeyMapInput]!) {\n createAwsDynamicSecret(\n organisationId: $organisationId\n environmentId: $environmentId\n path: $path\n name: $name\n description: $description\n defaultTtl: $defaultTtl\n maxTtl: $maxTtl\n authenticationId: $authenticationId\n config: $config\n keyMap: $keyMap\n ) {\n dynamicSecret {\n id\n name\n description\n provider\n createdAt\n updatedAt\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation CreateDynamicSecretLease($secretId: ID!, $ttl: Int!, $name: String!) {\n createDynamicSecretLease(secretId: $secretId, ttl: $ttl, name: $name) {\n lease {\n id\n name\n credentials {\n ... on AwsCredentialsType {\n accessKeyId\n secretAccessKey\n username\n }\n }\n expiresAt\n }\n }\n}"): (typeof documents)["mutation CreateDynamicSecretLease($secretId: ID!, $ttl: Int!, $name: String!) {\n createDynamicSecretLease(secretId: $secretId, ttl: $ttl, name: $name) {\n lease {\n id\n name\n credentials {\n ... on AwsCredentialsType {\n accessKeyId\n secretAccessKey\n username\n }\n }\n expiresAt\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation DeleteDynamicSecretOP($secretId: ID!) {\n deleteDynamicSecret(secretId: $secretId) {\n ok\n }\n}"): (typeof documents)["mutation DeleteDynamicSecretOP($secretId: ID!) {\n deleteDynamicSecret(secretId: $secretId) {\n ok\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation RenewDynamicSecretLeaseOP($leaseId: ID!, $ttl: Int!) {\n renewDynamicSecretLease(leaseId: $leaseId, ttl: $ttl) {\n lease {\n id\n name\n expiresAt\n status\n }\n }\n}"): (typeof documents)["mutation RenewDynamicSecretLeaseOP($leaseId: ID!, $ttl: Int!) {\n renewDynamicSecretLease(leaseId: $leaseId, ttl: $ttl) {\n lease {\n id\n name\n expiresAt\n status\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation RevokeDynamicSecretLeaseOP($leaseId: ID!) {\n revokeDynamicSecretLease(leaseId: $leaseId) {\n lease {\n id\n name\n expiresAt\n revokedAt\n status\n }\n }\n}"): (typeof documents)["mutation RevokeDynamicSecretLeaseOP($leaseId: ID!) {\n revokeDynamicSecretLease(leaseId: $leaseId) {\n lease {\n id\n name\n expiresAt\n revokedAt\n status\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "mutation UpdateDynamicSecret($dynamicSecretId: ID!, $organisationId: ID!, $path: String, $name: String!, $description: String, $defaultTtl: Int, $maxTtl: Int, $authenticationId: ID, $config: AWSConfigInput!, $keyMap: [KeyMapInput]!) {\n updateAwsDynamicSecret(\n organisationId: $organisationId\n dynamicSecretId: $dynamicSecretId\n path: $path\n name: $name\n description: $description\n defaultTtl: $defaultTtl\n maxTtl: $maxTtl\n authenticationId: $authenticationId\n config: $config\n keyMap: $keyMap\n ) {\n dynamicSecret {\n id\n name\n description\n provider\n createdAt\n updatedAt\n }\n }\n}"): (typeof documents)["mutation UpdateDynamicSecret($dynamicSecretId: ID!, $organisationId: ID!, $path: String, $name: String!, $description: String, $defaultTtl: Int, $maxTtl: Int, $authenticationId: ID, $config: AWSConfigInput!, $keyMap: [KeyMapInput]!) {\n updateAwsDynamicSecret(\n organisationId: $organisationId\n dynamicSecretId: $dynamicSecretId\n path: $path\n name: $name\n description: $description\n defaultTtl: $defaultTtl\n maxTtl: $maxTtl\n authenticationId: $authenticationId\n config: $config\n keyMap: $keyMap\n ) {\n dynamicSecret {\n id\n name\n description\n provider\n createdAt\n updatedAt\n }\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -555,6 +588,18 @@ export function graphql(source: "query GetRoles($orgId: ID!) {\n roles(orgId: $ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "query VerifyInvite($inviteId: ID!) {\n validateInvite(inviteId: $inviteId) {\n id\n organisation {\n id\n name\n }\n inviteeEmail\n invitedBy {\n fullName\n email\n }\n apps {\n id\n name\n }\n }\n}"): (typeof documents)["query VerifyInvite($inviteId: ID!) {\n validateInvite(inviteId: $inviteId) {\n id\n organisation {\n id\n name\n }\n inviteeEmail\n invitedBy {\n fullName\n email\n }\n apps {\n id\n name\n }\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetDynamicSecrets($orgId: ID!, $appId: ID, $envId: ID, $path: String) {\n dynamicSecrets(orgId: $orgId, appId: $appId, envId: $envId, path: $path) {\n id\n name\n environment {\n id\n name\n index\n app {\n id\n name\n }\n }\n path\n description\n provider\n config {\n ... on AWSConfigType {\n usernameTemplate\n iamPath\n }\n }\n keyMap {\n id\n keyName\n masked\n }\n defaultTtlSeconds\n maxTtlSeconds\n authentication {\n id\n name\n }\n createdAt\n }\n}"): (typeof documents)["query GetDynamicSecrets($orgId: ID!, $appId: ID, $envId: ID, $path: String) {\n dynamicSecrets(orgId: $orgId, appId: $appId, envId: $envId, path: $path) {\n id\n name\n environment {\n id\n name\n index\n app {\n id\n name\n }\n }\n path\n description\n provider\n config {\n ... on AWSConfigType {\n usernameTemplate\n iamPath\n }\n }\n keyMap {\n id\n keyName\n masked\n }\n defaultTtlSeconds\n maxTtlSeconds\n authentication {\n id\n name\n }\n createdAt\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetDynamicSecretProviders {\n dynamicSecretProviders {\n id\n name\n credentials\n configMap\n }\n}"): (typeof documents)["query GetDynamicSecretProviders {\n dynamicSecretProviders {\n id\n name\n credentials\n configMap\n }\n}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetDynamicSecretLeases($secretId: ID!, $orgId: ID!) {\n dynamicSecrets(secretId: $secretId, orgId: $orgId) {\n id\n leases {\n id\n name\n ttl\n createdAt\n expiresAt\n revokedAt\n status\n organisationMember {\n id\n fullName\n email\n avatarUrl\n self\n }\n serviceAccount {\n id\n name\n }\n events {\n id\n eventType\n createdAt\n metadata\n ipAddress\n userAgent\n organisationMember {\n id\n fullName\n email\n avatarUrl\n self\n }\n serviceAccount {\n id\n name\n }\n }\n }\n }\n}"): (typeof documents)["query GetDynamicSecretLeases($secretId: ID!, $orgId: ID!) {\n dynamicSecrets(secretId: $secretId, orgId: $orgId) {\n id\n leases {\n id\n name\n ttl\n createdAt\n expiresAt\n revokedAt\n status\n organisationMember {\n id\n fullName\n email\n avatarUrl\n self\n }\n serviceAccount {\n id\n name\n }\n events {\n id\n eventType\n createdAt\n metadata\n ipAddress\n userAgent\n organisationMember {\n id\n fullName\n email\n avatarUrl\n self\n }\n serviceAccount {\n id\n name\n }\n }\n }\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -562,7 +607,7 @@ export function graphql(source: "query GetAppEnvironments($appId: ID!, $memberId /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query GetAppSecrets($appId: ID!, $memberId: ID, $memberType: MemberType, $path: String) {\n appEnvironments(\n appId: $appId\n environmentId: null\n memberId: $memberId\n memberType: $memberType\n ) {\n id\n name\n envType\n identityKey\n wrappedSeed\n wrappedSalt\n createdAt\n app {\n name\n id\n }\n secretCount\n folderCount\n index\n members {\n email\n fullName\n avatarUrl\n }\n folders {\n id\n name\n path\n }\n secrets(path: $path) {\n id\n key\n value\n comment\n path\n }\n }\n sseEnabled(appId: $appId)\n serverPublicKey\n}"): (typeof documents)["query GetAppSecrets($appId: ID!, $memberId: ID, $memberType: MemberType, $path: String) {\n appEnvironments(\n appId: $appId\n environmentId: null\n memberId: $memberId\n memberType: $memberType\n ) {\n id\n name\n envType\n identityKey\n wrappedSeed\n wrappedSalt\n createdAt\n app {\n name\n id\n }\n secretCount\n folderCount\n index\n members {\n email\n fullName\n avatarUrl\n }\n folders {\n id\n name\n path\n }\n secrets(path: $path) {\n id\n key\n value\n comment\n path\n }\n }\n sseEnabled(appId: $appId)\n serverPublicKey\n}"]; +export function graphql(source: "query GetAppSecrets($appId: ID!, $memberId: ID, $memberType: MemberType, $path: String) {\n appEnvironments(\n appId: $appId\n environmentId: null\n memberId: $memberId\n memberType: $memberType\n ) {\n id\n name\n envType\n identityKey\n wrappedSeed\n wrappedSalt\n createdAt\n app {\n name\n id\n }\n secretCount\n folderCount\n index\n members {\n email\n fullName\n avatarUrl\n }\n folders {\n id\n name\n path\n }\n secrets(path: $path) {\n id\n key\n value\n comment\n path\n }\n dynamicSecrets(path: $path) {\n id\n name\n path\n description\n provider\n keyMap {\n id\n keyName\n }\n }\n }\n sseEnabled(appId: $appId)\n serverPublicKey\n}"): (typeof documents)["query GetAppSecrets($appId: ID!, $memberId: ID, $memberType: MemberType, $path: String) {\n appEnvironments(\n appId: $appId\n environmentId: null\n memberId: $memberId\n memberType: $memberType\n ) {\n id\n name\n envType\n identityKey\n wrappedSeed\n wrappedSalt\n createdAt\n app {\n name\n id\n }\n secretCount\n folderCount\n index\n members {\n email\n fullName\n avatarUrl\n }\n folders {\n id\n name\n path\n }\n secrets(path: $path) {\n id\n key\n value\n comment\n path\n }\n dynamicSecrets(path: $path) {\n id\n name\n path\n description\n provider\n keyMap {\n id\n keyName\n }\n }\n }\n sseEnabled(appId: $appId)\n serverPublicKey\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -598,7 +643,7 @@ export function graphql(source: "query GetSecretTags($orgId: ID!) {\n secretTag /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query GetSecrets($appId: ID!, $envId: ID!, $path: String) {\n secrets(envId: $envId, path: $path) {\n id\n key\n value\n path\n tags {\n id\n name\n color\n }\n comment\n createdAt\n updatedAt\n override {\n value\n isActive\n }\n environment {\n id\n app {\n id\n }\n }\n }\n folders(envId: $envId, path: $path) {\n id\n name\n path\n createdAt\n folderCount\n secretCount\n }\n appEnvironments(appId: $appId, environmentId: $envId) {\n id\n name\n envType\n identityKey\n app {\n name\n }\n }\n environmentKeys(appId: $appId, environmentId: $envId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n envSyncs(envId: $envId) {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n options\n isActive\n status\n lastSync\n createdAt\n }\n}"): (typeof documents)["query GetSecrets($appId: ID!, $envId: ID!, $path: String) {\n secrets(envId: $envId, path: $path) {\n id\n key\n value\n path\n tags {\n id\n name\n color\n }\n comment\n createdAt\n updatedAt\n override {\n value\n isActive\n }\n environment {\n id\n app {\n id\n }\n }\n }\n folders(envId: $envId, path: $path) {\n id\n name\n path\n createdAt\n folderCount\n secretCount\n }\n appEnvironments(appId: $appId, environmentId: $envId) {\n id\n name\n envType\n identityKey\n app {\n name\n }\n }\n environmentKeys(appId: $appId, environmentId: $envId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n envSyncs(envId: $envId) {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n options\n isActive\n status\n lastSync\n createdAt\n }\n}"]; +export function graphql(source: "query GetSecrets($appId: ID!, $envId: ID!, $path: String) {\n secrets(envId: $envId, path: $path) {\n id\n key\n value\n path\n tags {\n id\n name\n color\n }\n comment\n createdAt\n updatedAt\n override {\n value\n isActive\n }\n environment {\n id\n app {\n id\n }\n }\n }\n folders(envId: $envId, path: $path) {\n id\n name\n path\n createdAt\n folderCount\n secretCount\n }\n appEnvironments(appId: $appId, environmentId: $envId) {\n id\n name\n envType\n identityKey\n app {\n id\n name\n sseEnabled\n }\n }\n environmentKeys(appId: $appId, environmentId: $envId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n envSyncs(envId: $envId) {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n options\n isActive\n status\n lastSync\n createdAt\n }\n dynamicSecrets(envId: $envId, path: $path) {\n id\n name\n path\n description\n provider\n keyMap {\n id\n keyName\n masked\n }\n config {\n ... on AWSConfigType {\n usernameTemplate\n groups\n iamPath\n permissionBoundaryArn\n policyArns\n policyDocument\n }\n }\n defaultTtlSeconds\n maxTtlSeconds\n authentication {\n id\n name\n }\n createdAt\n }\n}"): (typeof documents)["query GetSecrets($appId: ID!, $envId: ID!, $path: String) {\n secrets(envId: $envId, path: $path) {\n id\n key\n value\n path\n tags {\n id\n name\n color\n }\n comment\n createdAt\n updatedAt\n override {\n value\n isActive\n }\n environment {\n id\n app {\n id\n }\n }\n }\n folders(envId: $envId, path: $path) {\n id\n name\n path\n createdAt\n folderCount\n secretCount\n }\n appEnvironments(appId: $appId, environmentId: $envId) {\n id\n name\n envType\n identityKey\n app {\n id\n name\n sseEnabled\n }\n }\n environmentKeys(appId: $appId, environmentId: $envId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n envSyncs(envId: $envId) {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n options\n isActive\n status\n lastSync\n createdAt\n }\n dynamicSecrets(envId: $envId, path: $path) {\n id\n name\n path\n description\n provider\n keyMap {\n id\n keyName\n masked\n }\n config {\n ... on AWSConfigType {\n usernameTemplate\n groups\n iamPath\n permissionBoundaryArn\n policyArns\n policyDocument\n }\n }\n defaultTtlSeconds\n maxTtlSeconds\n authentication {\n id\n name\n }\n createdAt\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/frontend/apollo/graphql.ts b/frontend/apollo/graphql.ts index 41d614bca..42c091cb2 100644 --- a/frontend/apollo/graphql.ts +++ b/frontend/apollo/graphql.ts @@ -32,6 +32,12 @@ export type Scalars = { * [iso8601](https://en.wikipedia.org/wiki/ISO_8601). */ DateTime: { input: any; output: any; } + /** + * The `GenericScalar` scalar type represents a generic + * GraphQL scalar value that could be: + * String, Boolean, Int, Float, List or Object. + */ + GenericScalar: { input: any; output: any; } /** * Allows use of a JSON String for input / output from the GraphQL schema. * @@ -41,6 +47,25 @@ export type Scalars = { JSONString: { input: any; output: any; } }; +export type AwsConfigInput = { + groups?: InputMaybe; + iamPath?: InputMaybe; + permissionBoundaryArn?: InputMaybe; + policyArns?: InputMaybe; + policyDocument?: InputMaybe; + usernameTemplate: Scalars['String']['input']; +}; + +export type AwsConfigType = { + __typename?: 'AWSConfigType'; + groups?: Maybe; + iamPath?: Maybe; + permissionBoundaryArn?: Maybe; + policyArns?: Maybe; + policyDocument?: Maybe; + usernameTemplate: Scalars['String']['output']; +}; + export type AwsSecretType = { __typename?: 'AWSSecretType'; arn?: Maybe; @@ -99,6 +124,37 @@ export enum ApiActivatedPhaseLicensePlanChoices { Pr = 'PR' } +export enum ApiDynamicSecretLeaseEventEventTypeChoices { + /** Active */ + Active = 'ACTIVE', + /** Created */ + Created = 'CREATED', + /** Expired */ + Expired = 'EXPIRED', + /** Renewed */ + Renewed = 'RENEWED', + /** Revoked */ + Revoked = 'REVOKED' +} + +export enum ApiDynamicSecretLeaseStatusChoices { + /** Active */ + Active = 'ACTIVE', + /** Created */ + Created = 'CREATED', + /** Expired */ + Expired = 'EXPIRED', + /** Renewed */ + Renewed = 'RENEWED', + /** Revoked */ + Revoked = 'REVOKED' +} + +export enum ApiDynamicSecretProviderChoices { + /** AWS */ + Aws = 'AWS' +} + export enum ApiEnvironmentEnvTypeChoices { /** Custom */ Custom = 'CUSTOM', @@ -187,6 +243,13 @@ export type AppType = { wrappedKeyShare: Scalars['String']['output']; }; +export type AwsCredentialsType = { + __typename?: 'AwsCredentialsType'; + accessKeyId?: Maybe; + secretAccessKey?: Maybe; + username?: Maybe; +}; + export enum BillingPeriodEnum { Monthly = 'MONTHLY', Yearly = 'YEARLY' @@ -237,6 +300,11 @@ export type CloudflareWorkerType = { scriptId?: Maybe; }; +export type CreateAwsDynamicSecretMutation = { + __typename?: 'CreateAWSDynamicSecretMutation'; + dynamicSecret?: Maybe; +}; + export type CreateAwsSecretsManagerSync = { __typename?: 'CreateAWSSecretsManagerSync'; sync?: Maybe; @@ -398,6 +466,11 @@ export type DeleteCustomRoleMutation = { ok?: Maybe; }; +export type DeleteDynamicSecretMutation = { + __typename?: 'DeleteDynamicSecretMutation'; + ok?: Maybe; +}; + export type DeleteEnvironmentMutation = { __typename?: 'DeleteEnvironmentMutation'; ok?: Maybe; @@ -468,6 +541,68 @@ export type DeleteUserTokenMutation = { ok?: Maybe; }; +export type DynamicSecretConfigUnion = AwsConfigType; + +export type DynamicSecretLeaseEventType = { + __typename?: 'DynamicSecretLeaseEventType'; + createdAt: Scalars['DateTime']['output']; + eventType: ApiDynamicSecretLeaseEventEventTypeChoices; + id: Scalars['ID']['output']; + ipAddress?: Maybe; + lease: DynamicSecretLeaseType; + metadata: Scalars['JSONString']['output']; + organisationMember?: Maybe; + serviceAccount?: Maybe; + userAgent?: Maybe; +}; + +export type DynamicSecretLeaseType = { + __typename?: 'DynamicSecretLeaseType'; + createdAt?: Maybe; + credentials?: Maybe; + deletedAt?: Maybe; + events?: Maybe>>; + expiresAt?: Maybe; + id: Scalars['String']['output']; + name: Scalars['String']['output']; + organisationMember?: Maybe; + revokedAt?: Maybe; + secret: DynamicSecretType; + serviceAccount?: Maybe; + /** Current status of the lease */ + status: ApiDynamicSecretLeaseStatusChoices; + ttl?: Maybe; +}; + +export type DynamicSecretProviderType = { + __typename?: 'DynamicSecretProviderType'; + configMap: Scalars['GenericScalar']['output']; + credentials: Scalars['GenericScalar']['output']; + id: Scalars['String']['output']; + name: Scalars['String']['output']; +}; + +export type DynamicSecretType = { + __typename?: 'DynamicSecretType'; + authentication?: Maybe; + config?: Maybe; + createdAt?: Maybe; + defaultTtlSeconds?: Maybe; + deletedAt?: Maybe; + description: Scalars['String']['output']; + environment: EnvironmentType; + folder?: Maybe; + id: Scalars['String']['output']; + keyMap?: Maybe>>; + leases: Array; + maxTtlSeconds?: Maybe; + name: Scalars['String']['output']; + path: Scalars['String']['output']; + /** Which provider this secret is associated with. */ + provider: ApiDynamicSecretProviderChoices; + updatedAt: Scalars['DateTime']['output']; +}; + export type EditSecretMutation = { __typename?: 'EditSecretMutation'; secret?: Maybe; @@ -546,6 +681,7 @@ export type EnvironmentType = { __typename?: 'EnvironmentType'; app: AppMembershipType; createdAt?: Maybe; + dynamicSecrets: Array>; envType: ApiEnvironmentEnvTypeChoices; folderCount?: Maybe; folders: Array>; @@ -563,6 +699,11 @@ export type EnvironmentType = { }; +export type EnvironmentTypeDynamicSecretsArgs = { + path?: InputMaybe; +}; + + export type EnvironmentTypeSecretsArgs = { path?: InputMaybe; }; @@ -659,6 +800,25 @@ export type KmsLogsResponseType = { logs?: Maybe>>; }; +export type KeyMap = { + __typename?: 'KeyMap'; + id?: Maybe; + keyName?: Maybe; + masked?: Maybe; +}; + +export type KeyMapInput = { + id: Scalars['String']['input']; + keyName: Scalars['String']['input']; +}; + +export type LeaseCredentialsUnion = AwsCredentialsType; + +export type LeaseDynamicSecret = { + __typename?: 'LeaseDynamicSecret'; + lease?: Maybe; +}; + export type LockboxInput = { allowedViews?: InputMaybe; data?: InputMaybe; @@ -687,10 +847,12 @@ export type Mutation = { bulkInviteOrganisationMembers?: Maybe; cancelSubscription?: Maybe; createApp?: Maybe; + createAwsDynamicSecret?: Maybe; createAwsSecretSync?: Maybe; createCloudflarePagesSync?: Maybe; createCloudflareWorkersSync?: Maybe; createCustomRole?: Maybe; + createDynamicSecretLease?: Maybe; createEnvironment?: Maybe; createEnvironmentKey?: Maybe; createEnvironmentToken?: Maybe; @@ -719,6 +881,7 @@ export type Mutation = { createVercelSync?: Maybe; deleteApp?: Maybe; deleteCustomRole?: Maybe; + deleteDynamicSecret?: Maybe; deleteEnvSync?: Maybe; deleteEnvironment?: Maybe; deleteInvitation?: Maybe; @@ -742,7 +905,9 @@ export type Mutation = { removeAppMember?: Maybe; removeOverride?: Maybe; renameEnvironment?: Maybe; + renewDynamicSecretLease?: Maybe; resumeSubscription?: Maybe; + revokeDynamicSecretLease?: Maybe; rotateAppKeys?: Maybe; setDefaultPaymentMethod?: Maybe; swapEnvironmentOrder?: Maybe; @@ -750,6 +915,7 @@ export type Mutation = { triggerSync?: Maybe; updateAccountNetworkAccessPolicies?: Maybe; updateAppName?: Maybe; + updateAwsDynamicSecret?: Maybe; updateCustomRole?: Maybe; updateMemberEnvironmentScope?: Maybe; updateMemberWrappedSecrets?: Maybe; @@ -800,6 +966,20 @@ export type MutationCreateAppArgs = { }; +export type MutationCreateAwsDynamicSecretArgs = { + authenticationId?: InputMaybe; + config: AwsConfigInput; + defaultTtl?: InputMaybe; + description?: InputMaybe; + environmentId: Scalars['ID']['input']; + keyMap: Array>; + maxTtl?: InputMaybe; + name: Scalars['String']['input']; + organisationId: Scalars['ID']['input']; + path?: InputMaybe; +}; + + export type MutationCreateAwsSecretSyncArgs = { credentialId?: InputMaybe; envId?: InputMaybe; @@ -836,6 +1016,13 @@ export type MutationCreateCustomRoleArgs = { }; +export type MutationCreateDynamicSecretLeaseArgs = { + name?: InputMaybe; + secretId: Scalars['ID']['input']; + ttl?: InputMaybe; +}; + + export type MutationCreateEnvironmentArgs = { adminKeys?: InputMaybe>>; environmentData: EnvironmentInput; @@ -1067,6 +1254,11 @@ export type MutationDeleteCustomRoleArgs = { }; +export type MutationDeleteDynamicSecretArgs = { + secretId: Scalars['ID']['input']; +}; + + export type MutationDeleteEnvSyncArgs = { syncId?: InputMaybe; }; @@ -1193,12 +1385,23 @@ export type MutationRenameEnvironmentArgs = { }; +export type MutationRenewDynamicSecretLeaseArgs = { + leaseId: Scalars['ID']['input']; + ttl?: InputMaybe; +}; + + export type MutationResumeSubscriptionArgs = { organisationId?: InputMaybe; subscriptionId: Scalars['String']['input']; }; +export type MutationRevokeDynamicSecretLeaseArgs = { + leaseId: Scalars['ID']['input']; +}; + + export type MutationRotateAppKeysArgs = { appToken: Scalars['String']['input']; id: Scalars['ID']['input']; @@ -1240,6 +1443,20 @@ export type MutationUpdateAppNameArgs = { }; +export type MutationUpdateAwsDynamicSecretArgs = { + authenticationId?: InputMaybe; + config: AwsConfigInput; + defaultTtl?: InputMaybe; + description?: InputMaybe; + dynamicSecretId: Scalars['ID']['input']; + keyMap: Array>; + maxTtl?: InputMaybe; + name: Scalars['String']['input']; + organisationId: Scalars['ID']['input']; + path?: InputMaybe; +}; + + export type MutationUpdateCustomRoleArgs = { color?: InputMaybe; description?: InputMaybe; @@ -1475,6 +1692,8 @@ export type Query = { clientIp?: Maybe; cloudflarePagesProjects?: Maybe>>; cloudflareWorkers?: Maybe>>; + dynamicSecretProviders?: Maybe>>; + dynamicSecrets?: Maybe>>; envSyncs?: Maybe>>; environmentKeys?: Maybe>>; environmentTokens?: Maybe>>; @@ -1568,6 +1787,15 @@ export type QueryCloudflareWorkersArgs = { }; +export type QueryDynamicSecretsArgs = { + appId?: InputMaybe; + envId?: InputMaybe; + orgId?: InputMaybe; + path?: InputMaybe; + secretId?: InputMaybe; +}; + + export type QueryEnvSyncsArgs = { envId?: InputMaybe; }; @@ -1846,6 +2074,16 @@ export type RenderServiceType = { updatedAt?: Maybe; }; +export type RenewLeaseMutation = { + __typename?: 'RenewLeaseMutation'; + lease?: Maybe; +}; + +export type RevokeLeaseMutation = { + __typename?: 'RevokeLeaseMutation'; + lease?: Maybe; +}; + export type RoleType = { __typename?: 'RoleType'; color?: Maybe; @@ -2081,6 +2319,11 @@ export type TriggerSync = { sync?: Maybe; }; +export type UpdateAwsDynamicSecretMutation = { + __typename?: 'UpdateAWSDynamicSecretMutation'; + dynamicSecret?: Maybe; +}; + export type UpdateAccountNetworkAccessPolicies = { __typename?: 'UpdateAccountNetworkAccessPolicies'; ok?: Maybe; @@ -2530,6 +2773,69 @@ export type RenameEnvMutationVariables = Exact<{ export type RenameEnvMutation = { __typename?: 'Mutation', renameEnvironment?: { __typename?: 'RenameEnvironmentMutation', environment?: { __typename?: 'EnvironmentType', id: string, name: string, updatedAt: any } | null } | null }; +export type CreateNewAwsDynamicSecretMutationVariables = Exact<{ + organisationId: Scalars['ID']['input']; + environmentId: Scalars['ID']['input']; + path?: InputMaybe; + name: Scalars['String']['input']; + description?: InputMaybe; + defaultTtl?: InputMaybe; + maxTtl?: InputMaybe; + authenticationId?: InputMaybe; + config: AwsConfigInput; + keyMap: Array> | InputMaybe; +}>; + + +export type CreateNewAwsDynamicSecretMutation = { __typename?: 'Mutation', createAwsDynamicSecret?: { __typename?: 'CreateAWSDynamicSecretMutation', dynamicSecret?: { __typename?: 'DynamicSecretType', id: string, name: string, description: string, provider: ApiDynamicSecretProviderChoices, createdAt?: any | null, updatedAt: any } | null } | null }; + +export type CreateDynamicSecretLeaseMutationVariables = Exact<{ + secretId: Scalars['ID']['input']; + ttl: Scalars['Int']['input']; + name: Scalars['String']['input']; +}>; + + +export type CreateDynamicSecretLeaseMutation = { __typename?: 'Mutation', createDynamicSecretLease?: { __typename?: 'LeaseDynamicSecret', lease?: { __typename?: 'DynamicSecretLeaseType', id: string, name: string, expiresAt?: any | null, credentials?: { __typename?: 'AwsCredentialsType', accessKeyId?: string | null, secretAccessKey?: string | null, username?: string | null } | null } | null } | null }; + +export type DeleteDynamicSecretOpMutationVariables = Exact<{ + secretId: Scalars['ID']['input']; +}>; + + +export type DeleteDynamicSecretOpMutation = { __typename?: 'Mutation', deleteDynamicSecret?: { __typename?: 'DeleteDynamicSecretMutation', ok?: boolean | null } | null }; + +export type RenewDynamicSecretLeaseOpMutationVariables = Exact<{ + leaseId: Scalars['ID']['input']; + ttl: Scalars['Int']['input']; +}>; + + +export type RenewDynamicSecretLeaseOpMutation = { __typename?: 'Mutation', renewDynamicSecretLease?: { __typename?: 'RenewLeaseMutation', lease?: { __typename?: 'DynamicSecretLeaseType', id: string, name: string, expiresAt?: any | null, status: ApiDynamicSecretLeaseStatusChoices } | null } | null }; + +export type RevokeDynamicSecretLeaseOpMutationVariables = Exact<{ + leaseId: Scalars['ID']['input']; +}>; + + +export type RevokeDynamicSecretLeaseOpMutation = { __typename?: 'Mutation', revokeDynamicSecretLease?: { __typename?: 'RevokeLeaseMutation', lease?: { __typename?: 'DynamicSecretLeaseType', id: string, name: string, expiresAt?: any | null, revokedAt?: any | null, status: ApiDynamicSecretLeaseStatusChoices } | null } | null }; + +export type UpdateDynamicSecretMutationVariables = Exact<{ + dynamicSecretId: Scalars['ID']['input']; + organisationId: Scalars['ID']['input']; + path?: InputMaybe; + name: Scalars['String']['input']; + description?: InputMaybe; + defaultTtl?: InputMaybe; + maxTtl?: InputMaybe; + authenticationId?: InputMaybe; + config: AwsConfigInput; + keyMap: Array> | InputMaybe; +}>; + + +export type UpdateDynamicSecretMutation = { __typename?: 'Mutation', updateAwsDynamicSecret?: { __typename?: 'UpdateAWSDynamicSecretMutation', dynamicSecret?: { __typename?: 'DynamicSecretType', id: string, name: string, description: string, provider: ApiDynamicSecretProviderChoices, createdAt?: any | null, updatedAt: any } | null } | null }; + export type CreateSharedSecretMutationVariables = Exact<{ input: LockboxInput; }>; @@ -3023,6 +3329,29 @@ export type VerifyInviteQueryVariables = Exact<{ export type VerifyInviteQuery = { __typename?: 'Query', validateInvite?: { __typename?: 'OrganisationMemberInviteType', id: string, inviteeEmail: string, organisation: { __typename?: 'OrganisationType', id: string, name: string }, invitedBy: { __typename?: 'OrganisationMemberType', fullName?: string | null, email?: string | null }, apps: Array<{ __typename?: 'AppMembershipType', id: string, name: string }> } | null }; +export type GetDynamicSecretsQueryVariables = Exact<{ + orgId: Scalars['ID']['input']; + appId?: InputMaybe; + envId?: InputMaybe; + path?: InputMaybe; +}>; + + +export type GetDynamicSecretsQuery = { __typename?: 'Query', dynamicSecrets?: Array<{ __typename?: 'DynamicSecretType', id: string, name: string, path: string, description: string, provider: ApiDynamicSecretProviderChoices, defaultTtlSeconds?: number | null, maxTtlSeconds?: number | null, createdAt?: any | null, environment: { __typename?: 'EnvironmentType', id: string, name: string, index: number, app: { __typename?: 'AppMembershipType', id: string, name: string } }, config?: { __typename?: 'AWSConfigType', usernameTemplate: string, iamPath?: string | null } | null, keyMap?: Array<{ __typename?: 'KeyMap', id?: string | null, keyName?: string | null, masked?: boolean | null } | null> | null, authentication?: { __typename?: 'ProviderCredentialsType', id: string, name: string } | null } | null> | null }; + +export type GetDynamicSecretProvidersQueryVariables = Exact<{ [key: string]: never; }>; + + +export type GetDynamicSecretProvidersQuery = { __typename?: 'Query', dynamicSecretProviders?: Array<{ __typename?: 'DynamicSecretProviderType', id: string, name: string, credentials: any, configMap: any } | null> | null }; + +export type GetDynamicSecretLeasesQueryVariables = Exact<{ + secretId: Scalars['ID']['input']; + orgId: Scalars['ID']['input']; +}>; + + +export type GetDynamicSecretLeasesQuery = { __typename?: 'Query', dynamicSecrets?: Array<{ __typename?: 'DynamicSecretType', id: string, leases: Array<{ __typename?: 'DynamicSecretLeaseType', id: string, name: string, ttl?: number | null, createdAt?: any | null, expiresAt?: any | null, revokedAt?: any | null, status: ApiDynamicSecretLeaseStatusChoices, organisationMember?: { __typename?: 'OrganisationMemberType', id: string, fullName?: string | null, email?: string | null, avatarUrl?: string | null, self?: boolean | null } | null, serviceAccount?: { __typename?: 'ServiceAccountType', id: string, name: string } | null, events?: Array<{ __typename?: 'DynamicSecretLeaseEventType', id: string, eventType: ApiDynamicSecretLeaseEventEventTypeChoices, createdAt: any, metadata: any, ipAddress?: string | null, userAgent?: string | null, organisationMember?: { __typename?: 'OrganisationMemberType', id: string, fullName?: string | null, email?: string | null, avatarUrl?: string | null, self?: boolean | null } | null, serviceAccount?: { __typename?: 'ServiceAccountType', id: string, name: string } | null } | null> | null }> } | null> | null }; + export type GetAppEnvironmentsQueryVariables = Exact<{ appId: Scalars['ID']['input']; memberId?: InputMaybe; @@ -3040,7 +3369,7 @@ export type GetAppSecretsQueryVariables = Exact<{ }>; -export type GetAppSecretsQuery = { __typename?: 'Query', sseEnabled?: boolean | null, serverPublicKey?: string | null, appEnvironments?: Array<{ __typename?: 'EnvironmentType', id: string, name: string, envType: ApiEnvironmentEnvTypeChoices, identityKey: string, wrappedSeed?: string | null, wrappedSalt?: string | null, createdAt?: any | null, secretCount?: number | null, folderCount?: number | null, index: number, app: { __typename?: 'AppMembershipType', name: string, id: string }, members: Array<{ __typename?: 'OrganisationMemberType', email?: string | null, fullName?: string | null, avatarUrl?: string | null } | null>, folders: Array<{ __typename?: 'SecretFolderType', id: string, name: string, path: string } | null>, secrets: Array<{ __typename?: 'SecretType', id: string, key: string, value: string, comment: string, path: string } | null> } | null> | null }; +export type GetAppSecretsQuery = { __typename?: 'Query', sseEnabled?: boolean | null, serverPublicKey?: string | null, appEnvironments?: Array<{ __typename?: 'EnvironmentType', id: string, name: string, envType: ApiEnvironmentEnvTypeChoices, identityKey: string, wrappedSeed?: string | null, wrappedSalt?: string | null, createdAt?: any | null, secretCount?: number | null, folderCount?: number | null, index: number, app: { __typename?: 'AppMembershipType', name: string, id: string }, members: Array<{ __typename?: 'OrganisationMemberType', email?: string | null, fullName?: string | null, avatarUrl?: string | null } | null>, folders: Array<{ __typename?: 'SecretFolderType', id: string, name: string, path: string } | null>, secrets: Array<{ __typename?: 'SecretType', id: string, key: string, value: string, comment: string, path: string } | null>, dynamicSecrets: Array<{ __typename?: 'DynamicSecretType', id: string, name: string, path: string, description: string, provider: ApiDynamicSecretProviderChoices, keyMap?: Array<{ __typename?: 'KeyMap', id?: string | null, keyName?: string | null } | null> | null } | null> } | null> | null }; export type GetAppSecretsLogsQueryVariables = Exact<{ appId: Scalars['ID']['input']; @@ -3115,7 +3444,7 @@ export type GetSecretsQueryVariables = Exact<{ }>; -export type GetSecretsQuery = { __typename?: 'Query', secrets?: Array<{ __typename?: 'SecretType', id: string, key: string, value: string, path: string, comment: string, createdAt?: any | null, updatedAt: any, tags: Array<{ __typename?: 'SecretTagType', id: string, name: string, color: string }>, override?: { __typename?: 'PersonalSecretType', value?: string | null, isActive: boolean } | null, environment: { __typename?: 'EnvironmentType', id: string, app: { __typename?: 'AppMembershipType', id: string } } } | null> | null, folders?: Array<{ __typename?: 'SecretFolderType', id: string, name: string, path: string, createdAt?: any | null, folderCount?: number | null, secretCount?: number | null } | null> | null, appEnvironments?: Array<{ __typename?: 'EnvironmentType', id: string, name: string, envType: ApiEnvironmentEnvTypeChoices, identityKey: string, app: { __typename?: 'AppMembershipType', name: string } } | null> | null, environmentKeys?: Array<{ __typename?: 'EnvironmentKeyType', id: string, identityKey: string, wrappedSeed: string, wrappedSalt: string } | null> | null, envSyncs?: Array<{ __typename?: 'EnvironmentSyncType', id: string, options: any, isActive: boolean, status: ApiEnvironmentSyncStatusChoices, lastSync?: any | null, createdAt?: any | null, environment: { __typename?: 'EnvironmentType', id: string, name: string, envType: ApiEnvironmentEnvTypeChoices }, serviceInfo?: { __typename?: 'ServiceType', id?: string | null, name?: string | null } | null } | null> | null }; +export type GetSecretsQuery = { __typename?: 'Query', secrets?: Array<{ __typename?: 'SecretType', id: string, key: string, value: string, path: string, comment: string, createdAt?: any | null, updatedAt: any, tags: Array<{ __typename?: 'SecretTagType', id: string, name: string, color: string }>, override?: { __typename?: 'PersonalSecretType', value?: string | null, isActive: boolean } | null, environment: { __typename?: 'EnvironmentType', id: string, app: { __typename?: 'AppMembershipType', id: string } } } | null> | null, folders?: Array<{ __typename?: 'SecretFolderType', id: string, name: string, path: string, createdAt?: any | null, folderCount?: number | null, secretCount?: number | null } | null> | null, appEnvironments?: Array<{ __typename?: 'EnvironmentType', id: string, name: string, envType: ApiEnvironmentEnvTypeChoices, identityKey: string, app: { __typename?: 'AppMembershipType', id: string, name: string, sseEnabled: boolean } } | null> | null, environmentKeys?: Array<{ __typename?: 'EnvironmentKeyType', id: string, identityKey: string, wrappedSeed: string, wrappedSalt: string } | null> | null, envSyncs?: Array<{ __typename?: 'EnvironmentSyncType', id: string, options: any, isActive: boolean, status: ApiEnvironmentSyncStatusChoices, lastSync?: any | null, createdAt?: any | null, environment: { __typename?: 'EnvironmentType', id: string, name: string, envType: ApiEnvironmentEnvTypeChoices }, serviceInfo?: { __typename?: 'ServiceType', id?: string | null, name?: string | null } | null } | null> | null, dynamicSecrets?: Array<{ __typename?: 'DynamicSecretType', id: string, name: string, path: string, description: string, provider: ApiDynamicSecretProviderChoices, defaultTtlSeconds?: number | null, maxTtlSeconds?: number | null, createdAt?: any | null, keyMap?: Array<{ __typename?: 'KeyMap', id?: string | null, keyName?: string | null, masked?: boolean | null } | null> | null, config?: { __typename?: 'AWSConfigType', usernameTemplate: string, groups?: string | null, iamPath?: string | null, permissionBoundaryArn?: string | null, policyArns?: string | null, policyDocument?: any | null } | null, authentication?: { __typename?: 'ProviderCredentialsType', id: string, name: string } | null } | null> | null }; export type GetServiceTokensQueryVariables = Exact<{ appId: Scalars['ID']['input']; @@ -3331,6 +3660,12 @@ export const InitAppEnvironmentsDocument = {"kind":"Document","definitions":[{"k export const LogSecretReadsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"LogSecretReads"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"ids"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"readSecret"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"ids"},"value":{"kind":"Variable","name":{"kind":"Name","value":"ids"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; export const RemovePersonalSecretDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemovePersonalSecret"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"secretId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"removeOverride"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"secretId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"secretId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; export const RenameEnvDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RenameEnv"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"environmentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"renameEnvironment"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"environmentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"environmentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateNewAwsDynamicSecretDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNewAWSDynamicSecret"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"environmentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"description"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"defaultTtl"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"maxTtl"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"authenticationId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"config"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AWSConfigInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"keyMap"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"KeyMapInput"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createAwsDynamicSecret"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"environmentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"environmentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"Argument","name":{"kind":"Name","value":"description"},"value":{"kind":"Variable","name":{"kind":"Name","value":"description"}}},{"kind":"Argument","name":{"kind":"Name","value":"defaultTtl"},"value":{"kind":"Variable","name":{"kind":"Name","value":"defaultTtl"}}},{"kind":"Argument","name":{"kind":"Name","value":"maxTtl"},"value":{"kind":"Variable","name":{"kind":"Name","value":"maxTtl"}}},{"kind":"Argument","name":{"kind":"Name","value":"authenticationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"authenticationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"config"},"value":{"kind":"Variable","name":{"kind":"Name","value":"config"}}},{"kind":"Argument","name":{"kind":"Name","value":"keyMap"},"value":{"kind":"Variable","name":{"kind":"Name","value":"keyMap"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dynamicSecret"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateDynamicSecretLeaseDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateDynamicSecretLease"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"secretId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"ttl"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createDynamicSecretLease"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"secretId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"secretId"}}},{"kind":"Argument","name":{"kind":"Name","value":"ttl"},"value":{"kind":"Variable","name":{"kind":"Name","value":"ttl"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lease"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"credentials"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AwsCredentialsType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"accessKeyId"}},{"kind":"Field","name":{"kind":"Name","value":"secretAccessKey"}},{"kind":"Field","name":{"kind":"Name","value":"username"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}}]}}]}}]} as unknown as DocumentNode; +export const DeleteDynamicSecretOpDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteDynamicSecretOP"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"secretId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteDynamicSecret"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"secretId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"secretId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; +export const RenewDynamicSecretLeaseOpDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RenewDynamicSecretLeaseOP"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"leaseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"ttl"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"renewDynamicSecretLease"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"leaseId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"leaseId"}}},{"kind":"Argument","name":{"kind":"Name","value":"ttl"},"value":{"kind":"Variable","name":{"kind":"Name","value":"ttl"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lease"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]} as unknown as DocumentNode; +export const RevokeDynamicSecretLeaseOpDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RevokeDynamicSecretLeaseOP"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"leaseId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"revokeDynamicSecretLease"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"leaseId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"leaseId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lease"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}},{"kind":"Field","name":{"kind":"Name","value":"revokedAt"}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]} as unknown as DocumentNode; +export const UpdateDynamicSecretDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateDynamicSecret"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"dynamicSecretId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"description"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"defaultTtl"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"maxTtl"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"authenticationId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"config"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"AWSConfigInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"keyMap"}},"type":{"kind":"NonNullType","type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"KeyMapInput"}}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateAwsDynamicSecret"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"dynamicSecretId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"dynamicSecretId"}}},{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"Argument","name":{"kind":"Name","value":"description"},"value":{"kind":"Variable","name":{"kind":"Name","value":"description"}}},{"kind":"Argument","name":{"kind":"Name","value":"defaultTtl"},"value":{"kind":"Variable","name":{"kind":"Name","value":"defaultTtl"}}},{"kind":"Argument","name":{"kind":"Name","value":"maxTtl"},"value":{"kind":"Variable","name":{"kind":"Name","value":"maxTtl"}}},{"kind":"Argument","name":{"kind":"Name","value":"authenticationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"authenticationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"config"},"value":{"kind":"Variable","name":{"kind":"Name","value":"config"}}},{"kind":"Argument","name":{"kind":"Name","value":"keyMap"},"value":{"kind":"Variable","name":{"kind":"Name","value":"keyMap"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dynamicSecret"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]}}]} as unknown as DocumentNode; export const CreateSharedSecretDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateSharedSecret"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"LockboxInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createLockbox"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lockbox"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"allowedViews"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}}]}}]}}]}}]} as unknown as DocumentNode; export const SwapEnvOrderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SwapEnvOrder"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"environment1Id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"environment2Id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"swapEnvironmentOrder"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"environment1Id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"environment1Id"}}},{"kind":"Argument","name":{"kind":"Name","value":"environment2Id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"environment2Id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; export const AcceptOrganisationInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AcceptOrganisationInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyring"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedRecovery"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOrganisationMember"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}},{"kind":"Argument","name":{"kind":"Name","value":"identityKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedKeyring"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyring"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedRecovery"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedRecovery"}}},{"kind":"Argument","name":{"kind":"Name","value":"inviteId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"orgMember"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"role"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]} as unknown as DocumentNode; @@ -3389,8 +3724,11 @@ export const GetOrganisationMembersDocument = {"kind":"Document","definitions":[ export const GetOrganisationPlanDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOrganisationPlan"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"organisationPlan"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"maxUsers"}},{"kind":"Field","name":{"kind":"Name","value":"maxApps"}},{"kind":"Field","name":{"kind":"Name","value":"maxEnvsPerApp"}},{"kind":"Field","name":{"kind":"Name","value":"seatsUsed"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"users"}},{"kind":"Field","name":{"kind":"Name","value":"serviceAccounts"}},{"kind":"Field","name":{"kind":"Name","value":"total"}}]}},{"kind":"Field","name":{"kind":"Name","value":"seatLimit"}},{"kind":"Field","name":{"kind":"Name","value":"appCount"}}]}}]}}]} as unknown as DocumentNode; export const GetRolesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetRoles"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"roles"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"}},{"kind":"Field","name":{"kind":"Name","value":"isDefault"}}]}}]}}]} as unknown as DocumentNode; export const VerifyInviteDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"VerifyInvite"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"validateInvite"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"inviteId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"inviteId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"organisation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"inviteeEmail"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}},{"kind":"Field","name":{"kind":"Name","value":"apps"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetDynamicSecretsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetDynamicSecrets"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dynamicSecrets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}},{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"config"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AWSConfigType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"usernameTemplate"}},{"kind":"Field","name":{"kind":"Name","value":"iamPath"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"keyMap"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"keyName"}},{"kind":"Field","name":{"kind":"Name","value":"masked"}}]}},{"kind":"Field","name":{"kind":"Name","value":"defaultTtlSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"maxTtlSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"authentication"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode; +export const GetDynamicSecretProvidersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetDynamicSecretProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dynamicSecretProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"credentials"}},{"kind":"Field","name":{"kind":"Name","value":"configMap"}}]}}]}}]} as unknown as DocumentNode; +export const GetDynamicSecretLeasesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetDynamicSecretLeases"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"secretId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"dynamicSecrets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"secretId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"secretId"}}},{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"leases"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"ttl"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}},{"kind":"Field","name":{"kind":"Name","value":"revokedAt"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"organisationMember"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"self"}}]}},{"kind":"Field","name":{"kind":"Name","value":"serviceAccount"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"events"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"eventType"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"metadata"}},{"kind":"Field","name":{"kind":"Name","value":"ipAddress"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"organisationMember"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"self"}}]}},{"kind":"Field","name":{"kind":"Name","value":"serviceAccount"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const GetAppEnvironmentsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAppEnvironments"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberType"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"MemberType"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"appEnvironments"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"environmentId"},"value":{"kind":"NullValue"}},{"kind":"Argument","name":{"kind":"Name","value":"memberId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}}},{"kind":"Argument","name":{"kind":"Name","value":"memberType"},"value":{"kind":"Variable","name":{"kind":"Name","value":"memberType"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSeed"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSalt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"secretCount"}},{"kind":"Field","name":{"kind":"Name","value":"folderCount"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"members"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"sseEnabled"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}]},{"kind":"Field","name":{"kind":"Name","value":"serverPublicKey"}}]}}]} as unknown as DocumentNode; -export const GetAppSecretsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAppSecrets"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberType"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"MemberType"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"appEnvironments"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"environmentId"},"value":{"kind":"NullValue"}},{"kind":"Argument","name":{"kind":"Name","value":"memberId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}}},{"kind":"Argument","name":{"kind":"Name","value":"memberType"},"value":{"kind":"Variable","name":{"kind":"Name","value":"memberType"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSeed"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSalt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"secretCount"}},{"kind":"Field","name":{"kind":"Name","value":"folderCount"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"members"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"folders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"path"}}]}},{"kind":"Field","name":{"kind":"Name","value":"secrets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"path"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"sseEnabled"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}]},{"kind":"Field","name":{"kind":"Name","value":"serverPublicKey"}}]}}]} as unknown as DocumentNode; +export const GetAppSecretsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAppSecrets"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberType"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"MemberType"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"appEnvironments"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"environmentId"},"value":{"kind":"NullValue"}},{"kind":"Argument","name":{"kind":"Name","value":"memberId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}}},{"kind":"Argument","name":{"kind":"Name","value":"memberType"},"value":{"kind":"Variable","name":{"kind":"Name","value":"memberType"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSeed"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSalt"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"secretCount"}},{"kind":"Field","name":{"kind":"Name","value":"folderCount"}},{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"members"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"folders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"path"}}]}},{"kind":"Field","name":{"kind":"Name","value":"secrets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"path"}}]}},{"kind":"Field","name":{"kind":"Name","value":"dynamicSecrets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"keyMap"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"keyName"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"sseEnabled"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}]},{"kind":"Field","name":{"kind":"Name","value":"serverPublicKey"}}]}}]} as unknown as DocumentNode; export const GetAppSecretsLogsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAppSecretsLogs"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"start"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"end"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"eventTypes"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberType"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"MemberType"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"environmentId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"secretLogs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"start"},"value":{"kind":"Variable","name":{"kind":"Name","value":"start"}}},{"kind":"Argument","name":{"kind":"Name","value":"end"},"value":{"kind":"Variable","name":{"kind":"Name","value":"end"}}},{"kind":"Argument","name":{"kind":"Name","value":"eventTypes"},"value":{"kind":"Variable","name":{"kind":"Name","value":"eventTypes"}}},{"kind":"Argument","name":{"kind":"Name","value":"memberId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}}},{"kind":"Argument","name":{"kind":"Name","value":"memberType"},"value":{"kind":"Variable","name":{"kind":"Name","value":"memberType"}}},{"kind":"Argument","name":{"kind":"Name","value":"environmentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"environmentId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"color"}}]}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"ipAddress"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"serviceToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"serviceAccount"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"deletedAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"serviceAccountToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"deletedAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"eventType"}},{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"secret"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"path"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"count"}}]}},{"kind":"Field","name":{"kind":"Name","value":"environmentKeys"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSeed"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSalt"}},{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetEnvironmentKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetEnvironmentKey"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"environmentKeys"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"environmentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSeed"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSalt"}}]}}]}}]} as unknown as DocumentNode; export const GetEnvironmentTokensDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetEnvironmentTokens"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"environmentTokens"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"environmentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedKeyShare"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode; @@ -3399,7 +3737,7 @@ export const GetOrgSecretKeysDocument = {"kind":"Document","definitions":[{"kind export const GetSecretHistoryDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSecretHistory"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"secrets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"history"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"color"}}]}},{"kind":"Field","name":{"kind":"Name","value":"version"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"ipAddress"}},{"kind":"Field","name":{"kind":"Name","value":"userAgent"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"serviceToken"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"serviceAccount"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"deletedAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"eventType"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"environmentKeys"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"environmentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSeed"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSalt"}}]}}]}}]} as unknown as DocumentNode; export const GetEnvSecretsKvDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetEnvSecretsKV"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"folders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"StringValue","value":"/","block":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"secrets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"StringValue","value":"/","block":false}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"path"}}]}},{"kind":"Field","name":{"kind":"Name","value":"environmentKeys"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"environmentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSeed"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSalt"}}]}}]}}]} as unknown as DocumentNode; export const GetSecretTagsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSecretTags"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"secretTags"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"color"}}]}}]}}]} as unknown as DocumentNode; -export const GetSecretsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSecrets"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"secrets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"color"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"override"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}}]}},{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"folders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"folderCount"}},{"kind":"Field","name":{"kind":"Name","value":"secretCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"appEnvironments"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"environmentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"environmentKeys"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"environmentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSeed"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSalt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"envSyncs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"serviceInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"options"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"lastSync"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode; +export const GetSecretsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSecrets"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"path"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"secrets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"tags"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"color"}}]}},{"kind":"Field","name":{"kind":"Name","value":"comment"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"override"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"value"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}}]}},{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"folders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"folderCount"}},{"kind":"Field","name":{"kind":"Name","value":"secretCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"appEnvironments"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"environmentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sseEnabled"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"environmentKeys"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"environmentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSeed"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedSalt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"envSyncs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"environment"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"envType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"serviceInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"options"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"lastSync"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}},{"kind":"Field","name":{"kind":"Name","value":"dynamicSecrets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"envId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envId"}}},{"kind":"Argument","name":{"kind":"Name","value":"path"},"value":{"kind":"Variable","name":{"kind":"Name","value":"path"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"path"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"keyMap"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"keyName"}},{"kind":"Field","name":{"kind":"Name","value":"masked"}}]}},{"kind":"Field","name":{"kind":"Name","value":"config"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"AWSConfigType"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"usernameTemplate"}},{"kind":"Field","name":{"kind":"Name","value":"groups"}},{"kind":"Field","name":{"kind":"Name","value":"iamPath"}},{"kind":"Field","name":{"kind":"Name","value":"permissionBoundaryArn"}},{"kind":"Field","name":{"kind":"Name","value":"policyArns"}},{"kind":"Field","name":{"kind":"Name","value":"policyDocument"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"defaultTtlSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"maxTtlSeconds"}},{"kind":"Field","name":{"kind":"Name","value":"authentication"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]} as unknown as DocumentNode; export const GetServiceTokensDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetServiceTokens"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serviceTokens"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"createdBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"self"}}]}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}},{"kind":"Field","name":{"kind":"Name","value":"keys"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetServiceAccountDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetServiceAccountDetail"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serviceAccounts"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}},{"kind":"Argument","name":{"kind":"Name","value":"serviceAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"role"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"color"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"handlers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedKeyring"}},{"kind":"Field","name":{"kind":"Name","value":"wrappedRecovery"}},{"kind":"Field","name":{"kind":"Name","value":"user"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"self"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"appMemberships"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"environments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"sseEnabled"}}]}},{"kind":"Field","name":{"kind":"Name","value":"networkPolicies"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"allowedIps"}},{"kind":"Field","name":{"kind":"Name","value":"isGlobal"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetServiceAccountHandlersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetServiceAccountHandlers"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serviceAccountHandlers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"role"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"permissions"}}]}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"self"}}]}}]}}]} as unknown as DocumentNode; diff --git a/frontend/apollo/schema.graphql b/frontend/apollo/schema.graphql index e726ab1c0..ce0f2e1b8 100644 --- a/frontend/apollo/schema.graphql +++ b/frontend/apollo/schema.graphql @@ -13,9 +13,22 @@ type Query { validateInvite(inviteId: ID): OrganisationMemberInviteType apps(organisationId: ID, appId: ID): [AppType] kmsLogs(appId: ID, start: BigInt, end: BigInt): KMSLogsResponseType - secretLogs(appId: ID, start: BigInt, end: BigInt, eventTypes: [String], memberId: ID, memberType: MemberType, environmentId: ID): SecretLogsResponseType + secretLogs( + appId: ID + start: BigInt + end: BigInt + eventTypes: [String] + memberId: ID + memberType: MemberType + environmentId: ID + ): SecretLogsResponseType appActivityChart(appId: ID, period: TimeRange): [ChartDataPointType] - appEnvironments(appId: ID, environmentId: ID, memberId: ID, memberType: MemberType): [EnvironmentType] + appEnvironments( + appId: ID + environmentId: ID + memberId: ID + memberType: MemberType + ): [EnvironmentType] appUsers(appId: ID): [OrganisationMemberType] appServiceAccounts(appId: ID): [ServiceAccountType] secrets(envId: ID, path: String, id: ID): [SecretType] @@ -49,10 +62,16 @@ type Query { testVaultCreds(credentialId: ID): Boolean testNomadCreds(credentialId: ID): Boolean validateAwsAssumeRoleAuth: AWSValidationResultType - validateAwsAssumeRoleCredentials(roleArn: String!, region: String, externalId: String): AWSValidationResultType + validateAwsAssumeRoleCredentials( + roleArn: String! + region: String + externalId: String + ): AWSValidationResultType stripeCheckoutDetails(stripeSessionId: String!): StripeCheckoutDetails stripeSubscriptionDetails(organisationId: ID): StripeSubscriptionDetails stripeCustomerPortalUrl(organisationId: ID!): String + dynamicSecretProviders: [DynamicSecretProviderType] + dynamicSecrets(secretId: ID, appId: ID, envId: ID, path: String, orgId: ID): [DynamicSecretType] } type OrganisationType { @@ -76,13 +95,19 @@ value as specified by scalar DateTime enum ApiOrganisationPlanChoices { - """Free""" + """ + Free + """ FR - """Pro""" + """ + Pro + """ PR - """Enterprise""" + """ + Enterprise + """ EN } @@ -176,6 +201,7 @@ type EnvironmentType { updatedAt: DateTime! folders: [SecretFolderType]! secrets(path: String): [SecretType]! + dynamicSecrets(path: String): [DynamicSecretType]! folderCount: Int secretCount: Int members: [OrganisationMemberType]! @@ -183,16 +209,24 @@ type EnvironmentType { } enum ApiEnvironmentEnvTypeChoices { - """Development""" + """ + Development + """ DEV - """Staging""" + """ + Staging + """ STAGING - """Production""" + """ + Production + """ PROD - """Custom""" + """ + Custom + """ CUSTOM } @@ -314,16 +348,24 @@ type ServiceAccountTokenType { } enum ApiSecretEventEventTypeChoices { - """Create""" + """ + Create + """ C - """Read""" + """ + Read + """ R - """Update""" + """ + Update + """ U - """Delete""" + """ + Delete + """ D } @@ -337,18 +379,27 @@ type PersonalSecretType { updatedAt: DateTime! } -type EnvironmentSyncType { +type DynamicSecretType { id: String! + name: String! + description: String! environment: EnvironmentType! + folder: SecretFolderType path: String! - options: JSONString! authentication: ProviderCredentialsType - isActive: Boolean! + + """ + Which provider this secret is associated with. + """ + provider: ApiDynamicSecretProviderChoices! + config: DynamicSecretConfigUnion + keyMap: [KeyMap] createdAt: DateTime - lastSync: DateTime - status: ApiEnvironmentSyncStatusChoices! - serviceInfo: ServiceType - history: [EnvironmentSyncEventType!]! + updatedAt: DateTime! + deletedAt: DateTime + leases: [DynamicSecretLeaseType!]! + defaultTtlSeconds: Int + maxTtlSeconds: Int } type ProviderCredentialsType { @@ -369,20 +420,169 @@ type ProviderType { authScheme: String } +enum ApiDynamicSecretProviderChoices { + """ + AWS + """ + AWS +} + +union DynamicSecretConfigUnion = AWSConfigType + +type AWSConfigType { + usernameTemplate: String! + iamPath: String + permissionBoundaryArn: String + groups: String + policyArns: String + policyDocument: GenericScalar +} + +""" +The `GenericScalar` scalar type represents a generic +GraphQL scalar value that could be: +String, Boolean, Int, Float, List or Object. +""" +scalar GenericScalar + +type KeyMap { + id: String + keyName: String + masked: Boolean +} + +type DynamicSecretLeaseType { + id: String! + name: String! + secret: DynamicSecretType! + organisationMember: OrganisationMemberType + serviceAccount: ServiceAccountType + ttl: Int + + """ + Current status of the lease + """ + status: ApiDynamicSecretLeaseStatusChoices! + credentials: LeaseCredentialsUnion + createdAt: DateTime + expiresAt: DateTime + revokedAt: DateTime + deletedAt: DateTime + events: [DynamicSecretLeaseEventType] +} + +enum ApiDynamicSecretLeaseStatusChoices { + """ + Created + """ + CREATED + + """ + Active + """ + ACTIVE + + """ + Renewed + """ + RENEWED + + """ + Revoked + """ + REVOKED + + """ + Expired + """ + EXPIRED +} + +union LeaseCredentialsUnion = AwsCredentialsType + +type AwsCredentialsType { + accessKeyId: String + secretAccessKey: String + username: String +} + +type DynamicSecretLeaseEventType { + id: ID! + lease: DynamicSecretLeaseType! + eventType: ApiDynamicSecretLeaseEventEventTypeChoices! + organisationMember: OrganisationMemberType + serviceAccount: ServiceAccountType + ipAddress: String + userAgent: String + metadata: JSONString! + createdAt: DateTime! +} + +enum ApiDynamicSecretLeaseEventEventTypeChoices { + """ + Created + """ + CREATED + + """ + Active + """ + ACTIVE + + """ + Renewed + """ + RENEWED + + """ + Revoked + """ + REVOKED + + """ + Expired + """ + EXPIRED +} + +type EnvironmentSyncType { + id: String! + environment: EnvironmentType! + path: String! + options: JSONString! + authentication: ProviderCredentialsType + isActive: Boolean! + createdAt: DateTime + lastSync: DateTime + status: ApiEnvironmentSyncStatusChoices! + serviceInfo: ServiceType + history: [EnvironmentSyncEventType!]! +} + enum ApiEnvironmentSyncStatusChoices { - """In progress""" + """ + In progress + """ IN_PROGRESS - """Completed""" + """ + Completed + """ COMPLETED - """cancelled""" + """ + cancelled + """ CANCELLED - """Timed out""" + """ + Timed out + """ TIMED_OUT - """Failed""" + """ + Failed + """ FAILED } @@ -403,19 +603,29 @@ type EnvironmentSyncEventType { } enum ApiEnvironmentSyncEventStatusChoices { - """In progress""" + """ + In progress + """ IN_PROGRESS - """Completed""" + """ + Completed + """ COMPLETED - """cancelled""" + """ + cancelled + """ CANCELLED - """Timed out""" + """ + Timed out + """ TIMED_OUT - """Failed""" + """ + Failed + """ FAILED } @@ -478,13 +688,19 @@ type ActivatedPhaseLicenseType { } enum ApiActivatedPhaseLicensePlanChoices { - """Free""" + """ + Free + """ FR - """Pro""" + """ + Pro + """ PR - """Enterprise""" + """ + Enterprise + """ EN } @@ -539,9 +755,13 @@ type KMSLogType implements Node { longitude: Float } -"""An object with an ID""" +""" +An object with an ID +""" interface Node { - """The ID of the object""" + """ + The ID of the object + """ id: ID! } @@ -771,63 +991,261 @@ enum PlanTypeEnum { ENTERPRISE } +type DynamicSecretProviderType { + id: String! + name: String! + credentials: GenericScalar! + configMap: GenericScalar! +} + type Mutation { - createOrganisation(id: ID!, identityKey: String!, name: String!, wrappedKeyring: String!, wrappedRecovery: String!): CreateOrganisationMutation - bulkInviteOrganisationMembers(invites: [InviteInput]!, orgId: ID!): BulkInviteOrganisationMembersMutation - createOrganisationMember(identityKey: String!, inviteId: ID!, orgId: ID!, wrappedKeyring: String, wrappedRecovery: String): CreateOrganisationMemberMutation + createOrganisation( + id: ID! + identityKey: String! + name: String! + wrappedKeyring: String! + wrappedRecovery: String! + ): CreateOrganisationMutation + bulkInviteOrganisationMembers( + invites: [InviteInput]! + orgId: ID! + ): BulkInviteOrganisationMembersMutation + createOrganisationMember( + identityKey: String! + inviteId: ID! + orgId: ID! + wrappedKeyring: String + wrappedRecovery: String + ): CreateOrganisationMemberMutation deleteOrganisationMember(memberId: ID!): DeleteOrganisationMemberMutation updateOrganisationMemberRole(memberId: ID!, roleId: ID!): UpdateOrganisationMemberRole - updateMemberWrappedSecrets(orgId: ID!, wrappedKeyring: String!, wrappedRecovery: String!): UpdateUserWrappedSecretsMutation + updateMemberWrappedSecrets( + orgId: ID! + wrappedKeyring: String! + wrappedRecovery: String! + ): UpdateUserWrappedSecretsMutation deleteInvitation(inviteId: ID!): DeleteInviteMutation - createApp(appSeed: String!, appToken: String!, appVersion: Int!, id: ID!, identityKey: String!, name: String!, organisationId: ID!, wrappedKeyShare: String!): CreateAppMutation + createApp( + appSeed: String! + appToken: String! + appVersion: Int! + id: ID! + identityKey: String! + name: String! + organisationId: ID! + wrappedKeyShare: String! + ): CreateAppMutation rotateAppKeys(appToken: String!, id: ID!, wrappedKeyShare: String!): RotateAppKeysMutation deleteApp(id: ID!): DeleteAppMutation updateAppName(id: ID!, name: String!): UpdateAppNameMutation - addAppMember(appId: ID, envKeys: [EnvironmentKeyInput], memberId: ID, memberType: MemberType): AddAppMemberMutation + addAppMember( + appId: ID + envKeys: [EnvironmentKeyInput] + memberId: ID + memberType: MemberType + ): AddAppMemberMutation bulkAddAppMembers(appId: ID!, members: [AppMemberInputType]!): BulkAddAppMembersMutation removeAppMember(appId: ID, memberId: ID, memberType: MemberType): RemoveAppMemberMutation - updateMemberEnvironmentScope(appId: ID, envKeys: [EnvironmentKeyInput], memberId: ID, memberType: MemberType): UpdateMemberEnvScopeMutation - createEnvironment(adminKeys: [EnvironmentKeyInput], environmentData: EnvironmentInput!, wrappedSalt: String, wrappedSeed: String): CreateEnvironmentMutation + updateMemberEnvironmentScope( + appId: ID + envKeys: [EnvironmentKeyInput] + memberId: ID + memberType: MemberType + ): UpdateMemberEnvScopeMutation + createEnvironment( + adminKeys: [EnvironmentKeyInput] + environmentData: EnvironmentInput! + wrappedSalt: String + wrappedSeed: String + ): CreateEnvironmentMutation deleteEnvironment(environmentId: ID!): DeleteEnvironmentMutation renameEnvironment(environmentId: ID!, name: String!): RenameEnvironmentMutation swapEnvironmentOrder(environment1Id: ID!, environment2Id: ID!): SwapEnvironmentOrderMutation - createEnvironmentKey(envId: ID!, identityKey: String!, userId: ID, wrappedSalt: String!, wrappedSeed: String!): CreateEnvironmentKeyMutation - createEnvironmentToken(envId: ID!, identityKey: String!, name: String!, token: String!, wrappedKeyShare: String!): CreateEnvironmentTokenMutation - createCustomRole(color: String, description: String, name: String, organisationId: ID!, permissions: JSONString): CreateCustomRoleMutation - updateCustomRole(color: String, description: String, id: ID!, name: String, permissions: JSONString): UpdateCustomRoleMutation + createEnvironmentKey( + envId: ID! + identityKey: String! + userId: ID + wrappedSalt: String! + wrappedSeed: String! + ): CreateEnvironmentKeyMutation + createEnvironmentToken( + envId: ID! + identityKey: String! + name: String! + token: String! + wrappedKeyShare: String! + ): CreateEnvironmentTokenMutation + createCustomRole( + color: String + description: String + name: String + organisationId: ID! + permissions: JSONString + ): CreateCustomRoleMutation + updateCustomRole( + color: String + description: String + id: ID! + name: String + permissions: JSONString + ): UpdateCustomRoleMutation deleteCustomRole(id: ID!): DeleteCustomRoleMutation - createNetworkAccessPolicy(allowedIps: String!, isGlobal: Boolean!, name: String, organisationId: ID!): CreateNetworkAccessPolicyMutation + createNetworkAccessPolicy( + allowedIps: String! + isGlobal: Boolean! + name: String + organisationId: ID! + ): CreateNetworkAccessPolicyMutation updateNetworkAccessPolicy(policyInputs: [UpdatePolicyInput]): UpdateNetworkAccessPolicyMutation deleteNetworkAccessPolicy(id: ID!): DeleteNetworkAccessPolicyMutation - updateAccountNetworkAccessPolicies(accountInputs: [AccountPolicyInput], organisationId: ID!): UpdateAccountNetworkAccessPolicies - createServiceAccount(handlers: [ServiceAccountHandlerInput], identityKey: String, name: String, organisationId: ID, roleId: ID, serverWrappedKeyring: String, serverWrappedRecovery: String): CreateServiceAccountMutation - enableServiceAccountThirdPartyAuth(serverWrappedKeyring: String, serverWrappedRecovery: String, serviceAccountId: ID): EnableServiceAccountThirdPartyAuthMutation - updateServiceAccountHandlers(handlers: [ServiceAccountHandlerInput], organisationId: ID): UpdateServiceAccountHandlersMutation + updateAccountNetworkAccessPolicies( + accountInputs: [AccountPolicyInput] + organisationId: ID! + ): UpdateAccountNetworkAccessPolicies + createServiceAccount( + handlers: [ServiceAccountHandlerInput] + identityKey: String + name: String + organisationId: ID + roleId: ID + serverWrappedKeyring: String + serverWrappedRecovery: String + ): CreateServiceAccountMutation + enableServiceAccountThirdPartyAuth( + serverWrappedKeyring: String + serverWrappedRecovery: String + serviceAccountId: ID + ): EnableServiceAccountThirdPartyAuthMutation + updateServiceAccountHandlers( + handlers: [ServiceAccountHandlerInput] + organisationId: ID + ): UpdateServiceAccountHandlersMutation updateServiceAccount(name: String, roleId: ID, serviceAccountId: ID): UpdateServiceAccountMutation deleteServiceAccount(serviceAccountId: ID): DeleteServiceAccountMutation - createServiceAccountToken(expiry: BigInt, identityKey: String!, name: String!, serviceAccountId: ID, token: String!, wrappedKeyShare: String!): CreateServiceAccountTokenMutation + createServiceAccountToken( + expiry: BigInt + identityKey: String! + name: String! + serviceAccountId: ID + token: String! + wrappedKeyShare: String! + ): CreateServiceAccountTokenMutation deleteServiceAccountToken(tokenId: ID): DeleteServiceAccountTokenMutation initEnvSync(appId: ID, envKeys: [EnvironmentKeyInput]): InitEnvSync deleteEnvSync(syncId: ID): DeleteSync triggerSync(syncId: ID): TriggerSync toggleSyncActive(syncId: ID): ToggleSyncActive updateSyncAuthentication(credentialId: ID, syncId: ID): UpdateSyncAuthentication - createProviderCredentials(credentials: JSONString, name: String, orgId: ID, provider: String): CreateProviderCredentials - updateProviderCredentials(credentialId: ID, credentials: JSONString, name: String): UpdateProviderCredentials + createProviderCredentials( + credentials: JSONString + name: String + orgId: ID + provider: String + ): CreateProviderCredentials + updateProviderCredentials( + credentialId: ID + credentials: JSONString + name: String + ): UpdateProviderCredentials deleteProviderCredentials(credentialId: ID): DeleteProviderCredentials - createCloudflarePagesSync(credentialId: ID, deploymentId: ID, envId: ID, path: String, projectEnv: String, projectName: String): CreateCloudflarePagesSync - createCloudflareWorkersSync(credentialId: ID, envId: ID, path: String, workerName: String): CreateCloudflareWorkersSync - createAwsSecretSync(credentialId: ID, envId: ID, kmsId: String, path: String, secretName: String): CreateAWSSecretsManagerSync - createGhActionsSync(credentialId: ID, envId: ID, owner: String, path: String, repoName: String): CreateGitHubActionsSync - createVaultSync(credentialId: ID, engine: String, envId: ID, path: String, vaultPath: String): CreateVaultSync - createNomadSync(credentialId: ID, envId: ID, nomadNamespace: String, nomadPath: String, path: String): CreateNomadSync - createGitlabCiSync(credentialId: ID, envId: ID, isGroup: Boolean, masked: Boolean, path: String, protected: Boolean, resourceId: String, resourcePath: String): CreateGitLabCISync - createRailwaySync(credentialId: ID, envId: ID, path: String, railwayEnvironment: RailwayResourceInput, railwayProject: RailwayResourceInput, railwayService: RailwayResourceInput): CreateRailwaySync - createVercelSync(credentialId: ID, envId: ID, environment: String, path: String, projectId: String, projectName: String, secretType: String, teamId: String, teamName: String): CreateVercelSync - createRenderSync(credentialId: ID, envId: ID, path: String, resourceId: String, resourceName: String, resourceType: RenderResourceType, secretFileName: String): CreateRenderSync - createUserToken(expiry: BigInt, identityKey: String!, name: String!, orgId: ID!, token: String!, wrappedKeyShare: String!): CreateUserTokenMutation + createCloudflarePagesSync( + credentialId: ID + deploymentId: ID + envId: ID + path: String + projectEnv: String + projectName: String + ): CreateCloudflarePagesSync + createCloudflareWorkersSync( + credentialId: ID + envId: ID + path: String + workerName: String + ): CreateCloudflareWorkersSync + createAwsSecretSync( + credentialId: ID + envId: ID + kmsId: String + path: String + secretName: String + ): CreateAWSSecretsManagerSync + createGhActionsSync( + credentialId: ID + envId: ID + owner: String + path: String + repoName: String + ): CreateGitHubActionsSync + createVaultSync( + credentialId: ID + engine: String + envId: ID + path: String + vaultPath: String + ): CreateVaultSync + createNomadSync( + credentialId: ID + envId: ID + nomadNamespace: String + nomadPath: String + path: String + ): CreateNomadSync + createGitlabCiSync( + credentialId: ID + envId: ID + isGroup: Boolean + masked: Boolean + path: String + protected: Boolean + resourceId: String + resourcePath: String + ): CreateGitLabCISync + createRailwaySync( + credentialId: ID + envId: ID + path: String + railwayEnvironment: RailwayResourceInput + railwayProject: RailwayResourceInput + railwayService: RailwayResourceInput + ): CreateRailwaySync + createVercelSync( + credentialId: ID + envId: ID + environment: String + path: String + projectId: String + projectName: String + secretType: String + teamId: String + teamName: String + ): CreateVercelSync + createRenderSync( + credentialId: ID + envId: ID + path: String + resourceId: String + resourceName: String + resourceType: RenderResourceType + secretFileName: String + ): CreateRenderSync + createUserToken( + expiry: BigInt + identityKey: String! + name: String! + orgId: ID! + token: String! + wrappedKeyShare: String! + ): CreateUserTokenMutation deleteUserToken(tokenId: ID!): DeleteUserTokenMutation - createServiceToken(appId: ID!, environmentKeys: [EnvironmentKeyInput], expiry: BigInt, identityKey: String!, name: String!, token: String!, wrappedKeyShare: String!): CreateServiceTokenMutation + createServiceToken( + appId: ID! + environmentKeys: [EnvironmentKeyInput] + expiry: BigInt + identityKey: String! + name: String! + token: String! + wrappedKeyShare: String! + ): CreateServiceTokenMutation deleteServiceToken(tokenId: ID!): DeleteServiceTokenMutation createSecretFolder(envId: ID, name: String, path: String): CreateSecretFolderMutation deleteSecretFolder(folderId: ID): DeleteSecretFolderMutation @@ -842,18 +1260,57 @@ type Mutation { createOverride(overrideData: PersonalSecretInput): CreatePersonalSecretMutation removeOverride(secretId: ID): DeletePersonalSecretMutation createLockbox(input: LockboxInput): CreateLockboxMutation - createSubscriptionCheckoutSession(billingPeriod: BillingPeriodEnum, organisationId: ID!, planType: PlanTypeEnum): CreateSubscriptionCheckoutSession + createSubscriptionCheckoutSession( + billingPeriod: BillingPeriodEnum + organisationId: ID! + planType: PlanTypeEnum + ): CreateSubscriptionCheckoutSession deletePaymentMethod(organisationId: ID, paymentMethodId: String): DeletePaymentMethodMutation cancelSubscription(organisationId: ID, subscriptionId: String!): UpdateSubscriptionResponse resumeSubscription(organisationId: ID, subscriptionId: String!): UpdateSubscriptionResponse - modifySubscription(billingPeriod: BillingPeriodEnum, organisationId: ID!, planType: PlanTypeEnum, subscriptionId: String!): UpdateSubscriptionResponse + modifySubscription( + billingPeriod: BillingPeriodEnum + organisationId: ID! + planType: PlanTypeEnum + subscriptionId: String! + ): UpdateSubscriptionResponse createSetupIntent(organisationId: ID): CreateSetupIntentMutation setDefaultPaymentMethod( organisationId: ID - """Payment Method ID to set as default""" + """ + Payment Method ID to set as default + """ paymentMethodId: String! ): SetDefaultPaymentMethodMutation + createAwsDynamicSecret( + authenticationId: ID + config: AWSConfigInput! + defaultTtl: Int + description: String + environmentId: ID! + keyMap: [KeyMapInput]! + maxTtl: Int + name: String! + organisationId: ID! + path: String + ): CreateAWSDynamicSecretMutation + updateAwsDynamicSecret( + authenticationId: ID + config: AWSConfigInput! + defaultTtl: Int + description: String + dynamicSecretId: ID! + keyMap: [KeyMapInput]! + maxTtl: Int + name: String! + organisationId: ID! + path: String + ): UpdateAWSDynamicSecretMutation + deleteDynamicSecret(secretId: ID!): DeleteDynamicSecretMutation + createDynamicSecretLease(name: String, secretId: ID!, ttl: Int): LeaseDynamicSecret + renewDynamicSecretLease(leaseId: ID!, ttl: Int): RenewLeaseMutation + revokeDynamicSecretLease(leaseId: ID!): RevokeLeaseMutation } type CreateOrganisationMutation { @@ -1254,4 +1711,42 @@ type CreateSetupIntentMutation { type SetDefaultPaymentMethodMutation { ok: Boolean -} \ No newline at end of file +} + +type CreateAWSDynamicSecretMutation { + dynamicSecret: DynamicSecretType +} + +input AWSConfigInput { + usernameTemplate: String! + iamPath: String = "/" + permissionBoundaryArn: String + groups: String + policyArns: String + policyDocument: GenericScalar +} + +input KeyMapInput { + id: String! + keyName: String! +} + +type UpdateAWSDynamicSecretMutation { + dynamicSecret: DynamicSecretType +} + +type DeleteDynamicSecretMutation { + ok: Boolean +} + +type LeaseDynamicSecret { + lease: DynamicSecretLeaseType +} + +type RenewLeaseMutation { + lease: DynamicSecretLeaseType +} + +type RevokeLeaseMutation { + lease: DynamicSecretLeaseType +} diff --git a/frontend/app/[team]/apps/[app]/_components/AppSecrets.tsx b/frontend/app/[team]/apps/[app]/_components/AppSecrets.tsx index f7950bbd8..ebb9e4f93 100644 --- a/frontend/app/[team]/apps/[app]/_components/AppSecrets.tsx +++ b/frontend/app/[team]/apps/[app]/_components/AppSecrets.tsx @@ -1,6 +1,5 @@ 'use client' -import { InitAppEnvironments } from '@/graphql/mutations/environments/initAppEnvironments.gql' import { BulkProcessSecrets } from '@/graphql/mutations/environments/bulkProcessSecrets.gql' import { GetAppSyncStatus } from '@/graphql/queries/syncing/getAppSyncStatus.gql' import { GetAppDetail } from '@/graphql/queries/getAppDetail.gql' @@ -10,7 +9,6 @@ import { EnvironmentType, SecretFolderType, SecretInput, SecretType } from '@/ap import _sodium from 'libsodium-wrappers-sumo' import { KeyringContext } from '@/contexts/keyringContext' import { MdPassword, MdSearchOff } from 'react-icons/md' - import { FaAngleDoubleDown, FaAngleDoubleUp, @@ -41,9 +39,7 @@ import { getUserKxPublicKey, arraysEqual, } from '@/utils/crypto' - import { EmptyState } from '@/components/common/EmptyState' - import { toast } from 'react-toastify' import { EnvSyncStatus } from '@/components/syncing/EnvSyncStatus' import { useAppSecrets } from '../_hooks/useAppSecrets' @@ -55,6 +51,7 @@ import MultiEnvImportDialog from '@/components/environments/secrets/import/Multi import { TbDownload } from 'react-icons/tb' import { duplicateKeysExist } from '@/utils/secrets' import { useWarnIfUnsavedChanges } from '@/hooks/warnUnsavedChanges' +import { AppDynamicSecretRow } from '@/ee/components/secrets/dynamic/AppDynamicSecretRow' export const AppSecrets = ({ team, app }: { team: string; app: string }) => { const { activeOrganisation: organisation } = useContext(organisationContext) @@ -107,7 +104,7 @@ export const AppSecrets = ({ team, app }: { team: string; app: string }) => { const [expandedSecrets, setExpandedSecrets] = useState([]) const [searchQuery, setSearchQuery] = useState('') - const [initAppEnvironments] = useMutation(InitAppEnvironments) + const [bulkProcessSecrets, { loading: bulkUpdatePending }] = useMutation(BulkProcessSecrets) const [isLoading, setIsLoading] = useState(false) @@ -128,7 +125,6 @@ export const AppSecrets = ({ team, app }: { team: string; app: string }) => { setExpandedSecrets(expandedSecrets.filter((id) => id !== secretId)) } - const allRowsAreExpanded = clientAppSecrets.every((secret) => expandedSecrets.includes(secret.id)) const allRowsAreCollapsed = expandedSecrets.length === 0 const unsavedChanges = @@ -155,10 +151,15 @@ export const AppSecrets = ({ team, app }: { team: string; app: string }) => { useWarnIfUnsavedChanges(unsavedChanges) - const { appEnvironments, appSecrets, appFolders, fetching, refetch } = useAppSecrets( - app, - userCanReadSecrets, - unsavedChanges ? 0 : 10000 // Poll every 10 seconds + const { appEnvironments, appSecrets, appFolders, appDynamicSecrets, fetching, refetch } = + useAppSecrets( + app, + userCanReadSecrets, + unsavedChanges ? 0 : 10000 // Poll every 10 seconds + ) + + const allRowsAreExpanded = [...clientAppSecrets, ...appDynamicSecrets].every((item) => + expandedSecrets.includes(item.id) ) useEffect(() => { @@ -184,6 +185,14 @@ export const AppSecrets = ({ team, app }: { team: string; app: string }) => { return searchRegex.test(folder.name) }) + const filteredDynamicSecrets = + searchQuery === '' + ? appDynamicSecrets + : appDynamicSecrets.filter((secret) => { + const searchRegex = new RegExp(searchQuery, 'i') + return searchRegex.test(secret.name) + }) + const { data: syncsData } = useQuery(GetAppSyncStatus, { variables: { appId: app, @@ -194,7 +203,10 @@ export const AppSecrets = ({ team, app }: { team: string; app: string }) => { const toggleAllExpanded = (expand: boolean) => { expand - ? setExpandedSecrets(clientAppSecrets.map((appSecret) => appSecret.id)) + ? setExpandedSecrets([ + ...clientAppSecrets.map((appSecret) => appSecret.id), + ...appDynamicSecrets.map((appDynamicSecret) => appDynamicSecret.id), + ]) : setExpandedSecrets([]) } @@ -855,7 +867,7 @@ export const AppSecrets = ({ team, app }: { team: string; app: string }) => { /> )} - {filteredSecrets.length > 0 && ( + {(filteredSecrets.length > 0 || filteredDynamicSecrets.length > 0) && (
+
diff --git a/frontend/components/environments/secrets/OverrideDialog.tsx b/frontend/components/environments/secrets/OverrideDialog.tsx index 1ec038c72..5f65bc779 100644 --- a/frontend/components/environments/secrets/OverrideDialog.tsx +++ b/frontend/components/environments/secrets/OverrideDialog.tsx @@ -113,7 +113,9 @@ export const OverrideDialog = (props: { activeOverride ? 'A Personal Secret is overriding this value' : 'Override this value' } > - {' '} + + + Override diff --git a/frontend/components/environments/secrets/SecretRow.tsx b/frontend/components/environments/secrets/SecretRow.tsx index 52a595e63..72eaed392 100644 --- a/frontend/components/environments/secrets/SecretRow.tsx +++ b/frontend/components/environments/secrets/SecretRow.tsx @@ -162,7 +162,7 @@ export default function SecretRow(props: {
- {isRevealed ? : }{' '} + {isRevealed ? : }{' '} {isRevealed ? 'Mask' : 'Reveal'} )} @@ -263,9 +263,7 @@ export default function SecretRow(props: { onClick={() => handleDelete(secret.id)} title={stagedForDelete ? 'Restore this secret' : 'Delete this secret'} > -
- {stagedForDelete ? : } -
+
{stagedForDelete ? : }
)}
@@ -332,7 +330,6 @@ export default function SecretRow(props: { onFocus={() => setExpanded(true)} disabled={stagedForDelete || !userCanUpdateSecrets} /> -
diff --git a/frontend/components/onboarding/Stepper.tsx b/frontend/components/onboarding/Stepper.tsx index 5b95d0105..575681bf2 100644 --- a/frontend/components/onboarding/Stepper.tsx +++ b/frontend/components/onboarding/Stepper.tsx @@ -11,9 +11,10 @@ export type Step = { interface StepperProps { steps: Step[] activeStep: number + align?: 'center' | 'left' } -export const Stepper = (props: StepperProps) => { +export const Stepper = ({ steps, activeStep, align = 'center' }: StepperProps) => { const ICON_WRAPPER_BASE = 'rounded-full transition duration-500 ease-in-out h-10 w-10 py-3 border text-center flex justify-center items-center' const LABEL_BASE = @@ -21,18 +22,18 @@ export const Stepper = (props: StepperProps) => { const THREAD_BASE = 'flex-auto border-t transition duration-500 ease-in-out' const stepIsComplete = (step: Step) => { - return step.index < props.activeStep + return step.index < activeStep } const stepIsActive = (step: Step) => { - return step.index === props.activeStep + return step.index === activeStep } return ( -
+
- {props.steps.map((step: Step, index: number) => ( + {steps.map((step: Step, index: number) => ( <>
{ {step.name}
- {index !== props.steps.length - 1 && ( + {index !== steps.length - 1 && (
{ ))}
-
-
- {props.steps[props.activeStep].title} -
-
- {props.steps[props.activeStep].description} +
+
+ {steps[activeStep].title}
+
{steps[activeStep].description}
) diff --git a/frontend/components/settings/organisation/PlanInfo.tsx b/frontend/components/settings/organisation/PlanInfo.tsx index e23556f25..2ad0abc62 100644 --- a/frontend/components/settings/organisation/PlanInfo.tsx +++ b/frontend/components/settings/organisation/PlanInfo.tsx @@ -129,7 +129,7 @@ export const PlanInfo = () => {
Compare plans
- {userCanUpdateBilling && } + {userCanUpdateBilling && }
)}
diff --git a/frontend/components/settings/organisation/UpsellDialog.tsx b/frontend/components/settings/organisation/UpsellDialog.tsx index 95cd35e1a..5937763ae 100644 --- a/frontend/components/settings/organisation/UpsellDialog.tsx +++ b/frontend/components/settings/organisation/UpsellDialog.tsx @@ -4,101 +4,113 @@ import { organisationContext } from '@/contexts/organisationContext' import { GetOrganisationPlan } from '@/graphql/queries/organisation/getOrganisationPlan.gql' import { isCloudHosted } from '@/utils/appConfig' import { useQuery } from '@apollo/client' -import { ReactNode, useContext, useRef } from 'react' +import { ReactNode, useContext, useRef, forwardRef, useImperativeHandle } from 'react' import dynamic from 'next/dynamic' import { userHasPermission } from '@/utils/access/permissions' import { EmptyState } from '@/components/common/EmptyState' import { FaBan } from 'react-icons/fa6' -export const UpsellDialog = ({ - title, - buttonLabel, - buttonVariant, -}: { +export type UpsellDialogHandle = { + openModal: () => void + closeModal: () => void +} + +interface UpsellDialogProps { title?: string buttonLabel?: ReactNode buttonVariant?: 'primary' | 'secondary' | 'outline' | 'danger' -}) => { - const { activeOrganisation } = useContext(organisationContext) +} - const userCanUpdateBilling = activeOrganisation - ? userHasPermission(activeOrganisation.role?.permissions, 'Billing', 'update') - : false +export const UpsellDialog = forwardRef( + ({ title, buttonLabel, buttonVariant }, ref) => { + const { activeOrganisation } = useContext(organisationContext) - const dialogRef = useRef<{ closeModal: () => void }>(null) + const userCanUpdateBilling = activeOrganisation + ? userHasPermission(activeOrganisation.role?.permissions, 'Billing', 'update') + : false - const closeModal = () => dialogRef?.current?.closeModal() + const dialogRef = useRef<{ openModal: () => void; closeModal: () => void }>(null) - // Dynamically import UpgradeDialog only if the app is cloud-hosted - const UpgradeDialog = isCloudHosted() ? dynamic(() => import('@/ee/billing/UpgradeDialog')) : null + const openModal = () => dialogRef.current?.openModal() + const closeModal = () => dialogRef.current?.closeModal() - const { data, loading } = useQuery(GetOrganisationPlan, { - variables: { organisationId: activeOrganisation?.id }, - skip: !activeOrganisation, - fetchPolicy: 'cache-and-network', - }) + useImperativeHandle(ref, () => ({ openModal, closeModal }), []) - if (!activeOrganisation || loading) return <> + // Dynamically import UpgradeDialog only if the app is cloud-hosted + const UpgradeDialog = isCloudHosted() + ? dynamic(() => import('@/ee/billing/UpgradeDialog')) + : null - return ( - {}} - ref={dialogRef} - > -
-
- Get access to all the features in Phase{' '} - {activeOrganisation.plan === ApiOrganisationPlanChoices.Fr - ? isCloudHosted() - ? 'Pro' - : 'Enterprise' - : 'Enterprise'} -
- {isCloudHosted() ? ( - UpgradeDialog && userCanUpdateBilling ? ( - - ) : ( - - -
- } - > - <> - - ) - ) : ( -
- Please contact us at{' '} - - info@phase.dev - {' '} - or get in touch via{' '} - - Slack - {' '} - to request an upgrade. + const { data, loading } = useQuery(GetOrganisationPlan, { + variables: { organisationId: activeOrganisation?.id }, + skip: !activeOrganisation, + fetchPolicy: 'cache-and-network', + }) + + if (!activeOrganisation || loading) return <> + + return ( + {}} + ref={dialogRef} + > +
+
+ Get access to all the features in Phase{' '} + {activeOrganisation.plan === ApiOrganisationPlanChoices.Fr + ? isCloudHosted() + ? 'Pro' + : 'Enterprise' + : 'Enterprise'}
- )} -
-
- ) -} + {isCloudHosted() ? ( + UpgradeDialog && userCanUpdateBilling ? ( + + ) : ( + + +
+ } + > + <> + + ) + ) : ( +
+ Please contact us at{' '} + + info@phase.dev + {' '} + or get in touch via{' '} + + Slack + {' '} + to request an upgrade. +
+ )} +
+ + ) + } +) + +UpsellDialog.displayName = 'UpsellDialog' diff --git a/frontend/components/syncing/AWS/AWSRegionPicker.tsx b/frontend/components/syncing/AWS/AWSRegionPicker.tsx index 6a285b3f4..ce143031b 100644 --- a/frontend/components/syncing/AWS/AWSRegionPicker.tsx +++ b/frontend/components/syncing/AWS/AWSRegionPicker.tsx @@ -32,7 +32,7 @@ export const AWSRegionPicker = (props: { onChange: (region: string) => void; val <>
- diff --git a/frontend/components/syncing/ProviderCredentialPicker.tsx b/frontend/components/syncing/ProviderCredentialPicker.tsx index 46859164f..67840d11a 100644 --- a/frontend/components/syncing/ProviderCredentialPicker.tsx +++ b/frontend/components/syncing/ProviderCredentialPicker.tsx @@ -46,17 +46,22 @@ export const ProviderCredentialPicker = (props: { : credentials const credentialMatchesFilter = - credential && providerFilter ? - (providerFilter === 'aws' ? - (credential.provider?.id === 'aws' || credential.provider?.id === 'aws_assume_role') : - credential.provider?.id === providerFilter - ) : true + credential && providerFilter + ? providerFilter === 'aws' + ? credential.provider?.id === 'aws' || credential.provider?.id === 'aws_assume_role' + : credential.provider?.id === providerFilter + : false // If no credential is selected, it doesn't match the filter useEffect(() => { - if (setDefault && filteredCredentials.length > 0 && !credentialMatchesFilter) + if ( + setDefault && + filteredCredentials.length > 0 && + (!credential || !credentialMatchesFilter) && + filteredCredentials[0] + ) { setCredential(filteredCredentials[0]) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [providerFilter, filteredCredentials, setDefault]) + } + }, [setDefault, filteredCredentials, credential, credentialMatchesFilter, setCredential]) const NewCredentialsLink = () => ( - {({ open }) => ( - <> - - -
- {credential?.name || 'Select credentials'} - + + {({ open }) => ( + <> + + +
-
-
- -
- {filteredCredentials.map((cred: ProviderCredentialsType) => ( - - {({ active, selected }) => ( -
- -
- {cred.name} -
-
+ > + {credential?.name || 'Select credentials'} + - ))} + /> +
+ + +
+ {filteredCredentials.map((cred: ProviderCredentialsType) => ( + + {({ active, selected }) => ( +
+ +
+ {cred.name} +
+
+ )} +
+ ))} -
- +
+ +
-
-
- - )} -
+ + + )} + +
) } diff --git a/frontend/ee/components/secrets/dynamic/AWSCredentials.tsx b/frontend/ee/components/secrets/dynamic/AWSCredentials.tsx new file mode 100644 index 000000000..356b1eeff --- /dev/null +++ b/frontend/ee/components/secrets/dynamic/AWSCredentials.tsx @@ -0,0 +1,99 @@ +import { DynamicSecretLeaseType, KeyMap } from '@/apollo/graphql' +import { Button } from '@/components/common/Button' +import CopyButton from '@/components/common/CopyButton' +import { Input } from '@/components/common/Input' +import clsx from 'clsx' +import { useState } from 'react' +import { FaRegEyeSlash, FaRegEye } from 'react-icons/fa6' + +export const AWSCredentials = ({ + lease, + keyMap, +}: { + lease: DynamicSecretLeaseType + keyMap: KeyMap[] +}) => { + const credentialKeyName = (credentialId: string) => + keyMap.find((key) => key.id === credentialId)?.keyName ?? credentialId + + const getInitialMaskedState = (credentialId: string) => { + const keyEntry = keyMap.find((key) => key.id === credentialId) + return keyEntry?.masked ?? true // default to masked if not specified + } + + const [reveal, setReveal] = useState({ + accessKeyId: !getInitialMaskedState('access_key_id'), + secretAccessKey: !getInitialMaskedState('secret_access_key'), + username: !getInitialMaskedState('username'), + }) + + const toggle = (k: keyof typeof reveal) => setReveal((s) => ({ ...s, [k]: !s[k] })) + + if (!lease.credentials) return <> + + return ( + <> +
+ {}} + readOnly + label={credentialKeyName('username')} + labelClassName="font-mono" + className={clsx( + 'cursor-text ph-no-capture font-mono', + reveal.username ? 'text-security-none' : 'text-security-disc' + )} + /> +
+ + +
+
+
+ {}} + readOnly + label={credentialKeyName('access_key_id')} + labelClassName="font-mono" + className={clsx( + 'cursor-text ph-no-capture font-mono', + reveal.accessKeyId ? 'text-security-none' : 'text-security-disc' + )} + /> +
+ + +
+
+ +
+ {}} + readOnly + label={credentialKeyName('secret_access_key')} + labelClassName="font-mono" + className={clsx( + 'cursor-text ph-no-capture font-mono', + reveal.secretAccessKey ? 'text-security-none' : 'text-security-disc' + )} + /> +
+ + +
+
+ + ) +} diff --git a/frontend/ee/components/secrets/dynamic/AppDynamicSecretRow.tsx b/frontend/ee/components/secrets/dynamic/AppDynamicSecretRow.tsx new file mode 100644 index 000000000..1feb2fa4c --- /dev/null +++ b/frontend/ee/components/secrets/dynamic/AppDynamicSecretRow.tsx @@ -0,0 +1,157 @@ +'use client' + +import { EnvironmentType, DynamicSecretType } from '@/apollo/graphql' + +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import clsx from 'clsx' +import { FaCheckCircle, FaChevronRight, FaExternalLinkAlt, FaTimesCircle } from 'react-icons/fa' +import { Disclosure, Transition } from '@headlessui/react' +import { FaBolt } from 'react-icons/fa6' + +type AppDynamicSecret = { + id: string + name: string + envs: { + env: EnvironmentType + dynamicSecret: DynamicSecretType | null + }[] +} + +export const AppDynamicSecretRow = ({ + appDynamicSecret, + isExpanded, + expand, + collapse, +}: { + appDynamicSecret: AppDynamicSecret + isExpanded: boolean + expand: (id: string) => void + collapse: (id: string) => void +}) => { + const pathname = usePathname() + + const tooltipText = (env: { env: EnvironmentType; dynamicSecret: DynamicSecretType | null }) => { + if (env.dynamicSecret === null) return `This dynamic secret is missing in ${env.env.name}` + else return 'This dynamic secret is present' + } + + const EnvDynamicSecret = ({ + envDynamicSecret, + }: { + envDynamicSecret: { + env: EnvironmentType + dynamicSecret: DynamicSecretType | null + } + }) => { + const { envDynamicSecret: envDS } = { envDynamicSecret } + + const EnvLabel = () => ( +
+
{envDS.env.name}
+ + +
+ ) + + return ( +
+ {envDS.dynamicSecret === null ? ( + missing + ) : ( + +
+ + + {/* Show key names from keyMap */} + {envDS.dynamicSecret.keyMap && envDS.dynamicSecret.keyMap.length > 0 && ( +
+ {envDS.dynamicSecret.keyMap.map((keyEntry, index) => ( + + {keyEntry?.keyName} + + ))} +
+ )} +
+ + )} +
+ ) + } + + return ( + <> + (isExpanded ? collapse(appDynamicSecret.id) : expand(appDynamicSecret.id))} + > + + + {appDynamicSecret.name} + + + {appDynamicSecret.envs.map((env) => ( + +
+ {env.dynamicSecret !== null ? ( + + ) : ( + + )} +
+ + ))} + + + {isExpanded && ( + +
+ {appDynamicSecret.envs.map((envDynamicSecret) => ( + + ))} +
+ + )} +
+ + ) +} diff --git a/frontend/ee/components/secrets/dynamic/CreateDynamicSecretDialog.tsx b/frontend/ee/components/secrets/dynamic/CreateDynamicSecretDialog.tsx new file mode 100644 index 000000000..edd58efa7 --- /dev/null +++ b/frontend/ee/components/secrets/dynamic/CreateDynamicSecretDialog.tsx @@ -0,0 +1,457 @@ +'use client' + +import { + forwardRef, + useCallback, + useContext, + useEffect, + useImperativeHandle, + useRef, + useState, +} from 'react' +import { + AwsConfigInput, + DynamicSecretProviderType, + EnvironmentType, + ProviderCredentialsType, + KeyMapInput, +} from '@/apollo/graphql' +import { GetDynamicSecretProviders } from '@/graphql/queries/secrets/dynamic/getProviders.gql' +import { CreateNewAWSDynamicSecret } from '@/graphql/mutations/environments/secrets/dynamic/createDynamicSecret.gql' +import { GetDynamicSecrets } from '@/graphql/queries/secrets/dynamic/getDynamicSecrets.gql' +import GenericDialog from '@/components/common/GenericDialog' +import { Input } from '@/components/common/Input' +import { Button } from '@/components/common/Button' +import { FaCogs } from 'react-icons/fa' +import { Step, Stepper } from '@/components/onboarding/Stepper' +import { ProviderCredentialPicker } from '@/components/syncing/ProviderCredentialPicker' +import { organisationContext } from '@/contexts/organisationContext' +import { toUpper } from 'lodash' +import { useMutation, useQuery } from '@apollo/client' +import { Card } from '@/components/common/Card' +import { ProviderIcon } from '@/components/syncing/ProviderIcon' +import { FaArrowRightLong } from 'react-icons/fa6' +import { MdOutlinePassword } from 'react-icons/md' +import { camelCase } from 'lodash' +import { toast } from 'react-toastify' +import { EnableSSEDialog } from '@/components/apps/EnableSSEDialog' +import { leaseTtlButtons, MINIMUM_LEASE_TTL } from '@/utils/dynamicSecrets' +import { Textarea } from '@/components/common/TextArea' +import { encryptAsymmetric } from '@/utils/crypto' + +type CreateDynamicSecretDialogRef = { + openModal: () => void + closeModal: () => void +} + +interface CreateDynamicSecretDialogProps { + environment: EnvironmentType + path: string +} + +export const CreateDynamicSecretDialog = forwardRef< + CreateDynamicSecretDialogRef, + CreateDynamicSecretDialogProps +>(({ environment, path }, ref) => { + const { activeOrganisation: organisation } = useContext(organisationContext) + + const { data } = useQuery(GetDynamicSecretProviders) + + const [createDynamicSecret] = useMutation(CreateNewAWSDynamicSecret) + + const dialogRef = useRef<{ closeModal: () => void; openModal: () => void }>(null) + + useImperativeHandle(ref, () => ({ + openModal: () => dialogRef.current?.openModal(), + closeModal: () => dialogRef.current?.closeModal(), + })) + + const [provider, setProvider] = useState(null) + const [activeStep, setActiveStep] = useState(0) + const [formData, setFormData] = useState({ + name: 'AWS IAM credentials', + description: '', + credential: null as ProviderCredentialsType | null, + config: { + usernameTemplate: '{{random}}', + iamPath: `/phase/`, + permissionBoundaryArn: undefined, + groups: '', + policyArns: '', + policyDocument: undefined, + } as AwsConfigInput, + keyMap: [] as KeyMapInput[], + defaultTTL: '3600', + maxTTL: '86400', + }) + + const handleCredentialChange = useCallback((cred: ProviderCredentialsType) => { + setFormData((prev) => ({ ...prev, credential: cred })) + }, []) + + const reset = () => { + setProvider(null) + setActiveStep(0) + setFormData({ + name: 'AWS IAM credentials', + description: '', + credential: null as ProviderCredentialsType | null, + config: { + usernameTemplate: '{{random}}', + iamPath: `/phase/`, + permissionBoundaryArn: undefined, + groups: '', + policyArns: '', + policyDocument: undefined, + } as AwsConfigInput, + keyMap: [] as KeyMapInput[], + defaultTTL: '3600', + maxTTL: '86400', + }) + } + + useEffect(() => { + if (!provider) return + + const initialKeyMap: KeyMapInput[] = provider.credentials.map((cred: any) => { + return { + id: cred.id, + keyName: cred.default_key_name || '', + } + }) + + setFormData((prev) => ({ + ...prev, + config: { + ...prev.config, + iamPath: `/phase/${organisation?.name}/${environment.app.name}/${environment.name}${path}`, + }, + keyMap: initialKeyMap, + })) + }, [environment.app.name, environment.name, organisation?.name, path, provider]) + + const steps: Step[] = [ + { + index: 0, + name: 'Provider', + icon: , + title: 'Provider Config', + description: 'Enter provider-specific configuration', + }, + { + index: 1, + name: 'Config', + icon: , + title: 'Config', + description: 'Define how secrets are created and mapped to outputs', + }, + ] + + const nextStep = () => setActiveStep((s) => Math.min(s + 1, steps.length - 1)) + const prevStep = () => setActiveStep((s) => Math.max(s - 1, 0)) + + const updateConfig = (key: string, value: string) => { + setFormData((prev) => ({ + ...prev, + config: { ...prev.config, [key]: value }, + })) + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + if (activeStep < steps.length - 1) { + nextStep() + } else { + // Ensure credentials are selected + if (formData.credential === null) { + toast.error('Please select authentication credentials!') + return false + } + + // Validate username template + const template = formData.config.usernameTemplate ?? '' + const randomPlaceholderRegex = /\{\{\s*random\s*\}\}/i + + if (!randomPlaceholderRegex.test(template)) { + toast.error('Username template must include {{random}} to generate unique usernames.') + return false + } + + if (parseInt(formData.defaultTTL, 10) > parseInt(formData.maxTTL, 10)) { + toast.error('Default TTL must be less than or equal to Max TTL') + return false + } + + if (parseInt(formData.maxTTL, 10) <= MINIMUM_LEASE_TTL) { + toast.error(`Max TTL must be greater than ${MINIMUM_LEASE_TTL} seconds`) + return false + } + + // Encrypt each keyName in keyMap + const encryptedKeyMap = await Promise.all( + formData.keyMap.map(async (key) => ({ + ...key, + keyName: key.keyName + ? await encryptAsymmetric(key.keyName, environment.identityKey) + : key.keyName, + })) + ) + + await createDynamicSecret({ + variables: { + organisationId: organisation?.id, + environmentId: environment.id, + path, + name: formData.name, + description: formData.description, + defaultTtl: parseInt(formData.defaultTTL, 10), + maxTtl: parseInt(formData.maxTTL, 10), + authenticationId: formData.credential?.id ?? null, + config: formData.config, + keyMap: encryptedKeyMap, + }, + refetchQueries: [{ query: GetDynamicSecrets, variables: { orgId: organisation?.id } }], + }) + + toast.success('Created new dynamic secret') + reset() + dialogRef.current?.closeModal() + } + } + + const ttlButtons = leaseTtlButtons + + const ProviderMenu = () => ( +
+ {data?.dynamicSecretProviders?.map((provider: DynamicSecretProviderType) => ( +
setProvider(provider)}> + +
+
+
+ +
+
+
+ {provider.name} +
+
+ Set up a dynamic secret for {provider.name} +
+
+
+
+
+
+ ))} +
+ ) + + return ( + + {!environment.app.sseEnabled ? ( +
+
+
+ Server-side encryption (SSE) +
+
+ Server-side encryption is required to use Dynamic Secrets. Click the button below to + enable SSE. +
+
+ +
+ +
+
+ ) : ( +
+
Configure a new dynamic secret
+ + {!provider && } + {provider && ( +
+ + + {/* Step 2: Config */} + {activeStep === 0 && ( +
+
+ +
+
+ {provider.configMap.map( + (field: { + id: keyof AwsConfigInput + label: string + help_text?: string + required?: boolean + default?: any + }) => ( +