diff --git a/backend/api/auth.py b/backend/api/auth.py index 20df3dd35..dcd126680 100644 --- a/backend/api/auth.py +++ b/backend/api/auth.py @@ -1,18 +1,22 @@ from api.utils.rest import ( get_org_member_from_user_token, + get_service_account_from_token, get_service_token, get_token_type, token_is_expired_or_deleted, ) from api.models import Environment -from api.utils.access.permissions import user_can_access_environment +from api.utils.access.permissions import ( + service_account_can_access_environment, + user_can_access_environment, +) from rest_framework import authentication, exceptions class PhaseTokenAuthentication(authentication.BaseAuthentication): def authenticate(self, request): - token_types = ["User", "Service"] + token_types = ["User", "Service", "ServiceAccount"] auth_token = request.headers.get("Authorization") @@ -24,7 +28,14 @@ def authenticate(self, request): if token_type not in token_types: raise exceptions.AuthenticationFailed("Invalid token") - auth = {"token": auth_token, "org_member": None, "service_token": None} + auth = { + "token": auth_token, + "auth_type": token_type, + "org_member": None, + "service_token": None, + "service_account": None, + "service_account_token": None, + } if token_is_expired_or_deleted(auth_token): raise exceptions.AuthenticationFailed("Token expired or deleted") @@ -62,9 +73,27 @@ def authenticate(self, request): except Exception as ex: raise exceptions.NotFound("User not found") - else: + elif token_type == "Service": service_token = get_service_token(auth_token) auth["service_token"] = service_token user = service_token.created_by.user + if token_type == "ServiceAccount": + + try: + service_token = get_service_token(auth_token) + service_account = get_service_account_from_token(auth_token) + user = service_token.created_by.user + auth["service_account"] = service_account + auth["service_account_token"] = service_token + + if not service_account_can_access_environment( + service_account.id, env.id + ): + raise exceptions.AuthenticationFailed( + "Service account cannot access this environment" + ) + except Exception as ex: + raise exceptions.NotFound("Service account not found") + return (user, auth) diff --git a/backend/api/migrations/0085_serviceaccount_environmentkey_paths_and_more.py b/backend/api/migrations/0085_serviceaccount_environmentkey_paths_and_more.py new file mode 100644 index 000000000..a2c4973ff --- /dev/null +++ b/backend/api/migrations/0085_serviceaccount_environmentkey_paths_and_more.py @@ -0,0 +1,58 @@ +# Generated by Django 4.2.15 on 2024-10-20 09:12 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0084_auto_20241008_0708'), + ] + + operations = [ + migrations.CreateModel( + name='ServiceAccount', + fields=[ + ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=255)), + ('identity_key', models.CharField(blank=True, max_length=256, null=True)), + ('server_wrapped_keyring', models.TextField(null=True)), + ('server_wrapped_recovery', models.TextField(null=True)), + ('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)), + ('apps', models.ManyToManyField(related_name='apps', to='api.app')), + ('organisation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.organisation')), + ('role', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.role')), + ], + ), + migrations.AddField( + model_name='environmentkey', + name='paths', + field=models.TextField(blank=True, null=True), + ), + migrations.CreateModel( + name='ServiceAccountHandler', + fields=[ + ('id', models.TextField(default=uuid.uuid4, primary_key=True, serialize=False)), + ('wrapped_keyring', models.TextField()), + ('wrapped_recovery', models.TextField()), + ('created_at', models.DateTimeField(auto_now_add=True, null=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('service_account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.serviceaccount')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.organisationmember')), + ], + ), + migrations.AddField( + model_name='environmentkey', + name='service_account', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.serviceaccount'), + ), + migrations.AddField( + model_name='servicetoken', + name='service_account', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.serviceaccount'), + ), + ] diff --git a/backend/api/migrations/0086_remove_servicetoken_service_account_and_more.py b/backend/api/migrations/0086_remove_servicetoken_service_account_and_more.py new file mode 100644 index 000000000..32b227646 --- /dev/null +++ b/backend/api/migrations/0086_remove_servicetoken_service_account_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.15 on 2024-10-22 07:29 + +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0085_serviceaccount_environmentkey_paths_and_more'), + ] + + operations = [ + migrations.RemoveField( + model_name='servicetoken', + name='service_account', + ), + migrations.CreateModel( + name='ServiceAccountToken', + fields=[ + ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('name', models.CharField(max_length=64)), + ('identity_key', models.CharField(max_length=256)), + ('token', models.CharField(max_length=64)), + ('wrapped_key_share', models.CharField(max_length=406)), + ('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)), + ('expires_at', models.DateTimeField(null=True)), + ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.organisationmember')), + ('service_account', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.serviceaccount')), + ], + ), + ] diff --git a/backend/api/migrations/0087_alter_serviceaccount_apps.py b/backend/api/migrations/0087_alter_serviceaccount_apps.py new file mode 100644 index 000000000..5441358d6 --- /dev/null +++ b/backend/api/migrations/0087_alter_serviceaccount_apps.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.15 on 2024-10-24 12:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0086_remove_servicetoken_service_account_and_more'), + ] + + operations = [ + migrations.AlterField( + model_name='serviceaccount', + name='apps', + field=models.ManyToManyField(related_name='service_accounts', to='api.app'), + ), + ] diff --git a/backend/api/migrations/0088_secretevent_service_account.py b/backend/api/migrations/0088_secretevent_service_account.py new file mode 100644 index 000000000..029d05ae5 --- /dev/null +++ b/backend/api/migrations/0088_secretevent_service_account.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.15 on 2024-10-28 08:43 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0087_alter_serviceaccount_apps'), + ] + + operations = [ + migrations.AddField( + model_name='secretevent', + name='service_account', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.serviceaccount'), + ), + ] diff --git a/backend/api/migrations/0089_alter_serviceaccounthandler_service_account.py b/backend/api/migrations/0089_alter_serviceaccounthandler_service_account.py new file mode 100644 index 000000000..c5b7e28d8 --- /dev/null +++ b/backend/api/migrations/0089_alter_serviceaccounthandler_service_account.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.15 on 2024-10-30 07:10 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0088_secretevent_service_account'), + ] + + operations = [ + migrations.AlterField( + model_name='serviceaccounthandler', + name='service_account', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='handlers', to='api.serviceaccount'), + ), + ] diff --git a/backend/api/migrations/0090_alter_serviceaccount_organisation.py b/backend/api/migrations/0090_alter_serviceaccount_organisation.py new file mode 100644 index 000000000..6c0911acf --- /dev/null +++ b/backend/api/migrations/0090_alter_serviceaccount_organisation.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.15 on 2024-10-30 07:11 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0089_alter_serviceaccounthandler_service_account'), + ] + + operations = [ + migrations.AlterField( + model_name='serviceaccount', + name='organisation', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='service_accounts', to='api.organisation'), + ), + ] diff --git a/backend/api/migrations/0091_add_managed_manager_service_roles.py b/backend/api/migrations/0091_add_managed_manager_service_roles.py new file mode 100644 index 000000000..a1f157709 --- /dev/null +++ b/backend/api/migrations/0091_add_managed_manager_service_roles.py @@ -0,0 +1,30 @@ +# Generated by Django 4.2.15 on 2024-11-04 08:36 + +from django.db import migrations +from api.utils.access.roles import default_roles + + +def add_default_roles(apps, schema_editor): + Organisation = apps.get_model("api", "Organisation") + Role = apps.get_model("api", "Role") + OrganisationMember = apps.get_model("api", "OrganisationMember") + + # Create default roles for each organisation + for organisation in Organisation.objects.all(): + for role_name, _ in default_roles.items(): + Role.objects.get_or_create( + name=role_name, + organisation=organisation, + is_default=True, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0090_alter_serviceaccount_organisation"), + ] + + operations = [ + migrations.RunPython(add_default_roles), + ] diff --git a/backend/api/migrations/0092_secretevent_service_account_token.py b/backend/api/migrations/0092_secretevent_service_account_token.py new file mode 100644 index 000000000..a0dacd704 --- /dev/null +++ b/backend/api/migrations/0092_secretevent_service_account_token.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.15 on 2024-11-13 14:47 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0091_add_managed_manager_service_roles'), + ] + + operations = [ + migrations.AddField( + model_name='secretevent', + name='service_account_token', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.serviceaccounttoken'), + ), + ] diff --git a/backend/api/migrations/0093_merge_20241116_0744.py b/backend/api/migrations/0093_merge_20241116_0744.py new file mode 100644 index 000000000..80596b522 --- /dev/null +++ b/backend/api/migrations/0093_merge_20241116_0744.py @@ -0,0 +1,14 @@ +# Generated by Django 4.2.15 on 2024-11-16 07:44 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('api', '0086_alter_environmentsync_service'), + ('api', '0092_secretevent_service_account_token'), + ] + + operations = [ + ] diff --git a/backend/api/models.py b/backend/api/models.py index 1918af683..193d3898a 100644 --- a/backend/api/models.py +++ b/backend/api/models.py @@ -12,10 +12,10 @@ from api.services import Providers, ServiceConfig from api.tasks import trigger_sync_tasks from backend.quotas import ( + can_add_account, can_add_app, can_add_environment, can_add_service_token, - can_add_user, ) @@ -215,10 +215,52 @@ def delete(self, *args, **kwargs): self.save() +class ServiceAccountManager(models.Manager): + def create(self, *args, **kwargs): + organisation = kwargs.get("organisation") + if not can_add_account(organisation): + raise ValueError("Cannot add more accounts to this organisation's plan.") + return super().create(*args, **kwargs) + + +class ServiceAccount(models.Model): + id = models.TextField(default=uuid4, primary_key=True, editable=False) + name = models.CharField(max_length=255) + organisation = models.ForeignKey( + Organisation, on_delete=models.CASCADE, related_name="service_accounts" + ) + role = models.ForeignKey( + Role, + on_delete=models.SET_NULL, + null=True, + blank=True, + ) + apps = models.ManyToManyField(App, related_name="service_accounts") + identity_key = models.CharField(max_length=256, null=True, blank=True) + server_wrapped_keyring = models.TextField(null=True) + server_wrapped_recovery = models.TextField(null=True) + created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True) + updated_at = models.DateTimeField(auto_now=True) + deleted_at = models.DateTimeField(null=True, blank=True) + objects = ServiceAccountManager() + + +class ServiceAccountHandler(models.Model): + id = models.TextField(default=uuid4, primary_key=True) + service_account = models.ForeignKey( + ServiceAccount, on_delete=models.CASCADE, related_name="handlers" + ) + user = models.ForeignKey(OrganisationMember, on_delete=models.CASCADE) + wrapped_keyring = models.TextField() + wrapped_recovery = models.TextField() + created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True) + updated_at = models.DateTimeField(auto_now=True) + + class OrganisationMemberInviteManager(models.Manager): def create(self, *args, **kwargs): organisation = kwargs.get("organisation") - if not can_add_user(organisation): + if not can_add_account(organisation): raise ValueError("Cannot add more users to this organisation's plan.") return super().create(*args, **kwargs) @@ -299,6 +341,10 @@ class EnvironmentKey(models.Model): user = models.ForeignKey( OrganisationMember, on_delete=models.CASCADE, blank=True, null=True ) + service_account = models.ForeignKey( + ServiceAccount, on_delete=models.CASCADE, blank=True, null=True + ) + paths = models.TextField(blank=True, null=True) identity_key = models.CharField(max_length=256) wrapped_seed = models.CharField(max_length=256) wrapped_salt = models.CharField(max_length=256) @@ -429,6 +475,22 @@ class ServiceToken(models.Model): objects = ServiceTokenManager() +class ServiceAccountToken(models.Model): + id = models.TextField(default=uuid4, primary_key=True, editable=False) + service_account = models.ForeignKey(ServiceAccount, on_delete=models.CASCADE) + name = models.CharField(max_length=64) + identity_key = models.CharField(max_length=256) + token = models.CharField(max_length=64) + wrapped_key_share = models.CharField(max_length=406) + created_by = models.ForeignKey( + OrganisationMember, on_delete=models.CASCADE, blank=True, null=True + ) + 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) + expires_at = models.DateTimeField(null=True) + + class UserToken(models.Model): id = models.TextField(default=uuid4, primary_key=True, editable=False) user = models.ForeignKey( @@ -517,6 +579,12 @@ class SecretEvent(models.Model): service_token = models.ForeignKey( ServiceToken, on_delete=models.SET_NULL, blank=True, null=True ) + service_account = models.ForeignKey( + ServiceAccount, on_delete=models.SET_NULL, blank=True, null=True + ) + service_account_token = models.ForeignKey( + ServiceAccountToken, on_delete=models.SET_NULL, blank=True, null=True + ) key = models.TextField() key_digest = models.TextField() value = models.TextField() diff --git a/backend/api/serializers.py b/backend/api/serializers.py index c1c607384..7fb3cec47 100644 --- a/backend/api/serializers.py +++ b/backend/api/serializers.py @@ -225,6 +225,65 @@ def to_representation(self, instance): return representation +class ServiceAccountTokenSerializer(serializers.ModelSerializer): + apps = EnvironmentKeySerializer(many=True, read_only=True) + + # New field 'userId' + account_id = serializers.UUIDField(source="service_account.id", read_only=True) + + # New field 'offline_enabled' with default value False + offline_enabled = serializers.BooleanField(default=False, read_only=True) + + organisation = OrganisationSerializer( + source="service_account.organisation", read_only=True + ) + + class Meta: + model = UserToken + fields = [ + "wrapped_key_share", + "account_id", + "offline_enabled", + "apps", + "organisation", + ] + + def to_representation(self, instance): + representation = super().to_representation(instance) + + # Filter environment_keys to include only those associated with the same user + service_account = instance.service_account + + if service_account is not None: + environment_keys = EnvironmentKey.objects.filter( + service_account=service_account, environment__app__deleted_at=None + ) + apps = [] + for key in environment_keys: + + serializer = EnvironmentKeySerializer(key) + index = find_index_by_id(apps, key.environment.app.id) + + app_data = { + "id": key.environment.app.id, + "name": key.environment.app.name, + "encryption": "E2E", + } + + if key.environment.app.sse_enabled: + app_data["encryption"] = "SSE" + + if index == -1: + app_data["environment_keys"] = [serializer.data] + apps.append(app_data) + else: + apps[index]["environment_keys"].append(serializer.data) + + representation["apps"] = apps + + return representation + + class LockboxSerializer(serializers.ModelSerializer): class Meta: model = Lockbox diff --git a/backend/api/utils/access/permissions.py b/backend/api/utils/access/permissions.py index 36fa45793..8db6e383e 100644 --- a/backend/api/utils/access/permissions.py +++ b/backend/api/utils/access/permissions.py @@ -3,9 +3,15 @@ Environment, EnvironmentKey, OrganisationMember, + ServiceAccount, ) from api.utils.access.roles import default_roles +# middlewares.py +from django.http import JsonResponse +from django.utils.deprecation import MiddlewareMixin + + admin_roles = ["Owner", "Admin"] @@ -40,19 +46,39 @@ def user_can_access_environment(user_id, env_id): ).exists() +def service_account_can_access_environment(account_id, env_id): + env = Environment.objects.get(id=env_id) + service_account = ServiceAccount.objects.get( + organisation=env.app.organisation, id=account_id, deleted_at=None + ) + return EnvironmentKey.objects.filter( + service_account=service_account, environment_id=env_id + ).exists() + + def member_can_access_org(member_id, org_id): return OrganisationMember.objects.filter( id=member_id, organisation_id=org_id, deleted_at=None ).exists() -def user_has_permission(user, action, resource, organisation, is_app_resource=False): +def user_has_permission( + account, + action, + resource, + organisation, + is_app_resource=False, + is_service_account=False, +): """Check if the user has the specified permission for a resource in an organization.""" try: # Get the user's membership in the organization - org_member = OrganisationMember.objects.get( - user=user, organisation=organisation, deleted_at=None - ) + if is_service_account: + org_member = account + else: + org_member = OrganisationMember.objects.get( + user=account, organisation=organisation, deleted_at=None + ) role = org_member.role if not role: return False # No role assigned, hence no permissions diff --git a/backend/api/utils/access/roles.py b/backend/api/utils/access/roles.py index 4a26b5e8a..408ec9275 100644 --- a/backend/api/utils/access/roles.py +++ b/backend/api/utils/access/roles.py @@ -2,7 +2,7 @@ "Owner": { "meta": { "version": 1, - "description": "The organisation owner. This role is unique to a single user, and grants access to all resources and actions.", + "description": "The organisation owner, limited to a single user, with full access to all resources and actions.", }, "permissions": { "Organisation": ["create", "read", "update", "delete"], @@ -10,6 +10,7 @@ "Apps": ["create", "read", "update", "delete"], "Members": ["create", "read", "update", "delete"], "ServiceAccounts": ["create", "read", "update", "delete"], + "ServiceAccountTokens": ["create", "read", "update", "delete"], "Roles": ["create", "read", "update", "delete"], "IntegrationCredentials": ["create", "read", "update", "delete"], }, @@ -20,6 +21,7 @@ "Logs": ["create", "read", "update", "delete"], "Tokens": ["create", "read", "update", "delete"], "Members": ["create", "read", "update", "delete"], + "ServiceAccounts": ["create", "read", "update", "delete"], "Integrations": ["create", "read", "update", "delete"], "EncryptionMode": ["read", "update"], }, @@ -28,7 +30,7 @@ "Admin": { "meta": { "version": 1, - "description": "Admin users have access to most resources and permissions, and have global access to all Apps and Environments.", + "description": "Administrative users with broad access to resources and global access to all Apps and Environments.", }, "permissions": { "Organisation": ["read", "update"], @@ -36,6 +38,7 @@ "Apps": ["create", "read", "update", "delete"], "Members": ["create", "read", "update", "delete"], "ServiceAccounts": ["create", "read", "update", "delete"], + "ServiceAccountTokens": ["create", "read", "update", "delete"], "Roles": ["create", "read", "update", "delete"], "IntegrationCredentials": ["create", "read", "update", "delete"], }, @@ -46,15 +49,44 @@ "Logs": ["create", "read", "update", "delete"], "Tokens": ["create", "read", "update", "delete"], "Members": ["create", "read", "update", "delete"], + "ServiceAccounts": ["create", "read", "update", "delete"], "Integrations": ["create", "read", "update", "delete"], "EncryptionMode": ["read", "update"], }, "global_access": True, }, + "Manager": { + "meta": { + "version": 1, + "description": "Management users with broad access to environments, secrets, and service accounts at the organisation level. Requires explicit access to Apps and Environments.", + }, + "permissions": { + "Organisation": ["read"], + "Billing": [], + "Apps": ["create", "read", "update", "delete"], + "Members": ["create", "read", "update", "delete"], + "ServiceAccounts": ["create", "read", "update", "delete"], + "ServiceAccountTokens": ["create", "read", "update", "delete"], + "Roles": ["create", "read", "update", "delete"], + "IntegrationCredentials": ["create", "read", "update", "delete"], + }, + "app_permissions": { + "Environments": ["read", "create", "update"], + "Secrets": ["create", "read", "update", "delete"], + "Lockbox": ["create", "read", "update", "delete"], + "Logs": ["create", "read", "update", "delete"], + "Tokens": ["create", "read", "update", "delete"], + "Members": ["create", "read", "update", "delete"], + "ServiceAccounts": ["create", "read", "update", "delete"], + "Integrations": ["create", "read", "update", "delete"], + "EncryptionMode": ["read", "update"], + }, + "global_access": False, + }, "Developer": { "meta": { "version": 1, - "description": "Developers have limited permissions to at the organisation level, and must be given explicit access to Apps and Environments.", + "description": "Development users with limited organisation-level permissions. Requires explicit access to Apps and Environments.", }, "permissions": { "Organisation": [], @@ -62,6 +94,7 @@ "Apps": ["read"], "Members": ["read"], "ServiceAccounts": [], + "ServiceAccountTokens": [], "Roles": ["read"], "IntegrationCredentials": [ "create", @@ -76,9 +109,38 @@ "Logs": ["read"], "Tokens": ["read", "create"], "Members": ["read"], + "ServiceAccounts": ["create"], "Integrations": ["create", "read", "update", "delete"], "EncryptionMode": ["read", "update"], }, "global_access": False, }, + "Service": { + "meta": { + "version": 1, + "description": "Default role for Service Accounts, providing programmatic access to secrets without access to other organisation or app resources.", + }, + "permissions": { + "Organisation": [], + "Billing": [], + "Apps": ["create", "read", "update"], + "Members": ["read"], + "ServiceAccounts": ["read"], + "ServiceAccountTokens": ["read"], + "Roles": ["read"], + "IntegrationCredentials": ["read"], + }, + "app_permissions": { + "Environments": ["read", "create", "update", "delete"], + "Secrets": ["create", "read", "update", "delete"], + "Lockbox": [], + "Logs": [], + "Tokens": [], + "Members": ["read"], + "ServiceAccounts": ["read"], + "Integrations": ["read"], + "EncryptionMode": ["read"], + }, + "global_access": False, + }, } diff --git a/backend/api/utils/audit_logging.py b/backend/api/utils/audit_logging.py index 9f315841a..e70d3d480 100644 --- a/backend/api/utils/audit_logging.py +++ b/backend/api/utils/audit_logging.py @@ -4,11 +4,22 @@ def log_secret_event( - secret, event_type, user, service_token=None, ip_address=None, user_agent=None + secret, + event_type, + user=None, + service_token=None, + service_account_token=None, + ip_address=None, + user_agent=None, ): """ Utility function to log secret events. """ + + service_account = None + if service_account_token is not None: + service_account = service_account_token.service_account + event = SecretEvent.objects.create( secret=secret, environment=secret.environment, @@ -16,6 +27,8 @@ def log_secret_event( path=secret.path, user=user, service_token=service_token, + service_account=service_account, + service_account_token=service_account_token, key=secret.key, key_digest=secret.key_digest, value=secret.value, diff --git a/backend/api/utils/organisations.py b/backend/api/utils/organisations.py index 444d2226f..f4949ce34 100644 --- a/backend/api/utils/organisations.py +++ b/backend/api/utils/organisations.py @@ -1,4 +1,4 @@ -from api.models import OrganisationMember, OrganisationMemberInvite +from api.models import OrganisationMember, OrganisationMemberInvite, ServiceAccount from django.utils import timezone @@ -10,6 +10,9 @@ def get_organisation_seats(organisation): + OrganisationMemberInvite.objects.filter( organisation=organisation, valid=True, expires_at__gte=timezone.now() ).count() + + ServiceAccount.objects.filter( + organisation=organisation, deleted_at=None + ).count() ) return seats diff --git a/backend/api/utils/rest.py b/backend/api/utils/rest.py index 3e5efa7d8..d133efa51 100644 --- a/backend/api/utils/rest.py +++ b/backend/api/utils/rest.py @@ -1,7 +1,15 @@ -from api.models import EnvironmentToken, ServiceToken, UserToken +from api.models import EnvironmentToken, ServiceAccountToken, ServiceToken, UserToken from django.utils import timezone import base64 +# Map HTTP methods to permission actions +METHOD_TO_ACTION = { + "GET": "read", + "POST": "create", + "PUT": "update", + "DELETE": "delete", +} + def get_client_ip(request): x_forwarded_for = request.META.get("HTTP_X_FORWARDED_FOR") @@ -49,13 +57,30 @@ def get_org_member_from_user_token(auth_token): return False +def get_service_account_from_token(auth_token): + token = auth_token.split(" ")[2] + + if not token: + return False + + try: + sa_token = ServiceAccountToken.objects.get(token=token) + return sa_token.service_account + except Exception as ex: + return False + + def get_service_token(auth_token): prefix, token_type, token_value = auth_token.split(" ") if token_type == "User": return None - return ServiceToken.objects.get(token=token_value) + elif token_type == "Service": + return ServiceToken.objects.get(token=token_value) + + elif token_type == "ServiceAccount": + return ServiceAccountToken.objects.get(token=token_value) def token_is_expired_or_deleted(auth_token): @@ -63,8 +88,10 @@ def token_is_expired_or_deleted(auth_token): if token_type == "User": token = UserToken.objects.get(token=token_value) - else: + elif token_type == "Service": token = ServiceToken.objects.get(token=token_value) + elif token_type == "ServiceAccount": + token = ServiceAccountToken.objects.get(token=token_value) return token.deleted_at is not None or ( token.expires_at is not None and token.expires_at < timezone.now() diff --git a/backend/api/views/auth.py b/backend/api/views/auth.py index 7594e6d21..773c64858 100644 --- a/backend/api/views/auth.py +++ b/backend/api/views/auth.py @@ -4,10 +4,11 @@ import jwt import os from api.serializers import ( + ServiceAccountTokenSerializer, ServiceTokenSerializer, UserTokenSerializer, ) -from api.models import ServiceToken, UserToken, CustomUser +from api.models import ServiceAccountToken, ServiceToken, UserToken, CustomUser from api.emails import send_login_email from api.utils.syncing.auth import store_oauth_token from backend.api.notifier import notify_slack @@ -122,7 +123,7 @@ def complete_login(self, request, app, token, response, **kwargs): raise OAuth2Error("Invalid id_token") from e login = self.get_provider().sociallogin_from_response(request, identity_data) email = identity_data.get("email") - full_name = identity_data.get("name") # Get the full name from the id_token + full_name = identity_data.get("name") # Get the full name from the id_token if CLOUD_HOSTED and not CustomUser.objects.filter(email=email).exists(): try: @@ -161,13 +162,12 @@ def complete_login(self, request, app, token, **kwargs): resp.raise_for_status() extra_data = resp.json() if app_settings.QUERY_EMAIL and not extra_data.get("email"): - extra_data["email"] = self.get_email(headers) + extra_data["email"] = self.get_email(headers) email = extra_data["email"] if CLOUD_HOSTED and not CustomUser.objects.filter(email=email).exists(): - - + try: # Notify Slack notify_slack(f"New user signup: {email}") @@ -175,7 +175,7 @@ def complete_login(self, request, app, token, **kwargs): print(f"Error notifying Slack: {e}") try: - full_name = extra_data.get("name", email.split('@')[0]) + full_name = extra_data.get("name", email.split("@")[0]) send_login_email(request, email, full_name, "GitHub") except Exception as e: print(f"Error sending email: {e}") @@ -268,9 +268,15 @@ def service_token_kms(request): token = auth_token.split(" ")[2] - service_token = ServiceToken.objects.get(token=token) + token_type = get_token_type(auth_token) - serializer = ServiceTokenSerializer(service_token) + if token_type == "Service": + service_token = ServiceToken.objects.get(token=token) + serializer = ServiceTokenSerializer(service_token) + + elif token_type == "ServiceAccount": + service_token = ServiceAccountToken.objects.get(token=token) + serializer = ServiceAccountTokenSerializer(service_token) return Response(serializer.data, status=status.HTTP_200_OK) @@ -285,7 +291,7 @@ def secrets_tokens(request): token_type = get_token_type(auth_token) - if token_type == "Service": + if token_type == "Service" or token_type == "ServiceAccount": return service_token_kms(request) elif token_type == "User": return user_token_kms(request) diff --git a/backend/api/views/secrets.py b/backend/api/views/secrets.py index 73fc3a790..75b807943 100644 --- a/backend/api/views/secrets.py +++ b/backend/api/views/secrets.py @@ -5,7 +5,6 @@ Secret, SecretEvent, SecretTag, - ServerEnvironmentKey, ) from api.serializers import ( SecretSerializer, @@ -18,22 +17,22 @@ get_environment_keys, ) from api.utils.access.permissions import ( - user_can_access_environment, user_has_permission, ) from api.utils.audit_logging import log_secret_event from api.utils.crypto import encrypt_asymmetric, validate_encrypted_string from api.utils.rest import ( + METHOD_TO_ACTION, get_resolver_request_meta, ) import json 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 django.views.decorators.csrf import csrf_exempt from django.http import JsonResponse from django.utils import timezone from djangorestframework_camel_case.render import ( @@ -45,32 +44,44 @@ class E2EESecretsView(APIView): authentication_classes = [PhaseTokenAuthentication] permission_classes = [IsAuthenticated] - @csrf_exempt - def dispatch(self, request, *args): - return super(E2EESecretsView, self).dispatch(request, *args) + def initial(self, request, *args, **kwargs): + super().initial(request, *args, **kwargs) - def get(self, request): + # 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}") - env_id = request.headers["environment"] - env = Environment.objects.get(id=env_id) - if not env.id: - return JsonResponse({"error": "Environment doesn't exist"}, status=404) + # 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 request.auth["org_member"]: if not user_has_permission( - request.auth["org_member"].user, - "read", + account, + action, "Secrets", - env.app.organisation, + organisation, True, + request.auth.get("service_account") is not None, ): - return JsonResponse( - { - "error": "You don't have permission to read secrets in this environment" - }, - status=403, + raise PermissionDenied( + f"You don't have permission to {action} secrets in this environment." ) + def get(self, request, *args, **kwargs): + + env_id = request.headers["environment"] + env = Environment.objects.get(id=env_id) + if not env.id: + return JsonResponse({"error": "Environment doesn't exist"}, status=404) + ip_address, user_agent = get_resolver_request_meta(request) secrets_filter = {"environment": env, "deleted_at": None} @@ -98,6 +109,7 @@ def get(self, request): SecretEvent.READ, request.auth["org_member"], request.auth["service_token"], + request.auth["service_account_token"], ip_address, user_agent, ) @@ -108,28 +120,13 @@ def get(self, request): return Response(serializer.data, status=status.HTTP_200_OK) - def post(self, request): + def post(self, request, *args, **kwargs): env_id = request.headers["environment"] env = Environment.objects.get(id=env_id) if not env: return JsonResponse({"error": "Environment doesn't exist"}, status=404) - if request.auth["org_member"]: - if not user_has_permission( - request.auth["org_member"].user, - "create", - "Secrets", - env.app.organisation, - True, - ): - return JsonResponse( - { - "error": "You don't have permission to create secrets in this environment" - }, - status=403, - ) - request_body = json.loads(request.body) ip_address, user_agent = get_resolver_request_meta(request) @@ -183,6 +180,7 @@ def post(self, request): SecretEvent.CREATE, request.auth["org_member"], request.auth["service_token"], + request.auth["service_account_token"], ip_address, user_agent, ) @@ -197,28 +195,13 @@ def post(self, request): return Response(status=status.HTTP_200_OK) - def put(self, request): + def put(self, request, *args, **kwargs): env_id = request.headers["environment"] env = Environment.objects.get(id=env_id) if not env: return JsonResponse({"error": "Environment doesn't exist"}, status=404) - if request.auth["org_member"]: - if not user_has_permission( - request.auth["org_member"].user, - "update", - "Secrets", - env.app.organisation, - True, - ): - return JsonResponse( - { - "error": "You don't have permission to update secrets in this environment" - }, - status=403, - ) - request_body = json.loads(request.body) ip_address, user_agent = get_resolver_request_meta(request) @@ -278,6 +261,7 @@ def put(self, request): SecretEvent.UPDATE, request.auth["org_member"], request.auth["service_token"], + request.auth["service_account_token"], ip_address, user_agent, ) @@ -296,7 +280,7 @@ def put(self, request): return Response(status=status.HTTP_200_OK) - def delete(self, request): + def delete(self, request, *args, **kwargs): request_body = json.loads(request.body) @@ -304,33 +288,10 @@ def delete(self, request): secrets_to_delete = Secret.objects.filter(id__in=request_body["secrets"]) - for secret in secrets_to_delete: - if not Secret.objects.filter(id=secret.id).exists(): - return JsonResponse({"error": "Secret doesn't exist"}, status=404) - if request.auth["org_member"]: - - if not user_has_permission( - request.auth["org_member"].user, - "delete", - "Secrets", - secret.environment.app.organisation, - True, - ): - return JsonResponse( - { - "error": "You don't have permission to delete secrets in this environment" - }, - status=403, - ) + if not secrets_to_delete.exists(): + return Response(status=status.HTTP_200_OK) - if request.auth[ - "org_member" - ] is not None and not user_can_access_environment( - request.auth["org_member"].user.userId, secret.environment.id - ): - return JsonResponse( - {"error": "You don't have access to this environment"}, status=403 - ) + env = secrets_to_delete[0].environment for secret in secrets_to_delete: secret.updated_at = timezone.now() @@ -342,6 +303,7 @@ def delete(self, request): SecretEvent.DELETE, request.auth["org_member"], request.auth["service_token"], + request.auth["service_account_token"], ip_address, user_agent, ) @@ -356,28 +318,40 @@ class PublicSecretsView(APIView): CamelCaseJSONRenderer, ] - @csrf_exempt - def dispatch(self, request, *args): - return super(PublicSecretsView, self).dispatch(request, *args) + def initial(self, request, *args, **kwargs): + super().initial(request, *args, **kwargs) - def get(self, request): - env = request.auth["environment"] + # 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 request.auth["org_member"]: if not user_has_permission( - request.auth["org_member"].user, - "read", + account, + action, "Secrets", - env.app.organisation, + organisation, True, + request.auth.get("service_account") is not None, ): - return JsonResponse( - { - "error": "You don't have permission to read secrets in this environment" - }, - status=403, + 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) @@ -406,6 +380,7 @@ def get(self, request): SecretEvent.READ, request.auth["org_member"], request.auth["service_token"], + request.auth["service_account_token"], ip_address, user_agent, ) @@ -418,25 +393,10 @@ def get(self, request): return Response(serializer.data, status=status.HTTP_200_OK) - def post(self, request): + def post(self, request, *args, **kwargs): env = request.auth["environment"] - if request.auth["org_member"]: - if not user_has_permission( - request.auth["org_member"].user, - "create", - "Secrets", - env.app.organisation, - True, - ): - return JsonResponse( - { - "error": "You don't have permission to create secrets in this environment" - }, - status=403, - ) - # 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) @@ -507,6 +467,7 @@ def post(self, request): SecretEvent.CREATE, request.auth["org_member"], request.auth["service_token"], + request.auth["service_account_token"], ip_address, user_agent, ) @@ -529,25 +490,10 @@ def post(self, request): return Response(serializer.data, status=status.HTTP_200_OK) - def put(self, request): + def put(self, request, *args, **kwargs): env = request.auth["environment"] - if request.auth["org_member"]: - if not user_has_permission( - request.auth["org_member"].user, - "update", - "Secrets", - env.app.organisation, - True, - ): - return JsonResponse( - { - "error": "You don't have permission to update secrets in this environment" - }, - status=403, - ) - # 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) @@ -638,6 +584,7 @@ def put(self, request): SecretEvent.UPDATE, request.auth["org_member"], request.auth["service_token"], + request.auth["service_account_token"], ip_address, user_agent, ) @@ -664,25 +611,10 @@ def put(self, request): return Response(serializer.data, status=status.HTTP_200_OK) - def delete(self, request): + def delete(self, request, *args, **kwargs): env = request.auth["environment"] - if request.auth["org_member"]: - if not user_has_permission( - request.auth["org_member"].user, - "delete", - "Secrets", - env.app.organisation, - True, - ): - return JsonResponse( - { - "error": "You don't have permission to delete secrets in this environment" - }, - status=403, - ) - # 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) @@ -697,15 +629,6 @@ def delete(self, request): if not Secret.objects.filter(id=secret.id).exists(): return JsonResponse({"error": "Secret does not exist"}, status=404) - if request.auth[ - "org_member" - ] is not None and not user_can_access_environment( - request.auth["org_member"].user.userId, secret.environment.id - ): - return JsonResponse( - {"error": "You don't have access to this environment"}, status=403 - ) - for secret in secrets_to_delete: secret.updated_at = timezone.now() secret.deleted_at = timezone.now() @@ -716,6 +639,7 @@ def delete(self, request): SecretEvent.DELETE, request.auth["org_member"], request.auth["service_token"], + request.auth["service_account_token"], ip_address, user_agent, ) diff --git a/backend/backend/exceptions.py b/backend/backend/exceptions.py index 61e138ce8..402cf0572 100644 --- a/backend/backend/exceptions.py +++ b/backend/backend/exceptions.py @@ -1,18 +1,26 @@ from rest_framework.views import exception_handler -from django.http import HttpResponse +from django.core.exceptions import PermissionDenied +from django.http import JsonResponse, HttpResponse def custom_exception_handler(exc, context): - # Call REST framework's default exception handler first, - # to get the standard error response. - response = exception_handler(exc, context) - print("EXCEPTION", exc) + """ + Custom exception handler to modify 'PermissionDenied' responses. + """ + # Handle PermissionDenied + if isinstance(exc, PermissionDenied): + # Extract the custom message and replace the key + error_message = str(exc) if exc else "Permission denied." + return JsonResponse( + {"error": error_message}, # Change "detail" to "error" + status=403, + ) - # set 404 as default response code - status_code = 404 + # Call REST framework's default exception handler for other exceptions + response = exception_handler(exc, context) - # Now add the HTTP status code to the response. - if response is not None: - status_code = response.status_code + if response is not None and "detail" in response.data: + # If "detail" exists in other exceptions, you can modify it too + response.data["error"] = response.data.pop("detail") - return HttpResponse(status=status_code) + return response diff --git a/backend/backend/graphene/mutations/app.py b/backend/backend/graphene/mutations/app.py index 5e726fa98..9f7618f40 100644 --- a/backend/backend/graphene/mutations/app.py +++ b/backend/backend/graphene/mutations/app.py @@ -3,13 +3,19 @@ from api.utils.access.permissions import ( user_can_access_app, user_has_permission, - user_is_admin, user_is_org_member, ) import graphene from graphql import GraphQLError -from api.models import App, EnvironmentKey, Organisation, OrganisationMember, Role -from backend.graphene.types import AppType +from api.models import ( + App, + EnvironmentKey, + Organisation, + OrganisationMember, + Role, + ServiceAccount, +) +from backend.graphene.types import AppType, MemberType from django.conf import settings from django.db.models import Q @@ -160,37 +166,54 @@ class Arguments: member_id = graphene.ID() app_id = graphene.ID() env_keys = graphene.List(EnvironmentKeyInput) + member_type = MemberType(required=False) app = graphene.Field(AppType) @classmethod - def mutate(cls, root, info, member_id, app_id, env_keys): + def mutate( + cls, root, info, member_id, app_id, env_keys, member_type=MemberType.USER + ): user = info.context.user app = App.objects.get(id=app_id) + if member_type == MemberType.USER: + permission_key = "Members" + member = OrganisationMember.objects.get(id=member_id, deleted_at=None) + else: + permission_key = "ServiceAccounts" + member = ServiceAccount.objects.get(id=member_id, deleted_at=None) + if not user_has_permission( - info.context.user, "create", "Members", app.organisation, True + info.context.user, "create", permission_key, app.organisation, True ): raise GraphQLError("You don't have permission to add members to this App") if not user_can_access_app(user.userId, app.id): raise GraphQLError("You don't have access to this app") - org_member = OrganisationMember.objects.get(id=member_id, deleted_at=None) - - app.members.add(org_member) + if member_type == MemberType.USER: + app.members.add(member) + else: + app.service_accounts.add(member) # Create new env keys for key in env_keys: - EnvironmentKey.objects.update_or_create( - environment_id=key.env_id, - user_id=key.user_id, - defaults={ - "wrapped_seed": key.wrapped_seed, - "wrapped_salt": key.wrapped_salt, - "identity_key": key.identity_key, - }, - ) + defaults = { + "wrapped_seed": key.wrapped_seed, + "wrapped_salt": key.wrapped_salt, + "identity_key": key.identity_key, + } + + condition = { + "environment_id": key.env_id, + "user_id": key.user_id if member_type == MemberType.USER else None, + "service_account_id": ( + key.user_id if member_type == MemberType.SERVICE else None + ), + } + + EnvironmentKey.objects.update_or_create(**condition, defaults=defaults) return AddAppMemberMutation(app=app) @@ -199,31 +222,55 @@ class RemoveAppMemberMutation(graphene.Mutation): class Arguments: member_id = graphene.ID() app_id = graphene.ID() + member_type = MemberType(required=False) # Add member_type argument app = graphene.Field(AppType) @classmethod - def mutate(cls, root, info, member_id, app_id): + def mutate(cls, root, info, member_id, app_id, member_type=MemberType.USER): user = info.context.user app = App.objects.get(id=app_id) + if member_type == MemberType.USER: + permission_key = "Members" + else: + permission_key = "ServiceAccounts" + if not user_has_permission( - info.context.user, "delete", "Members", app.organisation, True + info.context.user, "delete", permission_key, app.organisation, True ): raise GraphQLError( - "You don't have permission to remove members from this App" + f"You don't have permission to remove {permission_key} from this App" ) if not user_can_access_app(user.userId, app.id): raise GraphQLError("You don't have access to this app") - org_member = OrganisationMember.objects.get(id=member_id, deleted_at=None) - if org_member not in app.members.all(): - raise GraphQLError("This user is not a member of this app") - else: - app.members.remove(org_member) + member = None + if member_type == MemberType.USER: + member = OrganisationMember.objects.get(id=member_id) + elif member_type == MemberType.SERVICE: + member = ServiceAccount.objects.get(id=member_id) + + if not member: + raise GraphQLError("Invalid member type or ID") + + if member_type == MemberType.USER: + if member not in app.members.all(): + raise GraphQLError("This user is not a member of this app") + + app.members.remove(member) EnvironmentKey.objects.filter( environment__app=app, user_id=member_id ).delete() + elif member_type == MemberType.SERVICE: + if member not in app.service_accounts.all(): + raise GraphQLError("This service account is not a member of this app") + + app.service_accounts.remove(member) + EnvironmentKey.objects.filter( + environment__app=app, service_account_id=member_id + ).delete() + return RemoveAppMemberMutation(app=app) diff --git a/backend/backend/graphene/mutations/environment.py b/backend/backend/graphene/mutations/environment.py index 526d141a8..f4cfc489b 100644 --- a/backend/backend/graphene/mutations/environment.py +++ b/backend/backend/graphene/mutations/environment.py @@ -6,11 +6,11 @@ user_can_access_app, user_can_access_environment, user_has_permission, - user_is_admin, user_is_org_member, ) from api.utils.audit_logging import log_secret_event from api.utils.secrets import normalize_path_string + from backend.quotas import can_add_environment, can_use_custom_envs import graphene from graphql import GraphQLError @@ -28,6 +28,7 @@ SecretFolder, SecretTag, ServerEnvironmentKey, + ServiceAccount, UserToken, ServiceToken, ) @@ -36,6 +37,7 @@ EnvironmentKeyType, EnvironmentTokenType, EnvironmentType, + MemberType, PersonalSecretType, SecretFolderType, SecretTagType, @@ -342,43 +344,67 @@ def mutate( class UpdateMemberEnvScopeMutation(graphene.Mutation): class Arguments: member_id = graphene.ID() + member_type = MemberType(required=False) app_id = graphene.ID() env_keys = graphene.List(EnvironmentKeyInput) app = graphene.Field(AppType) @classmethod - def mutate(cls, root, info, member_id, app_id, env_keys): + def mutate( + cls, root, info, member_id, app_id, env_keys, member_type=MemberType.USER + ): user = info.context.user app = App.objects.get(id=app_id) + if member_type == MemberType.USER: + permission_key = "Members" + else: + permission_key = "ServiceAccounts" + if not user_has_permission( - info.context.user, "update", "Members", app.organisation, True + info.context.user, "update", permission_key, app.organisation, True ): raise GraphQLError("You don't have permission to update App member access") if not user_can_access_app(user.userId, app.id): raise GraphQLError("You don't have access to this app") - org_member = OrganisationMember.objects.get(id=member_id, deleted_at=None) - if org_member not in app.members.all(): - raise GraphQLError("This user does not have access to this app") - else: - # delete all existing keys - EnvironmentKey.objects.filter( - environment__app=app, user_id=member_id - ).delete() - - # set new keys - for key in env_keys: - EnvironmentKey.objects.create( - environment_id=key.env_id, - user_id=key.user_id, - wrapped_seed=key.wrapped_seed, - wrapped_salt=key.wrapped_salt, - identity_key=key.identity_key, + key_to_delete_filter = { + "environment__app": app, + } + + if member_type == MemberType.USER: + app_member = OrganisationMember.objects.get(id=member_id, deleted_at=None) + key_to_delete_filter["user_id"] = member_id + if app_member not in app.members.all(): + raise GraphQLError("This user does not have access to this app") + + elif member_type == MemberType.SERVICE: + app_member = ServiceAccount.objects.get(id=member_id) + key_to_delete_filter["service_account_id"] = member_id + if app_member not in app.service_accounts.all(): + raise GraphQLError( + "This service account does not have access to this app" ) + # delete all existing keys for this member + EnvironmentKey.objects.filter(**key_to_delete_filter).delete() + + # set new keys + for key in env_keys: + + EnvironmentKey.objects.create( + environment_id=key.env_id, + user_id=key.user_id if member_type == MemberType.USER else None, + service_account_id=( + key.user_id if member_type == MemberType.SERVICE else None + ), + wrapped_seed=key.wrapped_seed, + wrapped_salt=key.wrapped_salt, + identity_key=key.identity_key, + ) + return UpdateMemberEnvScopeMutation(app=app) @@ -729,7 +755,7 @@ def mutate(cls, root, info, secret_data): ) log_secret_event( - secret, SecretEvent.CREATE, org_member, None, ip_address, user_agent + secret, SecretEvent.CREATE, org_member, None, None, ip_address, user_agent ) return CreateSecretMutation(secret=secret) @@ -793,7 +819,13 @@ def mutate(cls, root, info, secrets_data): user=info.context.user, organisation=org, deleted_at=None ) log_secret_event( - secret, SecretEvent.CREATE, org_member, None, ip_address, user_agent + secret, + SecretEvent.CREATE, + org_member, + None, + None, + ip_address, + user_agent, ) return BulkCreateSecretMutation(secrets=created_secrets) @@ -848,7 +880,7 @@ def mutate(cls, root, info, id, secret_data): ) log_secret_event( - secret, SecretEvent.UPDATE, org_member, None, ip_address, user_agent + secret, SecretEvent.UPDATE, org_member, None, None, ip_address, user_agent ) return EditSecretMutation(secret=secret) @@ -906,7 +938,13 @@ def mutate(cls, root, info, secrets_data): user=info.context.user, organisation=org, deleted_at=None ) log_secret_event( - secret, SecretEvent.UPDATE, org_member, None, ip_address, user_agent + secret, + SecretEvent.UPDATE, + org_member, + None, + None, + ip_address, + user_agent, ) return BulkEditSecretMutation(secrets=updated_secrets) @@ -940,7 +978,7 @@ def mutate(cls, root, info, id): ) log_secret_event( - secret, SecretEvent.DELETE, org_member, None, ip_address, user_agent + secret, SecretEvent.DELETE, org_member, None, None, ip_address, user_agent ) return DeleteSecretMutation(secret=secret) @@ -978,7 +1016,13 @@ def mutate(cls, root, info, ids): user=info.context.user, organisation=org, deleted_at=None ) log_secret_event( - secret, SecretEvent.DELETE, org_member, None, ip_address, user_agent + secret, + SecretEvent.DELETE, + org_member, + None, + None, + ip_address, + user_agent, ) return BulkDeleteSecretMutation(secrets=deleted_secrets) @@ -1006,7 +1050,13 @@ def mutate(cls, root, info, ids): ) log_secret_event( - secret, SecretEvent.READ, org_member, None, ip_address, user_agent + secret, + SecretEvent.READ, + org_member, + None, + None, + ip_address, + user_agent, ) return ReadSecretMutation(ok=True) diff --git a/backend/backend/graphene/mutations/service_accounts.py b/backend/backend/graphene/mutations/service_accounts.py new file mode 100644 index 000000000..b8768e955 --- /dev/null +++ b/backend/backend/graphene/mutations/service_accounts.py @@ -0,0 +1,287 @@ +import graphene +from graphql import GraphQLError +from api.models import ( + Organisation, + OrganisationMember, + Role, + ServiceAccount, + ServiceAccountHandler, + ServiceAccountToken, +) +from api.utils.access.permissions import user_has_permission, user_is_org_member +from backend.graphene.types import ServiceAccountTokenType, ServiceAccountType +from datetime import datetime +from django.conf import settings + + +class ServiceAccountHandlerInput(graphene.InputObjectType): + service_account_id = graphene.ID(required=False) + member_id = graphene.ID(required=False) + wrapped_keyring = graphene.String(required=True) + wrapped_recovery = graphene.String(required=True) + + +class CreateServiceAccountMutation(graphene.Mutation): + class Arguments: + name = graphene.String() + organisation_id = graphene.ID() + role_id = graphene.ID() + handlers = graphene.List(ServiceAccountHandlerInput) + identity_key = graphene.String() + server_wrapped_keyring = graphene.String(required=False) + server_wrapped_recovery = graphene.String(required=False) + + service_account = graphene.Field(ServiceAccountType) + + @classmethod + def mutate( + cls, + root, + info, + name, + organisation_id, + role_id, + handlers, + identity_key, + server_wrapped_keyring=None, + server_wrapped_recovery=None, + ): + user = info.context.user + org = Organisation.objects.get(id=organisation_id) + + if not user_has_permission(user, "create", "ServiceAccounts", org): + raise GraphQLError( + "You don't have the permissions required to create Service Accounts in this organisation" + ) + + if handlers is None or len(handlers) == 0: + raise GraphQLError("At least one service account handler must be provided") + + service_account = ServiceAccount.objects.create( + name=name, + organisation=org, + role=Role.objects.get(id=role_id), + identity_key=identity_key, + server_wrapped_keyring=server_wrapped_keyring, + server_wrapped_recovery=server_wrapped_recovery, + ) + + for handler in handlers: + ServiceAccountHandler.objects.create( + service_account=service_account, + user_id=handler.member_id, + wrapped_keyring=handler.wrapped_keyring, + wrapped_recovery=handler.wrapped_recovery, + ) + + if settings.APP_HOST == "cloud": + from ee.billing.stripe import update_stripe_subscription_seats + + update_stripe_subscription_seats(org) + + return CreateServiceAccountMutation(service_account=service_account) + + +class EnableServiceAccountThirdPartyAuthMutation(graphene.Mutation): + class Arguments: + service_account_id = graphene.ID() + server_wrapped_keyring = graphene.String() + server_wrapped_recovery = graphene.String() + + service_account = graphene.Field(ServiceAccountType) + + @classmethod + def mutate( + cls, + root, + info, + service_account_id, + server_wrapped_keyring, + server_wrapped_recovery, + ): + user = info.context.user + service_account = ServiceAccount.objects.get(id=service_account_id) + + if not user_has_permission( + user, "update", "ServiceAccounts", service_account.organisation + ): + raise GraphQLError( + "You don't have the permissions required to update Service Accounts in this organisation" + ) + + service_account.server_wrapped_keyring = server_wrapped_keyring + service_account.server_wrapped_recovery = server_wrapped_recovery + service_account.save() + + return EnableServiceAccountThirdPartyAuthMutation( + service_account=service_account + ) + + +class UpdateServiceAccountMutation(graphene.Mutation): + class Arguments: + service_account_id = graphene.ID() + name = graphene.String() + role_id = graphene.ID() + + service_account = graphene.Field(ServiceAccountType) + + @classmethod + def mutate(cls, root, info, service_account_id, name, role_id): + user = info.context.user + service_account = ServiceAccount.objects.get(id=service_account_id) + + if not user_has_permission( + user, "update", "ServiceAccounts", service_account.organisation + ): + raise GraphQLError( + "You don't have the permissions required to update Service Accounts in this organisation" + ) + + role = Role.objects.get(id=role_id) + service_account.name = name + service_account.role = role + service_account.save() + + return UpdateServiceAccountMutation(service_account=service_account) + + +class UpdateServiceAccountHandlersMutation(graphene.Mutation): + class Arguments: + organisation_id = graphene.ID() + handlers = graphene.List(ServiceAccountHandlerInput) + + ok = graphene.Boolean() + + @classmethod + def mutate(cls, root, info, organisation_id, handlers): + user = info.context.user + org = Organisation.objects.get(id=organisation_id) + + if not user_is_org_member(user.userId, organisation_id): + raise GraphQLError( + "You are not a member of this organisation and cannot perform this operation" + ) + + for account in org.service_accounts.all(): + [handler.delete() for handler in account.handlers.all()] + + for handler in handlers: + service_account = ServiceAccount.objects.get(id=handler.service_account_id) + + if not ServiceAccountHandler.objects.filter( + service_account=service_account, user_id=handler.member_id + ).exists(): + ServiceAccountHandler.objects.create( + service_account=service_account, + user_id=handler.member_id, + wrapped_keyring=handler.wrapped_keyring, + wrapped_recovery=handler.wrapped_recovery, + ) + + return UpdateServiceAccountHandlersMutation(ok=True) + + +class DeleteServiceAccountMutation(graphene.Mutation): + class Arguments: + service_account_id = graphene.ID() + + ok = graphene.Boolean() + + @classmethod + def mutate(cls, root, info, service_account_id): + user = info.context.user + service_account = ServiceAccount.objects.get(id=service_account_id) + + if not user_has_permission( + user, "delete", "ServiceAccounts", service_account.organisation + ): + raise GraphQLError( + "You don't have the permissions required to delete Service Accounts in this organisation" + ) + + service_account.delete() + + if settings.APP_HOST == "cloud": + from ee.billing.stripe import update_stripe_subscription_seats + + update_stripe_subscription_seats(service_account.organisation) + + return DeleteServiceAccountMutation(ok=True) + + +class CreateServiceAccountTokenMutation(graphene.Mutation): + class Arguments: + service_account_id = graphene.ID() + name = graphene.String(required=True) + identity_key = graphene.String(required=True) + token = graphene.String(required=True) + wrapped_key_share = graphene.String(required=True) + expiry = graphene.BigInt(required=False) + + token = graphene.Field(ServiceAccountTokenType) + + @classmethod + def mutate( + cls, + root, + info, + service_account_id, + name, + identity_key, + token, + wrapped_key_share, + expiry, + ): + user = info.context.user + service_account = ServiceAccount.objects.get(id=service_account_id) + org_member = OrganisationMember.objects.get( + user=user, organisation=service_account.organisation, deleted_at=None + ) + + if not user_has_permission( + user, "create", "ServiceAccountTokens", service_account.organisation + ): + raise GraphQLError( + "You don't have the permissions required to create Service Tokens in this organisation" + ) + + if expiry is not None: + expires_at = datetime.fromtimestamp(expiry / 1000) + else: + expires_at = None + + token = ServiceAccountToken.objects.create( + service_account=service_account, + name=name, + identity_key=identity_key, + token=token, + wrapped_key_share=wrapped_key_share, + created_by=org_member, + expires_at=expires_at, + ) + + return CreateServiceAccountTokenMutation(token=token) + + +class DeleteServiceAccountTokenMutation(graphene.Mutation): + class Arguments: + token_id = graphene.ID() + + ok = graphene.Boolean() + + @classmethod + def mutate(cls, root, info, token_id): + user = info.context.user + token = ServiceAccountToken.objects.get(id=token_id) + + if not user_has_permission( + user, "delete", "ServiceAccountTokens", token.service_account.organisation + ): + raise GraphQLError( + "You don't have the permissions required to delete Service Tokens in this organisation" + ) + + token.delete() + + return DeleteServiceAccountTokenMutation(ok=True) diff --git a/backend/backend/graphene/queries/access.py b/backend/backend/graphene/queries/access.py index af2e68443..1eeaceb0e 100644 --- a/backend/backend/graphene/queries/access.py +++ b/backend/backend/graphene/queries/access.py @@ -1,8 +1,10 @@ from api.utils.access.permissions import user_has_permission, user_is_org_member -from api.models import Organisation, Role +from api.models import Organisation, OrganisationMember, Role from graphql import GraphQLError from django.db import transaction from api.utils.access.roles import default_roles +from itertools import chain +from django.db.models import Q, Case, When, Value, IntegerField @transaction.atomic @@ -24,9 +26,46 @@ def migrate_role_permissions(): def resolve_roles(root, info, org_id): org = Organisation.objects.get(id=org_id) - # migrate_role_permissions() + custom_order = Case( + When(name="Owner", then=Value(1)), + When(name="Admin", then=Value(2)), + When(name="Manager", then=Value(3)), + When(name="Developer", then=Value(4)), + When(name="Service", then=Value(5)), + default=Value(6), # For custom roles + output_field=IntegerField(), + ) if user_has_permission(info.context.user.userId, "read", "Roles", org): - return Role.objects.filter(organisation=org) + return Role.objects.filter(organisation=org).order_by( + "-is_default", custom_order + ) else: raise GraphQLError("You don't have permission to perform this action") + + +def resolve_organisation_global_access_users(root, info, organisation_id): + if not user_is_org_member(info.context.user.userId, organisation_id): + raise GraphQLError("You don't have access to this organisation") + + global_access_roles = Role.objects.filter( + Q(organisation_id=organisation_id) + & (Q(name__iexact="owner") | Q(name__iexact="admin")) + | Q(permissions__global_access=True) + ) + + members = OrganisationMember.objects.filter( + organisation_id=organisation_id, + role__in=global_access_roles, + deleted_at=None, + ) + + if not info.context.user.userId in [member.user_id for member in members]: + self_member = OrganisationMember.objects.filter( + organisation_id=organisation_id, + user_id=info.context.user.userId, + deleted_at=None, + ) + members = list(chain(members, self_member)) + + return members diff --git a/backend/backend/graphene/queries/service_accounts.py b/backend/backend/graphene/queries/service_accounts.py new file mode 100644 index 000000000..e39f89edd --- /dev/null +++ b/backend/backend/graphene/queries/service_accounts.py @@ -0,0 +1,60 @@ +from api.utils.access.permissions import ( + user_can_access_app, + user_has_permission, + user_is_org_member, +) +from api.models import App, Organisation, OrganisationMember, Role, ServiceAccount +from .access import resolve_organisation_global_access_users +from django.db.models import Q +from graphql import GraphQLError + + +def resolve_service_accounts(root, info, org_id, service_account_id=None): + org = Organisation.objects.get(id=org_id) + if user_has_permission(info.context.user.userId, "read", "ServiceAccounts", org): + + filter = {"organisation": org} + + if service_account_id is not None: + filter["id"] = service_account_id + + return ServiceAccount.objects.filter(**filter) + + +def resolve_service_account_handlers(root, info, org_id): + if not user_is_org_member(info.context.user.userId, org_id): + raise GraphQLError("You don't have access to this organisation") + + service_account_handler_roles = Role.objects.filter( + Q(organisation_id=org_id) + & ( + Q(name__iexact="owner") + | Q(name__iexact="admin") + | Q(permissions__global_access=True) # Check for global access roles + | Q(permissions__permissions__ServiceAccounts__gt=0) + ), + ) + + members = OrganisationMember.objects.filter( + organisation_id=org_id, + role__in=service_account_handler_roles, + deleted_at=None, + ) + + return members + + +def resolve_app_service_accounts(root, info, app_id): + app = App.objects.get(id=app_id) + + if not user_has_permission( + info.context.user, "read", "ServiceAccounts", app.organisation, True + ): + raise GraphQLError( + "You don't have permission to read service accounts in this App" + ) + + if not user_can_access_app(info.context.user.userId, app_id): + raise GraphQLError("You don't have access to this app") + + return app.service_accounts.filter(deleted_at=None) diff --git a/backend/backend/graphene/types.py b/backend/backend/graphene/types.py index 9c75c0502..50989a201 100644 --- a/backend/backend/graphene/types.py +++ b/backend/backend/graphene/types.py @@ -27,21 +27,30 @@ SecretFolder, SecretTag, ServerEnvironmentKey, + ServiceAccount, + ServiceAccountHandler, + ServiceAccountToken, ServiceToken, UserToken, ) from logs.dynamodb_models import KMSLog -from allauth.socialaccount.models import SocialAccount from django.utils import timezone +from datetime import datetime from api.utils.access.roles import default_roles +class SeatsUsed(ObjectType): + users = graphene.Int() + service_accounts = graphene.Int() + total = graphene.Int() + + class OrganisationPlanType(ObjectType): name = graphene.String() max_users = graphene.Int() max_apps = graphene.Int() max_envs_per_app = graphene.Int() - user_count = graphene.Int() + seats_used = graphene.Field(SeatsUsed) app_count = graphene.Int() @@ -122,13 +131,22 @@ def resolve_plan_detail(self, info): plan = PLAN_CONFIG[self.plan] - plan["user_count"] = ( - OrganisationMember.objects.filter( + plan["seats_used"] = { + "users": ( + OrganisationMember.objects.filter( + organisation=self, deleted_at=None + ).count() + + OrganisationMemberInvite.objects.filter( + organisation=self, valid=True, expires_at__gte=timezone.now() + ).count() + ), + "service_accounts": ServiceAccount.objects.filter( organisation=self, deleted_at=None - ).count() - + OrganisationMemberInvite.objects.filter( - organisation=self, valid=True, expires_at__gte=timezone.now() - ).count() + ).count(), + } + + plan["seats_used"]["total"] = ( + plan["seats_used"]["users"] + plan["seats_used"]["service_accounts"] ) plan["app_count"] = App.objects.filter( @@ -201,6 +219,35 @@ class Meta: ) +class ServiceAccountHandlerType(DjangoObjectType): + class Meta: + model = ServiceAccountHandler + fields = "__all__" + + +class ServiceAccountTokenType(DjangoObjectType): + + last_used = graphene.DateTime() + + class Meta: + model = ServiceAccountToken + fields = "__all__" + + def resolve_last_used(self, info): + event = ( + SecretEvent.objects.filter(service_account_token=self) + .order_by("timestamp") + .last() + ) + if event: + return event.timestamp + + +class MemberType(graphene.Enum): + USER = "user" + SERVICE = "service" + + class ProviderType(graphene.ObjectType): id = graphene.String(required=True) name = graphene.String(required=True) @@ -332,9 +379,14 @@ class Meta: "app_seed", "app_version", "sse_enabled", + "service_accounts", ) def resolve_environments(self, info): + + if hasattr(self, "filtered_environments"): + return self.filtered_environments + org_member = OrganisationMember.objects.get( organisation=self.organisation, user_id=info.context.user.userId, @@ -355,6 +407,63 @@ def resolve_members(self, info): return self.members.filter(deleted_at=None) +class ServiceAccountType(DjangoObjectType): + + third_party_auth_enabled = graphene.Boolean() + handlers = graphene.List(ServiceAccountHandlerType) + tokens = graphene.List(ServiceAccountTokenType) + app_memberships = graphene.List(graphene.NonNull(AppType)) + + class Meta: + model = ServiceAccount + fields = ( + "id", + "name", + "role", + "identity_key", + "created_at", + "updated_at", + ) + + def resolve_third_party_auth_enabled(self, info): + return ( + self.server_wrapped_keyring is not None + and self.server_wrapped_recovery is not None + ) + + def resolve_handlers(self, info): + return ServiceAccountHandler.objects.filter(service_account=self) + + def resolve_tokens(self, info): + return ServiceAccountToken.objects.filter(service_account=self) + + def resolve_app_memberships(self, info): + # Fetch all apps that this service account is related to + apps = self.apps.all() + + filtered_apps = [] + for app in apps: + # Get environments for the app + app_environments = Environment.objects.filter(app=app).order_by("index") + + # Check which environments the service account has access to + accessible_environments = [ + env + for env in app_environments + if EnvironmentKey.objects.filter( + service_account=self, environment=env + ).exists() + ] + + # Manually override the 'environments' field for this app instance + app.filtered_environments = accessible_environments + + # Add this app to the filtered list + filtered_apps.append(app) + + return filtered_apps + + class EnvironmentKeyType(DjangoObjectType): class Meta: model = EnvironmentKey @@ -500,6 +609,8 @@ class Meta: "timestamp", "user", "service_token", + "service_account", + "service_account_token", "ip_address", "user_agent", "environment", @@ -523,6 +634,10 @@ def resolve_user(self, info): ): return self.user + def resolve_service_account(self, info): + if self.service_account_token: + return self.service_account_token.service_account + class PersonalSecretType(DjangoObjectType): class Meta: diff --git a/backend/backend/quotas.py b/backend/backend/quotas.py index afd362891..bebb15938 100644 --- a/backend/backend/quotas.py +++ b/backend/backend/quotas.py @@ -10,8 +10,8 @@ PLAN_CONFIG = { "FR": { "name": "Free", - "max_users": 5 if CLOUD_HOSTED else 20, - "max_apps": 3 if CLOUD_HOSTED else 20, + "max_users": 5 if CLOUD_HOSTED else None, + "max_apps": 3 if CLOUD_HOSTED else None, "max_envs_per_app": 3, "max_tokens_per_app": 3, }, @@ -54,11 +54,12 @@ def can_add_app(organisation): return current_app_count < plan_limits["max_apps"] -def can_add_user(organisation): - """Check if a new user can be added to the organisation.""" +def can_add_account(organisation): + """Check if a new human or service account can be added to the organisation.""" OrganisationMember = apps.get_model("api", "OrganisationMember") OrganisationMemberInvite = apps.get_model("api", "OrganisationMemberInvite") + ServiceAccount = apps.get_model("api", "ServiceAccount") ActivatedPhaseLicense = apps.get_model("api", "ActivatedPhaseLicense") plan_limits = PLAN_CONFIG[organisation.plan] @@ -66,7 +67,8 @@ def can_add_user(organisation): organisation=organisation ).exists() - current_user_count = ( + # Calculate the current count of users and service accounts + current_human_user_count = ( OrganisationMember.objects.filter( organisation=organisation, deleted_at=None ).count() @@ -74,7 +76,12 @@ def can_add_user(organisation): organisation=organisation, valid=True, expires_at__gte=timezone.now() ).count() ) + current_service_account_count = ServiceAccount.objects.filter( + organisation=organisation, deleted_at=None + ).count() + total_account_count = current_human_user_count + current_service_account_count + # Determine the user limit if license_exists: license = ( ActivatedPhaseLicense.objects.filter(organisation=organisation) @@ -82,13 +89,15 @@ def can_add_user(organisation): .first() ) user_limit = license.seats - else: user_limit = plan_limits["max_users"] + # If there's no limit, allow unlimited additions if user_limit is None: return True - return current_user_count < user_limit + + # Check if the total account count is below the limit + return total_account_count < user_limit def can_add_environment(app): diff --git a/backend/backend/schema.py b/backend/backend/schema.py index e82982108..855bd8eea 100644 --- a/backend/backend/schema.py +++ b/backend/backend/schema.py @@ -3,6 +3,15 @@ from api.utils.syncing.github.actions import GitHubRepoType from api.utils.syncing.gitlab.main import GitLabGroupType, GitLabProjectType from api.utils.syncing.railway.main import RailwayProjectType +from backend.graphene.mutations.service_accounts import ( + CreateServiceAccountMutation, + CreateServiceAccountTokenMutation, + DeleteServiceAccountMutation, + DeleteServiceAccountTokenMutation, + EnableServiceAccountThirdPartyAuthMutation, + UpdateServiceAccountHandlersMutation, + UpdateServiceAccountMutation, +) from api.utils.syncing.vercel.main import VercelProjectType from .graphene.queries.syncing import ( resolve_vercel_projects, @@ -36,7 +45,15 @@ resolve_test_nomad_creds, resolve_railway_projects, ) -from .graphene.queries.access import resolve_roles +from .graphene.queries.access import ( + resolve_roles, + resolve_organisation_global_access_users, +) +from .graphene.queries.service_accounts import ( + resolve_service_accounts, + resolve_service_account_handlers, + resolve_app_service_accounts, +) from .graphene.queries.quotas import resolve_organisation_plan from .graphene.queries.license import resolve_license, resolve_organisation_license from .graphene.mutations.environment import ( @@ -91,6 +108,7 @@ AddAppMemberMutation, CreateAppMutation, DeleteAppMutation, + MemberType, RemoveAppMemberMutation, RotateAppKeysMutation, ) @@ -124,6 +142,8 @@ SecretFolderType, SecretTagType, SecretType, + ServiceAccountHandlerType, + ServiceAccountType, ServiceTokenType, ServiceType, TimeRange, @@ -144,6 +164,7 @@ SecretEvent, SecretFolder, SecretTag, + ServiceAccount, ServiceToken, UserToken, ) @@ -151,9 +172,7 @@ from datetime import datetime, timedelta from django.conf import settings from logs.models import KMSDBLog -from itertools import chain from django.utils import timezone -from django.db.models import Q CLOUD_HOSTED = settings.APP_HOST == "cloud" @@ -215,8 +234,12 @@ class Query(graphene.ObjectType): app_id=graphene.ID(), environment_id=graphene.ID(required=False), member_id=graphene.ID(required=False), + member_type=MemberType(), ) app_users = graphene.List(OrganisationMemberType, app_id=graphene.ID()) + + app_service_accounts = graphene.List(ServiceAccountType, app_id=graphene.ID()) + secrets = graphene.List( SecretType, env_id=graphene.ID(), path=graphene.String(required=False) ) @@ -237,6 +260,16 @@ class Query(graphene.ObjectType): user_tokens = graphene.List(UserTokenType, organisation_id=graphene.ID()) service_tokens = graphene.List(ServiceTokenType, app_id=graphene.ID()) + service_accounts = graphene.List( + ServiceAccountType, + org_id=graphene.ID(), + service_account_id=graphene.ID(required=False), + ) + + service_account_handlers = graphene.List( + OrganisationMemberType, org_id=graphene.ID() + ) + server_public_key = graphene.String() sse_enabled = graphene.Boolean(app_id=graphene.ID()) @@ -349,31 +382,7 @@ def resolve_organisation_members(root, info, organisation_id, role, user_id=None return OrganisationMember.objects.filter(**filter) - def resolve_organisation_global_access_users(root, info, organisation_id): - if not user_is_org_member(info.context.user.userId, organisation_id): - raise GraphQLError("You don't have access to this organisation") - - global_access_roles = Role.objects.filter( - Q(organisation_id=organisation_id) - & (Q(name__iexact="owner") | Q(name__iexact="admin")) - | Q(permissions__global_access=True) - ) - - members = OrganisationMember.objects.filter( - organisation_id=organisation_id, - role__in=global_access_roles, - deleted_at=None, - ) - - if not info.context.user.userId in [member.user_id for member in members]: - self_member = OrganisationMember.objects.filter( - organisation_id=organisation_id, - user_id=info.context.user.userId, - deleted_at=None, - ) - members = list(chain(members, self_member)) - - return members + resolve_organisation_global_access_users = resolve_organisation_global_access_users def resolve_organisation_invites(root, info, org_id): if not user_is_org_member(info.context.user.userId, org_id): @@ -421,7 +430,9 @@ def resolve_apps(root, info, organisation_id, app_id=None): filter["id"] = app_id return App.objects.filter(**filter) - def resolve_app_environments(root, info, app_id, environment_id, member_id=None): + def resolve_app_environments( + root, info, app_id, environment_id, member_id=None, member_type=MemberType.USER + ): app = App.objects.get(id=app_id) @@ -434,7 +445,10 @@ def resolve_app_environments(root, info, app_id, environment_id, member_id=None) raise GraphQLError("You don't have access to this app") if member_id is not None: - org_member = OrganisationMember.objects.get(id=member_id) + if member_type == MemberType.USER: + org_member = OrganisationMember.objects.get(id=member_id) + else: + org_member = ServiceAccount.objects.get(id=member_id) else: org_member = OrganisationMember.objects.get( organisation=app.organisation, @@ -449,13 +463,23 @@ def resolve_app_environments(root, info, app_id, environment_id, member_id=None) app_environments = Environment.objects.filter(**filter).order_by("index") - return [ - app_env - for app_env in app_environments - if EnvironmentKey.objects.filter( - user=org_member, environment_id=app_env.id - ).exists() - ] + if member_type == MemberType.USER: + return [ + app_env + for app_env in app_environments + if EnvironmentKey.objects.filter( + user=org_member, environment_id=app_env.id + ).exists() + ] + + else: + return [ + app_env + for app_env in app_environments + if EnvironmentKey.objects.filter( + service_account=org_member, environment_id=app_env.id + ).exists() + ] def resolve_app_users(root, info, app_id): app = App.objects.get(id=app_id) @@ -470,6 +494,8 @@ def resolve_app_users(root, info, app_id): return app.members.filter(deleted_at=None) + resolve_app_service_accounts = resolve_app_service_accounts + def resolve_secrets(root, info, env_id, path=None): org = Environment.objects.get(id=env_id).app.organisation @@ -572,6 +598,9 @@ def resolve_service_tokens(root, info, app_id): return ServiceToken.objects.filter(app=app, deleted_at=None) + resolve_service_accounts = resolve_service_accounts + resolve_service_account_handlers = resolve_service_account_handlers + def resolve_logs(root, info, app_id, start=0, end=0): if not user_can_access_app(info.context.user.userId, app_id): raise GraphQLError("You don't have access to this app") @@ -776,6 +805,17 @@ class Mutation(graphene.ObjectType): update_custom_role = UpdateCustomRoleMutation.Field() delete_custom_role = DeleteCustomRoleMutation.Field() + # Service Accounts + create_service_account = CreateServiceAccountMutation.Field() + enable_service_account_third_party_auth = ( + EnableServiceAccountThirdPartyAuthMutation.Field() + ) + update_service_account_handlers = UpdateServiceAccountHandlersMutation.Field() + update_service_account = UpdateServiceAccountMutation.Field() + delete_service_account = DeleteServiceAccountMutation.Field() + create_service_account_token = CreateServiceAccountTokenMutation.Field() + delete_service_account_token = DeleteServiceAccountTokenMutation.Field() + init_env_sync = InitEnvSync.Field() delete_env_sync = DeleteSync.Field() trigger_sync = TriggerSync.Field() diff --git a/backend/requirements.txt b/backend/requirements.txt index 03cfb6b4c..bfa966c94 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -8,7 +8,7 @@ autopep8==2.0.2 boto3==1.33.10 botocore==1.33.10 certifi==2023.7.22 -cffi==1.15.1 +cffi==1.17.1 charset-normalizer==3.0.1 click==8.1.7 constantly==15.1.0 @@ -35,7 +35,7 @@ jmespath==1.0.1 oauthlib==3.2.2 packaging==24.1 promise==2.3 -psycopg2-binary==2.9.6 +psycopg2-binary==2.9.10 pyasn1==0.4.8 pyasn1-modules==0.2.8 pycodestyle==2.10.0 diff --git a/backend/version.txt b/backend/version.txt index ee14c9d6e..873e6cc6b 100644 --- a/backend/version.txt +++ b/backend/version.txt @@ -1 +1 @@ -v2.33.0 +v2.34.0 diff --git a/frontend/apollo/gql.ts b/frontend/apollo/gql.ts index f9a5f97d4..aeba92a2f 100644 --- a/frontend/apollo/gql.ts +++ b/frontend/apollo/gql.ts @@ -16,9 +16,9 @@ const documents = { "mutation CreateRole($name: String!, $description: String!, $color: String!, $permissions: JSONString!, $organisationId: ID!) {\n createCustomRole(\n name: $name\n description: $description\n color: $color\n permissions: $permissions\n organisationId: $organisationId\n ) {\n role {\n id\n }\n }\n}": types.CreateRoleDocument, "mutation DeleteRole($id: ID!) {\n deleteCustomRole(id: $id) {\n ok\n }\n}": types.DeleteRoleDocument, "mutation UpdateRole($id: ID!, $name: String!, $description: String!, $color: String!, $permissions: JSONString!) {\n updateCustomRole(\n id: $id\n name: $name\n description: $description\n color: $color\n permissions: $permissions\n ) {\n role {\n id\n }\n }\n}": types.UpdateRoleDocument, - "mutation AddMemberToApp($memberId: ID!, $appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n addAppMember(memberId: $memberId, appId: $appId, envKeys: $envKeys) {\n app {\n id\n }\n }\n}": types.AddMemberToAppDocument, - "mutation RemoveMemberFromApp($memberId: ID!, $appId: ID!) {\n removeAppMember(memberId: $memberId, appId: $appId) {\n app {\n id\n }\n }\n}": types.RemoveMemberFromAppDocument, - "mutation UpdateEnvScope($memberId: ID!, $appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n updateMemberEnvironmentScope(\n memberId: $memberId\n appId: $appId\n envKeys: $envKeys\n ) {\n app {\n id\n }\n }\n}": types.UpdateEnvScopeDocument, + "mutation AddMemberToApp($memberId: ID!, $memberType: MemberType, $appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n addAppMember(\n memberId: $memberId\n memberType: $memberType\n appId: $appId\n envKeys: $envKeys\n ) {\n app {\n id\n }\n }\n}": types.AddMemberToAppDocument, + "mutation RemoveMemberFromApp($memberId: ID!, $memberType: MemberType, $appId: ID!) {\n removeAppMember(memberId: $memberId, memberType: $memberType, appId: $appId) {\n app {\n id\n }\n }\n}": types.RemoveMemberFromAppDocument, + "mutation UpdateEnvScope($memberId: ID!, $memberType: MemberType, $appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n updateMemberEnvironmentScope(\n memberId: $memberId\n memberType: $memberType\n appId: $appId\n envKeys: $envKeys\n ) {\n app {\n id\n }\n }\n}": types.UpdateEnvScopeDocument, "mutation InitStripeProUpgradeCheckout($organisationId: ID!, $billingPeriod: String!) {\n createProUpgradeCheckoutSession(\n organisationId: $organisationId\n billingPeriod: $billingPeriod\n ) {\n clientSecret\n }\n}": types.InitStripeProUpgradeCheckoutDocument, "mutation CreateApplication($id: ID!, $organisationId: ID!, $name: String!, $identityKey: String!, $appToken: String!, $appSeed: String!, $wrappedKeyShare: String!, $appVersion: Int!) {\n createApp(\n id: $id\n organisationId: $organisationId\n name: $name\n identityKey: $identityKey\n appToken: $appToken\n appSeed: $appSeed\n wrappedKeyShare: $wrappedKeyShare\n appVersion: $appVersion\n ) {\n app {\n id\n name\n identityKey\n }\n }\n}": types.CreateApplicationDocument, "mutation CreateOrg($id: ID!, $name: String!, $identityKey: String!, $wrappedKeyring: String!, $wrappedRecovery: String!) {\n createOrganisation(\n id: $id\n name: $name\n identityKey: $identityKey\n wrappedKeyring: $wrappedKeyring\n wrappedRecovery: $wrappedRecovery\n ) {\n organisation {\n id\n name\n memberId\n }\n }\n}": types.CreateOrgDocument, @@ -50,6 +50,12 @@ const documents = { "mutation UpdateMemberRole($memberId: ID!, $roleId: ID!) {\n updateOrganisationMemberRole(memberId: $memberId, roleId: $roleId) {\n orgMember {\n id\n role {\n name\n }\n }\n }\n}": types.UpdateMemberRoleDocument, "mutation UpdateWrappedSecrets($orgId: ID!, $wrappedKeyring: String!, $wrappedRecovery: String!) {\n updateMemberWrappedSecrets(\n orgId: $orgId\n wrappedKeyring: $wrappedKeyring\n wrappedRecovery: $wrappedRecovery\n ) {\n orgMember {\n id\n }\n }\n}": types.UpdateWrappedSecretsDocument, "mutation RotateAppKey($id: ID!, $appToken: String!, $wrappedKeyShare: String!) {\n rotateAppKeys(id: $id, appToken: $appToken, wrappedKeyShare: $wrappedKeyShare) {\n app {\n id\n }\n }\n}": types.RotateAppKeyDocument, + "mutation CreateServiceAccountOp($name: String!, $orgId: ID!, $roleId: ID!, $identityKey: String!, $handlers: [ServiceAccountHandlerInput], $serverWrappedKeyring: String, $serverWrappedRecovery: String) {\n createServiceAccount(\n name: $name\n organisationId: $orgId\n roleId: $roleId\n identityKey: $identityKey\n handlers: $handlers\n serverWrappedKeyring: $serverWrappedKeyring\n serverWrappedRecovery: $serverWrappedRecovery\n ) {\n serviceAccount {\n id\n }\n }\n}": types.CreateServiceAccountOpDocument, + "mutation CreateSAToken($serviceAccountId: ID!, $name: String!, $identityKey: String!, $token: String!, $wrappedKeyShare: String!, $expiry: BigInt) {\n createServiceAccountToken(\n serviceAccountId: $serviceAccountId\n name: $name\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n expiry: $expiry\n ) {\n token {\n id\n }\n }\n}": types.CreateSaTokenDocument, + "mutation DeleteServiceAccountOp($id: ID!) {\n deleteServiceAccount(serviceAccountId: $id) {\n ok\n }\n}": types.DeleteServiceAccountOpDocument, + "mutation DeleteServiceAccountTokenOp($id: ID!) {\n deleteServiceAccountToken(tokenId: $id) {\n ok\n }\n}": types.DeleteServiceAccountTokenOpDocument, + "mutation UpdateServiceAccountHandlerKeys($orgId: ID!, $handlers: [ServiceAccountHandlerInput]) {\n updateServiceAccountHandlers(organisationId: $orgId, handlers: $handlers) {\n ok\n }\n}": types.UpdateServiceAccountHandlerKeysDocument, + "mutation UpdateServiceAccountOp($serviceAccountId: ID!, $name: String!, $roleId: ID!) {\n updateServiceAccount(\n serviceAccountId: $serviceAccountId\n name: $name\n roleId: $roleId\n ) {\n serviceAccount {\n id\n name\n role {\n id\n name\n description\n permissions\n }\n }\n }\n}": types.UpdateServiceAccountOpDocument, "mutation CreateNewAWSSecretsSync($envId: ID!, $path: String!, $credentialId: ID!, $secretName: String!, $kmsId: String) {\n createAwsSecretSync(\n envId: $envId\n path: $path\n credentialId: $credentialId\n secretName: $secretName\n kmsId: $kmsId\n ) {\n sync {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n name\n }\n isActive\n lastSync\n createdAt\n }\n }\n}": types.CreateNewAwsSecretsSyncDocument, "mutation CreateNewCfPagesSync($envId: ID!, $path: String!, $projectName: String!, $deploymentId: ID!, $projectEnv: String!, $credentialId: ID!) {\n createCloudflarePagesSync(\n envId: $envId\n path: $path\n projectName: $projectName\n deploymentId: $deploymentId\n projectEnv: $projectEnv\n credentialId: $credentialId\n ) {\n sync {\n id\n environment {\n id\n name\n envType\n }\n serviceInfo {\n id\n name\n }\n isActive\n lastSync\n createdAt\n }\n }\n}": types.CreateNewCfPagesSyncDocument, "mutation DeleteProviderCreds($credentialId: ID!) {\n deleteProviderCredentials(credentialId: $credentialId) {\n ok\n }\n}": types.DeleteProviderCredsDocument, @@ -69,31 +75,34 @@ const documents = { "mutation CreateNewUserToken($orgId: ID!, $name: String!, $identityKey: String!, $token: String!, $wrappedKeyShare: String!, $expiry: BigInt) {\n createUserToken(\n orgId: $orgId\n name: $name\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n expiry: $expiry\n ) {\n ok\n }\n}": types.CreateNewUserTokenDocument, "mutation RevokeUserToken($tokenId: ID!) {\n deleteUserToken(tokenId: $tokenId) {\n ok\n }\n}": types.RevokeUserTokenDocument, "query GetAppMembers($appId: ID!) {\n appUsers(appId: $appId) {\n id\n identityKey\n email\n fullName\n avatarUrl\n createdAt\n role {\n id\n name\n description\n permissions\n color\n }\n }\n}": types.GetAppMembersDocument, + "query GetAppServiceAccounts($appId: ID!) {\n appServiceAccounts(appId: $appId) {\n id\n identityKey\n name\n createdAt\n role {\n id\n name\n description\n permissions\n color\n }\n tokens {\n id\n name\n }\n }\n}": types.GetAppServiceAccountsDocument, "query GetCheckoutDetails($stripeSessionId: String!) {\n stripeCheckoutDetails(stripeSessionId: $stripeSessionId) {\n paymentStatus\n customerEmail\n billingStartDate\n billingEndDate\n subscriptionId\n planName\n }\n}": types.GetCheckoutDetailsDocument, "query GetAppActivityChart($appId: ID!, $period: TimeRange) {\n appActivityChart(appId: $appId, period: $period) {\n index\n date\n data\n }\n}": types.GetAppActivityChartDocument, "query GetAppDetail($organisationId: ID!, $appId: ID!) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n appToken\n appSeed\n appVersion\n sseEnabled\n }\n}": types.GetAppDetailDocument, "query GetAppKmsLogs($appId: ID!, $start: BigInt, $end: BigInt) {\n logs(appId: $appId, start: $start, end: $end) {\n kms {\n id\n timestamp\n phaseNode\n eventType\n ipAddress\n country\n city\n phSize\n }\n }\n kmsLogsCount(appId: $appId)\n}": types.GetAppKmsLogsDocument, - "query GetApps($organisationId: ID!, $appId: ID) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n sseEnabled\n members {\n id\n email\n fullName\n avatarUrl\n }\n environments {\n id\n name\n envType\n syncs {\n id\n serviceInfo {\n id\n name\n provider {\n id\n name\n }\n }\n status\n }\n }\n }\n}": types.GetAppsDocument, + "query GetApps($organisationId: ID!, $appId: ID) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n sseEnabled\n members {\n id\n email\n fullName\n avatarUrl\n }\n serviceAccounts {\n id\n name\n }\n environments {\n id\n name\n envType\n syncs {\n id\n serviceInfo {\n id\n name\n provider {\n id\n name\n }\n }\n status\n }\n }\n }\n}": types.GetAppsDocument, "query GetDashboard($organisationId: ID!) {\n apps(organisationId: $organisationId) {\n id\n sseEnabled\n }\n userTokens(organisationId: $organisationId) {\n id\n }\n organisationInvites(orgId: $organisationId) {\n id\n }\n organisationMembers(organisationId: $organisationId, role: null) {\n id\n }\n savedCredentials(orgId: $organisationId) {\n id\n }\n syncs(orgId: $organisationId) {\n id\n }\n}": types.GetDashboardDocument, - "query GetOrganisations {\n organisations {\n id\n name\n identityKey\n createdAt\n plan\n planDetail {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n userCount\n appCount\n }\n role {\n name\n description\n color\n permissions\n }\n memberId\n keyring\n recovery\n }\n}": types.GetOrganisationsDocument, + "query GetOrganisations {\n organisations {\n id\n name\n identityKey\n createdAt\n plan\n planDetail {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n seatsUsed {\n users\n serviceAccounts\n total\n }\n appCount\n }\n role {\n name\n description\n color\n permissions\n }\n memberId\n keyring\n recovery\n }\n}": types.GetOrganisationsDocument, "query CheckOrganisationNameAvailability($name: String!) {\n organisationNameAvailable(name: $name)\n}": types.CheckOrganisationNameAvailabilityDocument, "query GetGlobalAccessUsers($organisationId: ID!) {\n organisationGlobalAccessUsers(organisationId: $organisationId) {\n id\n role {\n name\n permissions\n }\n identityKey\n self\n }\n}": types.GetGlobalAccessUsersDocument, "query GetInvites($orgId: ID!) {\n organisationInvites(orgId: $orgId) {\n id\n createdAt\n expiresAt\n invitedBy {\n email\n fullName\n self\n }\n inviteeEmail\n }\n}": types.GetInvitesDocument, "query GetLicenseData {\n license {\n id\n customerName\n organisationName\n expiresAt\n plan\n seats\n isActivated\n organisationOwner {\n fullName\n email\n }\n }\n}": types.GetLicenseDataDocument, "query GetOrgLicense($organisationId: ID!) {\n organisationLicense(organisationId: $organisationId) {\n id\n customerName\n issuedAt\n expiresAt\n activatedAt\n plan\n seats\n tokens\n }\n}": types.GetOrgLicenseDocument, "query GetOrganisationMembers($organisationId: ID!, $role: [String]) {\n organisationMembers(organisationId: $organisationId, role: $role) {\n id\n role {\n id\n name\n description\n permissions\n color\n }\n identityKey\n email\n fullName\n avatarUrl\n createdAt\n self\n }\n}": types.GetOrganisationMembersDocument, - "query GetOrganisationPlan($organisationId: ID!) {\n organisationPlan(organisationId: $organisationId) {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n userCount\n appCount\n }\n}": types.GetOrganisationPlanDocument, + "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 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 email\n }\n apps {\n id\n name\n }\n }\n}": types.VerifyInviteDocument, - "query GetAppEnvironments($appId: ID!, $memberId: ID) {\n appEnvironments(appId: $appId, environmentId: null, memberId: $memberId) {\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 GetAppSecretsLogs($appId: ID!, $start: BigInt, $end: BigInt) {\n logs(appId: $appId, start: $start, end: $end) {\n secrets {\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 eventType\n environment {\n id\n envType\n name\n }\n secret {\n id\n path\n }\n }\n }\n secretsLogsCount(appId: $appId)\n environmentKeys(appId: $appId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n environment {\n id\n }\n }\n}": types.GetAppSecretsLogsDocument, + "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 GetAppSecretsLogs($appId: ID!, $start: BigInt, $end: BigInt) {\n logs(appId: $appId, start: $start, end: $end) {\n secrets {\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 }\n serviceAccountToken {\n id\n name\n }\n eventType\n environment {\n id\n envType\n name\n }\n secret {\n id\n path\n }\n }\n }\n secretsLogsCount(appId: $appId)\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, "query GetFolders($envId: ID!, $path: String) {\n folders(envId: $envId, path: $path) {\n id\n name\n path\n createdAt\n folderCount\n secretCount\n }\n}": types.GetFoldersDocument, "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 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 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 eventType\n }\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 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 }\n eventType\n }\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 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 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, + "query GetServiceAccounts($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 tokens {\n id\n name\n createdAt\n expiresAt\n createdBy {\n fullName\n avatarUrl\n self\n }\n lastUsed\n }\n appMemberships {\n id\n name\n environments {\n id\n name\n }\n }\n }\n}": types.GetServiceAccountsDocument, "query GetOrganisationSyncs($orgId: ID!) {\n syncs(orgId: $orgId) {\n id\n environment {\n id\n name\n envType\n app {\n id\n name\n }\n }\n path\n serviceInfo {\n id\n name\n provider {\n id\n }\n }\n options\n isActive\n lastSync\n status\n authentication {\n id\n name\n credentials\n }\n createdAt\n history {\n id\n status\n createdAt\n completedAt\n meta\n }\n }\n savedCredentials(orgId: $orgId) {\n id\n name\n credentials\n createdAt\n provider {\n id\n name\n expectedCredentials\n optionalCredentials\n }\n syncCount\n }\n apps(organisationId: $orgId, appId: null) {\n id\n name\n identityKey\n createdAt\n sseEnabled\n members {\n id\n }\n environments {\n id\n name\n syncs {\n id\n serviceInfo {\n id\n name\n provider {\n id\n name\n }\n }\n status\n }\n }\n }\n}": types.GetOrganisationSyncsDocument, "query GetAwsSecrets($credentialId: ID!) {\n awsSecrets(credentialId: $credentialId) {\n name\n arn\n }\n}": types.GetAwsSecretsDocument, "query GetCfPages($credentialId: ID!) {\n cloudflarePagesProjects(credentialId: $credentialId) {\n name\n deploymentId\n environments\n }\n}": types.GetCfPagesDocument, @@ -140,15 +149,15 @@ export function graphql(source: "mutation UpdateRole($id: ID!, $name: String!, $ /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "mutation AddMemberToApp($memberId: ID!, $appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n addAppMember(memberId: $memberId, appId: $appId, envKeys: $envKeys) {\n app {\n id\n }\n }\n}"): (typeof documents)["mutation AddMemberToApp($memberId: ID!, $appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n addAppMember(memberId: $memberId, appId: $appId, envKeys: $envKeys) {\n app {\n id\n }\n }\n}"]; +export function graphql(source: "mutation AddMemberToApp($memberId: ID!, $memberType: MemberType, $appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n addAppMember(\n memberId: $memberId\n memberType: $memberType\n appId: $appId\n envKeys: $envKeys\n ) {\n app {\n id\n }\n }\n}"): (typeof documents)["mutation AddMemberToApp($memberId: ID!, $memberType: MemberType, $appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n addAppMember(\n memberId: $memberId\n memberType: $memberType\n appId: $appId\n envKeys: $envKeys\n ) {\n app {\n id\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 RemoveMemberFromApp($memberId: ID!, $appId: ID!) {\n removeAppMember(memberId: $memberId, appId: $appId) {\n app {\n id\n }\n }\n}"): (typeof documents)["mutation RemoveMemberFromApp($memberId: ID!, $appId: ID!) {\n removeAppMember(memberId: $memberId, appId: $appId) {\n app {\n id\n }\n }\n}"]; +export function graphql(source: "mutation RemoveMemberFromApp($memberId: ID!, $memberType: MemberType, $appId: ID!) {\n removeAppMember(memberId: $memberId, memberType: $memberType, appId: $appId) {\n app {\n id\n }\n }\n}"): (typeof documents)["mutation RemoveMemberFromApp($memberId: ID!, $memberType: MemberType, $appId: ID!) {\n removeAppMember(memberId: $memberId, memberType: $memberType, appId: $appId) {\n app {\n id\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 UpdateEnvScope($memberId: ID!, $appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n updateMemberEnvironmentScope(\n memberId: $memberId\n appId: $appId\n envKeys: $envKeys\n ) {\n app {\n id\n }\n }\n}"): (typeof documents)["mutation UpdateEnvScope($memberId: ID!, $appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n updateMemberEnvironmentScope(\n memberId: $memberId\n appId: $appId\n envKeys: $envKeys\n ) {\n app {\n id\n }\n }\n}"]; +export function graphql(source: "mutation UpdateEnvScope($memberId: ID!, $memberType: MemberType, $appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n updateMemberEnvironmentScope(\n memberId: $memberId\n memberType: $memberType\n appId: $appId\n envKeys: $envKeys\n ) {\n app {\n id\n }\n }\n}"): (typeof documents)["mutation UpdateEnvScope($memberId: ID!, $memberType: MemberType, $appId: ID!, $envKeys: [EnvironmentKeyInput]) {\n updateMemberEnvironmentScope(\n memberId: $memberId\n memberType: $memberType\n appId: $appId\n envKeys: $envKeys\n ) {\n app {\n id\n }\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -273,6 +282,30 @@ export function graphql(source: "mutation UpdateWrappedSecrets($orgId: ID!, $wra * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "mutation RotateAppKey($id: ID!, $appToken: String!, $wrappedKeyShare: String!) {\n rotateAppKeys(id: $id, appToken: $appToken, wrappedKeyShare: $wrappedKeyShare) {\n app {\n id\n }\n }\n}"): (typeof documents)["mutation RotateAppKey($id: ID!, $appToken: String!, $wrappedKeyShare: String!) {\n rotateAppKeys(id: $id, appToken: $appToken, wrappedKeyShare: $wrappedKeyShare) {\n app {\n id\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 CreateServiceAccountOp($name: String!, $orgId: ID!, $roleId: ID!, $identityKey: String!, $handlers: [ServiceAccountHandlerInput], $serverWrappedKeyring: String, $serverWrappedRecovery: String) {\n createServiceAccount(\n name: $name\n organisationId: $orgId\n roleId: $roleId\n identityKey: $identityKey\n handlers: $handlers\n serverWrappedKeyring: $serverWrappedKeyring\n serverWrappedRecovery: $serverWrappedRecovery\n ) {\n serviceAccount {\n id\n }\n }\n}"): (typeof documents)["mutation CreateServiceAccountOp($name: String!, $orgId: ID!, $roleId: ID!, $identityKey: String!, $handlers: [ServiceAccountHandlerInput], $serverWrappedKeyring: String, $serverWrappedRecovery: String) {\n createServiceAccount(\n name: $name\n organisationId: $orgId\n roleId: $roleId\n identityKey: $identityKey\n handlers: $handlers\n serverWrappedKeyring: $serverWrappedKeyring\n serverWrappedRecovery: $serverWrappedRecovery\n ) {\n serviceAccount {\n id\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 CreateSAToken($serviceAccountId: ID!, $name: String!, $identityKey: String!, $token: String!, $wrappedKeyShare: String!, $expiry: BigInt) {\n createServiceAccountToken(\n serviceAccountId: $serviceAccountId\n name: $name\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n expiry: $expiry\n ) {\n token {\n id\n }\n }\n}"): (typeof documents)["mutation CreateSAToken($serviceAccountId: ID!, $name: String!, $identityKey: String!, $token: String!, $wrappedKeyShare: String!, $expiry: BigInt) {\n createServiceAccountToken(\n serviceAccountId: $serviceAccountId\n name: $name\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n expiry: $expiry\n ) {\n token {\n id\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 DeleteServiceAccountOp($id: ID!) {\n deleteServiceAccount(serviceAccountId: $id) {\n ok\n }\n}"): (typeof documents)["mutation DeleteServiceAccountOp($id: ID!) {\n deleteServiceAccount(serviceAccountId: $id) {\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 DeleteServiceAccountTokenOp($id: ID!) {\n deleteServiceAccountToken(tokenId: $id) {\n ok\n }\n}"): (typeof documents)["mutation DeleteServiceAccountTokenOp($id: ID!) {\n deleteServiceAccountToken(tokenId: $id) {\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 UpdateServiceAccountHandlerKeys($orgId: ID!, $handlers: [ServiceAccountHandlerInput]) {\n updateServiceAccountHandlers(organisationId: $orgId, handlers: $handlers) {\n ok\n }\n}"): (typeof documents)["mutation UpdateServiceAccountHandlerKeys($orgId: ID!, $handlers: [ServiceAccountHandlerInput]) {\n updateServiceAccountHandlers(organisationId: $orgId, handlers: $handlers) {\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 UpdateServiceAccountOp($serviceAccountId: ID!, $name: String!, $roleId: ID!) {\n updateServiceAccount(\n serviceAccountId: $serviceAccountId\n name: $name\n roleId: $roleId\n ) {\n serviceAccount {\n id\n name\n role {\n id\n name\n description\n permissions\n }\n }\n }\n}"): (typeof documents)["mutation UpdateServiceAccountOp($serviceAccountId: ID!, $name: String!, $roleId: ID!) {\n updateServiceAccount(\n serviceAccountId: $serviceAccountId\n name: $name\n roleId: $roleId\n ) {\n serviceAccount {\n id\n name\n role {\n id\n name\n description\n permissions\n }\n }\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -349,6 +382,10 @@ export function graphql(source: "mutation RevokeUserToken($tokenId: ID!) {\n de * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "query GetAppMembers($appId: ID!) {\n appUsers(appId: $appId) {\n id\n identityKey\n email\n fullName\n avatarUrl\n createdAt\n role {\n id\n name\n description\n permissions\n color\n }\n }\n}"): (typeof documents)["query GetAppMembers($appId: ID!) {\n appUsers(appId: $appId) {\n id\n identityKey\n email\n fullName\n avatarUrl\n createdAt\n role {\n id\n name\n description\n permissions\n color\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 GetAppServiceAccounts($appId: ID!) {\n appServiceAccounts(appId: $appId) {\n id\n identityKey\n name\n createdAt\n role {\n id\n name\n description\n permissions\n color\n }\n tokens {\n id\n name\n }\n }\n}"): (typeof documents)["query GetAppServiceAccounts($appId: ID!) {\n appServiceAccounts(appId: $appId) {\n id\n identityKey\n name\n createdAt\n role {\n id\n name\n description\n permissions\n color\n }\n tokens {\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. */ @@ -368,7 +405,7 @@ export function graphql(source: "query GetAppKmsLogs($appId: ID!, $start: BigInt /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query GetApps($organisationId: ID!, $appId: ID) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n sseEnabled\n members {\n id\n email\n fullName\n avatarUrl\n }\n environments {\n id\n name\n envType\n syncs {\n id\n serviceInfo {\n id\n name\n provider {\n id\n name\n }\n }\n status\n }\n }\n }\n}"): (typeof documents)["query GetApps($organisationId: ID!, $appId: ID) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n sseEnabled\n members {\n id\n email\n fullName\n avatarUrl\n }\n environments {\n id\n name\n envType\n syncs {\n id\n serviceInfo {\n id\n name\n provider {\n id\n name\n }\n }\n status\n }\n }\n }\n}"]; +export function graphql(source: "query GetApps($organisationId: ID!, $appId: ID) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n sseEnabled\n members {\n id\n email\n fullName\n avatarUrl\n }\n serviceAccounts {\n id\n name\n }\n environments {\n id\n name\n envType\n syncs {\n id\n serviceInfo {\n id\n name\n provider {\n id\n name\n }\n }\n status\n }\n }\n }\n}"): (typeof documents)["query GetApps($organisationId: ID!, $appId: ID) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\n createdAt\n sseEnabled\n members {\n id\n email\n fullName\n avatarUrl\n }\n serviceAccounts {\n id\n name\n }\n environments {\n id\n name\n envType\n syncs {\n id\n serviceInfo {\n id\n name\n provider {\n id\n name\n }\n }\n status\n }\n }\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -376,7 +413,7 @@ export function graphql(source: "query GetDashboard($organisationId: ID!) {\n a /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query GetOrganisations {\n organisations {\n id\n name\n identityKey\n createdAt\n plan\n planDetail {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n userCount\n appCount\n }\n role {\n name\n description\n color\n permissions\n }\n memberId\n keyring\n recovery\n }\n}"): (typeof documents)["query GetOrganisations {\n organisations {\n id\n name\n identityKey\n createdAt\n plan\n planDetail {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n userCount\n appCount\n }\n role {\n name\n description\n color\n permissions\n }\n memberId\n keyring\n recovery\n }\n}"]; +export function graphql(source: "query GetOrganisations {\n organisations {\n id\n name\n identityKey\n createdAt\n plan\n planDetail {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n seatsUsed {\n users\n serviceAccounts\n total\n }\n appCount\n }\n role {\n name\n description\n color\n permissions\n }\n memberId\n keyring\n recovery\n }\n}"): (typeof documents)["query GetOrganisations {\n organisations {\n id\n name\n identityKey\n createdAt\n plan\n planDetail {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n seatsUsed {\n users\n serviceAccounts\n total\n }\n appCount\n }\n role {\n name\n description\n color\n permissions\n }\n memberId\n keyring\n recovery\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -404,7 +441,7 @@ export function graphql(source: "query GetOrganisationMembers($organisationId: I /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query GetOrganisationPlan($organisationId: ID!) {\n organisationPlan(organisationId: $organisationId) {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n userCount\n appCount\n }\n}"): (typeof documents)["query GetOrganisationPlan($organisationId: ID!) {\n organisationPlan(organisationId: $organisationId) {\n name\n maxUsers\n maxApps\n maxEnvsPerApp\n userCount\n appCount\n }\n}"]; +export function graphql(source: "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 appCount\n }\n}"): (typeof 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 appCount\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -416,11 +453,11 @@ export function graphql(source: "query VerifyInvite($inviteId: ID!) {\n validat /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query GetAppEnvironments($appId: ID!, $memberId: ID) {\n appEnvironments(appId: $appId, environmentId: null, memberId: $memberId) {\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}"): (typeof documents)["query GetAppEnvironments($appId: ID!, $memberId: ID) {\n appEnvironments(appId: $appId, environmentId: null, memberId: $memberId) {\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}"]; +export function graphql(source: "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}"): (typeof documents)["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}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "query GetAppSecretsLogs($appId: ID!, $start: BigInt, $end: BigInt) {\n logs(appId: $appId, start: $start, end: $end) {\n secrets {\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 eventType\n environment {\n id\n envType\n name\n }\n secret {\n id\n path\n }\n }\n }\n secretsLogsCount(appId: $appId)\n environmentKeys(appId: $appId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n environment {\n id\n }\n }\n}"): (typeof documents)["query GetAppSecretsLogs($appId: ID!, $start: BigInt, $end: BigInt) {\n logs(appId: $appId, start: $start, end: $end) {\n secrets {\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 eventType\n environment {\n id\n envType\n name\n }\n secret {\n id\n path\n }\n }\n }\n secretsLogsCount(appId: $appId)\n environmentKeys(appId: $appId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n environment {\n id\n }\n }\n}"]; +export function graphql(source: "query GetAppSecretsLogs($appId: ID!, $start: BigInt, $end: BigInt) {\n logs(appId: $appId, start: $start, end: $end) {\n secrets {\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 }\n serviceAccountToken {\n id\n name\n }\n eventType\n environment {\n id\n envType\n name\n }\n secret {\n id\n path\n }\n }\n }\n secretsLogsCount(appId: $appId)\n environmentKeys(appId: $appId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n environment {\n id\n }\n }\n}"): (typeof documents)["query GetAppSecretsLogs($appId: ID!, $start: BigInt, $end: BigInt) {\n logs(appId: $appId, start: $start, end: $end) {\n secrets {\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 }\n serviceAccountToken {\n id\n name\n }\n eventType\n environment {\n id\n envType\n name\n }\n secret {\n id\n path\n }\n }\n }\n secretsLogsCount(appId: $appId)\n environmentKeys(appId: $appId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n environment {\n id\n }\n }\n}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -444,11 +481,19 @@ 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 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 eventType\n }\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 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 eventType\n }\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 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 }\n eventType\n }\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 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 }\n eventType\n }\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}"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ export function graphql(source: "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}"): (typeof documents)["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}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "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}"): (typeof documents)["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}"]; +/** + * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. + */ +export function graphql(source: "query GetServiceAccounts($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 tokens {\n id\n name\n createdAt\n expiresAt\n createdBy {\n fullName\n avatarUrl\n self\n }\n lastUsed\n }\n appMemberships {\n id\n name\n environments {\n id\n name\n }\n }\n }\n}"): (typeof documents)["query GetServiceAccounts($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 tokens {\n id\n name\n createdAt\n expiresAt\n createdBy {\n fullName\n avatarUrl\n self\n }\n lastUsed\n }\n appMemberships {\n id\n name\n environments {\n id\n name\n }\n }\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 d1e475dba..09a36cba1 100644 --- a/frontend/apollo/graphql.ts +++ b/frontend/apollo/graphql.ts @@ -147,6 +147,7 @@ export type AppType = { identityKey: Scalars['String']['output']; members: Array>; name: Scalars['String']['output']; + serviceAccounts: Array; sseEnabled: Scalars['Boolean']['output']; wrappedKeyShare: Scalars['String']['output']; }; @@ -280,6 +281,16 @@ export type CreateSecretTagMutation = { tag?: Maybe; }; +export type CreateServiceAccountMutation = { + __typename?: 'CreateServiceAccountMutation'; + serviceAccount?: Maybe; +}; + +export type CreateServiceAccountTokenMutation = { + __typename?: 'CreateServiceAccountTokenMutation'; + token?: Maybe; +}; + export type CreateServiceTokenMutation = { __typename?: 'CreateServiceTokenMutation'; serviceToken?: Maybe; @@ -346,6 +357,16 @@ export type DeleteSecretMutation = { secret?: Maybe; }; +export type DeleteServiceAccountMutation = { + __typename?: 'DeleteServiceAccountMutation'; + ok?: Maybe; +}; + +export type DeleteServiceAccountTokenMutation = { + __typename?: 'DeleteServiceAccountTokenMutation'; + ok?: Maybe; +}; + export type DeleteServiceTokenMutation = { __typename?: 'DeleteServiceTokenMutation'; ok?: Maybe; @@ -366,6 +387,11 @@ export type EditSecretMutation = { secret?: Maybe; }; +export type EnableServiceAccountThirdPartyAuthMutation = { + __typename?: 'EnableServiceAccountThirdPartyAuthMutation'; + serviceAccount?: Maybe; +}; + export type EnvironmentInput = { appId: Scalars['ID']['input']; envType: Scalars['String']['input']; @@ -555,6 +581,11 @@ export type LogsResponseType = { secrets?: Maybe>>; }; +export enum MemberType { + Service = 'SERVICE', + User = 'USER' +} + export type Mutation = { __typename?: 'Mutation'; addAppMember?: Maybe; @@ -579,6 +610,8 @@ export type Mutation = { createSecretFolder?: Maybe; createSecretTag?: Maybe; createSecrets?: Maybe; + createServiceAccount?: Maybe; + createServiceAccountToken?: Maybe; createServiceToken?: Maybe; createUserToken?: Maybe; createVaultSync?: Maybe; @@ -593,10 +626,13 @@ export type Mutation = { deleteSecret?: Maybe; deleteSecretFolder?: Maybe; deleteSecrets?: Maybe; + deleteServiceAccount?: Maybe; + deleteServiceAccountToken?: Maybe; deleteServiceToken?: Maybe; deleteUserToken?: Maybe; editSecret?: Maybe; editSecrets?: Maybe; + enableServiceAccountThirdPartyAuth?: Maybe; initEnvSync?: Maybe; inviteOrganisationMember?: Maybe; readSecret?: Maybe; @@ -612,6 +648,8 @@ export type Mutation = { updateMemberWrappedSecrets?: Maybe; updateOrganisationMemberRole?: Maybe; updateProviderCredentials?: Maybe; + updateServiceAccount?: Maybe; + updateServiceAccountHandlers?: Maybe; updateSyncAuthentication?: Maybe; }; @@ -620,6 +658,7 @@ export type MutationAddAppMemberArgs = { appId?: InputMaybe; envKeys?: InputMaybe>>; memberId?: InputMaybe; + memberType?: InputMaybe; }; @@ -795,6 +834,27 @@ export type MutationCreateSecretsArgs = { }; +export type MutationCreateServiceAccountArgs = { + handlers?: InputMaybe>>; + identityKey?: InputMaybe; + name?: InputMaybe; + organisationId?: InputMaybe; + roleId?: InputMaybe; + serverWrappedKeyring?: InputMaybe; + serverWrappedRecovery?: InputMaybe; +}; + + +export type MutationCreateServiceAccountTokenArgs = { + expiry?: InputMaybe; + identityKey: Scalars['String']['input']; + name: Scalars['String']['input']; + serviceAccountId?: InputMaybe; + token: Scalars['String']['input']; + wrappedKeyShare: Scalars['String']['input']; +}; + + export type MutationCreateServiceTokenArgs = { appId: Scalars['ID']['input']; environmentKeys?: InputMaybe>>; @@ -886,6 +946,16 @@ export type MutationDeleteSecretsArgs = { }; +export type MutationDeleteServiceAccountArgs = { + serviceAccountId?: InputMaybe; +}; + + +export type MutationDeleteServiceAccountTokenArgs = { + tokenId?: InputMaybe; +}; + + export type MutationDeleteServiceTokenArgs = { tokenId: Scalars['ID']['input']; }; @@ -907,6 +977,13 @@ export type MutationEditSecretsArgs = { }; +export type MutationEnableServiceAccountThirdPartyAuthArgs = { + serverWrappedKeyring?: InputMaybe; + serverWrappedRecovery?: InputMaybe; + serviceAccountId?: InputMaybe; +}; + + export type MutationInitEnvSyncArgs = { appId?: InputMaybe; envKeys?: InputMaybe>>; @@ -928,6 +1005,7 @@ export type MutationReadSecretArgs = { export type MutationRemoveAppMemberArgs = { appId?: InputMaybe; memberId?: InputMaybe; + memberType?: InputMaybe; }; @@ -978,6 +1056,7 @@ export type MutationUpdateMemberEnvironmentScopeArgs = { appId?: InputMaybe; envKeys?: InputMaybe>>; memberId?: InputMaybe; + memberType?: InputMaybe; }; @@ -1001,6 +1080,19 @@ export type MutationUpdateProviderCredentialsArgs = { }; +export type MutationUpdateServiceAccountArgs = { + name?: InputMaybe; + roleId?: InputMaybe; + serviceAccountId?: InputMaybe; +}; + + +export type MutationUpdateServiceAccountHandlersArgs = { + handlers?: InputMaybe>>; + organisationId?: InputMaybe; +}; + + export type MutationUpdateSyncAuthenticationArgs = { credentialId?: InputMaybe; syncId?: InputMaybe; @@ -1055,7 +1147,7 @@ export type OrganisationPlanType = { maxEnvsPerApp?: Maybe; maxUsers?: Maybe; name?: Maybe; - userCount?: Maybe; + seatsUsed?: Maybe; }; export type OrganisationType = { @@ -1136,6 +1228,7 @@ export type Query = { __typename?: 'Query'; appActivityChart?: Maybe>>; appEnvironments?: Maybe>>; + appServiceAccounts?: Maybe>>; appUsers?: Maybe>>; apps?: Maybe>>; awsSecrets?: Maybe>>; @@ -1166,6 +1259,8 @@ export type Query = { secrets?: Maybe>>; secretsLogsCount?: Maybe; serverPublicKey?: Maybe; + serviceAccountHandlers?: Maybe>>; + serviceAccounts?: Maybe>>; serviceTokens?: Maybe>>; services?: Maybe>>; sseEnabled?: Maybe; @@ -1190,6 +1285,12 @@ export type QueryAppEnvironmentsArgs = { appId?: InputMaybe; environmentId?: InputMaybe; memberId?: InputMaybe; + memberType?: InputMaybe; +}; + + +export type QueryAppServiceAccountsArgs = { + appId?: InputMaybe; }; @@ -1333,6 +1434,17 @@ export type QuerySecretsLogsCountArgs = { }; +export type QueryServiceAccountHandlersArgs = { + orgId?: InputMaybe; +}; + + +export type QueryServiceAccountsArgs = { + orgId?: InputMaybe; + serviceAccountId?: InputMaybe; +}; + + export type QueryServiceTokensArgs = { appId?: InputMaybe; }; @@ -1440,6 +1552,13 @@ export type RotateAppKeysMutation = { app?: Maybe; }; +export type SeatsUsed = { + __typename?: 'SeatsUsed'; + serviceAccounts?: Maybe; + total?: Maybe; + users?: Maybe; +}; + export type SecretEventType = { __typename?: 'SecretEventType'; comment: Scalars['String']['output']; @@ -1450,6 +1569,8 @@ export type SecretEventType = { key: Scalars['String']['output']; path: Scalars['String']['output']; secret: SecretType; + serviceAccount?: Maybe; + serviceAccountToken?: Maybe; serviceToken?: Maybe; tags: Array; timestamp: Scalars['DateTime']['output']; @@ -1517,6 +1638,55 @@ export type ServerEnvironmentKeyType = { wrappedSeed: Scalars['String']['output']; }; +export type ServiceAccountHandlerInput = { + memberId?: InputMaybe; + serviceAccountId?: InputMaybe; + wrappedKeyring: Scalars['String']['input']; + wrappedRecovery: Scalars['String']['input']; +}; + +export type ServiceAccountHandlerType = { + __typename?: 'ServiceAccountHandlerType'; + createdAt?: Maybe; + id: Scalars['String']['output']; + serviceAccount: ServiceAccountType; + updatedAt: Scalars['DateTime']['output']; + user: OrganisationMemberType; + wrappedKeyring: Scalars['String']['output']; + wrappedRecovery: Scalars['String']['output']; +}; + +export type ServiceAccountTokenType = { + __typename?: 'ServiceAccountTokenType'; + createdAt?: Maybe; + createdBy?: Maybe; + deletedAt?: Maybe; + expiresAt?: Maybe; + id: Scalars['String']['output']; + identityKey: Scalars['String']['output']; + lastUsed?: Maybe; + name: Scalars['String']['output']; + secreteventSet: Array; + serviceAccount: ServiceAccountType; + token: Scalars['String']['output']; + updatedAt: Scalars['DateTime']['output']; + wrappedKeyShare: Scalars['String']['output']; +}; + +export type ServiceAccountType = { + __typename?: 'ServiceAccountType'; + appMemberships?: Maybe>; + createdAt?: Maybe; + handlers?: Maybe>>; + id: Scalars['String']['output']; + identityKey?: Maybe; + name: Scalars['String']['output']; + role?: Maybe; + thirdPartyAuthEnabled?: Maybe; + tokens?: Maybe>>; + updatedAt: Scalars['DateTime']['output']; +}; + export type ServiceTokenType = { __typename?: 'ServiceTokenType'; createdAt?: Maybe; @@ -1593,6 +1763,16 @@ export type UpdateProviderCredentials = { credential?: Maybe; }; +export type UpdateServiceAccountHandlersMutation = { + __typename?: 'UpdateServiceAccountHandlersMutation'; + ok?: Maybe; +}; + +export type UpdateServiceAccountMutation = { + __typename?: 'UpdateServiceAccountMutation'; + serviceAccount?: Maybe; +}; + export type UpdateSyncAuthentication = { __typename?: 'UpdateSyncAuthentication'; sync?: Maybe; @@ -1653,6 +1833,7 @@ export type UpdateRoleMutation = { __typename?: 'Mutation', updateCustomRole?: { export type AddMemberToAppMutationVariables = Exact<{ memberId: Scalars['ID']['input']; + memberType?: InputMaybe; appId: Scalars['ID']['input']; envKeys?: InputMaybe> | InputMaybe>; }>; @@ -1662,6 +1843,7 @@ export type AddMemberToAppMutation = { __typename?: 'Mutation', addAppMember?: { export type RemoveMemberFromAppMutationVariables = Exact<{ memberId: Scalars['ID']['input']; + memberType?: InputMaybe; appId: Scalars['ID']['input']; }>; @@ -1670,6 +1852,7 @@ export type RemoveMemberFromAppMutation = { __typename?: 'Mutation', removeAppMe export type UpdateEnvScopeMutationVariables = Exact<{ memberId: Scalars['ID']['input']; + memberType?: InputMaybe; appId: Scalars['ID']['input']; envKeys?: InputMaybe> | InputMaybe>; }>; @@ -1948,6 +2131,62 @@ export type RotateAppKeyMutationVariables = Exact<{ export type RotateAppKeyMutation = { __typename?: 'Mutation', rotateAppKeys?: { __typename?: 'RotateAppKeysMutation', app?: { __typename?: 'AppType', id: string } | null } | null }; +export type CreateServiceAccountOpMutationVariables = Exact<{ + name: Scalars['String']['input']; + orgId: Scalars['ID']['input']; + roleId: Scalars['ID']['input']; + identityKey: Scalars['String']['input']; + handlers?: InputMaybe> | InputMaybe>; + serverWrappedKeyring?: InputMaybe; + serverWrappedRecovery?: InputMaybe; +}>; + + +export type CreateServiceAccountOpMutation = { __typename?: 'Mutation', createServiceAccount?: { __typename?: 'CreateServiceAccountMutation', serviceAccount?: { __typename?: 'ServiceAccountType', id: string } | null } | null }; + +export type CreateSaTokenMutationVariables = Exact<{ + serviceAccountId: Scalars['ID']['input']; + name: Scalars['String']['input']; + identityKey: Scalars['String']['input']; + token: Scalars['String']['input']; + wrappedKeyShare: Scalars['String']['input']; + expiry?: InputMaybe; +}>; + + +export type CreateSaTokenMutation = { __typename?: 'Mutation', createServiceAccountToken?: { __typename?: 'CreateServiceAccountTokenMutation', token?: { __typename?: 'ServiceAccountTokenType', id: string } | null } | null }; + +export type DeleteServiceAccountOpMutationVariables = Exact<{ + id: Scalars['ID']['input']; +}>; + + +export type DeleteServiceAccountOpMutation = { __typename?: 'Mutation', deleteServiceAccount?: { __typename?: 'DeleteServiceAccountMutation', ok?: boolean | null } | null }; + +export type DeleteServiceAccountTokenOpMutationVariables = Exact<{ + id: Scalars['ID']['input']; +}>; + + +export type DeleteServiceAccountTokenOpMutation = { __typename?: 'Mutation', deleteServiceAccountToken?: { __typename?: 'DeleteServiceAccountTokenMutation', ok?: boolean | null } | null }; + +export type UpdateServiceAccountHandlerKeysMutationVariables = Exact<{ + orgId: Scalars['ID']['input']; + handlers?: InputMaybe> | InputMaybe>; +}>; + + +export type UpdateServiceAccountHandlerKeysMutation = { __typename?: 'Mutation', updateServiceAccountHandlers?: { __typename?: 'UpdateServiceAccountHandlersMutation', ok?: boolean | null } | null }; + +export type UpdateServiceAccountOpMutationVariables = Exact<{ + serviceAccountId: Scalars['ID']['input']; + name: Scalars['String']['input']; + roleId: Scalars['ID']['input']; +}>; + + +export type UpdateServiceAccountOpMutation = { __typename?: 'Mutation', updateServiceAccount?: { __typename?: 'UpdateServiceAccountMutation', serviceAccount?: { __typename?: 'ServiceAccountType', id: string, name: string, role?: { __typename?: 'RoleType', id: string, name?: string | null, description?: string | null, permissions?: any | null } | null } | null } | null }; + export type CreateNewAwsSecretsSyncMutationVariables = Exact<{ envId: Scalars['ID']['input']; path: Scalars['String']['input']; @@ -2132,6 +2371,13 @@ export type GetAppMembersQueryVariables = Exact<{ export type GetAppMembersQuery = { __typename?: 'Query', appUsers?: Array<{ __typename?: 'OrganisationMemberType', id: string, identityKey?: string | null, email?: string | null, fullName?: string | null, avatarUrl?: string | null, createdAt?: any | null, role?: { __typename?: 'RoleType', id: string, name?: string | null, description?: string | null, permissions?: any | null, color?: string | null } | null } | null> | null }; +export type GetAppServiceAccountsQueryVariables = Exact<{ + appId: Scalars['ID']['input']; +}>; + + +export type GetAppServiceAccountsQuery = { __typename?: 'Query', appServiceAccounts?: Array<{ __typename?: 'ServiceAccountType', id: string, identityKey?: string | null, name: string, createdAt?: any | null, role?: { __typename?: 'RoleType', id: string, name?: string | null, description?: string | null, permissions?: any | null, color?: string | null } | null, tokens?: Array<{ __typename?: 'ServiceAccountTokenType', id: string, name: string } | null> | null } | null> | null }; + export type GetCheckoutDetailsQueryVariables = Exact<{ stripeSessionId: Scalars['String']['input']; }>; @@ -2170,7 +2416,7 @@ export type GetAppsQueryVariables = Exact<{ }>; -export type GetAppsQuery = { __typename?: 'Query', apps?: Array<{ __typename?: 'AppType', id: string, name: string, identityKey: string, createdAt?: any | null, sseEnabled: boolean, members: Array<{ __typename?: 'OrganisationMemberType', id: string, email?: string | null, fullName?: string | null, avatarUrl?: string | null } | null>, environments: Array<{ __typename?: 'EnvironmentType', id: string, name: string, envType: ApiEnvironmentEnvTypeChoices, syncs: Array<{ __typename?: 'EnvironmentSyncType', id: string, status: ApiEnvironmentSyncStatusChoices, serviceInfo?: { __typename?: 'ServiceType', id?: string | null, name?: string | null, provider?: { __typename?: 'ProviderType', id: string, name: string } | null } | null } | null> } | null> } | null> | null }; +export type GetAppsQuery = { __typename?: 'Query', apps?: Array<{ __typename?: 'AppType', id: string, name: string, identityKey: string, createdAt?: any | null, sseEnabled: boolean, members: Array<{ __typename?: 'OrganisationMemberType', id: string, email?: string | null, fullName?: string | null, avatarUrl?: string | null } | null>, serviceAccounts: Array<{ __typename?: 'ServiceAccountType', id: string, name: string }>, environments: Array<{ __typename?: 'EnvironmentType', id: string, name: string, envType: ApiEnvironmentEnvTypeChoices, syncs: Array<{ __typename?: 'EnvironmentSyncType', id: string, status: ApiEnvironmentSyncStatusChoices, serviceInfo?: { __typename?: 'ServiceType', id?: string | null, name?: string | null, provider?: { __typename?: 'ProviderType', id: string, name: string } | null } | null } | null> } | null> } | null> | null }; export type GetDashboardQueryVariables = Exact<{ organisationId: Scalars['ID']['input']; @@ -2182,7 +2428,7 @@ export type GetDashboardQuery = { __typename?: 'Query', apps?: Array<{ __typenam export type GetOrganisationsQueryVariables = Exact<{ [key: string]: never; }>; -export type GetOrganisationsQuery = { __typename?: 'Query', organisations?: Array<{ __typename?: 'OrganisationType', id: string, name: string, identityKey: string, createdAt?: any | null, plan: ApiOrganisationPlanChoices, memberId?: string | null, keyring?: string | null, recovery?: string | null, planDetail?: { __typename?: 'OrganisationPlanType', name?: string | null, maxUsers?: number | null, maxApps?: number | null, maxEnvsPerApp?: number | null, userCount?: number | null, appCount?: number | null } | null, role?: { __typename?: 'RoleType', name?: string | null, description?: string | null, color?: string | null, permissions?: any | null } | null } | null> | null }; +export type GetOrganisationsQuery = { __typename?: 'Query', organisations?: Array<{ __typename?: 'OrganisationType', id: string, name: string, identityKey: string, createdAt?: any | null, plan: ApiOrganisationPlanChoices, memberId?: string | null, keyring?: string | null, recovery?: string | null, planDetail?: { __typename?: 'OrganisationPlanType', name?: string | null, maxUsers?: number | null, maxApps?: number | null, maxEnvsPerApp?: number | null, appCount?: number | null, seatsUsed?: { __typename?: 'SeatsUsed', users?: number | null, serviceAccounts?: number | null, total?: number | null } | null } | null, role?: { __typename?: 'RoleType', name?: string | null, description?: string | null, color?: string | null, permissions?: any | null } | null } | null> | null }; export type CheckOrganisationNameAvailabilityQueryVariables = Exact<{ name: Scalars['String']['input']; @@ -2230,7 +2476,7 @@ export type GetOrganisationPlanQueryVariables = Exact<{ }>; -export type GetOrganisationPlanQuery = { __typename?: 'Query', organisationPlan?: { __typename?: 'OrganisationPlanType', name?: string | null, maxUsers?: number | null, maxApps?: number | null, maxEnvsPerApp?: number | null, userCount?: number | null, appCount?: number | null } | null }; +export type GetOrganisationPlanQuery = { __typename?: 'Query', organisationPlan?: { __typename?: 'OrganisationPlanType', name?: string | null, maxUsers?: number | null, maxApps?: number | null, maxEnvsPerApp?: number | null, appCount?: number | null, seatsUsed?: { __typename?: 'SeatsUsed', users?: number | null, serviceAccounts?: number | null, total?: number | null } | null } | null }; export type GetRolesQueryVariables = Exact<{ orgId: Scalars['ID']['input']; @@ -2249,6 +2495,7 @@ export type VerifyInviteQuery = { __typename?: 'Query', validateInvite?: { __typ export type GetAppEnvironmentsQueryVariables = Exact<{ appId: Scalars['ID']['input']; memberId?: InputMaybe; + memberType?: InputMaybe; }>; @@ -2261,7 +2508,7 @@ export type GetAppSecretsLogsQueryVariables = Exact<{ }>; -export type GetAppSecretsLogsQuery = { __typename?: 'Query', secretsLogsCount?: number | null, logs?: { __typename?: 'LogsResponseType', secrets?: Array<{ __typename?: 'SecretEventType', id: string, path: string, key: string, value: string, version: number, comment: string, timestamp: any, ipAddress?: string | null, userAgent?: string | null, eventType: ApiSecretEventEventTypeChoices, tags: Array<{ __typename?: 'SecretTagType', id: string, name: string, color: string }>, user?: { __typename?: 'OrganisationMemberType', email?: string | null, username?: string | null, fullName?: string | null, avatarUrl?: string | null } | null, serviceToken?: { __typename?: 'ServiceTokenType', id: string, name: string } | null, environment: { __typename?: 'EnvironmentType', id: string, envType: ApiEnvironmentEnvTypeChoices, name: string }, secret: { __typename?: 'SecretType', id: string, path: string } } | null> | null } | null, environmentKeys?: Array<{ __typename?: 'EnvironmentKeyType', id: string, identityKey: string, wrappedSeed: string, wrappedSalt: string, environment: { __typename?: 'EnvironmentType', id: string } } | null> | null }; +export type GetAppSecretsLogsQuery = { __typename?: 'Query', secretsLogsCount?: number | null, logs?: { __typename?: 'LogsResponseType', secrets?: Array<{ __typename?: 'SecretEventType', id: string, path: string, key: string, value: string, version: number, comment: string, timestamp: any, ipAddress?: string | null, userAgent?: string | null, eventType: ApiSecretEventEventTypeChoices, tags: Array<{ __typename?: 'SecretTagType', id: string, name: string, color: string }>, user?: { __typename?: 'OrganisationMemberType', email?: string | null, username?: string | null, fullName?: string | null, avatarUrl?: string | null } | null, serviceToken?: { __typename?: 'ServiceTokenType', id: string, name: string } | null, serviceAccount?: { __typename?: 'ServiceAccountType', id: string, name: string } | null, serviceAccountToken?: { __typename?: 'ServiceAccountTokenType', id: string, name: string } | null, environment: { __typename?: 'EnvironmentType', id: string, envType: ApiEnvironmentEnvTypeChoices, name: string }, secret: { __typename?: 'SecretType', id: string, path: string } } | null> | null } | null, environmentKeys?: Array<{ __typename?: 'EnvironmentKeyType', id: string, identityKey: string, wrappedSeed: string, wrappedSalt: string, environment: { __typename?: 'EnvironmentType', id: string } } | null> | null }; export type GetEnvironmentKeyQueryVariables = Exact<{ envId: Scalars['ID']['input']; @@ -2307,7 +2554,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 }>, history?: Array<{ __typename?: 'SecretEventType', id: string, key: string, value: string, path: string, version: number, comment: string, timestamp: any, ipAddress?: string | null, userAgent?: string | null, eventType: ApiSecretEventEventTypeChoices, tags: Array<{ __typename?: 'SecretTagType', id: string, name: string, color: string }>, user?: { __typename?: 'OrganisationMemberType', email?: string | null, username?: string | null, fullName?: string | null, avatarUrl?: string | null } | null } | null> | null, override?: { __typename?: 'PersonalSecretType', value?: string | null, isActive: boolean } | null, environment: { __typename?: 'EnvironmentType', id: string, app: { __typename?: 'AppType', 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?: 'AppType', 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 }>, history?: Array<{ __typename?: 'SecretEventType', id: string, key: string, value: string, path: string, version: number, comment: string, timestamp: any, ipAddress?: string | null, userAgent?: string | null, eventType: ApiSecretEventEventTypeChoices, tags: Array<{ __typename?: 'SecretTagType', id: string, name: string, color: string }>, user?: { __typename?: 'OrganisationMemberType', email?: string | null, username?: string | null, fullName?: string | null, avatarUrl?: string | null } | null, serviceToken?: { __typename?: 'ServiceTokenType', id: string, name: string } | null, serviceAccount?: { __typename?: 'ServiceAccountType', id: string, name: string } | null } | null> | null, override?: { __typename?: 'PersonalSecretType', value?: string | null, isActive: boolean } | null, environment: { __typename?: 'EnvironmentType', id: string, app: { __typename?: 'AppType', 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?: 'AppType', 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 GetServiceTokensQueryVariables = Exact<{ appId: Scalars['ID']['input']; @@ -2316,6 +2563,21 @@ export type GetServiceTokensQueryVariables = Exact<{ export type GetServiceTokensQuery = { __typename?: 'Query', serviceTokens?: Array<{ __typename?: 'ServiceTokenType', id: string, name: string, createdAt?: any | null, expiresAt?: any | null, createdBy?: { __typename?: 'OrganisationMemberType', fullName?: string | null, avatarUrl?: string | null, self?: boolean | null } | null, keys: Array<{ __typename?: 'ServerEnvironmentKeyType', id: string, identityKey: string }> } | null> | null }; +export type GetServiceAccountHandlersQueryVariables = Exact<{ + orgId: Scalars['ID']['input']; +}>; + + +export type GetServiceAccountHandlersQuery = { __typename?: 'Query', serviceAccountHandlers?: Array<{ __typename?: 'OrganisationMemberType', id: string, email?: string | null, identityKey?: string | null, self?: boolean | null, role?: { __typename?: 'RoleType', name?: string | null, permissions?: any | null } | null } | null> | null }; + +export type GetServiceAccountsQueryVariables = Exact<{ + orgId: Scalars['ID']['input']; + id?: InputMaybe; +}>; + + +export type GetServiceAccountsQuery = { __typename?: 'Query', serviceAccounts?: Array<{ __typename?: 'ServiceAccountType', id: string, name: string, identityKey?: string | null, createdAt?: any | null, role?: { __typename?: 'RoleType', id: string, name?: string | null, description?: string | null, color?: string | null, permissions?: any | null } | null, handlers?: Array<{ __typename?: 'ServiceAccountHandlerType', id: string, wrappedKeyring: string, wrappedRecovery: string, user: { __typename?: 'OrganisationMemberType', self?: boolean | null } } | null> | null, tokens?: Array<{ __typename?: 'ServiceAccountTokenType', id: string, name: string, createdAt?: any | null, expiresAt?: any | null, lastUsed?: any | null, createdBy?: { __typename?: 'OrganisationMemberType', fullName?: string | null, avatarUrl?: string | null, self?: boolean | null } | null } | null> | null, appMemberships?: Array<{ __typename?: 'AppType', id: string, name: string, environments: Array<{ __typename?: 'EnvironmentType', id: string, name: string } | null> }> | null } | null> | null }; + export type GetOrganisationSyncsQueryVariables = Exact<{ orgId: Scalars['ID']['input']; }>; @@ -2419,9 +2681,9 @@ export type GetUserTokensQuery = { __typename?: 'Query', userTokens?: Array<{ __ export const CreateRoleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateRole"},"variableDefinitions":[{"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":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"color"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"permissions"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"JSONString"}}}},{"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":"createCustomRole"},"arguments":[{"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":"color"},"value":{"kind":"Variable","name":{"kind":"Name","value":"color"}}},{"kind":"Argument","name":{"kind":"Name","value":"permissions"},"value":{"kind":"Variable","name":{"kind":"Name","value":"permissions"}}},{"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":"role"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const DeleteRoleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteRole"},"variableDefinitions":[{"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":"deleteCustomRole"},"arguments":[{"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":"ok"}}]}}]}}]} as unknown as DocumentNode; export const UpdateRoleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateRole"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"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"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"description"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"color"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"permissions"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"JSONString"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateCustomRole"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"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":"color"},"value":{"kind":"Variable","name":{"kind":"Name","value":"color"}}},{"kind":"Argument","name":{"kind":"Name","value":"permissions"},"value":{"kind":"Variable","name":{"kind":"Name","value":"permissions"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"role"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; -export const AddMemberToAppDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AddMemberToApp"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}},"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"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envKeys"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentKeyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"addAppMember"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"memberId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}}},{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"envKeys"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envKeys"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; -export const RemoveMemberFromAppDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveMemberFromApp"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}},"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":"removeAppMember"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"memberId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}}},{"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":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; -export const UpdateEnvScopeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateEnvScope"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}},"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"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envKeys"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentKeyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMemberEnvironmentScope"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"memberId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}}},{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"envKeys"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envKeys"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; +export const AddMemberToAppDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"AddMemberToApp"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}},"type":{"kind":"NonNullType","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":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envKeys"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentKeyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"addAppMember"},"arguments":[{"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":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"envKeys"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envKeys"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; +export const RemoveMemberFromAppDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveMemberFromApp"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}},"type":{"kind":"NonNullType","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":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"removeAppMember"},"arguments":[{"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":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; +export const UpdateEnvScopeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateEnvScope"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}},"type":{"kind":"NonNullType","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":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"envKeys"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EnvironmentKeyInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMemberEnvironmentScope"},"arguments":[{"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":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"envKeys"},"value":{"kind":"Variable","name":{"kind":"Name","value":"envKeys"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const InitStripeProUpgradeCheckoutDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"InitStripeProUpgradeCheckout"},"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":"billingPeriod"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createProUpgradeCheckoutSession"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"billingPeriod"},"value":{"kind":"Variable","name":{"kind":"Name","value":"billingPeriod"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"clientSecret"}}]}}]}}]} as unknown as DocumentNode; export const CreateApplicationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateApplication"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"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":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"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":"appToken"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appSeed"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyShare"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appVersion"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createApp"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"Argument","name":{"kind":"Name","value":"identityKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"appToken"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appToken"}}},{"kind":"Argument","name":{"kind":"Name","value":"appSeed"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appSeed"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedKeyShare"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyShare"}}},{"kind":"Argument","name":{"kind":"Name","value":"appVersion"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appVersion"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"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":"identityKey"}}]}}]}}]}}]} as unknown as DocumentNode; export const CreateOrgDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOrg"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"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"}}}},{"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"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOrganisation"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"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"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"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":"memberId"}}]}}]}}]}}]} as unknown as DocumentNode; @@ -2453,6 +2715,12 @@ export const InviteMemberDocument = {"kind":"Document","definitions":[{"kind":"O export const UpdateMemberRoleDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMemberRole"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"roleId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOrganisationMemberRole"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"memberId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"memberId"}}},{"kind":"Argument","name":{"kind":"Name","value":"roleId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"roleId"}}}],"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":"role"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const UpdateWrappedSecretsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateWrappedSecrets"},"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":"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"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMemberWrappedSecrets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}},{"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"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"orgMember"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; export const RotateAppKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RotateAppKey"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"appToken"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyShare"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rotateAppKeys"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}},{"kind":"Argument","name":{"kind":"Name","value":"appToken"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appToken"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedKeyShare"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyShare"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"app"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateServiceAccountOpDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateServiceAccountOp"},"variableDefinitions":[{"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":"orgId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"roleId"}},"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":"handlers"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ServiceAccountHandlerInput"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"serverWrappedKeyring"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"serverWrappedRecovery"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createServiceAccount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}},{"kind":"Argument","name":{"kind":"Name","value":"roleId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"roleId"}}},{"kind":"Argument","name":{"kind":"Name","value":"identityKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"handlers"},"value":{"kind":"Variable","name":{"kind":"Name","value":"handlers"}}},{"kind":"Argument","name":{"kind":"Name","value":"serverWrappedKeyring"},"value":{"kind":"Variable","name":{"kind":"Name","value":"serverWrappedKeyring"}}},{"kind":"Argument","name":{"kind":"Name","value":"serverWrappedRecovery"},"value":{"kind":"Variable","name":{"kind":"Name","value":"serverWrappedRecovery"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serviceAccount"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; +export const CreateSaTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateSAToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"serviceAccountId"}},"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"}}}},{"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":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyShare"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"expiry"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createServiceAccountToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"serviceAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"serviceAccountId"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"Argument","name":{"kind":"Name","value":"identityKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedKeyShare"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyShare"}}},{"kind":"Argument","name":{"kind":"Name","value":"expiry"},"value":{"kind":"Variable","name":{"kind":"Name","value":"expiry"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"token"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]}}]} as unknown as DocumentNode; +export const DeleteServiceAccountOpDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteServiceAccountOp"},"variableDefinitions":[{"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":"deleteServiceAccount"},"arguments":[{"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":"ok"}}]}}]}}]} as unknown as DocumentNode; +export const DeleteServiceAccountTokenOpDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteServiceAccountTokenOp"},"variableDefinitions":[{"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":"deleteServiceAccountToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"tokenId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; +export const UpdateServiceAccountHandlerKeysDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateServiceAccountHandlerKeys"},"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":"handlers"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ServiceAccountHandlerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateServiceAccountHandlers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}},{"kind":"Argument","name":{"kind":"Name","value":"handlers"},"value":{"kind":"Variable","name":{"kind":"Name","value":"handlers"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; +export const UpdateServiceAccountOpDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateServiceAccountOp"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"serviceAccountId"}},"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"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"roleId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateServiceAccount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"serviceAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"serviceAccountId"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"Argument","name":{"kind":"Name","value":"roleId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"roleId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"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":"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":"permissions"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const CreateNewAwsSecretsSyncDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNewAWSSecretsSync"},"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":"path"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"secretName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"kmsId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createAwsSecretSync"},"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"}}},{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}},{"kind":"Argument","name":{"kind":"Name","value":"secretName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"secretName"}}},{"kind":"Argument","name":{"kind":"Name","value":"kmsId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"kmsId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sync"},"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":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"lastSync"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; export const CreateNewCfPagesSyncDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNewCfPagesSync"},"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":"path"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectName"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"deploymentId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"projectEnv"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createCloudflarePagesSync"},"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"}}},{"kind":"Argument","name":{"kind":"Name","value":"projectName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectName"}}},{"kind":"Argument","name":{"kind":"Name","value":"deploymentId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"deploymentId"}}},{"kind":"Argument","name":{"kind":"Name","value":"projectEnv"},"value":{"kind":"Variable","name":{"kind":"Name","value":"projectEnv"}}},{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sync"},"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":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"lastSync"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}}]}}]}}]}}]} as unknown as DocumentNode; export const DeleteProviderCredsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteProviderCreds"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteProviderCredentials"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; @@ -2472,31 +2740,34 @@ export const CreateNewVercelSyncDocument = {"kind":"Document","definitions":[{"k export const CreateNewUserTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateNewUserToken"},"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":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"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":"token"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyShare"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"expiry"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"BigInt"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createUserToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}},{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"Argument","name":{"kind":"Name","value":"identityKey"},"value":{"kind":"Variable","name":{"kind":"Name","value":"identityKey"}}},{"kind":"Argument","name":{"kind":"Name","value":"token"},"value":{"kind":"Variable","name":{"kind":"Name","value":"token"}}},{"kind":"Argument","name":{"kind":"Name","value":"wrappedKeyShare"},"value":{"kind":"Variable","name":{"kind":"Name","value":"wrappedKeyShare"}}},{"kind":"Argument","name":{"kind":"Name","value":"expiry"},"value":{"kind":"Variable","name":{"kind":"Name","value":"expiry"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; export const RevokeUserTokenDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RevokeUserToken"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"tokenId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteUserToken"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"tokenId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"tokenId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}}]}}]}}]} as unknown as DocumentNode; export const GetAppMembersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAppMembers"},"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":"appUsers"},"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":"email"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"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":"permissions"}},{"kind":"Field","name":{"kind":"Name","value":"color"}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetAppServiceAccountsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAppServiceAccounts"},"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":"appServiceAccounts"},"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":"name"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"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":"permissions"}},{"kind":"Field","name":{"kind":"Name","value":"color"}}]}},{"kind":"Field","name":{"kind":"Name","value":"tokens"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetCheckoutDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCheckoutDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"stripeSessionId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"stripeCheckoutDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"stripeSessionId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"stripeSessionId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"paymentStatus"}},{"kind":"Field","name":{"kind":"Name","value":"customerEmail"}},{"kind":"Field","name":{"kind":"Name","value":"billingStartDate"}},{"kind":"Field","name":{"kind":"Name","value":"billingEndDate"}},{"kind":"Field","name":{"kind":"Name","value":"subscriptionId"}},{"kind":"Field","name":{"kind":"Name","value":"planName"}}]}}]}}]} as unknown as DocumentNode; export const GetAppActivityChartDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAppActivityChart"},"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":"period"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"TimeRange"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"appActivityChart"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}},{"kind":"Argument","name":{"kind":"Name","value":"period"},"value":{"kind":"Variable","name":{"kind":"Name","value":"period"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"index"}},{"kind":"Field","name":{"kind":"Name","value":"date"}},{"kind":"Field","name":{"kind":"Name","value":"data"}}]}}]}}]} as unknown as DocumentNode; export const GetAppDetailDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAppDetail"},"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":"appId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apps"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}},{"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":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"appToken"}},{"kind":"Field","name":{"kind":"Name","value":"appSeed"}},{"kind":"Field","name":{"kind":"Name","value":"appVersion"}},{"kind":"Field","name":{"kind":"Name","value":"sseEnabled"}}]}}]}}]} as unknown as DocumentNode; export const GetAppKmsLogsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAppKmsLogs"},"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"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logs"},"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"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"kms"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"timestamp"}},{"kind":"Field","name":{"kind":"Name","value":"phaseNode"}},{"kind":"Field","name":{"kind":"Name","value":"eventType"}},{"kind":"Field","name":{"kind":"Name","value":"ipAddress"}},{"kind":"Field","name":{"kind":"Name","value":"country"}},{"kind":"Field","name":{"kind":"Name","value":"city"}},{"kind":"Field","name":{"kind":"Name","value":"phSize"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"kmsLogsCount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}]}]}}]} as unknown as DocumentNode; -export const GetAppsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetApps"},"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":"appId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apps"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}},{"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":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"sseEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"members"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"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":"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":"envType"}},{"kind":"Field","name":{"kind":"Name","value":"syncs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"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":"provider"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]}}]} as unknown as DocumentNode; +export const GetAppsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetApps"},"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":"appId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apps"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}},{"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":"identityKey"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"sseEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"members"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"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":"serviceAccounts"},"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":"envType"}},{"kind":"Field","name":{"kind":"Name","value":"syncs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"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":"provider"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const GetDashboardDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetDashboard"},"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":"apps"},"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":"id"}},{"kind":"Field","name":{"kind":"Name","value":"sseEnabled"}}]}},{"kind":"Field","name":{"kind":"Name","value":"userTokens"},"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":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"organisationInvites"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"organisationMembers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"role"},"value":{"kind":"NullValue"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"savedCredentials"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"kind":"Field","name":{"kind":"Name","value":"syncs"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"orgId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; -export const GetOrganisationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOrganisations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"organisations"},"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":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"plan"}},{"kind":"Field","name":{"kind":"Name","value":"planDetail"},"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":"userCount"}},{"kind":"Field","name":{"kind":"Name","value":"appCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"role"},"selectionSet":{"kind":"SelectionSet","selections":[{"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":"memberId"}},{"kind":"Field","name":{"kind":"Name","value":"keyring"}},{"kind":"Field","name":{"kind":"Name","value":"recovery"}}]}}]}}]} as unknown as DocumentNode; +export const GetOrganisationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOrganisations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"organisations"},"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":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"plan"}},{"kind":"Field","name":{"kind":"Name","value":"planDetail"},"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":"appCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"role"},"selectionSet":{"kind":"SelectionSet","selections":[{"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":"memberId"}},{"kind":"Field","name":{"kind":"Name","value":"keyring"}},{"kind":"Field","name":{"kind":"Name","value":"recovery"}}]}}]}}]} as unknown as DocumentNode; export const CheckOrganisationNameAvailabilityDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"CheckOrganisationNameAvailability"},"variableDefinitions":[{"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":"organisationNameAvailable"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}}]}]}}]} as unknown as DocumentNode; export const GetGlobalAccessUsersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetGlobalAccessUsers"},"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":"organisationGlobalAccessUsers"},"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":"id"}},{"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; export const GetInvitesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetInvites"},"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":"organisationInvites"},"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":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}},{"kind":"Field","name":{"kind":"Name","value":"invitedBy"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"email"}},{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"self"}}]}},{"kind":"Field","name":{"kind":"Name","value":"inviteeEmail"}}]}}]}}]} as unknown as DocumentNode; export const GetLicenseDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetLicenseData"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"license"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"customerName"}},{"kind":"Field","name":{"kind":"Name","value":"organisationName"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}},{"kind":"Field","name":{"kind":"Name","value":"plan"}},{"kind":"Field","name":{"kind":"Name","value":"seats"}},{"kind":"Field","name":{"kind":"Name","value":"isActivated"}},{"kind":"Field","name":{"kind":"Name","value":"organisationOwner"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"fullName"}},{"kind":"Field","name":{"kind":"Name","value":"email"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetOrgLicenseDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOrgLicense"},"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":"organisationLicense"},"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":"id"}},{"kind":"Field","name":{"kind":"Name","value":"customerName"}},{"kind":"Field","name":{"kind":"Name","value":"issuedAt"}},{"kind":"Field","name":{"kind":"Name","value":"expiresAt"}},{"kind":"Field","name":{"kind":"Name","value":"activatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"plan"}},{"kind":"Field","name":{"kind":"Name","value":"seats"}},{"kind":"Field","name":{"kind":"Name","value":"tokens"}}]}}]}}]} as unknown as DocumentNode; export const GetOrganisationMembersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOrganisationMembers"},"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":"role"}},"type":{"kind":"ListType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"organisationMembers"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"organisationId"}}},{"kind":"Argument","name":{"kind":"Name","value":"role"},"value":{"kind":"Variable","name":{"kind":"Name","value":"role"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"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":"permissions"}},{"kind":"Field","name":{"kind":"Name","value":"color"}}]}},{"kind":"Field","name":{"kind":"Name","value":"identityKey"}},{"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":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"self"}}]}}]}}]} as unknown as DocumentNode; -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":"userCount"}},{"kind":"Field","name":{"kind":"Name","value":"appCount"}}]}}]}}]} as unknown as DocumentNode; +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":"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":"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 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"}}}],"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"}}}],"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 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"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logs"},"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"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"secrets"},"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":"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":"secretsLogsCount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}]},{"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 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 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"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logs"},"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"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"secrets"},"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":"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":"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":"secretsLogsCount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"appId"}}}]},{"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; export const GetFoldersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetFolders"},"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":"path"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"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":"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"}}]}}]}}]} 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":"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":"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":"eventType"}}]}},{"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":"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":"eventType"}}]}},{"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 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 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; +export const GetServiceAccountsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetServiceAccounts"},"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":"tokens"},"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":"expiresAt"}},{"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":"lastUsed"}}]}},{"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"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const GetOrganisationSyncsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOrganisationSyncs"},"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":"syncs"},"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":"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":"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":"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":"provider"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"options"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"lastSync"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"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":"credentials"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"history"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"completedAt"}},{"kind":"Field","name":{"kind":"Name","value":"meta"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"savedCredentials"},"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":"credentials"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"provider"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"expectedCredentials"}},{"kind":"Field","name":{"kind":"Name","value":"optionalCredentials"}}]}},{"kind":"Field","name":{"kind":"Name","value":"syncCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"apps"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"organisationId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"orgId"}}},{"kind":"Argument","name":{"kind":"Name","value":"appId"},"value":{"kind":"NullValue"}}],"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":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"sseEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"members"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}},{"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":"syncs"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"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":"provider"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"status"}}]}}]}}]}}]}}]} as unknown as DocumentNode; export const GetAwsSecretsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAwsSecrets"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"awsSecrets"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"arn"}}]}}]}}]} as unknown as DocumentNode; export const GetCfPagesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetCfPages"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"cloudflarePagesProjects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"credentialId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"credentialId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"deploymentId"}},{"kind":"Field","name":{"kind":"Name","value":"environments"}}]}}]}}]} as unknown as DocumentNode; diff --git a/frontend/apollo/schema.graphql b/frontend/apollo/schema.graphql index 34cbdf77c..6b8a00dbd 100644 --- a/frontend/apollo/schema.graphql +++ b/frontend/apollo/schema.graphql @@ -14,8 +14,9 @@ type Query { kmsLogsCount(appId: ID, thisMonth: Boolean): Int secretsLogsCount(appId: ID): Int appActivityChart(appId: ID, period: TimeRange): [ChartDataPointType] - appEnvironments(appId: ID, environmentId: ID, memberId: ID): [EnvironmentType] + appEnvironments(appId: ID, environmentId: ID, memberId: ID, memberType: MemberType): [EnvironmentType] appUsers(appId: ID): [OrganisationMemberType] + appServiceAccounts(appId: ID): [ServiceAccountType] secrets(envId: ID, path: String): [SecretType] folders(envId: ID, path: String): [SecretFolderType] secretHistory(secretId: ID): [SecretEventType] @@ -24,6 +25,8 @@ type Query { environmentTokens(environmentId: ID): [EnvironmentTokenType] userTokens(organisationId: ID): [UserTokenType] serviceTokens(appId: ID): [ServiceTokenType] + serviceAccounts(orgId: ID, serviceAccountId: ID): [ServiceAccountType] + serviceAccountHandlers(orgId: ID): [OrganisationMemberType] serverPublicKey: String sseEnabled(appId: ID): Boolean providers: [ProviderType] @@ -97,10 +100,16 @@ type OrganisationPlanType { maxUsers: Int maxApps: Int maxEnvsPerApp: Int - userCount: Int + seatsUsed: SeatsUsed appCount: Int } +type SeatsUsed { + users: Int + serviceAccounts: Int + total: Int +} + type PhaseLicenseType { id: String customerName: String @@ -194,10 +203,86 @@ type AppType { wrappedKeyShare: String! createdAt: DateTime sseEnabled: Boolean! + serviceAccounts: [ServiceAccountType!]! environments: [EnvironmentType]! members: [OrganisationMemberType]! } +type ServiceAccountType { + id: String! + name: String! + role: RoleType + identityKey: String + createdAt: DateTime + updatedAt: DateTime! + thirdPartyAuthEnabled: Boolean + handlers: [ServiceAccountHandlerType] + tokens: [ServiceAccountTokenType] + appMemberships: [AppType!] +} + +type ServiceAccountHandlerType { + id: String! + serviceAccount: ServiceAccountType! + user: OrganisationMemberType! + wrappedKeyring: String! + wrappedRecovery: String! + createdAt: DateTime + updatedAt: DateTime! +} + +type ServiceAccountTokenType { + id: String! + serviceAccount: ServiceAccountType! + name: String! + identityKey: String! + token: String! + wrappedKeyShare: String! + createdBy: OrganisationMemberType + createdAt: DateTime + updatedAt: DateTime! + deletedAt: DateTime + expiresAt: DateTime + secreteventSet: [SecretEventType!]! + lastUsed: DateTime +} + +type SecretEventType { + id: String! + secret: SecretType! + environment: EnvironmentType! + path: String! + user: OrganisationMemberType + serviceToken: ServiceTokenType + serviceAccount: ServiceAccountType + serviceAccountToken: ServiceAccountTokenType + key: String! + value: String! + version: Int! + tags: [SecretTagType!]! + comment: String! + eventType: ApiSecretEventEventTypeChoices! + timestamp: DateTime! + ipAddress: String + userAgent: String +} + +type SecretType { + id: String! + environment: EnvironmentType! + folder: SecretFolderType + path: String! + key: String! + value: String! + version: Int! + tags: [SecretTagType!]! + comment: String! + createdAt: DateTime + updatedAt: DateTime! + history: [SecretEventType] + override: PersonalSecretType +} + type EnvironmentType { id: String! app: AppType! @@ -311,75 +396,6 @@ enum ApiEnvironmentSyncEventStatusChoices { FAILED } -type LogsResponseType { - kms: [KMSLogType] - secrets: [SecretEventType] -} - -type KMSLogType implements Node { - id: ID! - timestamp: BigInt - appId: String - phaseNode: String - eventType: String - ipAddress: String - phSize: Int - asn: Int - isp: String - edgeLocation: String - country: String - city: String - latitude: Float - longitude: Float -} - -"""An object with an ID""" -interface Node { - """The ID of the object""" - id: ID! -} - -""" -The `BigInt` scalar type represents non-fractional whole numeric values. -`BigInt` is not constrained to 32-bit like the `Int` type and thus is a less -compatible type. -""" -scalar BigInt - -type SecretEventType { - id: String! - secret: SecretType! - environment: EnvironmentType! - path: String! - user: OrganisationMemberType - serviceToken: ServiceTokenType - key: String! - value: String! - version: Int! - tags: [SecretTagType!]! - comment: String! - eventType: ApiSecretEventEventTypeChoices! - timestamp: DateTime! - ipAddress: String - userAgent: String -} - -type SecretType { - id: String! - environment: EnvironmentType! - folder: SecretFolderType - path: String! - key: String! - value: String! - version: Int! - tags: [SecretTagType!]! - comment: String! - createdAt: DateTime - updatedAt: DateTime! - history: [SecretEventType] - override: PersonalSecretType -} - type SecretFolderType { id: String! environment: EnvironmentType! @@ -444,6 +460,41 @@ enum ApiSecretEventEventTypeChoices { D } +type LogsResponseType { + kms: [KMSLogType] + secrets: [SecretEventType] +} + +type KMSLogType implements Node { + id: ID! + timestamp: BigInt + appId: String + phaseNode: String + eventType: String + ipAddress: String + phSize: Int + asn: Int + isp: String + edgeLocation: String + country: String + city: String + latitude: Float + longitude: Float +} + +"""An object with an ID""" +interface Node { + """The ID of the object""" + id: ID! +} + +""" +The `BigInt` scalar type represents non-fractional whole numeric values. +`BigInt` is not constrained to 32-bit like the `Int` type and thus is a less +compatible type. +""" +scalar BigInt + type ChartDataPointType { index: Int date: BigInt @@ -459,6 +510,11 @@ enum TimeRange { ALL_TIME } +enum MemberType { + USER + SERVICE +} + type EnvironmentKeyType { id: String! environment: EnvironmentType! @@ -606,9 +662,9 @@ type Mutation { 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 - addAppMember(appId: ID, envKeys: [EnvironmentKeyInput], memberId: ID): AddAppMemberMutation - removeAppMember(appId: ID, memberId: ID): RemoveAppMemberMutation - updateMemberEnvironmentScope(appId: ID, envKeys: [EnvironmentKeyInput], memberId: ID): UpdateMemberEnvScopeMutation + addAppMember(appId: ID, envKeys: [EnvironmentKeyInput], memberId: ID, memberType: MemberType): AddAppMemberMutation + 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 deleteEnvironment(environmentId: ID!): DeleteEnvironmentMutation renameEnvironment(environmentId: ID!, name: String!): RenameEnvironmentMutation @@ -618,6 +674,13 @@ type Mutation { 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 + 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 + deleteServiceAccountToken(tokenId: ID): DeleteServiceAccountTokenMutation initEnvSync(appId: ID, envKeys: [EnvironmentKeyInput]): InitEnvSync deleteEnvSync(syncId: ID): DeleteSync triggerSync(syncId: ID): TriggerSync @@ -759,6 +822,41 @@ type DeleteCustomRoleMutation { ok: Boolean } +type CreateServiceAccountMutation { + serviceAccount: ServiceAccountType +} + +input ServiceAccountHandlerInput { + serviceAccountId: ID + memberId: ID + wrappedKeyring: String! + wrappedRecovery: String! +} + +type EnableServiceAccountThirdPartyAuthMutation { + serviceAccount: ServiceAccountType +} + +type UpdateServiceAccountHandlersMutation { + ok: Boolean +} + +type UpdateServiceAccountMutation { + serviceAccount: ServiceAccountType +} + +type DeleteServiceAccountMutation { + ok: Boolean +} + +type CreateServiceAccountTokenMutation { + token: ServiceAccountTokenType +} + +type DeleteServiceAccountTokenMutation { + ok: Boolean +} + type InitEnvSync { app: AppType } diff --git a/frontend/app/[team]/access/layout.tsx b/frontend/app/[team]/access/layout.tsx index c3e169c79..4e9221919 100644 --- a/frontend/app/[team]/access/layout.tsx +++ b/frontend/app/[team]/access/layout.tsx @@ -5,9 +5,7 @@ import { Tab } from '@headlessui/react' import clsx from 'clsx' import Link from 'next/link' import { usePathname } from 'next/navigation' -import { userHasPermission } from '@/utils/access/permissions' import { organisationContext } from '@/contexts/organisationContext' -import { useRouter } from 'next/router' export default function AccessLayout({ params, @@ -29,6 +27,10 @@ export default function AccessLayout({ name: 'Members', link: 'members', }, + { + name: 'Service Accounts', + link: 'service-accounts', + }, { name: 'Roles', link: 'roles', @@ -53,7 +55,7 @@ export default function AccessLayout({ return (
@@ -68,8 +70,10 @@ export default function AccessLayout({ {tab.name} diff --git a/frontend/app/[team]/access/members/page.tsx b/frontend/app/[team]/access/members/page.tsx index c73f4a665..81d32ce7c 100644 --- a/frontend/app/[team]/access/members/page.tsx +++ b/frontend/app/[team]/access/members/page.tsx @@ -40,20 +40,21 @@ import clsx from 'clsx' import { copyToClipBoard } from '@/utils/clipboard' import { toast } from 'react-toastify' import { Avatar } from '@/components/common/Avatar' -import { userHasGlobalAccess, userIsAdmin } from '@/utils/access/permissions' +import { PermissionPolicy, userHasGlobalAccess, userIsAdmin } from '@/utils/access/permissions' import { RoleLabel } from '@/components/users/RoleLabel' import { KeyringContext } from '@/contexts/keyringContext' import { Alert } from '@/components/common/Alert' import { Input } from '@/components/common/Input' import CopyButton from '@/components/common/CopyButton' -import { getInviteLink, unwrapEnvSecretsForUser, wrapEnvSecretsForUser } from '@/utils/crypto' +import { getInviteLink, unwrapEnvSecretsForUser, wrapEnvSecretsForAccount } from '@/utils/crypto' import { isCloudHosted } from '@/utils/appConfig' import { UpsellDialog } from '@/components/settings/organisation/UpsellDialog' import { useSearchParams } from 'next/navigation' import { userHasPermission } from '@/utils/access/permissions' import { EmptyState } from '@/components/common/EmptyState' import Spinner from '@/components/common/Spinner' +import { updateServiceAccountHandlers } from '@/utils/crypto/service-accounts' const handleCopy = (val: string) => { copyToClipBoard(val) @@ -97,24 +98,28 @@ const RoleSelector = (props: { member: OrganisationMemberType }) => { /** * Handles the assignment of a user to a global access role. - * Env keys for all apps, are fetched and decrypted by the active user, then each key is re-encrypted for the new user and saved on the backend via the addMemberToApp mutation + * Env keys for all apps are fetched and decrypted by the active user, + * then each key is re-encrypted for the new user and saved on the backend via the addMemberToApp mutation. * - * @returns {void} + * @returns {Promise} */ - const assignGlobalAccess = () => { - if (appsData) { - const apps = appsData.apps + const assignGlobalAccess = async (): Promise => { + if (!appsData) { + return Promise.reject(new Error('No apps data available')) + } - // Function to process an individual app - const processApp = async (app: AppType) => { - // fetch envs for the app - const { data: appEnvsData } = await getAppEnvs({ variables: { appId: app.id } }) + const apps = appsData.apps + // Function to process an individual app + const processApp = async (app: AppType) => { + try { + // Fetch envs for the app + const { data: appEnvsData } = await getAppEnvs({ variables: { appId: app.id } }) const appEnvironments = appEnvsData.appEnvironments as EnvironmentType[] - // construct promises to encrypt each env key for the target user + // Construct promises to encrypt each env key for the target user const envKeyPromises = appEnvironments.map(async (env: EnvironmentType) => { - // fetch the current wrapped key for the environment + // Fetch the current wrapped key for the environment const { data } = await getEnvKey({ variables: { envId: env.id, @@ -128,17 +133,20 @@ const RoleSelector = (props: { member: OrganisationMemberType }) => { identityKey, } = data.environmentKeys[0] - // unwrap env keys for current logged in user + // Unwrap env keys for current logged in user const { seed, salt } = await unwrapEnvSecretsForUser( userWrappedSeed, userWrappedSalt, keyring! ) - // re-encrypt the env key for the target user - const { wrappedSeed, wrappedSalt } = await wrapEnvSecretsForUser({ seed, salt }, member) + // Re-encrypt the env key for the target user + const { wrappedSeed, wrappedSalt } = await wrapEnvSecretsForAccount( + { seed, salt }, + member + ) - // resolve the promise with the mutation payload + // Return the mutation payload return { envId: env.id, userId: member.id, @@ -148,40 +156,43 @@ const RoleSelector = (props: { member: OrganisationMemberType }) => { } }) - // get mutation payloads with wrapped keys for each environment + // Get mutation payloads with wrapped keys for each environment const envKeyInputs = await Promise.all(envKeyPromises) - // add the user to this app, with wrapped keys for each environment + // Add the user to this app, with wrapped keys for each environment await addMemberToApp({ variables: { memberId: member.id, appId: app.id, envKeys: envKeyInputs }, }) + } catch (error) { + console.error(`Error processing app ${app.id}:`, error) + throw error // Propagate the error to be caught later } + } - // Process each app sequentially - const processAppsSequentially = async () => { - for (const app of apps) { - await processApp(app) - } + try { + // Process each app sequentially using for...of to ensure async operations complete in order + for (const app of apps) { + await processApp(app) } - // Call the function to process all apps sequentially - processAppsSequentially() - .then(async () => { - // All apps have been processed - const adminRole = roleOptions.find( - (option: RoleType) => option.name?.toLowerCase() === 'admin' - ) - await updateRole({ - variables: { - memberId: member.id, - roleId: adminRole.id, - }, - }) - toast.success('Updated member role', { autoClose: 2000 }) - }) - .catch((error) => { - console.error('Error processing apps:', error) + // After all apps have been processed, assign the global admin role + const adminRole = roleOptions.find( + (option: RoleType) => option.name?.toLowerCase() === 'admin' + ) + + if (adminRole) { + await updateRole({ + variables: { + memberId: member.id, + roleId: adminRole.id, + }, }) + } else { + throw new Error('Admin role not found') + } + } catch (error) { + console.error('Error assigning global access:', error) + throw error // Ensure the promise rejects if any error occurs } } @@ -189,6 +200,9 @@ const RoleSelector = (props: { member: OrganisationMemberType }) => { const newRoleHasGlobalAccess = userHasGlobalAccess(newRole.permissions) const currentUserHasGlobalAccess = userHasGlobalAccess(organisation?.role?.permissions) + const newRolePolicy: PermissionPolicy = JSON.parse(newRole.permissions) + const newRoleHasServiceAccountAccess = newRolePolicy.permissions['ServiceAccounts'].length > 0 + if (newRoleHasGlobalAccess && !currentUserHasGlobalAccess) { toast.error('You cannot assign users to this role as it requires global access!', { autoClose: 5000, @@ -206,16 +220,31 @@ const RoleSelector = (props: { member: OrganisationMemberType }) => { setRole(newRole) - if (newRoleHasGlobalAccess) assignGlobalAccess() - else { - await updateRole({ - variables: { - memberId: member.id, - roleId: newRole.id, - }, + const processUpdate = async () => { + return new Promise(async (resolve, reject) => { + try { + if (newRoleHasGlobalAccess) await assignGlobalAccess() + else { + await updateRole({ + variables: { + memberId: member.id, + roleId: newRole.id, + }, + }) + } + //if (newRoleHasServiceAccountAccess) + await updateServiceAccountHandlers(organisation!.id, keyring!) + resolve(true) + } catch (error) { + reject(error) + } }) - toast.success('Updated member role', { autoClose: 2000 }) } + await toast.promise(processUpdate, { + pending: 'Updating role...', + success: 'Updated role!', + error: 'Something went wrong!', + }) } const roleOptions = roleData?.roles.filter((option: RoleType) => option.name !== 'Owner') || [] @@ -287,7 +316,7 @@ const InviteDialog = (props: { organisationId: string }) => { const upsell = isCloudHosted() && activeOrganisation?.plan === ApiOrganisationPlanChoices.Fr && - data?.organisationPlan.userCount === data?.organisationPlan.maxUsers + data?.organisationPlan.seatsUsed.total === data?.organisationPlan.maxUsers const [createInvite, { error, loading: mutationLoading }] = useMutation(InviteMember) diff --git a/frontend/app/[team]/access/service-accounts/[account]/_components/CreateServiceAccountTokenDialog.tsx b/frontend/app/[team]/access/service-accounts/[account]/_components/CreateServiceAccountTokenDialog.tsx new file mode 100644 index 000000000..98349c654 --- /dev/null +++ b/frontend/app/[team]/access/service-accounts/[account]/_components/CreateServiceAccountTokenDialog.tsx @@ -0,0 +1,326 @@ +import { ServiceAccountType } from '@/apollo/graphql' +import { Button } from '@/components/common/Button' +import GenericDialog from '@/components/common/GenericDialog' +import { + compareExpiryOptions, + ExpiryOptionT, + humanReadableExpiry, + tokenExpiryOptions, +} from '@/utils/tokens' +import { Fragment, useContext, useRef, useState } from 'react' +import { FaCheckCircle, FaCircle, FaExternalLinkSquareAlt, FaPlus } from 'react-icons/fa' +import { GetServiceAccounts } from '@/graphql/queries/service-accounts/getServiceAccounts.gql' +import { CreateSAToken } from '@/graphql/mutations/service-accounts/createServiceAccountToken.gql' +import { organisationContext } from '@/contexts/organisationContext' +import { + getUserKxPublicKey, + getUserKxPrivateKey, + decryptAsymmetric, + OrganisationKeyring, +} from '@/utils/crypto' +import { useMutation } from '@apollo/client' +import { toast } from 'react-toastify' +import { KeyringContext } from '@/contexts/keyringContext' +import { generateSAToken } from '@/utils/crypto/service-accounts' +import { Alert } from '@/components/common/Alert' +import CopyButton from '@/components/common/CopyButton' +import { CliCommand } from '@/components/dashboard/CliCommand' +import { getApiHost } from '@/utils/appConfig' +import { Tab, RadioGroup } from '@headlessui/react' +import clsx from 'clsx' +import Link from 'next/link' +import { userHasPermission } from '@/utils/access/permissions' + +export const CreateServiceAccountTokenDialog = ({ + serviceAccount, +}: { + serviceAccount: ServiceAccountType +}) => { + const { activeOrganisation: organisation } = useContext(organisationContext) + const { keyring } = useContext(KeyringContext) + + const userCanCreateTokens = organisation + ? userHasPermission(organisation.role?.permissions, 'ServiceAccountTokens', 'create') + : false + + const serviceAccountHandler = serviceAccount.handlers?.find( + (handler) => handler?.user.self === true + ) + + const dialogRef = useRef<{ closeModal: () => void }>(null) + + const [name, setName] = useState('') + const [expiry, setExpiry] = useState(tokenExpiryOptions[0]) + + const [cliSAToken, setCliSAToken] = useState('') + const [apiSAToken, setApiSAToken] = useState('') + const [createPending, setCreatePending] = useState(false) + + const [createToken] = useMutation(CreateSAToken) + + const reset = () => { + setName('') + setExpiry(tokenExpiryOptions[0]) + setCliSAToken('') + setApiSAToken('') + } + + const closeModal = () => { + if (dialogRef.current) dialogRef.current.closeModal() + } + + const handleCreateNewSAToken = async (event: { preventDefault: () => void }) => { + return new Promise(async (resolve, reject) => { + event.preventDefault() + + if (name.length === 0) { + toast.error('You must enter a name for the token') + reject() + } + + if (serviceAccountHandler && keyring) { + setCreatePending(true) + const wrappedKeyring = serviceAccountHandler.wrappedKeyring + + const userKxKeys = { + publicKey: await getUserKxPublicKey(keyring.publicKey), + privateKey: await getUserKxPrivateKey(keyring.privateKey), + } + + const serviceAccountKeyringString = await decryptAsymmetric( + wrappedKeyring, + userKxKeys.privateKey, + userKxKeys.publicKey + ) + + const serviceAccountKeys = JSON.parse(serviceAccountKeyringString) as OrganisationKeyring + + const saKxKeys = { + publicKey: await getUserKxPublicKey(serviceAccountKeys.publicKey), + privateKey: await getUserKxPrivateKey(serviceAccountKeys.privateKey), + } + + const { pssService, mutationPayload } = await generateSAToken( + serviceAccount.id, + saKxKeys, + name, + expiry.getExpiry() + ) + + await createToken({ + variables: mutationPayload, + refetchQueries: [ + { + query: GetServiceAccounts, + variables: { + orgId: organisation!.id, + }, + }, + ], + }) + + setCliSAToken(pssService) + setApiSAToken(`ServiceAccount ${mutationPayload.token}`) + setCreatePending(false) + toast.success('Created new service account token!') + resolve(true) + } else { + console.log('keyring unavailable') + reject() + } + }) + } + + if (!userCanCreateTokens) return <> + + return ( + + {' '} + Create token + + } + buttonVariant="primary" + ref={dialogRef} + onClose={reset} + > +
+
Create a new token for this service account
+ + {cliSAToken ? ( +
+
+
+
{name}
+
{humanReadableExpiry(expiry)}
+
+
+ + + Copy this token. You won't see it again! + + + + + + {({ selected }) => ( +
+ CLI / SDK / Kubernetes +
+ )} +
+ + {({ selected }) => ( +
+ API +
+ )} +
+
+ + +
+
+
+ + Service token + +
+ {cliSAToken && ( +
+ +
+ )} +
+
+ + {cliSAToken} + +
+
+
+ +
+ +
+ You will need to enable server-side encryption (SSE) for any Apps that you + want to manage secrets with via the Public API. + +
+ Docs +
+ +
+
+ +
+
+ + API token + +
+ {apiSAToken && ( +
+ +
+ )} +
+
+ + {apiSAToken} + +
+ +
+
+
+ Example with curl +
+ + + +
+ +
+
+
+
+
+
+ ) : ( +
+
+ + setName(e.target.value)} /> +
+ +
+ + + + +
+ {tokenExpiryOptions.map((option) => ( + + {({ checked }) => ( +
+ +
+ )} +
+ ))} +
+
+ {humanReadableExpiry(expiry)} +
+ +
+ + +
+
+ )} +
+
+ ) +} diff --git a/frontend/app/[team]/access/service-accounts/[account]/_components/DeleteServiceAccountTokenDialog.tsx b/frontend/app/[team]/access/service-accounts/[account]/_components/DeleteServiceAccountTokenDialog.tsx new file mode 100644 index 000000000..160bfdeac --- /dev/null +++ b/frontend/app/[team]/access/service-accounts/[account]/_components/DeleteServiceAccountTokenDialog.tsx @@ -0,0 +1,59 @@ +import { FaTrash } from 'react-icons/fa' +import { DeleteServiceAccountTokenOp } from '@/graphql/mutations/service-accounts/deleteServiceAccountToken.gql' +import { GetServiceAccounts } from '@/graphql/queries/service-accounts/getServiceAccounts.gql' +import { useMutation } from '@apollo/client' +import { toast } from 'react-toastify' +import { useContext, useRef } from 'react' +import { organisationContext } from '@/contexts/organisationContext' +import { ServiceAccountTokenType, ServiceAccountType } from '@/apollo/graphql' +import { Button } from '@/components/common/Button' +import GenericDialog from '@/components/common/GenericDialog' +import { useRouter } from 'next/navigation' +import { userHasPermission } from '@/utils/access/permissions' + +export const DeleteServiceAccountTokenDialog = ({ token }: { token: ServiceAccountTokenType }) => { + const { activeOrganisation: organisation } = useContext(organisationContext) + + const userCanDeleteTokens = organisation + ? userHasPermission(organisation.role?.permissions, 'ServiceAccountTokens', 'delete') + : false + + const dialogRef = useRef<{ closeModal: () => void }>(null) + + const [deleteToken] = useMutation(DeleteServiceAccountTokenOp) + + const handleDelete = async () => { + const deleted = await deleteToken({ + variables: { id: token.id }, + refetchQueries: [{ query: GetServiceAccounts, variables: { orgId: organisation!.id } }], + }) + if (deleted.data.deleteServiceAccountToken.ok) { + toast.success('Deleted token!') + if (dialogRef.current) dialogRef.current.closeModal() + } + } + + if (!userCanDeleteTokens) return <> + + return ( + + Delete + + } + buttonVariant="danger" + ref={dialogRef} + > +
+
Are you sure you want to delete this token?
+
+ +
+
+
+ ) +} diff --git a/frontend/app/[team]/access/service-accounts/[account]/page.tsx b/frontend/app/[team]/access/service-accounts/[account]/page.tsx new file mode 100644 index 000000000..c2d61f6c4 --- /dev/null +++ b/frontend/app/[team]/access/service-accounts/[account]/page.tsx @@ -0,0 +1,327 @@ +'use client' + +import Spinner from '@/components/common/Spinner' +import { organisationContext } from '@/contexts/organisationContext' +import { GetServiceAccounts } from '@/graphql/queries/service-accounts/getServiceAccounts.gql' +import { UpdateServiceAccountOp } from '@/graphql/mutations/service-accounts/updateServiceAccount.gql' +import { userHasPermission } from '@/utils/access/permissions' +import { relativeTimeFromDates } from '@/utils/time' +import { useMutation, useQuery } from '@apollo/client' +import Link from 'next/link' +import { useContext, useEffect, useState } from 'react' +import { FaBan, FaBoxOpen, FaChevronLeft, FaCog, FaEdit, FaKey, FaRobot } from 'react-icons/fa' +import { CreateServiceAccountTokenDialog } from './_components/CreateServiceAccountTokenDialog' +import { DeleteServiceAccountDialog } from '../_components/DeleteServiceAccountDialog' +import { ServiceAccountTokenType, ServiceAccountType } from '@/apollo/graphql' +import { Avatar } from '@/components/common/Avatar' +import { EmptyState } from '@/components/common/EmptyState' +import { DeleteServiceAccountTokenDialog } from './_components/DeleteServiceAccountTokenDialog' +import { ServiceAccountRoleSelector } from '../_components/RoleSelector' +import { Button } from '@/components/common/Button' +import { toast } from 'react-toastify' +import { AppType } from 'next/app' +import { env } from 'process' +import { Card } from '@/components/common/Card' + +export default function ServiceAccount({ params }: { params: { team: string; account: string } }) { + const { activeOrganisation: organisation } = useContext(organisationContext) + + const [name, setName] = useState('') + + const userCanReadSA = organisation + ? userHasPermission(organisation?.role?.permissions, 'ServiceAccounts', 'read') + : false + + const userCanReadTokens = organisation + ? userHasPermission(organisation.role?.permissions, 'ServiceAccountTokens', 'read') + : false + + const userCanReadAppMemberships = organisation + ? userHasPermission(organisation?.role?.permissions, 'ServiceAccounts', 'read', true) + : false + + const userCanUpdateSA = organisation + ? userHasPermission(organisation?.role?.permissions, 'ServiceAccounts', 'update') + : false + + const userCanDeleteSA = organisation + ? userHasPermission(organisation?.role?.permissions, 'ServiceAccounts', 'delete') + : false + + const { data, loading } = useQuery(GetServiceAccounts, { + variables: { orgId: organisation?.id, id: params.account }, + skip: !organisation || !userCanReadSA, + }) + + const [updateAccount] = useMutation(UpdateServiceAccountOp) + + const account: ServiceAccountType = data?.serviceAccounts[0] + + const nameUpdated = account ? account.name !== name : false + + const updateName = async () => { + if (!userCanUpdateSA) { + toast.error("You don't have the permissions requried to update Service Accounts") + } + await updateAccount({ + variables: { + serviceAccountId: account.id, + roleId: account.role!.id, + name, + }, + refetchQueries: [ + { query: GetServiceAccounts, variables: { orgId: organisation?.id, id: params.account } }, + ], + }) + + toast.success('Updated account name!') + } + + const resetName = () => setName(account.name) + + useEffect(() => { + if (account) setName(account.name) + }, [account]) + + if (!userCanReadSA) + return ( +
+ + +
+ } + > + <> + + + ) + + if (loading) + return ( +
+ +
+ ) + + if (!account) + return ( +
+ + +
+ } + > + <> + + + ) + + return ( +
+
+ + Back to service accounts + +
+
+
+
+ +
{' '} +

+ setName(e.target.value)} + readOnly={!userCanUpdateSA} + maxLength={64} + /> + + {nameUpdated ? ( +
+ + + +
+ ) : ( +
+ +
+ )} +

+
+ +
+
+
Role
+
Manage the role for this account
+
+
+
+ +
+
{account.role!.description}
+
+
+ +
+
+
Apps
+
+ Manage the Apps and Environments that this account has access to +
+
+ + {userCanReadAppMemberships ? ( +
+ {account.appMemberships?.map((app) => ( + +
+
+
{app.name}
+ + + +
+
+
+ Environments +
+
+ {app.environments.map((app) => app!.name).join(' + ')} +
+
+
+
+ ))} + {account.appMemberships?.length === 0 && ( +
+ + +
+ } + > + <> + +
+ )} +
+ ) : ( + + +
+ } + > + <> + + )} + + +
+
+
Tokens
+
Manage tokens for this service account
+
+
+ +
+ + {userCanReadTokens ? ( +
+ {account.tokens!.map((token) => ( +
+
+ {token!.name} +
+ +
+ Created {relativeTimeFromDates(new Date(token?.createdAt))} by{' '} + + + {token?.createdBy?.fullName} + +
+ +
+ Expires{' '} + {token!.expiresAt ? relativeTimeFromDates(new Date(token?.expiresAt)) : 'never'} +
+ +
+ Last used{' '} + {token!.lastUsed ? relativeTimeFromDates(new Date(token?.lastUsed)) : 'never'} +
+ +
+ +
+
+ ))} +
+ ) : ( + + +
+ } + > + <> + + )} + + + {userCanDeleteSA && ( +
+
+
Danger Zone
+
+ These actions are destructive and cannot be reversed +
+
+ +
+
+
Delete account
+
+ Permanently delete this service account and all associated tokens +
+
+ +
+
+ )} + +
+ ) +} diff --git a/frontend/app/[team]/access/service-accounts/_components/CreateServiceAccountDialog.tsx b/frontend/app/[team]/access/service-accounts/_components/CreateServiceAccountDialog.tsx new file mode 100644 index 000000000..2b1f2d68c --- /dev/null +++ b/frontend/app/[team]/access/service-accounts/_components/CreateServiceAccountDialog.tsx @@ -0,0 +1,239 @@ +import { ApiOrganisationPlanChoices, OrganisationMemberType, RoleType } from '@/apollo/graphql' +import GenericDialog from '@/components/common/GenericDialog' +import { Fragment, useContext, useEffect, useRef, useState } from 'react' +import { FaChevronDown, FaPlus } from 'react-icons/fa' +import { GetServiceAccounts } from '@/graphql/queries/service-accounts/getServiceAccounts.gql' +import { GetServiceAccountHandlers } from '@/graphql/queries/service-accounts/getServiceAccountHandlers.gql' +import { GetRoles } from '@/graphql/queries/organisation/getRoles.gql' +import { GetServerKey } from '@/graphql/queries/syncing/getServerKey.gql' +import { CreateServiceAccountOp } from '@/graphql/mutations/service-accounts/createServiceAccount.gql' +import { GetOrganisationPlan } from '@/graphql/queries/organisation/getOrganisationPlan.gql' +import { organisationContext } from '@/contexts/organisationContext' +import { useMutation, useQuery } from '@apollo/client' +import { + organisationSeed, + organisationKeyring, + getUserKxPublicKey, + encryptAsymmetric, +} from '@/utils/crypto' +import { Input } from '@/components/common/Input' +import { RoleLabel } from '@/components/users/RoleLabel' +import { Listbox } from '@headlessui/react' +import clsx from 'clsx' +import { ToggleSwitch } from '@/components/common/ToggleSwitch' +import { Button } from '@/components/common/Button' +import { toast } from 'react-toastify' +import { isCloudHosted } from '@/utils/appConfig' +import { UpsellDialog } from '@/components/settings/organisation/UpsellDialog' + +const bip39 = require('bip39') + +export const CreateServiceAccountDialog = () => { + const { activeOrganisation: organisation } = useContext(organisationContext) + + const { data: roleData, loading: roleDataPending } = useQuery(GetRoles, { + variables: { orgId: organisation?.id }, + skip: !organisation, + }) + + const { data: serviceAccountHandlerData } = useQuery(GetServiceAccountHandlers, { + variables: { orgId: organisation?.id }, + skip: !organisation, + }) + + const { data } = useQuery(GetOrganisationPlan, { + variables: { organisationId: organisation?.id }, + fetchPolicy: 'cache-and-network', + skip: !organisation, + }) + + const [createServiceAccount] = useMutation(CreateServiceAccountOp) + + const { data: serverKeyData } = useQuery(GetServerKey) + + const dialogRef = useRef<{ closeModal: () => void }>(null) + + const [name, setName] = useState('') + const [role, setRole] = useState(null) + const [thirdParty, setThirdParty] = useState(false) + const [createPending, setCreatePending] = useState(false) + + const reset = () => { + setName('') + setThirdParty(false) + } + + const upsell = + isCloudHosted() && + organisation?.plan === ApiOrganisationPlanChoices.Fr && + data?.organisationPlan.seatsUsed.total === data?.organisationPlan.maxUsers + + const roleOptions = + roleData?.roles.filter( + (option: RoleType) => option.name !== 'Owner' && option.name !== 'Admin' + ) || [] + + useEffect(() => { + if (roleData?.roles) { + const defaultRole = roleData?.roles.find( + (role: RoleType) => role.name?.toLowerCase() === 'service' + ) + if (defaultRole) setRole(defaultRole) + } + }, [roleData]) + + const handleCreateServiceAccount = (e: { preventDefault: () => void }) => { + return new Promise((resolve) => { + e.preventDefault() + setCreatePending(true) + setTimeout(async () => { + // Compute new keys for service account + const mnemonic = bip39.generateMnemonic(256) + const accountSeed = await organisationSeed(mnemonic, organisation!.id) + const keyring = await organisationKeyring(accountSeed) + + // Wrap keys for server if required + let serverKeys = undefined + if (thirdParty) { + const serverKey = serverKeyData.serverPublicKey + + const serverEncryptedKeyring = await encryptAsymmetric(JSON.stringify(keyring), serverKey) + + const serverEncryptedMnemonic = await encryptAsymmetric(mnemonic, serverKey) + + serverKeys = { + serverEncryptedKeyring, + serverEncryptedMnemonic, + } + } + + // Wrap keys for service account handlers + const handlers: OrganisationMemberType[] = serviceAccountHandlerData.serviceAccountHandlers + + const handlerWrappingPromises = handlers.map(async (handler) => { + const kxKey = await getUserKxPublicKey(handler.identityKey!) + const wrappedKeyring = await encryptAsymmetric(JSON.stringify(keyring), kxKey) + const wrappedRecovery = await encryptAsymmetric(mnemonic, kxKey) + return { + memberId: handler.id, + wrappedKeyring, + wrappedRecovery, + } + }) + + const handlerKeys = await Promise.all(handlerWrappingPromises) + + await createServiceAccount({ + variables: { + name, + orgId: organisation!.id, + roleId: role!.id, + identityKey: keyring.publicKey, + serverWrappedKeyring: serverKeys?.serverEncryptedKeyring || null, + serverWrappedRecovery: serverKeys?.serverEncryptedMnemonic || null, + handlers: handlerKeys, + }, + refetchQueries: [ + { + query: GetServiceAccounts, + variables: { orgId: organisation!.id }, + }, + ], + }) + + setCreatePending(false) + reset() + + if (dialogRef.current) dialogRef.current.closeModal() + + toast.success('Created new service account!') + + resolve(true) + }, 500) + }) + } + + if (upsell) + return ( + + Create Service Account + + } + /> + ) + + return ( + + Create Service Account + + } + buttonVariant="primary" + size="lg" + ref={dialogRef} + > +
+
+ +
+ + + {({ open }) => ( + <> + +
+ {role ? : <>Select a role} + +
+
+ + {roleOptions.map((role: RoleType) => ( + + {({ active, selected }) => ( +
+ +
+ )} +
+ ))} +
+ + )} +
+
+ {/*
+ + setThirdParty(!thirdParty)} /> +
*/} +
+
+ +
+
+
+ ) +} diff --git a/frontend/app/[team]/access/service-accounts/_components/DeleteServiceAccountDialog.tsx b/frontend/app/[team]/access/service-accounts/_components/DeleteServiceAccountDialog.tsx new file mode 100644 index 000000000..04b37a976 --- /dev/null +++ b/frontend/app/[team]/access/service-accounts/_components/DeleteServiceAccountDialog.tsx @@ -0,0 +1,62 @@ +import { FaTrash } from 'react-icons/fa' +import { DeleteServiceAccountOp } from '@/graphql/mutations/service-accounts/deleteServiceAccount.gql' +import { GetServiceAccounts } from '@/graphql/queries/service-accounts/getServiceAccounts.gql' +import { useMutation } from '@apollo/client' +import { toast } from 'react-toastify' +import { useContext, useRef } from 'react' +import { organisationContext } from '@/contexts/organisationContext' +import { ServiceAccountType } from '@/apollo/graphql' +import { Button } from '@/components/common/Button' +import GenericDialog from '@/components/common/GenericDialog' +import { useRouter } from 'next/navigation' + +export const DeleteServiceAccountDialog = ({ account }: { account: ServiceAccountType }) => { + const { activeOrganisation: organisation } = useContext(organisationContext) + + const dialogRef = useRef<{ closeModal: () => void }>(null) + + const [deleteAccount] = useMutation(DeleteServiceAccountOp) + + const handleDelete = async () => { + const deleted = await deleteAccount({ + variables: { id: account.id }, + refetchQueries: [{ query: GetServiceAccounts, variables: { orgId: organisation!.id } }], + }) + if (deleted.data.deleteServiceAccount.ok) { + toast.success('Deleted service account!') + if (dialogRef.current) { + dialogRef.current.closeModal() + } + handleRedirect() + } + } + + const router = useRouter() + + const handleRedirect = () => router.push(`/${organisation?.name}/access/service-accounts`) + + return ( + + Delete + + } + buttonVariant="danger" + ref={dialogRef} + > +
+
+ Are you sure you want to delete this service account? This will delete all service tokens + associated with this account. +
+
+ +
+
+
+ ) +} diff --git a/frontend/app/[team]/access/service-accounts/_components/RoleSelector.tsx b/frontend/app/[team]/access/service-accounts/_components/RoleSelector.tsx new file mode 100644 index 000000000..1629889a0 --- /dev/null +++ b/frontend/app/[team]/access/service-accounts/_components/RoleSelector.tsx @@ -0,0 +1,130 @@ +import { ServiceAccountType, RoleType } from '@/apollo/graphql' +import { RoleLabel } from '@/components/users/RoleLabel' +import { KeyringContext } from '@/contexts/keyringContext' +import { organisationContext } from '@/contexts/organisationContext' +import { userHasPermission } from '@/utils/access/permissions' +import { useQuery, useMutation } from '@apollo/client' +import { Listbox } from '@headlessui/react' +import clsx from 'clsx' +import { useContext, useState, Fragment } from 'react' +import { FaChevronDown } from 'react-icons/fa' +import { toast } from 'react-toastify' +import { GetApps } from '@/graphql/queries/getApps.gql' +import { GetRoles } from '@/graphql/queries/organisation/getRoles.gql' +import { UpdateServiceAccountOp } from '@/graphql/mutations/service-accounts/updateServiceAccount.gql' + +export const ServiceAccountRoleSelector = (props: { + account: ServiceAccountType + displayOnly?: boolean +}) => { + const { account, displayOnly } = props + + const { activeOrganisation: organisation } = useContext(organisationContext) + const { keyring } = useContext(KeyringContext) + + const userCanReadApps = userHasPermission(organisation?.role?.permissions, 'Apps', 'read') + + const userCanUpdateAccountRoles = organisation + ? userHasPermission(organisation.role!.permissions, 'ServiceAccounts', 'update') && + userHasPermission(organisation.role!.permissions, 'Roles', 'read') + : false + + const { data: appsData } = useQuery(GetApps, { + variables: { organisationId: organisation!.id }, + skip: !userCanReadApps, + }) + + const { data: roleData, loading: roleDataPending } = useQuery(GetRoles, { + variables: { orgId: organisation!.id }, + skip: !userCanUpdateAccountRoles, + }) + + const [updateRole] = useMutation(UpdateServiceAccountOp) + + const [role, setRole] = useState(account.role!) + + const isOwner = role.name!.toLowerCase() === 'owner' + + const handleUpdateRole = async (newRole: RoleType) => { + setRole(newRole) + + const processUpdate = async () => { + return new Promise(async (resolve, reject) => { + try { + await updateRole({ + variables: { + serviceAccountId: account.id, + roleId: newRole.id, + name: account.name, + }, + }) + + resolve(true) + } catch (error) { + reject(error) + } + }) + } + await toast.promise(processUpdate, { + pending: 'Updating role...', + success: 'Updated role!', + error: 'Something went wrong!', + }) + } + + const roleOptions = + roleData?.roles.filter( + (option: RoleType) => option.name !== 'Owner' && option.name !== 'Admin' + ) || [] + + const disabled = isOwner || !userCanUpdateAccountRoles || displayOnly + + if (roleDataPending) return <> + + return disabled ? ( + + ) : ( +
+ + {({ open }) => ( + <> + +
+ + {!disabled && ( + + )} +
+
+ + {roleOptions.map((role: RoleType) => ( + + {({ active, selected }) => ( +
+ +
+ )} +
+ ))} +
+ + )} +
+
+ ) +} diff --git a/frontend/app/[team]/access/service-accounts/page.tsx b/frontend/app/[team]/access/service-accounts/page.tsx new file mode 100644 index 000000000..eb2acb119 --- /dev/null +++ b/frontend/app/[team]/access/service-accounts/page.tsx @@ -0,0 +1,126 @@ +'use client' + +import { ServiceAccountType } from '@/apollo/graphql' +import { EmptyState } from '@/components/common/EmptyState' +import { organisationContext } from '@/contexts/organisationContext' +import { GetServiceAccounts } from '@/graphql/queries/service-accounts/getServiceAccounts.gql' +import { userHasPermission } from '@/utils/access/permissions' +import { useQuery } from '@apollo/client' +import { useContext } from 'react' +import { FaBan, FaChevronRight } from 'react-icons/fa' +import { CreateServiceAccountDialog } from './_components/CreateServiceAccountDialog' +import { FaRobot } from 'react-icons/fa6' +import { relativeTimeFromDates } from '@/utils/time' +import Link from 'next/link' +import { Button } from '@/components/common/Button' +import { ServiceAccountRoleSelector } from './_components/RoleSelector' + +export default function ServiceAccounts({ params }: { params: { team: string } }) { + const { activeOrganisation: organisation } = useContext(organisationContext) + + const userCanReadSA = organisation + ? userHasPermission(organisation?.role?.permissions, 'ServiceAccounts', 'read') + : false + + const userCanCreateSA = organisation + ? userHasPermission(organisation?.role?.permissions, 'ServiceAccounts', 'create') + : false + + const { data, loading } = useQuery(GetServiceAccounts, { + variables: { orgId: organisation?.id }, + skip: !organisation || !userCanReadSA, + }) + + return ( +
+
+
+

{params.team} Service Accounts

+

Manage service accounts.

+
+
+ {userCanCreateSA && data?.serviceAccounts.length > 0 && ( +
+ +
+ )} + + {userCanReadSA ? ( + data?.serviceAccounts.length === 0 ? ( + + +
+ } + > + <> + + + + ) : ( + + + + + + + + + + + {data?.serviceAccounts.map((account: ServiceAccountType) => ( + + + + + + + + + + ))} + +
+ Account name + + Role + + Created +
+
+ +
+ {account.name} +
+ + + {relativeTimeFromDates(new Date(account.createdAt))} + + + + +
+ ) + ) : ( + + +
+ } + > + <> + + )} + + +
+ ) +} diff --git a/frontend/app/[team]/apps/[app]/access/layout.tsx b/frontend/app/[team]/apps/[app]/access/layout.tsx new file mode 100644 index 000000000..38d3614bb --- /dev/null +++ b/frontend/app/[team]/apps/[app]/access/layout.tsx @@ -0,0 +1,90 @@ +'use client' + +import { Fragment, useContext, useEffect, useMemo, useState } from 'react' +import { Tab } from '@headlessui/react' +import clsx from 'clsx' +import Link from 'next/link' +import { usePathname } from 'next/navigation' + +export default function AccessLayout({ + params, + children, +}: { + params: { team: string; app: string } + children: React.ReactNode +}) { + const path = usePathname() + + const [tabIndex, setTabIndex] = useState(0) + + const tabs = useMemo( + () => [ + { + name: 'Members', + link: 'members', + }, + { + name: 'Service Accounts', + link: 'service-accounts', + }, + { + name: 'Service Tokens', + link: 'tokens', + isLegacy: true, + }, + ], + [] + ) + + useEffect(() => { + const activeTabIndex = () => { + const currentUrl = path?.split('/')[5] || '' + const index = tabs.findIndex((tab) => tab.link === currentUrl) + return index >= 0 ? index : 0 + } + + setTabIndex(activeTabIndex()) + }, [path, tabs]) + + return ( +
+
+

Access

+
Manage user and service account access in this App
+
+ + setTabIndex(index)}> +
+ + {tabs.map((tab) => ( + + {({ selected }) => ( + + {tab.name}{' '} + {tab.isLegacy && ( + + Legacy + + )} + + )} + + ))} + +
{children}
+
+
+
+ ) +} diff --git a/frontend/app/[team]/apps/[app]/members/page.tsx b/frontend/app/[team]/apps/[app]/access/members/page.tsx similarity index 89% rename from frontend/app/[team]/apps/[app]/members/page.tsx rename to frontend/app/[team]/apps/[app]/access/members/page.tsx index c6c773ec4..1395b59db 100644 --- a/frontend/app/[team]/apps/[app]/members/page.tsx +++ b/frontend/app/[team]/apps/[app]/access/members/page.tsx @@ -8,16 +8,18 @@ import GetAppMembers from '@/graphql/queries/apps/getAppMembers.gql' import { GetAppEnvironments } from '@/graphql/queries/secrets/getAppEnvironments.gql' import { GetEnvironmentKey } from '@/graphql/queries/secrets/getEnvironmentKey.gql' import { useLazyQuery, useMutation, useQuery } from '@apollo/client' -import { Fragment, useContext, useEffect, useState } from 'react' +import { Fragment, useContext, useEffect, useMemo, useState } from 'react' import { OrganisationMemberType, EnvironmentType } from '@/apollo/graphql' import { Button } from '@/components/common/Button' import { organisationContext } from '@/contexts/organisationContext' import { relativeTimeFromDates } from '@/utils/time' import { Combobox, Dialog, Listbox, Transition } from '@headlessui/react' import { + FaArrowRight, FaBan, FaCheckSquare, FaChevronDown, + FaCog, FaPlus, FaSquare, FaTimes, @@ -33,9 +35,10 @@ import { userHasGlobalAccess, userHasPermission, userIsAdmin } from '@/utils/acc import { RoleLabel } from '@/components/users/RoleLabel' import { Alert } from '@/components/common/Alert' import Link from 'next/link' -import { unwrapEnvSecretsForUser, wrapEnvSecretsForUser } from '@/utils/crypto' +import { unwrapEnvSecretsForUser, wrapEnvSecretsForAccount } from '@/utils/crypto' import { EmptyState } from '@/components/common/EmptyState' import Spinner from '@/components/common/Spinner' +import loading from '@/app/loading' export default function Members({ params }: { params: { team: string; app: string } }) { const { keyring } = useContext(KeyringContext) @@ -162,7 +165,7 @@ export default function Members({ params }: { params: { team: string; app: strin keyring! ) - const { wrappedSeed, wrappedSalt } = await wrapEnvSecretsForUser( + const { wrappedSeed, wrappedSalt } = await wrapEnvSecretsForAccount( { seed, salt }, selectedMember! ) @@ -238,17 +241,17 @@ export default function Members({ params }: { params: { team: string; app: strin {memberOptions.length === 0 ? (
-

- All organisation members are added to this App. You can invite more - users from the{' '} - - organisation members - {' '} - page. -

+
+

+ All organisation members are added to this App. You can invite more + users from the organisation members page. +

+ + + +
) : ( @@ -271,7 +274,7 @@ export default function Members({ params }: { params: { team: string; app: strin onChange={(event) => setQuery(event.target.value)} required displayValue={(person: OrganisationMemberType) => - person?.fullName || person?.email! + person ? person?.fullName || person?.email! : 'Select a user' } />
@@ -460,9 +463,7 @@ export default function Members({ params }: { params: { team: string; app: strin <>
@@ -526,16 +527,33 @@ export default function Members({ params }: { params: { team: string; app: strin ) } - const ManageUserAccessDialog = (props: { member: OrganisationMemberType }) => { + const ManageUserAccessDialog = ({ member }: { member: OrganisationMemberType }) => { const [updateScope] = useMutation(UpdateEnvScope) - const [getUserEnvScope] = useLazyQuery(GetAppEnvironments) + // Get environments that the active user has access to const { data: appEnvsData } = useQuery(GetAppEnvironments, { variables: { appId: params.app, }, }) + // Get the environemnts that the member has access to + const { data: userEnvScopeData } = useQuery(GetAppEnvironments, { + variables: { + appId: params.app, + memberId: member.id, + }, + }) + + const envScope: Array> = useMemo(() => { + return ( + userEnvScopeData?.appEnvironments.map((env: EnvironmentType) => ({ + id: env.id, + name: env.name, + })) ?? [] + ) + }, [userEnvScopeData]) + const envOptions = appEnvsData?.appEnvironments.map((env: EnvironmentType) => { const { id, name } = env @@ -548,7 +566,7 @@ export default function Members({ params }: { params: { team: string; app: strin const [isOpen, setIsOpen] = useState(false) - const [envScope, setEnvScope] = useState>>([]) + const [scope, setScope] = useState>>([]) const [showEnvHint, setShowEnvHint] = useState(false) const memberHasGlobalAccess = (user: OrganisationMemberType) => @@ -563,36 +581,13 @@ export default function Members({ params }: { params: { team: string; app: strin } useEffect(() => { - if (isOpen) { - const handleGetCurrentSCope = async () => { - const { data: currentScope } = await getUserEnvScope({ - variables: { - appId: params.app, - memberId: props.member.id, - }, - fetchPolicy: 'no-cache', - }) - - setEnvScope( - currentScope?.appEnvironments.map((env: EnvironmentType) => { - const { id, name } = env - - return { - id, - name, - } - }) ?? [] - ) - } - - if (isOpen) handleGetCurrentSCope() - } - }, [getUserEnvScope, isOpen, props.member.id]) + setScope(envScope) + }, [envScope]) const handleUpdateScope = async (e: { preventDefault: () => void }) => { e.preventDefault() - if (envScope.length === 0) { + if (scope.length === 0) { setShowEnvHint(true) return false } @@ -600,7 +595,7 @@ export default function Members({ params }: { params: { team: string; app: strin const appEnvironments = appEnvsData.appEnvironments as EnvironmentType[] const envKeyPromises = appEnvironments - .filter((env) => envScope.map((selectedEnv) => selectedEnv.id).includes(env.id)) + .filter((env) => scope.map((selectedEnv) => selectedEnv.id).includes(env.id)) .map(async (env: EnvironmentType) => { const { data } = await getEnvKey({ variables: { @@ -621,14 +616,14 @@ export default function Members({ params }: { params: { team: string; app: strin keyring! ) - const { wrappedSeed, wrappedSalt } = await wrapEnvSecretsForUser( + const { wrappedSeed, wrappedSalt } = await wrapEnvSecretsForAccount( { seed, salt }, - props.member! + member! ) return { envId: env.id, - userId: props.member!.id, + userId: member!.id, identityKey, wrappedSeed, wrappedSalt, @@ -638,11 +633,14 @@ export default function Members({ params }: { params: { team: string; app: strin const envKeyInputs = await Promise.all(envKeyPromises) await updateScope({ - variables: { memberId: props.member!.id, appId: params.app, envKeys: envKeyInputs }, + variables: { memberId: member!.id, appId: params.app, envKeys: envKeyInputs }, refetchQueries: [ { - query: GetAppMembers, - variables: { appId: params.app }, + query: GetAppEnvironments, + variables: { + appId: params.app, + memberId: member.id, + }, }, ], }) @@ -650,12 +648,25 @@ export default function Members({ params }: { params: { team: string; app: strin toast.success('Updated user access', { autoClose: 2000 }) } + const allowUpdateScope = + member.email !== session?.user?.email && + member.role!.name!.toLowerCase() !== 'owner' && + userCanUpdateMemberAccess + return ( <> -
- +
+ + {envScope.map((env) => env.name).join(' + ')} + + + {allowUpdateScope && ( +
+ +
+ )}
@@ -686,7 +697,7 @@ export default function Members({ params }: { params: { team: string; app: strin

- Manage access for {props.member.fullName || props.member.email} + Manage access for {member.fullName || member.email}

@@ -827,7 +838,11 @@ export default function Members({ params }: { params: { team: string; app: strin ) return ( -
+
+
+

Members

+
Manage access for human users to this App
+
{userCanReadAppMembers ? (
{userCanAddAppMembers && ( @@ -844,14 +859,12 @@ export default function Members({ params }: { params: { team: string; app: strin - Joined + Environment Access - {(userCanRemoveAppMembers || userCanUpdateMemberAccess) && ( - - )} + {userCanRemoveAppMembers && } - + {data?.appUsers.map((member: OrganisationMemberType) => ( @@ -869,20 +882,16 @@ export default function Members({ params }: { params: { team: string; app: strin
- - {relativeTimeFromDates(new Date(member.createdAt))} + + - {(userCanRemoveAppMembers || userCanUpdateMemberAccess) && ( + + {userCanRemoveAppMembers && ( {member.email !== session?.user?.email && member.role!.name!.toLowerCase() !== 'owner' && (
- {userCanUpdateMemberAccess && ( - - )} - {userCanRemoveAppMembers && ( - - )} +
)} diff --git a/frontend/app/[team]/apps/[app]/access/service-accounts/page.tsx b/frontend/app/[team]/apps/[app]/access/service-accounts/page.tsx new file mode 100644 index 000000000..16596c688 --- /dev/null +++ b/frontend/app/[team]/apps/[app]/access/service-accounts/page.tsx @@ -0,0 +1,965 @@ +'use client' + +import { GetServiceAccounts } from '@/graphql/queries/service-accounts/getServiceAccounts.gql' +import AddMemberToApp from '@/graphql/mutations/apps/addAppMember.gql' +import RemoveMemberFromApp from '@/graphql/mutations/apps/removeAppMember.gql' +import UpdateEnvScope from '@/graphql/mutations/apps/updateEnvScope.gql' +import { GetAppServiceAccounts } from '@/graphql/queries/apps/getAppServiceAccounts.gql' +import { GetAppEnvironments } from '@/graphql/queries/secrets/getAppEnvironments.gql' +import { GetEnvironmentKey } from '@/graphql/queries/secrets/getEnvironmentKey.gql' +import { useLazyQuery, useMutation, useQuery } from '@apollo/client' +import { Fragment, useContext, useEffect, useMemo, useState } from 'react' +import { EnvironmentType, ServiceAccountType, MemberType } from '@/apollo/graphql' +import { Button } from '@/components/common/Button' +import { organisationContext } from '@/contexts/organisationContext' +import { Combobox, Dialog, Listbox, Transition } from '@headlessui/react' +import { + FaArrowRight, + FaBan, + FaCheckSquare, + FaChevronDown, + FaCog, + FaKey, + FaPlus, + FaRobot, + FaSquare, + FaTimes, + FaTrash, +} from 'react-icons/fa' +import clsx from 'clsx' +import { toast } from 'react-toastify' +import { useSession } from 'next-auth/react' +import { KeyringContext } from '@/contexts/keyringContext' +import { userHasGlobalAccess, userHasPermission } from '@/utils/access/permissions' +import { RoleLabel } from '@/components/users/RoleLabel' +import { Alert } from '@/components/common/Alert' +import Link from 'next/link' +import { unwrapEnvSecretsForUser, wrapEnvSecretsForAccount } from '@/utils/crypto' +import { EmptyState } from '@/components/common/EmptyState' +import Spinner from '@/components/common/Spinner' + +export default function ServiceAccounts({ params }: { params: { team: string; app: string } }) { + const { keyring } = useContext(KeyringContext) + const { activeOrganisation: organisation } = useContext(organisationContext) + + // Permissions + const userCanReadAppSA = organisation + ? userHasPermission(organisation?.role?.permissions, 'ServiceAccounts', 'read', true) + : false + const userCanReadEnvironments = organisation + ? userHasPermission(organisation?.role?.permissions, 'Environments', 'read', true) + : false + + // AppServiceAccounts:create + ServiceAccounts: read + const userCanAddAppSA = organisation + ? userHasPermission(organisation?.role?.permissions, 'ServiceAccounts', 'create', true) && + userHasPermission(organisation?.role?.permissions, 'ServiceAccounts', 'read') + : false + const userCanRemoveAppSA = organisation + ? userHasPermission(organisation?.role?.permissions, 'ServiceAccounts', 'delete', true) + : false + // AppMembers:update + Environments:read + const userCanUpdateSAAccess = organisation + ? userHasPermission(organisation?.role?.permissions, 'ServiceAccounts', 'update', true) && + userHasPermission(organisation?.role?.permissions, 'Environments', 'read', true) + : false + + const { data, loading } = useQuery(GetAppServiceAccounts, { + variables: { appId: params.app }, + skip: !userCanReadAppSA, + }) + + const [getEnvKey] = useLazyQuery(GetEnvironmentKey) + + const { data: session } = useSession() + + const AddAccountDialog = () => { + const { data: serviceAccountsData } = useQuery(GetServiceAccounts, { + variables: { + orgId: organisation?.id, + }, + skip: !organisation || !userCanAddAppSA, + }) + + const accountOptions = + serviceAccountsData?.serviceAccounts.filter( + (account: ServiceAccountType) => + !data?.appServiceAccounts + .map((account: ServiceAccountType) => account.id) + .includes(account.id) + ) ?? [] + + const [addMember] = useMutation(AddMemberToApp) + + const { data: appEnvsData } = useQuery(GetAppEnvironments, { + variables: { + appId: params.app, + }, + skip: !userCanReadEnvironments, + }) + + const envOptions = + appEnvsData?.appEnvironments.map((env: EnvironmentType) => { + const { id, name } = env + + return { + id, + name, + } + }) ?? [] + + const [isOpen, setIsOpen] = useState(false) + const [selectedAccount, setSelectedAccount] = useState(null) + const [query, setQuery] = useState('') + const [envScope, setEnvScope] = useState>>([]) + const [showEnvHint, setShowEnvHint] = useState(false) + + const filteredAccounts = + query === '' + ? accountOptions + : accountOptions.filter((account: ServiceAccountType) => { + return account.name.toLowerCase().includes(query.toLowerCase()) + }) + + const closeModal = () => { + setIsOpen(false) + } + + const openModal = () => { + setIsOpen(true) + } + + const handleAddMember = async (e: { preventDefault: () => void }) => { + e.preventDefault() + + if (envScope.length === 0) { + setShowEnvHint(true) + return false + } + + const appEnvironments = appEnvsData.appEnvironments as EnvironmentType[] + + const envKeyPromises = appEnvironments + .filter((env) => envScope.map((selectedEnv) => selectedEnv.id).includes(env.id)) + .map(async (env: EnvironmentType) => { + const { data } = await getEnvKey({ + variables: { + envId: env.id, + appId: params.app, + }, + }) + + const { + wrappedSeed: userWrappedSeed, + wrappedSalt: userWrappedSalt, + identityKey, + } = data.environmentKeys[0] + + const { seed, salt } = await unwrapEnvSecretsForUser( + userWrappedSeed, + userWrappedSalt, + keyring! + ) + + console.log('unwrapped env secrets', seed, salt) + + const { wrappedSeed, wrappedSalt } = await wrapEnvSecretsForAccount( + { seed, salt }, + selectedAccount! + ) + + return { + envId: env.id, + userId: selectedAccount!.id, + identityKey, + wrappedSeed, + wrappedSalt, + } + }) + + const envKeyInputs = await Promise.all(envKeyPromises) + + await addMember({ + variables: { + memberId: selectedAccount!.id, + memberType: MemberType.Service, + appId: params.app, + envKeys: envKeyInputs, + }, + refetchQueries: [ + { + query: GetAppServiceAccounts, + variables: { appId: params.app }, + }, + ], + }) + + toast.success('Added account to App', { autoClose: 2000 }) + } + + return ( + <> +
+ +
+ + + + +
+ + +
+
+ + + +

+ Add service account +

+ + +
+ + {accountOptions.length === 0 ? ( +
+ +
+

+ All organisation servuce accounts are added to this App. You can + create more service accounts from the organisation access page. +

+ + + +
+
+
+ ) : ( + + + {({ open }) => ( + <> +
+ + + +
+ setQuery(event.target.value)} + required + displayValue={(account: ServiceAccountType) => + account ? account.name : 'Select an account' + } + /> +
+ + + +
+
+
+ + +
+ {filteredAccounts.map((account: ServiceAccountType) => ( + + {({ active, selected }) => ( +
+
+ +
+ + {account.name} + +
+ )} +
+ ))} +
+
+
+ + )} +
+ + {userCanReadEnvironments ? ( +
+ {envScope.length === 0 && showEnvHint && ( + + Select an environment scope + + )} + + {({ open }) => ( + <> + + + + +
+ + {envScope + .map((env: Partial) => env.name) + .join(' + ')} + + +
+
+ + +
+ {envOptions.map((env: Partial) => ( + + {({ active, selected }) => ( +
+ {selected ? ( + + ) : ( + + )} + + {env.name} + +
+ )} +
+ ))} +
+
+
+ + )} +
+
+ ) : ( + + You don't have permission to read Environments. This permission is + required to set an environment scope for users in this App. + + )} + +
+ + +
+ + )} +
+
+
+
+
+
+ + ) + } + + const RemoveAccountConfirmDialog = (props: { account: ServiceAccountType }) => { + const { account } = props + + const [removeMember] = useMutation(RemoveMemberFromApp) + + const [isOpen, setIsOpen] = useState(false) + + const closeModal = () => { + setIsOpen(false) + } + + const openModal = () => { + setIsOpen(true) + } + + const handleRemoveMember = async () => { + await removeMember({ + variables: { memberId: account.id, memberType: MemberType.Service, appId: params.app }, + refetchQueries: [ + { + query: GetAppServiceAccounts, + variables: { appId: params.app }, + }, + ], + }) + toast.success('Removed member from app', { autoClose: 2000 }) + } + + return ( + <> +
+ +
+ + + + +
+ + +
+
+ + + +

+ Remove member +

+ + +
+ +
+

+ Are you sure you want to remove {account.name} from this app? +

+
+ + +
+
+
+
+
+
+
+
+ + ) + } + + const ManageAccountAccessDialog = ({ account }: { account: ServiceAccountType }) => { + const [updateScope] = useMutation(UpdateEnvScope) + + // Get environments that the active user has access to + const { data: appEnvsData } = useQuery(GetAppEnvironments, { + variables: { + appId: params.app, + }, + }) + + // Get the environemnts that the account has access to + const { data: userEnvScopeData } = useQuery(GetAppEnvironments, { + variables: { + appId: params.app, + memberId: account.id, + memberType: MemberType.Service, + }, + }) + + const envScope: Array> = useMemo(() => { + return ( + userEnvScopeData?.appEnvironments.map((env: EnvironmentType) => ({ + id: env.id, + name: env.name, + })) ?? [] + ) + }, [userEnvScopeData]) + + const envOptions = + appEnvsData?.appEnvironments.map((env: EnvironmentType) => { + const { id, name } = env + + return { + id, + name, + } + }) ?? [] + + const [isOpen, setIsOpen] = useState(false) + + const [scope, setScope] = useState>>([]) + const [showEnvHint, setShowEnvHint] = useState(false) + + const memberHasGlobalAccess = (account: ServiceAccountType) => + userHasGlobalAccess(account.role?.permissions) + + const closeModal = () => { + setIsOpen(false) + } + + const openModal = () => { + setIsOpen(true) + } + + useEffect(() => { + setScope(envScope) + }, [envScope]) + + const handleUpdateScope = async (e: { preventDefault: () => void }) => { + e.preventDefault() + + if (scope.length === 0) { + setShowEnvHint(true) + return false + } + + const appEnvironments = appEnvsData.appEnvironments as EnvironmentType[] + + const envKeyPromises = appEnvironments + .filter((env) => scope.map((selectedEnv) => selectedEnv.id).includes(env.id)) + .map(async (env: EnvironmentType) => { + const { data } = await getEnvKey({ + variables: { + envId: env.id, + appId: params.app, + }, + }) + + const { + wrappedSeed: userWrappedSeed, + wrappedSalt: userWrappedSalt, + identityKey, + } = data.environmentKeys[0] + + const { seed, salt } = await unwrapEnvSecretsForUser( + userWrappedSeed, + userWrappedSalt, + keyring! + ) + + const { wrappedSeed, wrappedSalt } = await wrapEnvSecretsForAccount( + { seed, salt }, + account! + ) + + return { + envId: env.id, + userId: account!.id, + identityKey, + wrappedSeed, + wrappedSalt, + } + }) + + const envKeyInputs = await Promise.all(envKeyPromises) + + await updateScope({ + variables: { + memberId: account!.id, + memberType: MemberType.Service, + appId: params.app, + envKeys: envKeyInputs, + }, + refetchQueries: [ + { + query: GetAppEnvironments, + variables: { + appId: params.app, + memberId: account.id, + memberType: MemberType.Service, + }, + }, + ], + }) + + toast.success('Updated account access', { autoClose: 2000 }) + } + + const allowUpdateScope = userCanUpdateSAAccess + + return ( + <> +
+ + {envScope.map((env) => env.name).join(' + ')} + + {allowUpdateScope && ( +
+ +
+ )} +
+ + + + +
+ + +
+
+ + + +
+

+ Manage access for {account.name} +

+

+ Manage the environment scope for this service account +

+
+ + +
+ +
+ {memberHasGlobalAccess(account) && ( + +

+ This user's role grants them access to all environments in this + App. To restrict their access, change their role from the{' '} + + organisation members + {' '} + page. +

+
+ )} + +
+
+ {scope.length === 0 && showEnvHint && ( + + Select an environment scope + + )} + + {({ open }) => ( + <> + + + + +
+ + {scope + .map((env: Partial) => env.name) + .join(' + ')} + + +
+
+ + +
+ {envOptions.map((env: Partial) => ( + + {({ active, selected }) => ( +
+ {selected ? ( + + ) : ( + + )} + + {env.name} + +
+ )} +
+ ))} +
+
+
+ + )} +
+
+ +
+
+ +
+ {account.tokens?.length! > 0 ? account.tokens?.length! : 'No'} active + tokens +
+
+ + + +
+
+ +
+ + +
+
+
+
+
+
+
+
+ + ) + } + + if (!organisation || loading) + return ( +
+ +
+ ) + + return ( +
+
+

Service Accounts

+
Manage access for service accounts to this App
+
+ {userCanReadAppSA ? ( +
+ {userCanAddAppSA && data?.appServiceAccounts.length > 0 && ( +
+ +
+ )} + + {data?.appServiceAccounts.length === 0 ? ( + + +
+ } + > + <> + + + + ) : ( + + + + + + + {userCanRemoveAppSA && } + + + + {data?.appServiceAccounts.map((account: ServiceAccountType) => ( + + + + + + {userCanRemoveAppSA && ( + + )} + + ))} + +
+ Account + + Environment Access +
+
+ +
+
+
+ {account.name} + +
+
+
+
+ +
+
+
+ +
+
+ )} +
+ ) : ( + + +
+ } + > + <> + + )} +
+ ) +} diff --git a/frontend/app/[team]/apps/[app]/tokens/page.tsx b/frontend/app/[team]/apps/[app]/access/tokens/page.tsx similarity index 98% rename from frontend/app/[team]/apps/[app]/tokens/page.tsx rename to frontend/app/[team]/apps/[app]/access/tokens/page.tsx index 797fb57b6..5feace5e9 100644 --- a/frontend/app/[team]/apps/[app]/tokens/page.tsx +++ b/frontend/app/[team]/apps/[app]/access/tokens/page.tsx @@ -259,7 +259,7 @@ export default function Tokens({ params }: { params: { team: string; app: string role="button" onClick={() => setActivePanel('secrets')} className={clsx( - 'p-4 cursor-pointer border-l transition ease -ml-px w-60', + 'p-4 cursor-pointer border-l rounded-r-lg transition ease -ml-px w-60', activePanel === 'secrets' ? 'bg-zinc-300 dark:bg-zinc-800 font-semibold border-emerald-500' : 'bg-zinc-200 dark:bg-zinc-900 hover:font-semibold border-neutral-500/40' @@ -272,7 +272,7 @@ export default function Tokens({ params }: { params: { team: string; app: string role="button" onClick={() => setActivePanel('kms')} className={clsx( - 'p-4 cursor-pointer border-l transition ease -ml-px w-60', + 'p-4 cursor-pointer border-l rounded-r-lg transition ease -ml-px w-60', activePanel === 'kms' ? 'bg-zinc-300 dark:bg-zinc-800 font-semibold border-emerald-500' : 'bg-zinc-200 dark:bg-zinc-900 hover:font-semibold border-neutral-500/40' diff --git a/frontend/app/[team]/apps/[app]/layout.tsx b/frontend/app/[team]/apps/[app]/layout.tsx index e2cf3d987..e92291223 100644 --- a/frontend/app/[team]/apps/[app]/layout.tsx +++ b/frontend/app/[team]/apps/[app]/layout.tsx @@ -39,21 +39,17 @@ export default function AppLayout({ link: '', }, { - name: 'Service tokens', - link: 'tokens', - }, - { - name: 'Logs', - link: 'logs', - }, - { - name: 'Members', - link: 'members', + name: 'Access', + link: 'access/members', }, { name: 'Syncing', link: 'syncing', }, + { + name: 'Logs', + link: 'logs', + }, { name: 'Settings', link: 'settings', @@ -64,7 +60,7 @@ export default function AppLayout({ const activeTabIndex = () => { if (app) { const currentUrl = path?.split('/')[4] || '' - const index = tabs.findIndex((tab) => tab.link === currentUrl) + const index = tabs.findIndex((tab) => tab.link.split('/')[0] === currentUrl) return index >= 0 ? index : 0 } return 0 @@ -97,8 +93,10 @@ export default function AppLayout({ {tab.name} diff --git a/frontend/app/[team]/apps/[app]/settings/page.tsx b/frontend/app/[team]/apps/[app]/settings/page.tsx index 1073937bd..d65c4b3cb 100644 --- a/frontend/app/[team]/apps/[app]/settings/page.tsx +++ b/frontend/app/[team]/apps/[app]/settings/page.tsx @@ -131,9 +131,9 @@ export default function AppSettings({ params }: { params: { team: string; app: s

Danger Zone

These actions may result in permanent loss of data

-
+
-

Delete App

+

Delete App

Permanently delete this App

diff --git a/frontend/app/[team]/apps/page.tsx b/frontend/app/[team]/apps/page.tsx index e047033a1..2ce817c98 100644 --- a/frontend/app/[team]/apps/page.tsx +++ b/frontend/app/[team]/apps/page.tsx @@ -48,7 +48,7 @@ export default function AppsHome({ params }: { params: { team: string } }) { >

Apps

{userCanViewApps ? ( -
+
{apps?.map((app) => ( diff --git a/frontend/app/providers.tsx b/frontend/app/providers.tsx index 4978c8628..202a2832e 100644 --- a/frontend/app/providers.tsx +++ b/frontend/app/providers.tsx @@ -5,6 +5,7 @@ import { SessionProvider } from 'next-auth/react' import { ApolloProvider } from '@apollo/client' import { graphQlClient } from '@/apollo/client' import { KeyringProvider } from '@/contexts/keyringContext' +import { SidebarProvider } from '@/contexts/sidebarContext' import { OrganisationProvider } from '@/contexts/organisationContext' import posthog from 'posthog-js' import { PostHogProvider } from 'posthog-js/react' @@ -21,15 +22,17 @@ export default function Providers({ children }: { children: React.ReactNode }) { return ( - - - - - {children} - - - - + + + + + + {children} + + + + + ) } diff --git a/frontend/components/access/AccessTemplateSelector.tsx b/frontend/components/access/AccessTemplateSelector.tsx index 407678b79..603f17d81 100644 --- a/frontend/components/access/AccessTemplateSelector.tsx +++ b/frontend/components/access/AccessTemplateSelector.tsx @@ -95,13 +95,18 @@ export const AccessTemplateSelector = ({ } return ( -
+
{({ open }) => ( <> {({ value }) => ( -
+
{value.icon} {value.name} @@ -116,7 +121,7 @@ export const AccessTemplateSelector = ({
)} - + {accessTemplates.map((template) => ( {({ active, selected }) => ( diff --git a/frontend/components/access/CreateRoleDialog.tsx b/frontend/components/access/CreateRoleDialog.tsx index a98f1dca7..467bd65d8 100644 --- a/frontend/components/access/CreateRoleDialog.tsx +++ b/frontend/components/access/CreateRoleDialog.tsx @@ -364,6 +364,7 @@ export const CreateRoleDialog = () => { {camelCaseToSpaces(resource)} + {resource === 'Tokens' && '(Legacy)'} { const { activeOrganisation: organisation } = useContext(organisationContext) + const { keyring } = useContext(KeyringContext) const ownerRolePolicy = parsePermissions(ownerRole.permissions) @@ -86,7 +90,7 @@ export const ManageRoleDialog = ({ role, ownerRole }: { role: RoleType; ownerRol }) } - const handleUpdateRole = async (e: { preventDefault: () => void }) => { + const handleFormSubmit = async (e: { preventDefault: () => void }) => { e.preventDefault() if (!stringContainsCharacters(name)) { @@ -94,22 +98,40 @@ export const ManageRoleDialog = ({ role, ownerRole }: { role: RoleType; ownerRol return false } - const updated = await updateRole({ - variables: { - id: role.id, - name, - description, - color, - permissions: JSON.stringify(rolePolicy), - }, - refetchQueries: [{ query: GetRoles, variables: { orgId: organisation!.id } }], + toast.promise(handleUpdateRole, { + pending: 'Updating role...', + success: 'Updated role!', + error: 'Something went wrong!', }) + } - if (updated.data.updateCustomRole.role.id) { - if (dialogRef.current) dialogRef.current.closeModal() + const handleUpdateRole = () => { + return new Promise(async (resolve, reject) => { + const existingRolePolicy: PermissionPolicy = JSON.parse(role.permissions) + const mustUpdateServiceAccountHandlers = !arraysEqual( + rolePolicy!.permissions!['ServiceAccounts'], + existingRolePolicy.permissions['ServiceAccounts'] + ) + + const updated = await updateRole({ + variables: { + id: role.id, + name, + description, + color, + permissions: JSON.stringify(rolePolicy), + }, + refetchQueries: [{ query: GetRoles, variables: { orgId: organisation!.id } }], + }) - toast.success('Updated role!') - } + if (mustUpdateServiceAccountHandlers) + await updateServiceAccountHandlers(organisation!.id, keyring!) + + if (updated.data.updateCustomRole.role.id) { + resolve(true) + if (dialogRef.current) dialogRef.current.closeModal() + } else reject + }) } return ( @@ -124,7 +146,7 @@ export const ManageRoleDialog = ({ role, ownerRole }: { role: RoleType; ownerRol size="lg" ref={dialogRef} > -
+
{role.isDefault && (
@@ -347,7 +369,8 @@ export const ManageRoleDialog = ({ role, ownerRole }: { role: RoleType; ownerRol ([resource, actions]) => ( - {camelCaseToSpaces(resource)} + {camelCaseToSpaces(resource)}{' '} + {resource === 'Tokens' && '(Legacy)'} { - const { name, id, members, environments } = props.app + const { name, id, members, serviceAccounts, environments } = props.app const totalSyncCount = environments ? environments.reduce((acc, env) => acc + (env!.syncs?.length || 0), 0) @@ -32,6 +32,8 @@ export const AppCard = (props: AppCardProps) => { const surplusMemberCount = members.length > 5 ? members.length - 5 : 0 + const surplusServiceAccountsCount = serviceAccounts.length > 5 ? serviceAccounts.length - 5 : 0 + const surplusEnvCount = environments.length > 5 ? environments.length - 5 : 0 const surplusSynCount = providers.length > 5 ? providers.length - 5 : 0 @@ -65,6 +67,33 @@ export const AppCard = (props: AppCardProps) => {
+ {serviceAccounts.length > 0 && ( +
+
+ + {serviceAccounts.length} +
+ + {serviceAccounts.length > 1 ? 'Service Accounts' : 'Service Account'} + +
+ {serviceAccounts.slice(0, 5).map((account) => ( +
+ + {account?.name.slice(0, 1)} + +
+ ))} + {surplusMemberCount > 0 && ( + +{surplusMemberCount} + )} +
+
+ )} +
diff --git a/frontend/components/apps/tokens/CreateUserTokenDialog.tsx b/frontend/components/apps/tokens/CreateUserTokenDialog.tsx index 664c7e8a8..36f45b703 100644 --- a/frontend/components/apps/tokens/CreateUserTokenDialog.tsx +++ b/frontend/components/apps/tokens/CreateUserTokenDialog.tsx @@ -3,7 +3,12 @@ import { Button } from '@/components/common/Button' import { KeyringContext } from '@/contexts/keyringContext' -import { ExpiryOptionT, humanReadableExpiry, tokenExpiryOptions } from '@/utils/tokens' +import { + compareExpiryOptions, + ExpiryOptionT, + humanReadableExpiry, + tokenExpiryOptions, +} from '@/utils/tokens' import { useMutation } from '@apollo/client' import { Dialog, RadioGroup, Tab, Transition } from '@headlessui/react' import clsx from 'clsx' @@ -19,10 +24,6 @@ import Link from 'next/link' import { getApiHost } from '@/utils/appConfig' import { getUserKxPublicKey, getUserKxPrivateKey, generateUserToken } from '@/utils/crypto' -const compareExpiryOptions = (a: ExpiryOptionT, b: ExpiryOptionT) => { - return a.getExpiry() === b.getExpiry() -} - export const CreateUserTokenDialog = (props: { organisationId: string }) => { const { organisationId } = props diff --git a/frontend/components/apps/tokens/SecretTokens.tsx b/frontend/components/apps/tokens/SecretTokens.tsx index 1a7f19d95..e4bcbeb2d 100644 --- a/frontend/components/apps/tokens/SecretTokens.tsx +++ b/frontend/components/apps/tokens/SecretTokens.tsx @@ -2,19 +2,23 @@ import { RevokeServiceToken } from '@/graphql/mutations/environments/deleteServi import { GetServiceTokens } from '@/graphql/queries/secrets/getServiceTokens.gql' import { GetAppEnvironments } from '@/graphql/queries/secrets/getAppEnvironments.gql' import { EnvironmentType, ServiceTokenType, UserTokenType } from '@/apollo/graphql' -import { useLazyQuery, useMutation, useQuery } from '@apollo/client' -import { useState, useEffect, useContext, Fragment } from 'react' +import { useMutation, useQuery } from '@apollo/client' +import { useState, useContext, Fragment } from 'react' import { Button } from '@/components/common/Button' -import { FaKey, FaTimes, FaTrashAlt } from 'react-icons/fa' +import { FaExclamationTriangle, FaTimes, FaTrashAlt } from 'react-icons/fa' import { relativeTimeFromDates } from '@/utils/time' import { Dialog, Transition } from '@headlessui/react' import { clsx } from 'clsx' import { organisationContext } from '@/contexts/organisationContext' -import { userHasPermission, userIsAdmin } from '@/utils/access/permissions' +import { userHasPermission } from '@/utils/access/permissions' import { Avatar } from '@/components/common/Avatar' import { CreateServiceTokenDialog } from './CreateServiceTokenDialog' import { MdKey } from 'react-icons/md' import { toast } from 'react-toastify' +import Spinner from '@/components/common/Spinner' +import { EmptyState } from '@/components/common/EmptyState' +import Link from 'next/link' +import { Alert } from '@/components/common/Alert' export const SecretTokens = (props: { organisationId: string; appId: string }) => { const { organisationId, appId } = props @@ -34,7 +38,7 @@ export const SecretTokens = (props: { organisationId: string; appId: string }) = userHasPermission(organisation?.role?.permissions, 'Tokens', 'create', true) && userHasPermission(organisation?.role?.permissions, 'Environments', 'read', true) - const { data: serviceTokensData } = useQuery(GetServiceTokens, { + const { data: serviceTokensData, loading } = useQuery(GetServiceTokens, { variables: { appId, }, @@ -218,6 +222,38 @@ export const SecretTokens = (props: { organisationId: string; appId: string }) = ) } + if (loading) + return ( +
+ +
+ ) + + if (serviceTokensData?.serviceTokens.length === 0) + return ( +
+ + +
+ } + > +
+
+ Service Accounts give you better control over access to secrets across apps, and let + you manage permissions more easily via defined roles. +
+ + + +
+ +
+ ) + return (
@@ -229,6 +265,21 @@ export const SecretTokens = (props: { organisationId: string; appId: string }) =

+ +
+

+ Service Tokens are being deprecated in favour of Service Accounts. +

+

+ Service Accounts give you better control over access to secrets across apps, and let + you manage permissions more easily via defined roles. +

+ + + +
+
+ {usercanCreateTokens && (
diff --git a/frontend/components/common/Accordion.tsx b/frontend/components/common/Accordion.tsx new file mode 100644 index 000000000..42748f4e1 --- /dev/null +++ b/frontend/components/common/Accordion.tsx @@ -0,0 +1,70 @@ +import { Disclosure, Transition } from '@headlessui/react' +import clsx from 'clsx' + +interface AccordionProps { + title?: string + description?: string + children: React.ReactNode + buttonContent?: (open: boolean) => React.ReactNode // Custom button content with open state + defaultOpen?: boolean + className?: string +} + +const Accordion: React.FC = ({ + title, + description, + children, + buttonContent, + defaultOpen = false, + className = '', +}) => { + return ( + + {({ open }) => ( + <> + + {buttonContent ? ( + buttonContent(open) // Render custom content with access to the `open` state + ) : ( +
+
+
{title}
+ {description &&
{description}
} +
+
+ ▼ +
+
+ )} +
+ + + {children} + + + )} +
+ ) +} + +export default Accordion diff --git a/frontend/components/common/CommandPalette.tsx b/frontend/components/common/CommandPalette.tsx index afb496a64..f43b6d59f 100644 --- a/frontend/components/common/CommandPalette.tsx +++ b/frontend/components/common/CommandPalette.tsx @@ -14,6 +14,7 @@ import { FaMoon, FaPlus, FaProjectDiagram, + FaRobot, FaSearch, FaSun, FaUserPlus, @@ -96,6 +97,13 @@ const CommandPalette: React.FC = () => { icon: , action: () => handleNavigation(`/${activeOrganisation?.name}/access/members`), }, + { + id: 'go-service-accounts', + name: 'Go to Service Accounts', + description: 'Manage organization service accounts', + icon: , + action: () => handleNavigation(`/${activeOrganisation?.name}/access/service-accounts`), + }, { id: 'go-integrations', name: 'Go to Integrations', @@ -155,7 +163,7 @@ const CommandPalette: React.FC = () => { name: 'Invite a User', description: 'Invite a new user to the organization', icon: , - action: () => handleNavigation(`/${activeOrganisation?.name}/members?invite=true`), + action: () => handleNavigation(`/${activeOrganisation?.name}/access/members?invite=true`), }) const externalResources: CommandItem[] = [ @@ -207,14 +215,24 @@ const CommandPalette: React.FC = () => { name: `Service tokens`, description: `Manage service tokens for ${app.name}`, icon: , - action: () => handleNavigation(`/${activeOrganisation?.name}/apps/${app.id}/tokens`), + action: () => + handleNavigation(`/${activeOrganisation?.name}/apps/${app.id}/access/tokens`), }, { id: `${app.id}-members`, name: `Members`, description: `Manage members in ${app.name}`, icon: , - action: () => handleNavigation(`/${activeOrganisation?.name}/apps/${app.id}/members`), + action: () => + handleNavigation(`/${activeOrganisation?.name}/apps/${app.id}/access/members`), + }, + { + id: `${app.id}-service-accounts`, + name: `Service Accounts`, + description: `Manage service accounts in ${app.name}`, + icon: , + action: () => + handleNavigation(`/${activeOrganisation?.name}/apps/${app.id}/access/service-accounts`), }, { id: `${app.id}-syncing`, diff --git a/frontend/components/common/EmptyState.tsx b/frontend/components/common/EmptyState.tsx index 6a82beb41..2028facc1 100644 --- a/frontend/components/common/EmptyState.tsx +++ b/frontend/components/common/EmptyState.tsx @@ -13,7 +13,7 @@ export const EmptyState = (props: { {graphic}
{title}
-
{subtitle}
+
{subtitle}
{children}
diff --git a/frontend/components/dashboard/GetStarted.tsx b/frontend/components/dashboard/GetStarted.tsx index 471005ec0..62903046f 100644 --- a/frontend/components/dashboard/GetStarted.tsx +++ b/frontend/components/dashboard/GetStarted.tsx @@ -419,7 +419,7 @@ export const GetStarted = (props: { organisation: OrganisationType }) => { {!memberAdded && (
- + { }) const [getAppEnvs, { data: appEnvsData }] = useLazyQuery(GetAppEnvironments) + const orgContext = usePathname()?.split('/')[2] + const apps = appsData?.apps as AppType[] const envs: EnvironmentType[] = appEnvsData?.appEnvironments ?? [] @@ -37,7 +39,7 @@ export const NavBar = (props: { team: string }) => { const appPage = usePathname()?.split('/')[4] - const activeApp = apps?.find((app) => app.id === appId) + const activeApp = orgContext === 'apps' ? apps?.find((app) => app.id === appId) : undefined useEffect(() => { if (activeApp) getAppEnvs({ variables: { appId: activeApp.id } }) @@ -47,29 +49,42 @@ export const NavBar = (props: { team: string }) => { const activeEnv = activeApp ? envs.find((env) => env.id === envId) : undefined return ( -
+
- + / - {!activeApp && ({props.team})} - - {activeApp && ({props.team})} + + {props.team} + {activeApp && /} {activeApp && (appPage ? ( - {activeApp.name} + + {activeApp.name} + ) : ( - {activeApp.name} + + {activeApp.name} + ))} - {appPage && /} + {activeApp && appPage && /} - {appPage && ( + {activeApp && appPage && ( { {activeEnv && /} - {activeEnv && ({activeEnv.name})} + {activeEnv && ( + + {activeEnv.name} + + )} + + {!activeApp && orgContext && /} + {!activeApp && {orgContext}}
diff --git a/frontend/components/layout/Sidebar.tsx b/frontend/components/layout/Sidebar.tsx index 737060624..ef4625182 100644 --- a/frontend/components/layout/Sidebar.tsx +++ b/frontend/components/layout/Sidebar.tsx @@ -4,19 +4,21 @@ import Link from 'next/link' import { usePathname } from 'next/navigation' import clsx from 'clsx' import { - FaChevronDown, FaCog, FaCubes, FaExchangeAlt, FaHome, - FaKey, FaPlus, FaUsersCog, FaProjectDiagram, + FaAngleDoubleLeft, + FaAngleDoubleRight, + FaChevronDown, } from 'react-icons/fa' import { organisationContext } from '@/contexts/organisationContext' +import { SidebarContext } from '@/contexts/sidebarContext' import { Fragment, useContext } from 'react' -import { OrganisationType } from '@/apollo/graphql' +import { ApiOrganisationPlanChoices, OrganisationType } from '@/apollo/graphql' import { Menu, Transition } from '@headlessui/react' import { Button } from '../common/Button' import { PlanLabel } from '../settings/organisation/PlanLabel' @@ -28,66 +30,121 @@ export type SidebarLinkT = { active: boolean } -const SidebarLink = (props: SidebarLinkT) => { - const { name, href, icon, active } = props - +const SidebarLink = ({ + name, + href, + icon, + active, + collapsed, +}: SidebarLinkT & { collapsed: boolean }) => { return ( - -
+
+
+
{icon}
+ + + {' '} + {name}{' '} + +
+ {collapsed && ( +
+ {name} +
)} - > -
{icon}
- {name}
) } const Sidebar = () => { + const { sidebarState, setSidebarState } = useContext(SidebarContext) + const collapsed = sidebarState === 'collapsed' const team = usePathname()?.split('/')[1] - const { organisations, activeOrganisation } = useContext(organisationContext) - const showOrgsMenu = organisations && organisations.length > 1 - const isOwner = organisations?.some((org) => org.role!.name!.toLowerCase() === 'owner') const OrgsMenu = () => { - const OrgLabel = () => ( -
-
-
- + const planStyle = () => { + if (activeOrganisation?.plan === ApiOrganisationPlanChoices.Fr) + return 'ring-neutral-500/40 bg-neutral-500/40 text-zinc-900 dark:bg-zinc-800 dark:text-neutral-500' + if (activeOrganisation?.plan === ApiOrganisationPlanChoices.Pr) + return 'ring-emerald-400/10 bg-emerald-400 text-zinc-900 dark:bg-emerald-400/10 dark:text-emerald-400' + if (activeOrganisation?.plan === ApiOrganisationPlanChoices.En) + return 'ring-amber-400/10 bg-amber-400 text-zinc-900 dark:bg-amber-400/10 dark:text-amber-400' + } + + const OrgLabel = ({ open }: { open?: boolean }) => ( +
+ {collapsed ? ( +
+ + {activeOrganisation?.name?.[0]?.toUpperCase()} + +
+ ) : ( +
+
+ +
+ + {activeOrganisation?.name} +
- + )} + {showOrgsMenu && !collapsed && ( + + )} + {collapsed && ( +
{activeOrganisation?.name} - -
+
+ )}
) if (!showOrgsMenu) return return ( - + {({ open }) => ( <> - -
- - -
+ + { leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95" > - +
{organisations?.map((org: OrganisationType) => ( @@ -148,60 +205,71 @@ const Sidebar = () => { { name: 'Home', href: `/${team}`, - icon: , + icon: , active: usePathname() === `/${team}`, }, { name: 'Apps', href: `/${team}/apps`, - icon: , + icon: , active: usePathname()?.split('/')[2] === 'apps', }, { name: 'Integrations', href: `/${team}/integrations`, - icon: , + icon: , active: usePathname() === `/${team}/integrations`, }, { name: 'Access Control', href: `/${team}/access/members`, - icon: , + icon: , active: usePathname()?.split('/')[2] === `access`, }, { name: 'Settings', href: `/${team}/settings`, - icon: , + icon: , active: usePathname() === `/${team}/settings`, }, ] return ( -
+
diff --git a/frontend/components/logs/SecretLogs.tsx b/frontend/components/logs/SecretLogs.tsx index 5efb2e438..b4490d27a 100644 --- a/frontend/components/logs/SecretLogs.tsx +++ b/frontend/components/logs/SecretLogs.tsx @@ -9,7 +9,14 @@ import { } from '@/apollo/graphql' import { Disclosure, Transition } from '@headlessui/react' import clsx from 'clsx' -import { FaBan, FaChevronRight, FaExternalLinkAlt, FaKey } from 'react-icons/fa' +import { + FaArrowRight, + FaBan, + FaChevronRight, + FaExternalLinkAlt, + FaKey, + FaRobot, +} from 'react-icons/fa' import { FiRefreshCw, FiChevronsDown } from 'react-icons/fi' import { dateToUnixTimestamp, relativeTimeFromDates } from '@/utils/time' import { ReactNode, useContext, useEffect, useRef, useState } from 'react' @@ -237,6 +244,32 @@ export default function SecretLogs(props: { app: string }) { if (eventType === ApiSecretEventEventTypeChoices.D) return 'Deleted secret' } + const logCreatedBy = (log: SecretEventType) => { + if (log.user) + return ( +
+ + {log.user.fullName || log.user.email} +
+ ) + else if (log.serviceToken) + return ( +
+ {log.serviceToken ? log.serviceToken.name : 'Service token'} +
+ ) + else if (log.serviceAccount) + return ( +
+
+ +
{' '} + {log.serviceAccount.name} + {log.serviceAccountToken && ` (${log.serviceAccountToken.name})`} +
+ ) + } + return ( {({ open }) => ( @@ -253,7 +286,7 @@ export default function SecretLogs(props: { app: string }) { {/* */} @@ -266,16 +299,7 @@ export default function SecretLogs(props: { app: string }) {
- {log.user ? ( -
- - {log.user.fullName || log.user.email} -
- ) : ( -
- {log.serviceToken ? log.serviceToken.name : 'Service token'} -
- )} + {logCreatedBy(log)}
@@ -310,8 +334,10 @@ export default function SecretLogs(props: { app: string }) {
@@ -334,17 +360,19 @@ export default function SecretLogs(props: { app: string }) { - {' '} - {log.user ? ( -
- - {log.user.fullName || log.user.email} -
- ) : ( -
- {log.serviceToken ? log.serviceToken.name : 'Service token'} -
- )} +
+ {logCreatedBy(log)}{' '} + {log.serviceAccount && ( + + + + )} +
{log.ipAddress} @@ -426,7 +454,7 @@ export default function SecretLogs(props: { app: string }) { - User + Account Event Environment Secret diff --git a/frontend/components/settings/organisation/PlanInfo.tsx b/frontend/components/settings/organisation/PlanInfo.tsx index 5255ae465..261846304 100644 --- a/frontend/components/settings/organisation/PlanInfo.tsx +++ b/frontend/components/settings/organisation/PlanInfo.tsx @@ -8,7 +8,16 @@ import { PlanLabel } from './PlanLabel' import Spinner from '@/components/common/Spinner' import { calculatePercentage } from '@/utils/dataUnits' import { Button } from '@/components/common/Button' -import { FaCheckCircle, FaCube, FaCubes, FaTimesCircle, FaUser, FaUsersCog } from 'react-icons/fa' +import { + FaCheckCircle, + FaChevronDown, + FaCog, + FaCube, + FaCubes, + FaTimesCircle, + FaUser, + FaUsersCog, +} from 'react-icons/fa' import Link from 'next/link' import { ActivatedPhaseLicenseType, ApiOrganisationPlanChoices } from '@/apollo/graphql' import { isCloudHosted } from '@/utils/appConfig' @@ -20,15 +29,16 @@ import { useSearchParams } from 'next/navigation' import { PostCheckoutScreen } from '@/ee/billing/PostCheckoutScreen' import { UpsellDialog } from './UpsellDialog' import { userHasPermission } from '@/utils/access/permissions' +import Accordion from '@/components/common/Accordion' +import clsx from 'clsx' const plansInfo = { FR: { id: ApiOrganisationPlanChoices.Fr, name: 'Free', description: 'Try Phase without any commitments.', - seats: isCloudHosted() ? '5 Users' : 'Unlimited Users', + seats: isCloudHosted() ? '5 Users / Service Accounts' : 'Unlimited Users', apps: isCloudHosted() ? '3 Apps' : 'Unlimited Apps', - tokens: isCloudHosted() ? '3 Service Tokens per app' : 'Unlimited Service Tokens per app', featureSummary: [ 'End-to-end Encryption', 'Google/GitHub/Gitlab SSO', @@ -39,7 +49,6 @@ const plansInfo = { 'Community Support', ], notIncluded: [ - ...['SAML SSO', 'Priority Support'], ...(isCloudHosted() ? [ '90-day audit log retention', @@ -56,7 +65,6 @@ const plansInfo = { name: 'Pro', seats: 'Unlimited Users', apps: 'Unlimited Apps', - tokens: isCloudHosted() ? '10 Service Tokens per app' : 'Unlimited Service Tokens per app', featureSummary: [ 'End-to-end Encryption', 'Google/GitHub/Gitlab SSO', @@ -67,10 +75,7 @@ const plansInfo = { 'Priority Support', ], notIncluded: [ - ...['SAML SSO', 'Dedicated Support'], - ...(isCloudHosted() - ? ['Unlimited audit log retention', 'Unlimited Environments', 'Unlimited Service Tokens'] - : []), + ...(isCloudHosted() ? ['Unlimited audit log retention', 'Unlimited Environments'] : []), ], }, EN: { @@ -80,10 +85,9 @@ const plansInfo = { 'Secure existing data in your enterprise workload. Get full onboarding and priority technical support.', seats: 'Unlimited Users', apps: 'Unlimited Apps', - tokens: 'Unlimited Service Tokens per app', featureSummary: [ 'End-to-end Encryption', - 'Google/GitHub/Gitlab/SAML SSO', + 'Google/GitHub/Gitlab SSO', 'Role-based Access Control', 'Secret Versioning', 'Secret Referencing', @@ -100,7 +104,7 @@ const PlanFeatureItem = (props: { iconType: 'check' | 'cross' | 'user' | 'app' | 'env' | 'key' }) => { return ( -
+
{props.iconType === 'check' && } {props.iconType === 'cross' && } {props.iconType === 'user' && } @@ -137,13 +141,28 @@ export const PlanInfo = () => { const license = (): ActivatedPhaseLicenseType | null => licenseData?.organisationLicense || null + const seatsUsed = data ? data.organisationPlan.seatsUsed.total : 0 + + const seatLimit = data ? license()?.seats || data.organisationPlan.maxUsers : undefined + const appQuotaUsage = data ? calculatePercentage(data.organisationPlan.appCount, data.organisationPlan.maxApps) : 0 + const seatQuotaUsage = data + ? calculatePercentage(seatsUsed, license()?.seats || data.organisationPlan.maxUsers) + : 0 + const memberQuotaUsage = data ? calculatePercentage( - data.organisationPlan.userCount, + data.organisationPlan.seatsUsed.users, + license()?.seats || data.organisationPlan.maxUsers + ) + : 0 + + const serviceAccountQuotaUsage = data + ? calculatePercentage( + data.organisationPlan.seatsUsed.serviceAccounts, license()?.seats || data.organisationPlan.maxUsers ) : 0 @@ -185,20 +204,16 @@ export const PlanInfo = () => {
- {planInfo && ( + {planInfo && isCloudHosted() && (
- {license()?.seats ? `${license()?.seats} Users` : planInfo.seats} + {license()?.seats ? `${license()?.seats} Users / Service Accounts` : planInfo.seats} {planInfo.apps} - - {license()?.tokens - ? `${license()?.tokens} Service Tokens per App` - : planInfo.tokens} - + {planInfo.featureSummary.map((feature) => ( {feature} @@ -221,21 +236,107 @@ export const PlanInfo = () => {
-
Usage
+
+
Usage
+
+ Details of seat and app quota usage for your Organisation plan +
+
+ + ( +
+
+
+
Seats
+ +
+
{`${seatsUsed} ${seatLimit ? `of ${seatLimit}` : ''} Seats used`}
+
+ {seatLimit && ( + + )} +
+ )} + > +
+
+
+
+
Members
+ + + +
+ +
{`${data.organisationPlan.seatsUsed.users} Seats used`}
+
+ {seatLimit && ( + + )} +
+ +
+
+
+
+ Service Accounts +
+ + + +
+ +
{`${data.organisationPlan.seatsUsed.serviceAccounts} Seats used`}
+
+ {seatLimit && ( + + )} +
+
+
Apps
{`${data.organisationPlan.appCount} ${data.organisationPlan.maxApps ? `of ${data.organisationPlan.maxApps}` : ''} Apps used`}
- {activeOrganisation.plan === ApiOrganisationPlanChoices.Fr && ( + {data.organisationPlan.maxApps && ( )}
@@ -246,30 +347,6 @@ export const PlanInfo = () => {
- -
-
-
Members
-
{`${data.organisationPlan.userCount} ${license()?.seats || data.organisationPlan.maxUsers ? `of ${license()?.seats || data.organisationPlan.maxUsers}` : ''} Seats used`}
-
- {(activeOrganisation.plan === ApiOrganisationPlanChoices.Fr || license()?.seats) && ( - - )} -
- - - -
-
{searchParams?.get('stripe_session_id') && ( diff --git a/frontend/components/users/RoleLabel.tsx b/frontend/components/users/RoleLabel.tsx index 141282933..1639d4e4a 100644 --- a/frontend/components/users/RoleLabel.tsx +++ b/frontend/components/users/RoleLabel.tsx @@ -15,8 +15,12 @@ export const RoleLabel = ({ role, size }: { role: RoleType; size?: 'sm' | 'md' | return 'ring-emerald-400/10 bg-emerald-400 text-black dark:bg-zinc-800 dark:text-emerald-400' else if (role.name!.toLowerCase() === 'owner') return 'ring-amber-400/10 bg-amber-400 text-black dark:bg-zinc-800 dark:text-amber-400' + else if (role.name!.toLowerCase() === 'manager') + return 'ring-cyan-500/40 bg-cyan-500/40 text-black dark:bg-zinc-800 dark:text-cyan-300' else if (role.name!.toLowerCase() === 'developer') return 'ring-neutral-500/40 bg-neutral-500/40 text-black dark:bg-zinc-800 dark:text-neutral-300' + else if (role.name!.toLowerCase() === 'service') + return 'ring-purple-500/40 bg-purple-500/40 text-black dark:bg-zinc-800 dark:text-purple-300' else return '' } diff --git a/frontend/contexts/sidebarContext.tsx b/frontend/contexts/sidebarContext.tsx new file mode 100644 index 000000000..9a519673b --- /dev/null +++ b/frontend/contexts/sidebarContext.tsx @@ -0,0 +1,45 @@ +import React, { createContext, useState, useEffect } from 'react' + +type SidebarState = 'expanded' | 'collapsed' + +interface SidebarContextValue { + sidebarState: SidebarState + setSidebarState: (state: SidebarState) => void +} + +const getInitialState = (): SidebarState => { + if (typeof window !== 'undefined' && window.localStorage) { + const storedState = window.localStorage.getItem('sidebar-state') + if (storedState === 'collapsed') { + return 'collapsed' + } + } + return 'expanded' // Default state is expanded +} + +export const SidebarContext = createContext({ + sidebarState: 'expanded', + setSidebarState: () => {}, +}) + +interface SidebarProviderProps { + children: React.ReactNode +} + +export const SidebarProvider: React.FC = ({ children }) => { + const [sidebarState, setSidebarState] = useState('expanded') + + useEffect(() => { + setSidebarState(getInitialState()) + }, []) + + useEffect(() => { + localStorage.setItem('sidebar-state', sidebarState) + }, [sidebarState]) + + return ( + + {children} + + ) +} diff --git a/frontend/graphql/mutations/apps/addAppMember.gql b/frontend/graphql/mutations/apps/addAppMember.gql index 04d236b35..b5f56d8a4 100644 --- a/frontend/graphql/mutations/apps/addAppMember.gql +++ b/frontend/graphql/mutations/apps/addAppMember.gql @@ -1,5 +1,10 @@ -mutation AddMemberToApp($memberId: ID!, $appId: ID!, $envKeys: [EnvironmentKeyInput]) { - addAppMember(memberId: $memberId, appId: $appId, envKeys: $envKeys) { +mutation AddMemberToApp( + $memberId: ID! + $memberType: MemberType + $appId: ID! + $envKeys: [EnvironmentKeyInput] +) { + addAppMember(memberId: $memberId, memberType: $memberType, appId: $appId, envKeys: $envKeys) { app { id } diff --git a/frontend/graphql/mutations/apps/removeAppMember.gql b/frontend/graphql/mutations/apps/removeAppMember.gql index 61251f9d2..ce3bf1588 100644 --- a/frontend/graphql/mutations/apps/removeAppMember.gql +++ b/frontend/graphql/mutations/apps/removeAppMember.gql @@ -1,5 +1,5 @@ -mutation RemoveMemberFromApp($memberId: ID!, $appId: ID!) { - removeAppMember(memberId: $memberId, appId: $appId) { +mutation RemoveMemberFromApp($memberId: ID!, $memberType: MemberType, $appId: ID!) { + removeAppMember(memberId: $memberId, memberType: $memberType, appId: $appId) { app { id } diff --git a/frontend/graphql/mutations/apps/updateEnvScope.gql b/frontend/graphql/mutations/apps/updateEnvScope.gql index 3981df89b..f85eda8de 100644 --- a/frontend/graphql/mutations/apps/updateEnvScope.gql +++ b/frontend/graphql/mutations/apps/updateEnvScope.gql @@ -1,5 +1,15 @@ -mutation UpdateEnvScope($memberId: ID!, $appId: ID!, $envKeys: [EnvironmentKeyInput]) { - updateMemberEnvironmentScope(memberId: $memberId, appId: $appId, envKeys: $envKeys) { +mutation UpdateEnvScope( + $memberId: ID! + $memberType: MemberType + $appId: ID! + $envKeys: [EnvironmentKeyInput] +) { + updateMemberEnvironmentScope( + memberId: $memberId + memberType: $memberType + appId: $appId + envKeys: $envKeys + ) { app { id } diff --git a/frontend/graphql/mutations/service-accounts/createServiceAccount.gql b/frontend/graphql/mutations/service-accounts/createServiceAccount.gql new file mode 100644 index 000000000..de82a340e --- /dev/null +++ b/frontend/graphql/mutations/service-accounts/createServiceAccount.gql @@ -0,0 +1,23 @@ +mutation CreateServiceAccountOp( + $name: String! + $orgId: ID! + $roleId: ID! + $identityKey: String! + $handlers: [ServiceAccountHandlerInput] + $serverWrappedKeyring: String + $serverWrappedRecovery: String +) { + createServiceAccount( + name: $name + organisationId: $orgId + roleId: $roleId + identityKey: $identityKey + handlers: $handlers + serverWrappedKeyring: $serverWrappedKeyring + serverWrappedRecovery: $serverWrappedRecovery + ) { + serviceAccount { + id + } + } +} diff --git a/frontend/graphql/mutations/service-accounts/createServiceAccountToken.gql b/frontend/graphql/mutations/service-accounts/createServiceAccountToken.gql new file mode 100644 index 000000000..2dede6698 --- /dev/null +++ b/frontend/graphql/mutations/service-accounts/createServiceAccountToken.gql @@ -0,0 +1,21 @@ +mutation CreateSAToken( + $serviceAccountId: ID! + $name: String! + $identityKey: String! + $token: String! + $wrappedKeyShare: String! + $expiry: BigInt +) { + createServiceAccountToken( + serviceAccountId: $serviceAccountId + name: $name + identityKey: $identityKey + token: $token + wrappedKeyShare: $wrappedKeyShare + expiry: $expiry + ) { + token { + id + } + } +} diff --git a/frontend/graphql/mutations/service-accounts/deleteServiceAccount.gql b/frontend/graphql/mutations/service-accounts/deleteServiceAccount.gql new file mode 100644 index 000000000..62c949f10 --- /dev/null +++ b/frontend/graphql/mutations/service-accounts/deleteServiceAccount.gql @@ -0,0 +1,5 @@ +mutation DeleteServiceAccountOp($id: ID!) { + deleteServiceAccount(serviceAccountId: $id) { + ok + } +} diff --git a/frontend/graphql/mutations/service-accounts/deleteServiceAccountToken.gql b/frontend/graphql/mutations/service-accounts/deleteServiceAccountToken.gql new file mode 100644 index 000000000..38c1091d7 --- /dev/null +++ b/frontend/graphql/mutations/service-accounts/deleteServiceAccountToken.gql @@ -0,0 +1,5 @@ +mutation DeleteServiceAccountTokenOp($id: ID!) { + deleteServiceAccountToken(tokenId: $id) { + ok + } +} diff --git a/frontend/graphql/mutations/service-accounts/updateHandlerKeys.gql b/frontend/graphql/mutations/service-accounts/updateHandlerKeys.gql new file mode 100644 index 000000000..ecb0badc0 --- /dev/null +++ b/frontend/graphql/mutations/service-accounts/updateHandlerKeys.gql @@ -0,0 +1,5 @@ +mutation UpdateServiceAccountHandlerKeys($orgId: ID!, $handlers: [ServiceAccountHandlerInput]) { + updateServiceAccountHandlers(organisationId: $orgId, handlers: $handlers) { + ok + } +} diff --git a/frontend/graphql/mutations/service-accounts/updateServiceAccount.gql b/frontend/graphql/mutations/service-accounts/updateServiceAccount.gql new file mode 100644 index 000000000..d88859d4a --- /dev/null +++ b/frontend/graphql/mutations/service-accounts/updateServiceAccount.gql @@ -0,0 +1,14 @@ +mutation UpdateServiceAccountOp($serviceAccountId: ID!, $name: String!, $roleId: ID!) { + updateServiceAccount(serviceAccountId: $serviceAccountId, name: $name, roleId: $roleId) { + serviceAccount { + id + name + role { + id + name + description + permissions + } + } + } +} diff --git a/frontend/graphql/queries/apps/getAppServiceAccounts.gql b/frontend/graphql/queries/apps/getAppServiceAccounts.gql new file mode 100644 index 000000000..e6b5d6427 --- /dev/null +++ b/frontend/graphql/queries/apps/getAppServiceAccounts.gql @@ -0,0 +1,19 @@ +query GetAppServiceAccounts($appId: ID!) { + appServiceAccounts(appId: $appId) { + id + identityKey + name + createdAt + role { + id + name + description + permissions + color + } + tokens { + id + name + } + } +} diff --git a/frontend/graphql/queries/getApps.gql b/frontend/graphql/queries/getApps.gql index 3dc8320d8..70ab2ec9a 100644 --- a/frontend/graphql/queries/getApps.gql +++ b/frontend/graphql/queries/getApps.gql @@ -11,6 +11,10 @@ query GetApps($organisationId: ID!, $appId: ID) { fullName avatarUrl } + serviceAccounts { + id + name + } environments { id name diff --git a/frontend/graphql/queries/getOrganisations.gql b/frontend/graphql/queries/getOrganisations.gql index 545196343..e443bca40 100644 --- a/frontend/graphql/queries/getOrganisations.gql +++ b/frontend/graphql/queries/getOrganisations.gql @@ -10,7 +10,11 @@ query GetOrganisations { maxUsers maxApps maxEnvsPerApp - userCount + seatsUsed { + users + serviceAccounts + total + } appCount } role { diff --git a/frontend/graphql/queries/organisation/getOrganisationPlan.gql b/frontend/graphql/queries/organisation/getOrganisationPlan.gql index bee9c89b5..bd5e7244f 100644 --- a/frontend/graphql/queries/organisation/getOrganisationPlan.gql +++ b/frontend/graphql/queries/organisation/getOrganisationPlan.gql @@ -4,7 +4,11 @@ query GetOrganisationPlan($organisationId: ID!) { maxUsers maxApps maxEnvsPerApp - userCount + seatsUsed { + users + serviceAccounts + total + } appCount } } diff --git a/frontend/graphql/queries/secrets/getAppEnvironments.gql b/frontend/graphql/queries/secrets/getAppEnvironments.gql index 32f459946..77ccd54ac 100644 --- a/frontend/graphql/queries/secrets/getAppEnvironments.gql +++ b/frontend/graphql/queries/secrets/getAppEnvironments.gql @@ -1,5 +1,10 @@ -query GetAppEnvironments($appId: ID!, $memberId: ID) { - appEnvironments(appId: $appId, environmentId: null, memberId: $memberId) { +query GetAppEnvironments($appId: ID!, $memberId: ID, $memberType: MemberType) { + appEnvironments( + appId: $appId + environmentId: null + memberId: $memberId + memberType: $memberType + ) { id name envType diff --git a/frontend/graphql/queries/secrets/getAppSecretsLogs.gql b/frontend/graphql/queries/secrets/getAppSecretsLogs.gql index 9f2044753..6ecee8e18 100644 --- a/frontend/graphql/queries/secrets/getAppSecretsLogs.gql +++ b/frontend/graphql/queries/secrets/getAppSecretsLogs.gql @@ -17,7 +17,7 @@ query GetAppSecretsLogs($appId: ID!, $start: BigInt, $end: BigInt) { userAgent user { email - username + username fullName avatarUrl } @@ -25,6 +25,14 @@ query GetAppSecretsLogs($appId: ID!, $start: BigInt, $end: BigInt) { id name } + serviceAccount { + id + name + } + serviceAccountToken { + id + name + } eventType environment { id diff --git a/frontend/graphql/queries/secrets/getSecrets.gql b/frontend/graphql/queries/secrets/getSecrets.gql index c6d80b433..d06f3a4c5 100644 --- a/frontend/graphql/queries/secrets/getSecrets.gql +++ b/frontend/graphql/queries/secrets/getSecrets.gql @@ -33,6 +33,14 @@ query GetSecrets($appId: ID!, $envId: ID!, $path: String) { fullName avatarUrl } + serviceToken { + id + name + } + serviceAccount { + id + name + } eventType } override { diff --git a/frontend/graphql/queries/service-accounts/getServiceAccountHandlers.gql b/frontend/graphql/queries/service-accounts/getServiceAccountHandlers.gql new file mode 100644 index 000000000..7dd987db0 --- /dev/null +++ b/frontend/graphql/queries/service-accounts/getServiceAccountHandlers.gql @@ -0,0 +1,12 @@ +query GetServiceAccountHandlers($orgId: ID!) { + serviceAccountHandlers(orgId: $orgId) { + id + email + role { + name + permissions + } + identityKey + self + } +} diff --git a/frontend/graphql/queries/service-accounts/getServiceAccounts.gql b/frontend/graphql/queries/service-accounts/getServiceAccounts.gql new file mode 100644 index 000000000..755774b9f --- /dev/null +++ b/frontend/graphql/queries/service-accounts/getServiceAccounts.gql @@ -0,0 +1,43 @@ +query GetServiceAccounts($orgId: ID!, $id: ID) { + serviceAccounts(orgId: $orgId, serviceAccountId: $id) { + id + name + identityKey + role { + id + name + description + color + permissions + } + createdAt + handlers { + id + wrappedKeyring + wrappedRecovery + user { + self + } + } + tokens { + id + name + createdAt + expiresAt + createdBy { + fullName + avatarUrl + self + } + lastUsed + } + appMemberships { + id + name + environments { + id + name + } + } + } +} diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 2db4bd330..744050479 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -25,6 +25,9 @@ module.exports = { }, typography: require('./utils/typography'), extend: { + screens: { + '1080p': '1920px', + }, colors: { primary: { 200: '#f7ff73', diff --git a/frontend/utils/crypto/environments.ts b/frontend/utils/crypto/environments.ts index 8b62ecada..e80fc1c5e 100644 --- a/frontend/utils/crypto/environments.ts +++ b/frontend/utils/crypto/environments.ts @@ -6,6 +6,7 @@ import { EnvironmentType, OrganisationMemberType, SecretType, + ServiceAccountType, } from '@/apollo/graphql' import { EnvKeypair, OrganisationKeyring } from './types' @@ -223,19 +224,19 @@ export const generateUserToken = async ( * Wraps environment secrets for a user. * * @param {{ seed: string; salt: string }} envSecrets - The environment secrets to be wrapped. - * @param {OrganisationMemberType} user - The user for whom the secrets are wrapped. + * @param {OrganisationMemberType | ServiceAccountType} account - The target account for whom the secrets are wrapped. * @returns {Promise<{ user: OrganisationMemberType; wrappedSeed: string; wrappedSalt: string }>} - An object containing the wrapped environment secrets and user information. */ -export const wrapEnvSecretsForUser = async ( +export const wrapEnvSecretsForAccount = async ( envSecrets: { seed: string; salt: string }, - user: OrganisationMemberType + account: OrganisationMemberType | ServiceAccountType ) => { - const userPubKey = await getUserKxPublicKey(user.identityKey!) + const userPubKey = await getUserKxPublicKey(account.identityKey!) const wrappedSeed = await encryptAsymmetric(envSecrets.seed, userPubKey) const wrappedSalt = await encryptAsymmetric(envSecrets.salt, userPubKey) return { - user, + user: account, wrappedSeed, wrappedSalt, } @@ -401,12 +402,12 @@ export const createNewEnv = async ( (user: OrganisationMemberType) => user.role!.name?.toLowerCase() === "owner" ) - const ownerWrappedEnv = await wrapEnvSecretsForUser({ seed, salt }, owner!) + const ownerWrappedEnv = await wrapEnvSecretsForAccount({ seed, salt }, owner!) const globalAccessUsersWrappedEnv = await Promise.all( globalAccessUsers .filter((user) => user.role!.name?.toLowerCase() !== "owner") .map(async (admin) => { - const adminWrappedEnvSecret = await wrapEnvSecretsForUser({ seed, salt }, admin) + const adminWrappedEnvSecret = await wrapEnvSecretsForAccount({ seed, salt }, admin) return adminWrappedEnvSecret }) ) diff --git a/frontend/utils/crypto/service-accounts.ts b/frontend/utils/crypto/service-accounts.ts new file mode 100644 index 000000000..797479c53 --- /dev/null +++ b/frontend/utils/crypto/service-accounts.ts @@ -0,0 +1,127 @@ +import { graphQlClient } from "@/apollo/client"; +import { newEnvWrapKey, newEnvToken } from "./environments"; +import { decryptAsymmetric, encryptAsymmetric, getWrappedKeyShare } from "./general"; +import { splitSecret } from "./keyshares"; +import { GetServiceAccounts } from '@/graphql/queries/service-accounts/getServiceAccounts.gql' +import { GetServiceAccountHandlers } from '@/graphql/queries/service-accounts/getServiceAccountHandlers.gql' +import { UpdateServiceAccountHandlerKeys } from '@/graphql/mutations/service-accounts/updateHandlerKeys.gql' +import { GetServerKey } from '@/graphql/queries/syncing/getServerKey.gql' +import { OrganisationKeyring } from "./types"; +import { OrganisationMemberType, ServiceAccountHandlerInput, ServiceAccountHandlerType, ServiceAccountType } from "@/apollo/graphql"; +import { getUserKxPublicKey, getUserKxPrivateKey } from "./users"; + +/** + * Generates a service account token. + * + * @param {string} serviceAccountId - The Service Account ID. + * @param {{ publicKey: string; privateKey: string }} saKeyring - The service account keyring. + * @returns {Promise<{ pssService: string; mutationPayload: object }>} - An object containing the user token and mutation payload. + */ +export const generateSAToken = async ( + serviceAccountId: string, + saKeyring: { publicKey: string; privateKey: string }, + name: string, + expiry: number | null +) => { + const wrapKey = await newEnvWrapKey() + const token = await newEnvToken() + + const keyShares = await splitSecret(saKeyring.privateKey) + const wrappedKeyShare = await getWrappedKeyShare(keyShares[1], wrapKey) + + const pssService = `pss_service:v2:${token}:${saKeyring.publicKey}:${keyShares[0]}:${wrapKey}` + const mutationPayload = { + serviceAccountId, + name, + identityKey: saKeyring.publicKey, + token, + wrappedKeyShare, + expiry, + } + + return { + pssService, + mutationPayload, + } +} + + + +/** + * Updates the service account handlers for all service accounts in the organisation. + * Fetches all service accounts, all handlers, and encrypts each account's keys for each handler. + * The promise will resolve if the operation is successful or reject with an error message otherwise. + * + * @param {string} orgId - The organisation ID. + * @param {OrganisationKeyring} userKeyring - The current active user keyring. + * @returns {Promise} + */ +export const updateServiceAccountHandlers = async (orgId: string, userKeyring: OrganisationKeyring) => { + return new Promise(async (resolve, reject) => { + // Fetch service accounts + const { data: serviceAccountsData } = await graphQlClient.query({ query: GetServiceAccounts, variables: { orgId }, fetchPolicy: 'network-only' }) + const serviceAccounts = serviceAccountsData?.serviceAccounts || [] + + // Fetch service account handlers + const { data: handlersData } = await graphQlClient.query({ query: GetServiceAccountHandlers, variables: { orgId }, fetchPolicy: 'network-only' }) + const handlers = handlersData.serviceAccountHandlers + + // Current user kx keys + const userKxKeys = { + publicKey: await getUserKxPublicKey(userKeyring.publicKey), + privateKey: await getUserKxPrivateKey(userKeyring.privateKey), + } + + let handlerInputs: ServiceAccountHandlerInput[] = [] + + const handlerInputPromises = serviceAccounts.map(async (account: ServiceAccountType) => { + + // Get the account wrapped keys for the current user + const selfHandler: ServiceAccountHandlerType = account.handlers?.find( + (handler) => handler?.user.self === true + )! + + // Unwrap the keyring and recovery for this account + const serviceAccountKeyringString = await decryptAsymmetric( + selfHandler.wrappedKeyring, + userKxKeys.privateKey, + userKxKeys.publicKey + ) + + const serviceAccountRecoveryString = await decryptAsymmetric( + selfHandler.wrappedRecovery, + userKxKeys.privateKey, + userKxKeys.publicKey + ) + + // Wrap the keyring and recovery for each handler + const handlerWrappingPromises = handlers.map(async (handler: OrganisationMemberType) => { + + const kxKey = await getUserKxPublicKey(handler.identityKey!) + const wrappedKeyring = await encryptAsymmetric(serviceAccountKeyringString, kxKey) + const wrappedRecovery = await encryptAsymmetric(serviceAccountRecoveryString, kxKey) + return { + serviceAccountId: account.id, + memberId: handler.id, + wrappedKeyring, + wrappedRecovery, + } + }) + + const handlerKeys = await Promise.all(handlerWrappingPromises) + + return handlerKeys // Return the result of this async operation + }) + + // Wait for all handler input promises to resolve and flatten the array + const allHandlerInputs = await Promise.all(handlerInputPromises) + handlerInputs = allHandlerInputs.flat() // Flatten the nested arrays + + + const {data: result } = await graphQlClient.mutate({ mutation: UpdateServiceAccountHandlerKeys, variables: { orgId, handlers: handlerInputs }}) + if (result.updateServiceAccountHandlers.ok) resolve("Success") + else reject("Failed to update service account handlers") + + }) + +} \ No newline at end of file diff --git a/frontend/utils/tokens.ts b/frontend/utils/tokens.ts index d77a5842a..ddd493608 100644 --- a/frontend/utils/tokens.ts +++ b/frontend/utils/tokens.ts @@ -32,3 +32,8 @@ export const humanReadableExpiry = (expiryOption: ExpiryOptionT) => expiryOption.getExpiry() === null ? 'This token will never expire.' : `This token will expire on ${new Date(expiryOption.getExpiry()!).toLocaleDateString()}.` + + +export const compareExpiryOptions = (a: ExpiryOptionT, b: ExpiryOptionT) => { + return a.getExpiry() === b.getExpiry() +} \ No newline at end of file