diff --git a/.env.dev.example b/.env.dev.example
index 72ab1f614..cd3b0a9be 100644
--- a/.env.dev.example
+++ b/.env.dev.example
@@ -15,7 +15,7 @@ HTTP_PROTOCOL=http://
NEXTAUTH_URL=http://localhost
OAUTH_REDIRECT_URI=http://localhost
BACKEND_API_BASE=http://backend:8000
-NEXT_PUBLIC_BACKEND_API_BASE=http://localhost/ph-backend
+NEXT_PUBLIC_BACKEND_API_BASE=https://localhost/service
NEXT_PUBLIC_NEXTAUTH_PROVIDERS=google,github,gitlab
# WARNING: Replace this with a cryptographically strong random value. You can use `openssl rand -hex 32` to generate this.
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 1a8213362..765df6263 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -5,7 +5,7 @@
"package-lock.json": true
},
"editor.defaultFormatter": "dbaeumer.vscode-eslint",
- "editor.formatOnSave": false,
+ "editor.formatOnSave": true,
"editor.codeActionsOnSave": [
"source.addMissingImports",
"source.fixAll.eslint"
@@ -21,5 +21,9 @@
"editor.formatOnSave": true,
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
- "prettier.ignorePath": ".gitignore" // Don't run prettier for files listed in .gitignore
+ "prettier.ignorePath": ".gitignore",
+ "[python]": {
+ "editor.defaultFormatter": "ms-python.autopep8",
+ "editor.formatOnSave": true
+ } // Don't run prettier for files listed in .gitignore
}
diff --git a/backend/README.md b/backend/README.md
index 24bfad395..99d9b9bec 100644
--- a/backend/README.md
+++ b/backend/README.md
@@ -1,9 +1,9 @@
# Phase Console - Backend
-Python Django REST api + Postgres
+Django + Graphene + DRF
### Generate graphql schema for frontend
```bash
-./manage.py graphql_schema --schema backend.schema.schema --out ../dashboard/apollo/schema.graphql
+./manage.py graphql_schema --schema backend.schema.schema --out ../frontend/apollo/schema.graphql
```
diff --git a/backend/api/emails.py b/backend/api/emails.py
new file mode 100644
index 000000000..86d3e36df
--- /dev/null
+++ b/backend/api/emails.py
@@ -0,0 +1,78 @@
+from django.conf import settings
+from django.core.mail import send_mail
+from django.template.loader import render_to_string
+from datetime import datetime
+import os
+from api.utils import encode_string_to_base64, get_client_ip
+
+
+def send_email(subject, recipient_list, template_name, context):
+ """
+ Send email via SMTP gateway through Django's email backend.
+ """
+ # Load the template
+ email_html_message = render_to_string(template_name, context)
+
+ # Get the DEFAULT_FROM_EMAIL from settings
+ default_from_email = getattr(settings, "DEFAULT_FROM_EMAIL")
+
+ # Send the email
+ send_mail(
+ subject,
+ '', # plain text content can be empty as we're sending HTML
+ default_from_email,
+ recipient_list,
+ html_message=email_html_message
+ )
+
+
+def send_login_email(request, email, provider):
+ user_agent = request.META.get('HTTP_USER_AGENT', 'Unknown')
+ ip_address = get_client_ip(request)
+ timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
+
+ # Creating context dictionary
+ context = {
+ 'auth': provider,
+ 'email': email,
+ 'ip': ip_address,
+ 'user_agent': user_agent,
+ 'timestamp': timestamp
+ }
+
+ send_email(
+ 'New Login Alert - Phase Console',
+ [email],
+ 'backend/api/email_templates/login.html',
+ context
+ )
+
+
+def send_inite_email(invite):
+ organisation = invite.organisation.name
+
+ invited_by_social_acc = invite.invited_by.user.socialaccount_set.first()
+
+ name = invited_by_social_acc.extra_data.get('name')
+
+ if name is not None:
+ invited_by_name = name
+ else:
+ invited_by_name = invite.invited_by.user.email
+
+ invite_code = encode_string_to_base64(str(invite.id))
+
+ invite_link = f"{os.getenv('ALLOWED_ORIGINS')}/invite/{invite_code}"
+
+ context = {
+ 'organisation': organisation,
+ 'invited_by': invited_by_name,
+ 'invite_link': invite_link
+ }
+
+ send_email(
+ f"Invite - {organisation} on Phase",
+ [invite.invitee_email],
+ 'backend/api/email_templates/invite.html',
+ context
+ )
diff --git a/backend/api/migrations/0017_environment_secret_secrettag_secretevent_and_more.py b/backend/api/migrations/0017_environment_secret_secrettag_secretevent_and_more.py
new file mode 100644
index 000000000..f9a0c1149
--- /dev/null
+++ b/backend/api/migrations/0017_environment_secret_secrettag_secretevent_and_more.py
@@ -0,0 +1,106 @@
+# Generated by Django 4.2.3 on 2023-07-31 10:52
+
+from django.conf import settings
+import django.contrib.postgres.fields
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0016_organisation_plan'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='Environment',
+ fields=[
+ ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('name', models.CharField(max_length=64)),
+ ('env_type', models.CharField(choices=[('dev', 'Development'), ('staging', 'Staging'), ('prod', 'Production')], default='dev', max_length=7)),
+ ('wrapped_seed', models.CharField(max_length=208)),
+ ('wrapped_salt', models.CharField(max_length=208)),
+ ('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)),
+ ('is_deleted', models.BooleanField(default=False)),
+ ('app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.app')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='Secret',
+ fields=[
+ ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('collection', models.TextField(blank=True, null=True)),
+ ('key', models.TextField()),
+ ('key_digest', models.TextField()),
+ ('value', models.TextField()),
+ ('version', models.IntegerField(default=1)),
+ ('tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=64), size=10)),
+ ('comment', models.TextField()),
+ ('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)),
+ ('environment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.environment')),
+ ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='SecretTag',
+ fields=[
+ ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('name', models.CharField(max_length=64)),
+ ('organisation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.organisation')),
+ ],
+ ),
+ migrations.CreateModel(
+ name='SecretEvent',
+ fields=[
+ ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('collection', models.TextField(blank=True, null=True)),
+ ('key', models.TextField()),
+ ('key_digest', models.TextField()),
+ ('value', models.TextField()),
+ ('version', models.IntegerField(default=1)),
+ ('tags', django.contrib.postgres.fields.ArrayField(base_field=models.CharField(max_length=64), size=10)),
+ ('comment', models.TextField()),
+ ('event_type', models.CharField(choices=[('C', 'Create'), ('R', 'Read'), ('U', 'Update'), ('D', 'Delete')], default='C', max_length=1)),
+ ('timestamp', models.BigIntegerField()),
+ ('environment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.environment')),
+ ('secret', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.secret')),
+ ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='EnvironmentSecret',
+ fields=[
+ ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('identity_key', models.CharField(max_length=256)),
+ ('environment_token', models.CharField(max_length=64)),
+ ('wrapped_key_share', models.CharField(max_length=406)),
+ ('token', models.CharField(max_length=64)),
+ ('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)),
+ ('environment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.environment')),
+ ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.CreateModel(
+ name='EnvironmentKey',
+ fields=[
+ ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('identity_key', models.CharField(max_length=256)),
+ ('environment_token', models.CharField(max_length=64)),
+ ('wrapped_seed', models.CharField(max_length=208)),
+ ('wrapped_salt', models.CharField(max_length=208)),
+ ('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)),
+ ('environment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.environment')),
+ ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ ]
diff --git a/backend/api/migrations/0018_rename_environment_token_environmentsecret_name_and_more.py b/backend/api/migrations/0018_rename_environment_token_environmentsecret_name_and_more.py
new file mode 100644
index 000000000..1f098d870
--- /dev/null
+++ b/backend/api/migrations/0018_rename_environment_token_environmentsecret_name_and_more.py
@@ -0,0 +1,60 @@
+# Generated by Django 4.2.3 on 2023-08-01 07:57
+
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0017_environment_secret_secrettag_secretevent_and_more'),
+ ]
+
+ operations = [
+ migrations.RenameField(
+ model_name='environmentsecret',
+ old_name='environment_token',
+ new_name='name',
+ ),
+ migrations.RemoveField(
+ model_name='environmentkey',
+ name='environment_token',
+ ),
+ migrations.RemoveField(
+ model_name='secret',
+ name='collection',
+ ),
+ migrations.AddField(
+ model_name='secrettag',
+ name='created_at',
+ field=models.DateTimeField(auto_now_add=True, null=True),
+ ),
+ migrations.AddField(
+ model_name='secrettag',
+ name='deleted_at',
+ field=models.DateTimeField(blank=True, null=True),
+ ),
+ migrations.AddField(
+ model_name='secrettag',
+ name='updated_at',
+ field=models.DateTimeField(auto_now=True),
+ ),
+ migrations.CreateModel(
+ name='SecretFolder',
+ fields=[
+ ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('name', models.CharField(max_length=64)),
+ ('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)),
+ ('environment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.environment')),
+ ('parent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.secretfolder')),
+ ],
+ ),
+ migrations.AddField(
+ model_name='secret',
+ name='folder',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.secretfolder'),
+ ),
+ ]
diff --git a/backend/api/migrations/0019_remove_secret_user_remove_secretevent_collection_and_more.py b/backend/api/migrations/0019_remove_secret_user_remove_secretevent_collection_and_more.py
new file mode 100644
index 000000000..cc4af5830
--- /dev/null
+++ b/backend/api/migrations/0019_remove_secret_user_remove_secretevent_collection_and_more.py
@@ -0,0 +1,27 @@
+# Generated by Django 4.2.3 on 2023-08-02 07:03
+
+from django.db import migrations, models
+import django.db.models.deletion
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0018_rename_environment_token_environmentsecret_name_and_more'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='secret',
+ name='user',
+ ),
+ migrations.RemoveField(
+ model_name='secretevent',
+ name='collection',
+ ),
+ migrations.AddField(
+ model_name='secretevent',
+ name='folder',
+ field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.secretfolder'),
+ ),
+ ]
diff --git a/backend/api/migrations/0020_remove_organisation_owner_and_more.py b/backend/api/migrations/0020_remove_organisation_owner_and_more.py
new file mode 100644
index 000000000..f0a16b88d
--- /dev/null
+++ b/backend/api/migrations/0020_remove_organisation_owner_and_more.py
@@ -0,0 +1,56 @@
+# Generated by Django 4.2.3 on 2023-08-04 09:30
+
+from django.conf import settings
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+def migrate_org_owners(apps, schema_editor):
+ OrgModel = apps.get_model('api', 'Organisation')
+ OrgMemberModel = apps.get_model('api', 'OrganisationMember')
+
+ for org in OrgModel.objects.all():
+ OrgMemberModel.objects.create(user=org.owner, organisation=org, role='owner', identity_key=org.identity_key, created_at=org.created_at)
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0019_remove_secret_user_remove_secretevent_collection_and_more'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='OrganisationMember',
+ fields=[
+ ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('role', models.CharField(choices=[('owner', 'Owner'), ('admin', 'Admin'), ('dev', 'Developer')], default='dev', max_length=5)),
+ ('identity_key', models.CharField(blank=True, max_length=256, null=True)),
+ ('wrapped_keyring', models.TextField(blank=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True, null=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('deleted_at', models.DateTimeField(auto_now=True)),
+ ('organisation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='users', to='api.organisation')),
+ ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='organisation', to=settings.AUTH_USER_MODEL)),
+ ],
+ ),
+ migrations.AlterField(
+ model_name='environmentkey',
+ name='user',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.organisationmember'),
+ ),
+ migrations.AlterField(
+ model_name='environmentsecret',
+ name='user',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.organisationmember'),
+ ),
+ migrations.AlterField(
+ model_name='secretevent',
+ name='user',
+ field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.organisationmember'),
+ ),
+ migrations.RunPython(migrate_org_owners),
+ migrations.RemoveField(
+ model_name='organisation',
+ name='owner',
+ ),
+ ]
diff --git a/backend/api/migrations/0021_remove_secretevent_timestamp.py b/backend/api/migrations/0021_remove_secretevent_timestamp.py
new file mode 100644
index 000000000..936f72816
--- /dev/null
+++ b/backend/api/migrations/0021_remove_secretevent_timestamp.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.3 on 2023-08-04 09:42
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0020_remove_organisation_owner_and_more'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='secretevent',
+ name='timestamp',
+ ),
+ ]
diff --git a/backend/api/migrations/0022_secretevent_timestamp.py b/backend/api/migrations/0022_secretevent_timestamp.py
new file mode 100644
index 000000000..ca51b3695
--- /dev/null
+++ b/backend/api/migrations/0022_secretevent_timestamp.py
@@ -0,0 +1,20 @@
+# Generated by Django 4.2.3 on 2023-08-04 09:54
+
+from django.db import migrations, models
+import django.utils.timezone
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0021_remove_secretevent_timestamp'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='secretevent',
+ name='timestamp',
+ field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
+ preserve_default=False,
+ ),
+ ]
diff --git a/backend/api/migrations/0023_environment_identity_key.py b/backend/api/migrations/0023_environment_identity_key.py
new file mode 100644
index 000000000..9724ddf23
--- /dev/null
+++ b/backend/api/migrations/0023_environment_identity_key.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.2.3 on 2023-08-09 13:23
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0022_secretevent_timestamp'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='environment',
+ name='identity_key',
+ field=models.CharField(default='', max_length=256),
+ preserve_default=False,
+ ),
+ ]
diff --git a/backend/api/migrations/0024_alter_environment_wrapped_salt_and_more.py b/backend/api/migrations/0024_alter_environment_wrapped_salt_and_more.py
new file mode 100644
index 000000000..346581de7
--- /dev/null
+++ b/backend/api/migrations/0024_alter_environment_wrapped_salt_and_more.py
@@ -0,0 +1,33 @@
+# Generated by Django 4.2.3 on 2023-08-12 09:16
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0023_environment_identity_key'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='environment',
+ name='wrapped_salt',
+ field=models.CharField(max_length=256),
+ ),
+ migrations.AlterField(
+ model_name='environment',
+ name='wrapped_seed',
+ field=models.CharField(max_length=256),
+ ),
+ migrations.AlterField(
+ model_name='environmentkey',
+ name='wrapped_salt',
+ field=models.CharField(max_length=256),
+ ),
+ migrations.AlterField(
+ model_name='environmentkey',
+ name='wrapped_seed',
+ field=models.CharField(max_length=256),
+ ),
+ ]
diff --git a/backend/api/migrations/0025_rename_environmentsecret_environmenttoken_usertoken.py b/backend/api/migrations/0025_rename_environmentsecret_environmenttoken_usertoken.py
new file mode 100644
index 000000000..7b70a0e3b
--- /dev/null
+++ b/backend/api/migrations/0025_rename_environmentsecret_environmenttoken_usertoken.py
@@ -0,0 +1,33 @@
+# Generated by Django 4.2.3 on 2023-08-12 10:28
+
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0024_alter_environment_wrapped_salt_and_more'),
+ ]
+
+ operations = [
+ migrations.RenameModel(
+ old_name='EnvironmentSecret',
+ new_name='EnvironmentToken',
+ ),
+ migrations.CreateModel(
+ name='UserToken',
+ 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)),
+ ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.organisationmember')),
+ ],
+ ),
+ ]
diff --git a/backend/api/migrations/0026_secretfolder_color_remove_secret_tags_secret_tags.py b/backend/api/migrations/0026_secretfolder_color_remove_secret_tags_secret_tags.py
new file mode 100644
index 000000000..85430e626
--- /dev/null
+++ b/backend/api/migrations/0026_secretfolder_color_remove_secret_tags_secret_tags.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.2.3 on 2023-08-29 08:21
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0025_rename_environmentsecret_environmenttoken_usertoken'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='secretfolder',
+ name='color',
+ field=models.CharField(default='', max_length=64),
+ preserve_default=False,
+ ),
+ migrations.RemoveField(
+ model_name='secret',
+ name='tags',
+ ),
+ migrations.AddField(
+ model_name='secret',
+ name='tags',
+ field=models.ManyToManyField(to='api.secrettag'),
+ ),
+ ]
diff --git a/backend/api/migrations/0027_remove_secretfolder_color_secrettag_color.py b/backend/api/migrations/0027_remove_secretfolder_color_secrettag_color.py
new file mode 100644
index 000000000..b2fc0597d
--- /dev/null
+++ b/backend/api/migrations/0027_remove_secretfolder_color_secrettag_color.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.3 on 2023-08-29 08:35
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0026_secretfolder_color_remove_secret_tags_secret_tags'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='secretfolder',
+ name='color',
+ ),
+ migrations.AddField(
+ model_name='secrettag',
+ name='color',
+ field=models.CharField(default='', max_length=64),
+ preserve_default=False,
+ ),
+ ]
diff --git a/backend/api/migrations/0028_remove_secretevent_tags_secretevent_tags.py b/backend/api/migrations/0028_remove_secretevent_tags_secretevent_tags.py
new file mode 100644
index 000000000..0b20cd757
--- /dev/null
+++ b/backend/api/migrations/0028_remove_secretevent_tags_secretevent_tags.py
@@ -0,0 +1,22 @@
+# Generated by Django 4.2.3 on 2023-08-29 08:42
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0027_remove_secretfolder_color_secrettag_color'),
+ ]
+
+ operations = [
+ migrations.RemoveField(
+ model_name='secretevent',
+ name='tags',
+ ),
+ migrations.AddField(
+ model_name='secretevent',
+ name='tags',
+ field=models.ManyToManyField(to='api.secrettag'),
+ ),
+ ]
diff --git a/backend/api/migrations/0029_servicetoken.py b/backend/api/migrations/0029_servicetoken.py
new file mode 100644
index 000000000..19e103a4d
--- /dev/null
+++ b/backend/api/migrations/0029_servicetoken.py
@@ -0,0 +1,32 @@
+# Generated by Django 4.2.3 on 2023-09-06 08:34
+
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0028_remove_secretevent_tags_secretevent_tags'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ServiceToken',
+ fields=[
+ ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('identity_key', models.CharField(max_length=256)),
+ ('token', models.CharField(max_length=64)),
+ ('wrapped_key_share', models.CharField(max_length=406)),
+ ('name', models.CharField(max_length=64)),
+ ('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)),
+ ('app', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.app')),
+ ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.organisationmember')),
+ ('keys', models.ManyToManyField(to='api.environmentkey')),
+ ],
+ ),
+ ]
diff --git a/backend/api/migrations/0030_usertoken_expires_at.py b/backend/api/migrations/0030_usertoken_expires_at.py
new file mode 100644
index 000000000..96558b80e
--- /dev/null
+++ b/backend/api/migrations/0030_usertoken_expires_at.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.3 on 2023-09-09 09:08
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0029_servicetoken'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='usertoken',
+ name='expires_at',
+ field=models.DateTimeField(null=True),
+ ),
+ ]
diff --git a/backend/api/migrations/0031_organisationmemberinvite.py b/backend/api/migrations/0031_organisationmemberinvite.py
new file mode 100644
index 000000000..98b901bb7
--- /dev/null
+++ b/backend/api/migrations/0031_organisationmemberinvite.py
@@ -0,0 +1,28 @@
+# Generated by Django 4.2.3 on 2023-09-12 08:18
+
+from django.db import migrations, models
+import django.db.models.deletion
+import uuid
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0030_usertoken_expires_at'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='OrganisationMemberInvite',
+ fields=[
+ ('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
+ ('invitee_email', models.EmailField(max_length=254)),
+ ('valid', models.BooleanField(default=True)),
+ ('created_at', models.DateTimeField(auto_now_add=True, null=True)),
+ ('updated_at', models.DateTimeField(auto_now=True)),
+ ('expires_at', models.DateTimeField()),
+ ('invited_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.organisationmember')),
+ ('organisation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invites', to='api.organisation')),
+ ],
+ ),
+ ]
diff --git a/backend/api/migrations/0032_organisationmemberinvite_apps.py b/backend/api/migrations/0032_organisationmemberinvite_apps.py
new file mode 100644
index 000000000..68f7a410d
--- /dev/null
+++ b/backend/api/migrations/0032_organisationmemberinvite_apps.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.3 on 2023-09-12 13:04
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0031_organisationmemberinvite'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='organisationmemberinvite',
+ name='apps',
+ field=models.ManyToManyField(to='api.app'),
+ ),
+ ]
diff --git a/backend/api/migrations/0033_organisationmemberinvite_role.py b/backend/api/migrations/0033_organisationmemberinvite_role.py
new file mode 100644
index 000000000..31b73a6a6
--- /dev/null
+++ b/backend/api/migrations/0033_organisationmemberinvite_role.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.3 on 2023-09-14 08:29
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0032_organisationmemberinvite_apps'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='organisationmemberinvite',
+ name='role',
+ field=models.CharField(choices=[('owner', 'Owner'), ('admin', 'Admin'), ('dev', 'Developer')], default='dev', max_length=5),
+ ),
+ ]
diff --git a/backend/api/migrations/0034_organisationmember_apps.py b/backend/api/migrations/0034_organisationmember_apps.py
new file mode 100644
index 000000000..fbbd26429
--- /dev/null
+++ b/backend/api/migrations/0034_organisationmember_apps.py
@@ -0,0 +1,29 @@
+# Generated by Django 4.2.3 on 2023-09-15 13:19
+
+from django.db import migrations, models
+
+
+def set_org_member_apps(apps, schema_editor):
+ OrgMemberModel = apps.get_model('api', 'OrganisationMember')
+ AppModel = apps.get_model('api', 'App')
+
+ for org_member in OrgMemberModel.objects.all():
+ org_apps = AppModel.objects.filter(
+ organisation=org_member.organisation, is_deleted=False)
+ org_member.apps.set(org_apps)
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0033_organisationmemberinvite_role'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='organisationmember',
+ name='apps',
+ field=models.ManyToManyField(to='api.app'),
+ ),
+ migrations.RunPython(set_org_member_apps)
+ ]
diff --git a/backend/api/migrations/0035_alter_organisationmember_deleted_at.py b/backend/api/migrations/0035_alter_organisationmember_deleted_at.py
new file mode 100644
index 000000000..222284eb5
--- /dev/null
+++ b/backend/api/migrations/0035_alter_organisationmember_deleted_at.py
@@ -0,0 +1,27 @@
+# Generated by Django 4.2.3 on 2023-09-16 08:50
+
+from django.db import migrations, models
+
+
+def reset_deleted_field(apps, schema_editor):
+ OrgMemberModel = apps.get_model('api', 'OrganisationMember')
+
+ for org_member in OrgMemberModel.objects.all():
+ org_member.deleted_at = None
+ org_member.save()
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0034_organisationmember_apps'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='organisationmember',
+ name='deleted_at',
+ field=models.DateTimeField(blank=True, null=True),
+ ),
+ migrations.RunPython(reset_deleted_field)
+ ]
diff --git a/backend/api/migrations/0036_alter_organisationmember_apps.py b/backend/api/migrations/0036_alter_organisationmember_apps.py
new file mode 100644
index 000000000..e666c0f31
--- /dev/null
+++ b/backend/api/migrations/0036_alter_organisationmember_apps.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.3 on 2023-09-20 06:43
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0035_alter_organisationmember_deleted_at'),
+ ]
+
+ operations = [
+ migrations.AlterField(
+ model_name='organisationmember',
+ name='apps',
+ field=models.ManyToManyField(related_name='members', to='api.app'),
+ ),
+ ]
diff --git a/backend/api/migrations/0037_organisationmember_wrapped_recovery.py b/backend/api/migrations/0037_organisationmember_wrapped_recovery.py
new file mode 100644
index 000000000..22f225887
--- /dev/null
+++ b/backend/api/migrations/0037_organisationmember_wrapped_recovery.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.3 on 2023-09-27 08:01
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0036_alter_organisationmember_apps'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='organisationmember',
+ name='wrapped_recovery',
+ field=models.TextField(blank=True),
+ ),
+ ]
diff --git a/backend/api/migrations/0038_secretevent_ip_address_secretevent_user_agent.py b/backend/api/migrations/0038_secretevent_ip_address_secretevent_user_agent.py
new file mode 100644
index 000000000..8c4d62426
--- /dev/null
+++ b/backend/api/migrations/0038_secretevent_ip_address_secretevent_user_agent.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.3 on 2023-10-05 15:31
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('api', '0037_organisationmember_wrapped_recovery'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='secretevent',
+ name='ip_address',
+ field=models.GenericIPAddressField(blank=True, null=True),
+ ),
+ migrations.AddField(
+ model_name='secretevent',
+ name='user_agent',
+ field=models.TextField(blank=True, null=True),
+ ),
+ ]
diff --git a/backend/api/models.py b/backend/api/models.py
index c5f50c7d9..f0d05a3cb 100644
--- a/backend/api/models.py
+++ b/backend/api/models.py
@@ -1,13 +1,15 @@
from django.db import models
+from django.contrib.postgres.fields import ArrayField
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager, PermissionsMixin
from uuid import uuid4
from backend.api.kv import write
import json
-
+from django.utils import timezone
from django.conf import settings
CLOUD_HOSTED = settings.APP_HOST == 'cloud'
+
class CustomUserManager(BaseUserManager):
def create_user(self, username, email, password=None):
"""
@@ -72,10 +74,8 @@ class Organisation(models.Model):
(PRO_PLAN, 'Pro'),
(ENTERPRISE_PLAN, 'Enterprise')
]
-
+
id = models.TextField(default=uuid4, primary_key=True, editable=False)
- owner = models.ForeignKey(
- CustomUser, related_name='organisation', on_delete=models.CASCADE)
name = models.CharField(max_length=64, unique=True)
identity_key = models.CharField(max_length=256)
created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True)
@@ -85,7 +85,7 @@ class Organisation(models.Model):
choices=PLAN_TIERS,
default=FREE_PLAN,
)
- list_display = ('owner', 'name', 'identity_key', 'id')
+ list_display = ('name', 'identity_key', 'id')
def __str__(self):
return self.name
@@ -94,7 +94,7 @@ def __str__(self):
class App(models.Model):
id = models.TextField(default=uuid4, primary_key=True, editable=False)
organisation = models.ForeignKey(Organisation, on_delete=models.CASCADE)
- name = name = models.CharField(max_length=64)
+ name = models.CharField(max_length=64)
identity_key = models.CharField(max_length=256)
app_version = models.IntegerField(null=False, blank=False, default=1)
app_token = models.CharField(max_length=64)
@@ -122,3 +122,222 @@ def save(self, *args, **kwargs):
def __str__(self):
return self.name
+
+
+class OrganisationMember(models.Model):
+ OWNER = 'owner'
+ ADMIN = 'admin'
+ DEVELOPER = 'dev'
+
+ USER_ROLES = [
+ (OWNER, 'Owner'),
+ (ADMIN, 'Admin'),
+ (DEVELOPER, 'Developer')
+ ]
+
+ id = models.TextField(default=uuid4, primary_key=True, editable=False)
+ user = models.ForeignKey(
+ CustomUser, related_name='organisation', on_delete=models.CASCADE)
+ organisation = models.ForeignKey(
+ Organisation, related_name='users', on_delete=models.CASCADE)
+ role = models.CharField(
+ max_length=5,
+ choices=USER_ROLES,
+ default=DEVELOPER,
+ )
+ apps = models.ManyToManyField(App, related_name='members')
+ identity_key = models.CharField(max_length=256, null=True, blank=True)
+ wrapped_keyring = models.TextField(blank=True)
+ wrapped_recovery = models.TextField(blank=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)
+
+ def delete(self, *args, **kwargs):
+ """
+ Soft delete the object by setting the 'deleted_at' field.
+ """
+ self.deleted_at = timezone.now()
+ self.save()
+
+
+class OrganisationMemberInvite(models.Model):
+ id = models.TextField(default=uuid4, primary_key=True, editable=False)
+ organisation = models.ForeignKey(
+ Organisation, related_name='invites', on_delete=models.CASCADE)
+ apps = models.ManyToManyField(App)
+ role = models.CharField(
+ max_length=5,
+ choices=OrganisationMember.USER_ROLES,
+ default=OrganisationMember.DEVELOPER,
+ )
+ invited_by = models.ForeignKey(
+ OrganisationMember, on_delete=models.CASCADE)
+ invitee_email = models.EmailField()
+ valid = models.BooleanField(default=True)
+ created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True)
+ updated_at = models.DateTimeField(auto_now=True)
+ expires_at = models.DateTimeField()
+
+
+class Environment(models.Model):
+
+ DEVELOPMENT = "dev"
+ STAGING = "staging"
+ PRODUCTION = "prod"
+
+ ENV_TYPES = [
+ (DEVELOPMENT, 'Development'),
+ (STAGING, 'Staging'),
+ (PRODUCTION, 'Production')
+ ]
+
+ id = models.TextField(default=uuid4, primary_key=True, editable=False)
+ app = models.ForeignKey(App, on_delete=models.CASCADE)
+ name = models.CharField(max_length=64)
+ env_type = models.CharField(
+ max_length=7,
+ choices=ENV_TYPES,
+ default=DEVELOPMENT,
+ )
+ identity_key = models.CharField(max_length=256)
+ wrapped_seed = models.CharField(max_length=256)
+ wrapped_salt = models.CharField(max_length=256)
+ 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)
+ is_deleted = models.BooleanField(default=False)
+
+
+class EnvironmentKey(models.Model):
+ id = models.TextField(default=uuid4, primary_key=True, editable=False)
+ environment = models.ForeignKey(Environment, on_delete=models.CASCADE)
+ user = models.ForeignKey(
+ OrganisationMember, on_delete=models.CASCADE, 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)
+ created_at = models.DateTimeField(auto_now_add=True, blank=True, null=True)
+ updated_at = models.DateTimeField(auto_now=True)
+ deleted_at = models.DateTimeField(blank=True, null=True)
+
+ def delete(self, *args, **kwargs):
+ self.deleted_at = timezone.now()
+ self.save()
+
+
+class EnvironmentToken(models.Model):
+ id = models.TextField(default=uuid4, primary_key=True, editable=False)
+ environment = models.ForeignKey(Environment, on_delete=models.CASCADE)
+ user = models.ForeignKey(
+ OrganisationMember, on_delete=models.CASCADE, blank=True, null=True)
+ 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, blank=True, null=True)
+ updated_at = models.DateTimeField(auto_now=True)
+ deleted_at = models.DateTimeField(blank=True, null=True)
+
+
+class ServiceToken(models.Model):
+ id = models.TextField(default=uuid4, primary_key=True, editable=False)
+ app = models.ForeignKey(App, on_delete=models.CASCADE)
+ keys = models.ManyToManyField(EnvironmentKey)
+ identity_key = models.CharField(max_length=256)
+ token = models.CharField(max_length=64)
+ wrapped_key_share = models.CharField(max_length=406)
+ name = models.CharField(max_length=64)
+ 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(
+ OrganisationMember, on_delete=models.CASCADE, blank=True, null=True)
+ 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, 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 SecretFolder(models.Model):
+ id = models.TextField(default=uuid4, primary_key=True, editable=False)
+ environment = models.ForeignKey(Environment, on_delete=models.CASCADE)
+ parent = models.ForeignKey('self', on_delete=models.CASCADE)
+ name = models.CharField(max_length=64)
+ 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)
+
+
+class SecretTag(models.Model):
+ id = models.TextField(default=uuid4, primary_key=True, editable=False)
+ organisation = models.ForeignKey(Organisation, on_delete=models.CASCADE)
+ name = models.CharField(max_length=64)
+ color = models.CharField(max_length=64)
+ 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)
+
+
+class Secret(models.Model):
+ id = models.TextField(default=uuid4, primary_key=True, editable=False)
+ environment = models.ForeignKey(Environment, on_delete=models.CASCADE)
+ folder = models.ForeignKey(
+ SecretFolder, on_delete=models.CASCADE, null=True)
+ key = models.TextField()
+ key_digest = models.TextField()
+ value = models.TextField()
+ version = models.IntegerField(default=1)
+ tags = models.ManyToManyField(SecretTag)
+ comment = models.TextField()
+ 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)
+
+
+class SecretEvent(models.Model):
+
+ CREATE = "C"
+ READ = "R"
+ UPDATE = "U"
+ DELETE = "D"
+
+ EVENT_TYPES = [
+ (CREATE, 'Create'),
+ (READ, 'Read'),
+ (UPDATE, 'Update'),
+ (DELETE, 'Delete')
+ ]
+
+ id = models.TextField(default=uuid4, primary_key=True, editable=False)
+ secret = models.ForeignKey(Secret, on_delete=models.CASCADE)
+ environment = models.ForeignKey(Environment, on_delete=models.CASCADE)
+ folder = models.ForeignKey(
+ SecretFolder, on_delete=models.CASCADE, null=True)
+ user = models.ForeignKey(
+ OrganisationMember, on_delete=models.SET_NULL, blank=True, null=True)
+ key = models.TextField()
+ key_digest = models.TextField()
+ value = models.TextField()
+ version = models.IntegerField(default=1)
+ tags = models.ManyToManyField(SecretTag)
+ comment = models.TextField()
+ event_type = models.CharField(
+ max_length=1,
+ choices=EVENT_TYPES,
+ default=CREATE,
+ )
+ timestamp = models.DateTimeField(auto_now_add=True)
+ ip_address = models.GenericIPAddressField(null=True, blank=True)
+ user_agent = models.TextField(null=True, blank=True)
diff --git a/backend/api/serializers.py b/backend/api/serializers.py
index 365174f96..a3e44c364 100644
--- a/backend/api/serializers.py
+++ b/backend/api/serializers.py
@@ -1,8 +1,15 @@
-from rest_framework.serializers import ModelSerializer
-from .models import CustomUser, Organisation
+from rest_framework import serializers
+from .models import CustomUser, Environment, EnvironmentKey, Organisation, Secret, ServiceToken, UserToken
-class CustomUserSerializer(ModelSerializer):
+def find_index_by_id(dictionaries, target_id):
+ for index, dictionary in enumerate(dictionaries):
+ if dictionary.get('id') == target_id:
+ return index
+ return -1
+
+
+class CustomUserSerializer(serializers.ModelSerializer):
class Meta:
model = CustomUser
fields = [
@@ -22,10 +29,108 @@ def create(self, validated_data):
return user
-class OrganisationSerializer(ModelSerializer):
+class OrganisationSerializer(serializers.ModelSerializer):
class Meta:
model = Organisation
fields = ['id', 'name', 'identity_key', 'created_at']
def create(self, validated_data):
return Organisation(**validated_data)
+
+
+class SecretSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Secret
+ fields = '__all__'
+
+
+class EnvironmentSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Environment
+ fields = ['id', 'name', 'env_type']
+
+
+class EnvironmentKeySerializer(serializers.ModelSerializer):
+ environment = EnvironmentSerializer()
+
+ class Meta:
+ model = EnvironmentKey
+ fields = '__all__'
+
+
+class UserTokenSerializer(serializers.ModelSerializer):
+ apps = EnvironmentKeySerializer(many=True, read_only=True)
+
+ # New field 'userId'
+ user_id = serializers.UUIDField(source='user.id', read_only=True)
+
+ # New field 'offline_enabled' with default value False
+ offline_enabled = serializers.BooleanField(default=False, read_only=True)
+
+ class Meta:
+ model = UserToken
+ fields = ['wrapped_key_share', 'user_id', 'offline_enabled', 'apps']
+
+ def to_representation(self, instance):
+ representation = super().to_representation(instance)
+
+ # Filter environment_keys to include only those associated with the same user
+ user = instance.user
+
+ if user is not None:
+ environment_keys = EnvironmentKey.objects.filter(
+ user=user, 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', # Adding encryption to each app
+ }
+
+ 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 ServiceTokenSerializer(serializers.ModelSerializer):
+ apps = EnvironmentKeySerializer(many=True, read_only=True)
+
+ class Meta:
+ model = ServiceToken
+ fields = ['wrapped_key_share', 'apps']
+
+ def to_representation(self, instance):
+ representation = super().to_representation(instance)
+
+ environment_keys = instance.keys.all()
+ 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', # Adding encryption to each app
+ }
+
+ 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
diff --git a/backend/api/templates/backend/api/email_templates/invite.html b/backend/api/templates/backend/api/email_templates/invite.html
new file mode 100644
index 000000000..591bec380
--- /dev/null
+++ b/backend/api/templates/backend/api/email_templates/invite.html
@@ -0,0 +1,149 @@
+
+
+
+ Phase Invite
+
+
+
+
+
+
+
+
+ |
+
+ Phase Invite
+ |
+
+
+
+
+ You have been invited to join {{ organisation }} on
+ Phase by {{ invited_by }}.
+
+
+
+
Click the link below to accept the invite
+
Join
+
+
+
+
+
+
diff --git a/backend/api/templates/backend/api/email_templates/login.html b/backend/api/templates/backend/api/email_templates/login.html
new file mode 100644
index 000000000..62a821092
--- /dev/null
+++ b/backend/api/templates/backend/api/email_templates/login.html
@@ -0,0 +1,90 @@
+
+
+
+ New Login Alert
+
+
+
+
+
+
+
+ |
+
+ Login alert
+ |
+
+
+
+
You have logged into the Phase Console via {{ auth }} on {{ email }}.
+
+
+
+ IP address: |
+ {{ ip }} |
+
+
+ User Agent: |
+ {{ user_agent }} |
+
+
+ Timestamp: |
+ {{ timestamp }} |
+
+
+
+
If this wasn't you, please check your account security or contact support.
+
+
+
+
diff --git a/backend/api/utils.py b/backend/api/utils.py
index 29a2fa520..7fc5cbcc9 100644
--- a/backend/api/utils.py
+++ b/backend/api/utils.py
@@ -1,7 +1,73 @@
+from api.models import EnvironmentToken, ServiceToken, UserToken
+from django.utils import timezone
+import base64
+
+
def get_client_ip(request):
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
ip = x_forwarded_for.split(',')[0]
else:
ip = request.META.get('REMOTE_ADDR')
- return ip
\ No newline at end of file
+ return ip
+
+
+def get_resolver_request_meta(request):
+ user_agent = request.META.get('HTTP_USER_AGENT', 'Unknown')
+ ip_address = get_client_ip(request)
+
+ return ip_address, user_agent
+
+
+def get_token_type(auth_token):
+ return auth_token.split(" ")[1]
+
+
+def get_env_from_service_token(auth_token):
+ token = auth_token.split(" ")[2]
+
+ if not token:
+ return False
+
+ try:
+ env_token = EnvironmentToken.objects.get(token=token)
+ return env_token.environment, env_token.user
+ except Exception as ex:
+ return False
+
+
+def get_org_member_from_user_token(auth_token):
+ token = auth_token.split(" ")[2]
+
+ if not token:
+ return False
+
+ try:
+ user_token = UserToken.objects.get(token=token)
+ return user_token.user
+ except Exception as ex:
+ return False
+
+
+def token_is_expired_or_deleted(auth_token):
+ prefix, token_type, token_value = auth_token.split(" ")
+
+ if token_type == 'User':
+ token = UserToken.objects.get(token=token_value)
+ else:
+ token = ServiceToken.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())
+
+
+def encode_string_to_base64(s):
+ # Convert string to bytes
+ byte_representation = s.encode('utf-8')
+
+ # Base64 encode the bytes
+ base64_bytes = base64.b64encode(byte_representation)
+
+ # Convert the encoded bytes back to a string
+ base64_string = base64_bytes.decode('utf-8')
+
+ return base64_string
diff --git a/backend/api/views.py b/backend/api/views.py
index 3389bfd7e..e15bfdef0 100644
--- a/backend/api/views.py
+++ b/backend/api/views.py
@@ -1,5 +1,9 @@
from datetime import datetime
+import json
+from api.serializers import EnvironmentKeySerializer, SecretSerializer, ServiceTokenSerializer, UserTokenSerializer
+from api.emails import send_login_email
+from backend.graphene.utils.permissions import user_can_access_environment
from dj_rest_auth.registration.views import SocialLoginView
from django.contrib.auth.mixins import LoginRequiredMixin
from graphene_django.views import GraphQLView
@@ -8,9 +12,9 @@
from rest_framework.permissions import AllowAny
from rest_framework.response import Response
from django.http import JsonResponse, HttpResponse
-from api.utils import get_client_ip
+from api.utils import get_client_ip, get_env_from_service_token, get_org_member_from_user_token, get_resolver_request_meta, get_token_type, token_is_expired_or_deleted
from logs.models import KMSDBLog
-from .models import App
+from .models import App, Environment, EnvironmentKey, EnvironmentToken, Secret, SecretEvent, SecretTag, ServiceToken, UserToken
import jwt
import requests
from django.contrib.auth import logout
@@ -23,10 +27,17 @@
from allauth.socialaccount.providers.google.views import GoogleOAuth2Adapter
from allauth.socialaccount.providers.github.views import GitHubOAuth2Adapter
from allauth.socialaccount.providers.oauth2.views import OAuth2Adapter
+from rest_framework.views import APIView
+from rest_framework import status
+from django.views.decorators.csrf import csrf_exempt
+from django.utils import timezone
+
CLOUD_HOSTED = settings.APP_HOST == 'cloud'
# for custom gitlab adapter class
+
+
def _check_errors(response):
# 403 error's are presented as user-facing errors
if response.status_code == 403:
@@ -76,9 +87,18 @@ 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 = login.email_addresses[0]
+
if CLOUD_HOSTED and not CustomUser.objects.filter(email=email).exists():
- # new user
- notify_slack(f"New user signup: {email}")
+ try:
+ # Notify Slack
+ notify_slack(f"New user signup: {email}")
+ except Exception as e:
+ print(f"Error notifying Slack: {e}")
+
+ try:
+ send_login_email(request, email, 'Google')
+ except Exception as e:
+ print(f"Error sending email: {e}")
return login
@@ -108,9 +128,19 @@ def complete_login(self, request, app, token, **kwargs):
extra_data["email"] = self.get_email(headers)
email = extra_data["email"]
+
if CLOUD_HOSTED and not CustomUser.objects.filter(email=email).exists():
- # new user
- notify_slack(f"New user signup: {email}")
+ try:
+ # Notify Slack
+ notify_slack(f"New user signup: {email}")
+ except Exception as e:
+ print(f"Error notifying Slack: {e}")
+
+ try:
+ send_login_email(request, email, 'GitHub')
+ except Exception as e:
+ print(f"Error sending email: {e}")
+
return self.get_provider().sociallogin_from_response(request, extra_data)
@@ -128,7 +158,6 @@ class CustomGitLabOAuth2Adapter(OAuth2Adapter):
provider_base_url, provider_api_version)
def complete_login(self, request, app, token, response):
- print('logging in')
response = requests.get(self.profile_url, params={
"access_token": token.token})
data = _check_errors(response)
@@ -136,9 +165,19 @@ def complete_login(self, request, app, token, response):
email = login.email_addresses[0]
- if CLOUD_HOSTED and not CustomUser.objects.filter(email=email).exists():
- # new user
- notify_slack(f"New user signup: {email}")
+ if CLOUD_HOSTED:
+ # Check if user exists and notify Slack for new user signup
+ if not CustomUser.objects.filter(email=email).exists():
+ try:
+ notify_slack(f"New user signup: {email}")
+ except Exception as e:
+ print(f"Error notifying Slack: {e}")
+
+ try:
+ send_login_email(request, email, 'GitLab')
+ except Exception as e:
+ print(f"Error sending email: {e}")
+
return login
@@ -172,8 +211,8 @@ def logout_view(request):
@api_view(['GET'])
@permission_classes([AllowAny])
-def health_check(request):
- return JsonResponse({
+def health_check(request):
+ return JsonResponse({
'status': 'alive'
})
@@ -194,7 +233,8 @@ def kms(request, app_id):
app = App.objects.get(app_token=app_token)
try:
timestamp = datetime.now().timestamp() * 1000
- KMSDBLog.objects.create(app_id=app_id, event_type=event_type, phase_node=phase_node, ph_size=float(ph_size), ip_address=ip_address, timestamp=timestamp)
+ KMSDBLog.objects.create(app_id=app_id, event_type=event_type, phase_node=phase_node, ph_size=float(
+ ph_size), ip_address=ip_address, timestamp=timestamp)
except:
pass
return JsonResponse({
@@ -204,6 +244,282 @@ def kms(request, app_id):
return HttpResponse(status=404)
+def user_token_kms(request):
+ auth_token = request.headers['authorization']
+
+ token = auth_token.split(' ')[2]
+
+ user_token = UserToken.objects.get(token=token)
+
+ serializer = UserTokenSerializer(user_token)
+
+ return Response(serializer.data, status=status.HTTP_200_OK)
+
+
+def service_token_kms(request):
+ auth_token = request.headers['authorization']
+
+ token = auth_token.split(' ')[2]
+
+ service_token = ServiceToken.objects.get(token=token)
+
+ serializer = ServiceTokenSerializer(service_token)
+
+ return Response(serializer.data, status=status.HTTP_200_OK)
+
+
+@api_view(['GET'])
+@permission_classes([AllowAny])
+def secrets_tokens(request):
+ auth_token = request.headers['authorization']
+
+ if token_is_expired_or_deleted(auth_token):
+ return HttpResponse(status=403)
+
+ token_type = get_token_type(auth_token)
+
+ if token_type == 'Service':
+ return service_token_kms(request)
+ elif token_type == 'User':
+ return user_token_kms(request)
+ else:
+ return HttpResponse(status=403)
+
+
class PrivateGraphQLView(LoginRequiredMixin, GraphQLView):
raise_exception = True
pass
+
+
+class SecretsView(APIView):
+ permission_classes = [AllowAny, ]
+
+ @csrf_exempt
+ def dispatch(self, request, *args):
+ return super(SecretsView, self).dispatch(request, *args)
+
+ def get(self, request):
+ auth_token = request.headers['authorization']
+
+ if token_is_expired_or_deleted(auth_token):
+ return HttpResponse(status=403)
+
+ token_type = get_token_type(auth_token)
+
+ env_id = request.headers['environment']
+ env = Environment.objects.get(id=env_id)
+
+ ip_address, user_agent = get_resolver_request_meta(request)
+
+ if token_type == 'User':
+ try:
+ org_member = get_org_member_from_user_token(auth_token)
+
+ if not user_can_access_environment(org_member.user.userId, env_id):
+ return HttpResponse(status=403)
+ except Exception as ex:
+ print('EX:', ex)
+ return HttpResponse(status=404)
+
+ else:
+ org_member = None
+
+ if not env.id:
+ return HttpResponse(status=404)
+
+ secrets_filter = {
+ 'environment': env,
+ 'deleted_at': None
+ }
+
+ try:
+ key_digest = request.headers['keydigest']
+ if key_digest:
+ secrets_filter['key_digest'] = key_digest
+ except:
+ pass
+
+ secrets = Secret.objects.filter(**secrets_filter)
+
+ for secret in secrets:
+ read_event = SecretEvent.objects.create(secret=secret, environment=secret.environment, user=org_member, key=secret.key, key_digest=secret.key_digest,
+ value=secret.value, comment=secret.comment, event_type=SecretEvent.READ, ip_address=ip_address, user_agent=user_agent)
+ read_event.tags.set(secret.tags.all())
+
+ serializer = SecretSerializer(secrets, many=True)
+
+ return Response(serializer.data, status=status.HTTP_200_OK)
+
+ def post(self, request):
+ auth_token = request.headers['authorization']
+
+ if token_is_expired_or_deleted(auth_token):
+ return HttpResponse(status=403)
+
+ token_type = get_token_type(auth_token)
+
+ env_id = request.headers['environment']
+ env = Environment.objects.get(id=env_id)
+
+ if token_type == 'User':
+ try:
+ user = get_org_member_from_user_token(auth_token)
+
+ if not user_can_access_environment(user.user.userId, env_id):
+ return HttpResponse(status=403)
+ except:
+ return HttpResponse(status=404)
+ else:
+ user = None
+
+ if not env:
+ return HttpResponse(status=404)
+
+ request_body = json.loads(request.body)
+
+ ip_address, user_agent = get_resolver_request_meta(request)
+
+ for secret in request_body['secrets']:
+
+ tags = SecretTag.objects.filter(
+ id__in=secret['tags'])
+
+ secret_data = {
+ 'environment': env,
+ 'key': secret['key'],
+ 'key_digest': secret['keyDigest'],
+ 'value': secret['value'],
+ 'folder_id': secret['folderId'],
+ 'version': 1,
+ 'comment': secret['comment'],
+ }
+
+ secret_obj = Secret.objects.create(**secret_data)
+ secret_obj.tags.set(tags)
+
+ event = SecretEvent.objects.create(
+ **{**secret_data, **{
+ 'user': user,
+ 'secret': secret_obj,
+ 'event_type': SecretEvent.CREATE,
+ 'ip_address': ip_address,
+ 'user_agent': user_agent
+ }})
+ event.tags.set(tags)
+
+ return Response(status=status.HTTP_200_OK)
+
+ def put(self, request):
+ auth_token = request.headers['authorization']
+
+ if token_is_expired_or_deleted(auth_token):
+ return HttpResponse(status=403)
+
+ token_type = get_token_type(auth_token)
+
+ env_id = request.headers['environment']
+ env = Environment.objects.get(id=env_id)
+
+ if token_type == 'User':
+ try:
+ user = get_org_member_from_user_token(auth_token)
+
+ if not user_can_access_environment(user.user.userId, env_id):
+ return HttpResponse(status=403)
+ except:
+ return HttpResponse(status=404)
+
+ else:
+ user = None
+
+ request_body = json.loads(request.body)
+
+ ip_address, user_agent = get_resolver_request_meta(request)
+
+ for secret in request_body['secrets']:
+ secret_obj = Secret.objects.get(id=secret['id'])
+
+ tags = SecretTag.objects.filter(
+ id__in=secret['tags'])
+
+ secret_data = {
+ 'environment': env,
+ 'key': secret['key'],
+ 'key_digest': secret['keyDigest'],
+ 'value': secret['value'],
+ 'folder_id': secret['folderId'],
+ 'version': secret_obj.version + 1,
+ 'comment': secret['comment'],
+ }
+
+ for key, value in secret_data.items():
+ setattr(secret_obj, key, value)
+
+ secret_obj.updated_at = timezone.now()
+ secret_obj.tags.set(tags)
+ secret_obj.save()
+
+ event = SecretEvent.objects.create(
+ **{**secret_data, **{
+ 'user': user,
+ 'secret': secret_obj,
+ 'event_type': SecretEvent.UPDATE,
+ 'ip_address': ip_address,
+ 'user_agent': user_agent
+ }})
+ event.tags.set(tags)
+
+ return Response(status=status.HTTP_200_OK)
+
+ def delete(self, request):
+ auth_token = request.headers['authorization']
+
+ if token_is_expired_or_deleted(auth_token):
+ return HttpResponse(status=403)
+
+ token_type = get_token_type(auth_token)
+
+ env_id = request.headers['environment']
+
+ if token_type == 'User':
+ try:
+ user = get_org_member_from_user_token(auth_token)
+
+ if not user_can_access_environment(user.user.userId, env_id):
+ return HttpResponse(status=403)
+ except:
+ return HttpResponse(status=404)
+
+ else:
+ user = None
+
+ request_body = json.loads(request.body)
+
+ ip_address, user_agent = get_resolver_request_meta(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 HttpResponse(status=404)
+
+ if user is not None and not user_can_access_environment(user.user.userId, secret.environment.id):
+ return HttpResponse(status=403)
+
+ for secret in secrets_to_delete:
+ secret.updated_at = timezone.now()
+ secret.deleted_at = timezone.now()
+ secret.save()
+
+ most_recent_event_copy = SecretEvent.objects.filter(
+ secret=secret).order_by('version').last()
+
+ # setting the pk to None and then saving it creates a copy of the instance with updated fields
+ most_recent_event_copy.id = None
+ most_recent_event_copy.event_type = SecretEvent.DELETE
+ most_recent_event_copy.ip_address = ip_address
+ most_recent_event_copy.user_agent = user_agent
+ most_recent_event_copy.save()
+
+ return Response(status=status.HTTP_200_OK)
diff --git a/backend/backend/exceptions.py b/backend/backend/exceptions.py
index 1b8f2263d..61e138ce8 100644
--- a/backend/backend/exceptions.py
+++ b/backend/backend/exceptions.py
@@ -6,6 +6,7 @@ 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)
# set 404 as default response code
status_code = 404
diff --git a/backend/backend/graphene/mutations/app.py b/backend/backend/graphene/mutations/app.py
new file mode 100644
index 000000000..0aa017268
--- /dev/null
+++ b/backend/backend/graphene/mutations/app.py
@@ -0,0 +1,181 @@
+from backend.api.kv import delete, purge
+from backend.graphene.mutations.environment import EnvironmentKeyInput
+from backend.graphene.utils.permissions import user_can_access_app, user_is_admin, user_is_org_member
+from ee.feature_flags import allow_new_app
+import graphene
+from django.utils import timezone
+from graphql import GraphQLError
+from api.models import App, EnvironmentKey, Organisation, OrganisationMember
+from backend.graphene.types import AppType
+from django.conf import settings
+
+CLOUD_HOSTED = settings.APP_HOST == 'cloud'
+
+
+class CreateAppMutation(graphene.Mutation):
+ class Arguments:
+ id = graphene.ID(required=True)
+ organisation_id = graphene.ID(required=True)
+ name = graphene.String(required=True)
+ identity_key = graphene.String(required=True)
+ app_token = graphene.String(required=True)
+ app_seed = graphene.String(required=True)
+ wrapped_key_share = graphene.String(required=True)
+ app_version = graphene.Int(required=True)
+
+ app = graphene.Field(AppType)
+
+ @classmethod
+ def mutate(cls, root, info, id, organisation_id, name, identity_key, app_token, app_seed, wrapped_key_share, app_version):
+ user = info.context.user
+ org = Organisation.objects.get(id=organisation_id)
+ if not user_is_org_member(user.userId, organisation_id):
+ raise GraphQLError("You don't have access to this organisation")
+
+ if allow_new_app(org) == False:
+ raise GraphQLError(
+ 'You have reached the App limit for your current plan. Please upgrade your account to add more.')
+
+ if App.objects.filter(identity_key=identity_key).exists():
+ raise GraphQLError("This app already exists")
+
+ app = App.objects.create(id=id, organisation=org, name=name, identity_key=identity_key,
+ app_token=app_token, app_seed=app_seed, wrapped_key_share=wrapped_key_share, app_version=app_version)
+
+ org_member = OrganisationMember.objects.get(
+ organisation=org, user=info.context.user, deleted_at=None)
+ org_member.apps.add(app)
+
+ admin_roles = [OrganisationMember.ADMIN, OrganisationMember.OWNER]
+
+ org_admins = org.users.filter(role__in=admin_roles)
+ for admin in org_admins:
+ admin.apps.add(app)
+
+ return CreateAppMutation(app=app)
+
+
+class RotateAppKeysMutation(graphene.Mutation):
+ class Arguments:
+ id = graphene.ID(required=True)
+ app_token = graphene.String(required=True)
+ wrapped_key_share = graphene.String(required=True)
+
+ app = graphene.Field(AppType)
+
+ @classmethod
+ def mutate(cls, root, info, id, app_token, wrapped_key_share):
+ user = info.context.user
+ app = App.objects.get(id=id)
+
+ if not user_can_access_app(user.userId, app.id):
+ raise GraphQLError("You don't have access to this app")
+
+ if CLOUD_HOSTED:
+ # delete current keys from cloudflare KV
+ deleted = delete(app.app_token)
+
+ # purge keys from cloudflare cache
+ purged = purge(
+ f"phApp:v{app.app_version}:{app.identity_key}/{app.app_token}")
+
+ if not deleted or not purged:
+ raise GraphQLError(
+ "Failed to delete app keys. Please try again.")
+
+ app.app_token = app_token
+ app.wrapped_key_share = wrapped_key_share
+ app.save()
+
+ return RotateAppKeysMutation(app=app)
+
+
+class DeleteAppMutation(graphene.Mutation):
+ class Arguments:
+ id = graphene.ID(required=True)
+
+ app = graphene.Field(AppType)
+
+ @classmethod
+ def mutate(cls, root, info, id):
+ user = info.context.user
+ app = App.objects.get(id=id)
+
+ if not user_can_access_app(user.userId, app.id):
+ raise GraphQLError("You don't have access to this app")
+ if not user_is_admin(user.userId, app.organisation.id):
+ raise GraphQLError(
+ "You don't have permission to perform that action.")
+
+ if CLOUD_HOSTED:
+ # delete current keys from cloudflare KV
+ deleted = delete(app.app_token)
+
+ # purge keys from cloudflare cache
+ purged = purge(
+ f"phApp:v{app.app_version}:{app.identity_key}/{app.app_token}")
+
+ if not deleted or not purged:
+ raise GraphQLError(
+ "Failed to delete app keys. Please try again.")
+
+ app.wrapped_key_share = ""
+ app.is_deleted = True
+ app.deleted_at = timezone.now()
+ app.save()
+
+ return DeleteAppMutation(app=app)
+
+
+class AddAppMemberMutation(graphene.Mutation):
+ class Arguments:
+ member_id = graphene.ID()
+ 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):
+ user = info.context.user
+ app = App.objects.get(id=app_id)
+
+ 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)
+ 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)
+
+ return AddAppMemberMutation(app=app)
+
+
+class RemoveAppMemberMutation(graphene.Mutation):
+ class Arguments:
+ member_id = graphene.ID()
+ app_id = graphene.ID()
+
+ app = graphene.Field(AppType)
+
+ @classmethod
+ def mutate(cls, root, info, member_id, app_id):
+ user = info.context.user
+ app = App.objects.get(id=app_id)
+
+ 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)
+ EnvironmentKey.objects.filter(
+ environment__app=app, user_id=member_id).delete()
+
+ return RemoveAppMemberMutation(app=app)
diff --git a/backend/backend/graphene/mutations/environment.py b/backend/backend/graphene/mutations/environment.py
new file mode 100644
index 000000000..47e7a9ec8
--- /dev/null
+++ b/backend/backend/graphene/mutations/environment.py
@@ -0,0 +1,490 @@
+from django.utils import timezone
+from api.utils import get_resolver_request_meta
+from backend.graphene.utils.permissions import member_can_access_org, user_can_access_app, user_can_access_environment, user_is_org_member
+import graphene
+from graphql import GraphQLError
+from api.models import App, Environment, EnvironmentKey, EnvironmentToken, Organisation, OrganisationMember, Secret, SecretEvent, SecretFolder, SecretTag, UserToken, ServiceToken
+from backend.graphene.types import AppType, EnvironmentKeyType, EnvironmentTokenType, EnvironmentType, SecretFolderType, SecretTagType, SecretType, ServiceTokenType, UserTokenType
+from datetime import datetime
+
+
+class EnvironmentInput(graphene.InputObjectType):
+ app_id = graphene.ID(required=True)
+ name = graphene.String(required=True)
+ env_type = graphene.String(required=True)
+ wrapped_seed = graphene.String(required=True)
+ wrapped_salt = graphene.String(required=True)
+ identity_key = graphene.String(required=True)
+
+
+class EnvironmentKeyInput(graphene.InputObjectType):
+ env_id = graphene.ID(required=True)
+ user_id = graphene.ID(required=False)
+ identity_key = graphene.String(required=True)
+ wrapped_seed = graphene.String(required=True)
+ wrapped_salt = graphene.String(required=True)
+
+
+class SecretInput(graphene.InputObjectType):
+ env_id = graphene.ID(required=False)
+ folder_id = graphene.ID(required=False)
+ key = graphene.String(required=True)
+ key_digest = graphene.String(required=True)
+ value = graphene.String(required=True)
+ tags = graphene.List(graphene.String)
+ comment = graphene.String()
+
+
+class CreateEnvironmentMutation(graphene.Mutation):
+ class Arguments:
+ environment_data = EnvironmentInput(required=True)
+ admin_keys = graphene.List(EnvironmentKeyInput)
+
+ environment = graphene.Field(EnvironmentType)
+
+ @classmethod
+ def mutate(cls, root, info, environment_data, admin_keys):
+ user_id = info.context.user.userId
+
+ if not user_can_access_app(user_id, environment_data.app_id):
+ raise GraphQLError("You don't have access to this app")
+
+ app = App.objects.get(id=environment_data.app_id)
+
+ environment = Environment.objects.create(app=app, name=environment_data.name, env_type=environment_data.env_type,
+ identity_key=environment_data.identity_key, wrapped_seed=environment_data.wrapped_seed, wrapped_salt=environment_data.wrapped_salt)
+
+ org_owner = OrganisationMember.objects.get(
+ organisation=environment.app.organisation, role=OrganisationMember.OWNER, deleted_at=None)
+
+ EnvironmentKey.objects.create(environment=environment, user=org_owner,
+ identity_key=environment_data.identity_key, wrapped_seed=environment_data.wrapped_seed, wrapped_salt=environment_data.wrapped_salt)
+ for key in admin_keys:
+ EnvironmentKey.objects.create(
+ environment=environment, user_id=key.user_id, wrapped_seed=key.wrapped_seed, wrapped_salt=key.wrapped_salt, identity_key=key.identity_key)
+
+ return CreateEnvironmentMutation(environment=environment)
+
+
+class CreateEnvironmentKeyMutation(graphene.Mutation):
+ class Arguments:
+ # id = graphene.ID(required=True)
+ env_id = graphene.ID(required=True)
+ user_id = graphene.ID(required=False)
+ identity_key = graphene.String(required=True)
+ wrapped_seed = graphene.String(required=True)
+ wrapped_salt = graphene.String(required=True)
+
+ environment_key = graphene.Field(EnvironmentKeyType)
+
+ @classmethod
+ def mutate(cls, root, info, env_id, identity_key, wrapped_seed, wrapped_salt, user_id=None,):
+
+ env = Environment.objects.get(id=env_id)
+
+ # check that the user attempting the mutation has access
+ if not user_can_access_app(info.context.user.userId, env.app.id):
+ raise GraphQLError("You don't have access to this app")
+
+ # check that the user for whom we are adding a key has access
+ if not user_id is not None and member_can_access_org(user_id, env.app.organisation.id):
+ raise GraphQLError("This user doesn't have access to this app")
+
+ if user_id is not None:
+ org_member = OrganisationMember.objects.get(id=user_id)
+
+ if EnvironmentKey.objects.filter(environment=env, user_id=org_member).exists():
+ raise GraphQLError(
+ "This user already has access to this environment")
+
+ environment_key = EnvironmentKey.objects.create(
+ environment=env, user_id=user_id, identity_key=identity_key, wrapped_seed=wrapped_seed, wrapped_salt=wrapped_salt)
+
+ return CreateEnvironmentKeyMutation(environment_key=environment_key)
+
+
+class UpdateMemberEnvScopeMutation(graphene.Mutation):
+ class Arguments:
+ member_id = graphene.ID()
+ 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):
+ user = info.context.user
+ app = App.objects.get(id=app_id)
+
+ 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)
+
+ return UpdateMemberEnvScopeMutation(app=app)
+
+
+class CreateEnvironmentTokenMutation(graphene.Mutation):
+ class Arguments:
+ env_id = graphene.ID(required=True)
+ name = graphene.String(required=True)
+ identity_key = graphene.String(required=True)
+ token = graphene.String(required=True)
+ wrapped_key_share = graphene.String(required=True)
+
+ environment_token = graphene.Field(EnvironmentTokenType)
+
+ @classmethod
+ def mutate(cls, root, info, env_id, name, identity_key, token, wrapped_key_share):
+ user = info.context.user
+ if user_can_access_environment(user.userId, env_id):
+
+ env = Environment.objects.get(id=env_id)
+ org_member = OrganisationMember.objects.get(
+ organisation=env.app.organisation, user_id=user.userId, deleted_at=None)
+
+ environment_token = EnvironmentToken.objects.create(
+ environment_id=env_id, user=org_member, name=name, identity_key=identity_key, token=token, wrapped_key_share=wrapped_key_share)
+
+ return CreateEnvironmentTokenMutation(environment_token=environment_token)
+
+
+class CreateUserTokenMutation(graphene.Mutation):
+ class Arguments:
+ org_id = graphene.ID(required=True)
+ 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)
+
+ ok = graphene.Boolean()
+ user_token = graphene.Field(UserTokenType)
+
+ @classmethod
+ def mutate(cls, root, info, org_id, name, identity_key, token, wrapped_key_share, expiry):
+ user = info.context.user
+ if user_is_org_member(user.userId, org_id):
+
+ org_member = OrganisationMember.objects.get(
+ organisation_id=org_id, user_id=user.userId, deleted_at=None)
+
+ if expiry is not None:
+ expires_at = datetime.fromtimestamp(expiry / 1000)
+ else:
+ expires_at = None
+
+ user_token = UserToken.objects.create(
+ user=org_member, name=name, identity_key=identity_key, token=token, wrapped_key_share=wrapped_key_share, expires_at=expires_at)
+
+ return CreateUserTokenMutation(user_token=user_token, ok=True)
+
+ else:
+ raise GraphQLError(
+ "You don't have permission to perform this action")
+
+
+class DeleteUserTokenMutation(graphene.Mutation):
+ class Arguments:
+ token_id = graphene.ID(required=True)
+
+ ok = graphene.Boolean()
+
+ @classmethod
+ def mutate(cls, root, info, token_id):
+ user = info.context.user
+ token = UserToken.objects.get(id=token_id)
+ org = token.user.organisation
+
+ if user_is_org_member(user.userId, org.id):
+ token.deleted_at = timezone.now()
+ token.save()
+
+ return DeleteUserTokenMutation(ok=True)
+ else:
+ raise GraphQLError(
+ "You don't have permission to perform this action")
+
+
+class CreateServiceTokenMutation(graphene.Mutation):
+ class Arguments:
+ app_id = graphene.ID(required=True)
+ environment_keys = graphene.List(EnvironmentKeyInput)
+ identity_key = graphene.String(required=True)
+ token = graphene.String(required=True)
+ wrapped_key_share = graphene.String(required=True)
+ name = graphene.String(required=True)
+ expiry = graphene.BigInt(required=False)
+
+ service_token = graphene.Field(ServiceTokenType)
+
+ @classmethod
+ def mutate(cls, root, info, app_id, environment_keys, identity_key, token, wrapped_key_share, name, expiry):
+ user = info.context.user
+ app = App.objects.get(id=app_id)
+
+ if user_is_org_member(user.userId, app.organisation.id):
+
+ org_member = OrganisationMember.objects.get(
+ organisation_id=app.organisation.id, user_id=user.userId, deleted_at=None)
+
+ env_keys = EnvironmentKey.objects.bulk_create([EnvironmentKey(
+ environment_id=key.env_id, identity_key=key.identity_key, wrapped_seed=key.wrapped_seed, wrapped_salt=key.wrapped_salt) for key in environment_keys])
+
+ if expiry is not None:
+ expires_at = datetime.fromtimestamp(expiry / 1000)
+ else:
+ expires_at = None
+
+ service_token = ServiceToken.objects.create(
+ app=app, identity_key=identity_key, token=token, wrapped_key_share=wrapped_key_share, name=name, created_by=org_member, expires_at=expires_at)
+
+ service_token.keys.set(env_keys)
+
+ return CreateServiceTokenMutation(service_token=service_token)
+
+
+class DeleteServiceTokenMutation(graphene.Mutation):
+ class Arguments:
+ token_id = graphene.ID(required=True)
+
+ ok = graphene.Boolean()
+
+ @classmethod
+ def mutate(cls, root, info, token_id):
+ user = info.context.user
+ token = ServiceToken.objects.get(id=token_id)
+ org = token.app.organisation
+
+ if user_is_org_member(user.userId, org.id):
+ token.deleted_at = timezone.now()
+ token.save()
+
+ return DeleteServiceTokenMutation(ok=True)
+ else:
+ raise GraphQLError(
+ "You don't have permission to perform this action")
+
+
+class CreateSecretFolderMutation(graphene.Mutation):
+ class Arguments:
+ id = graphene.ID(required=True)
+ env_id = graphene.ID(required=True)
+ parent_folder_id = graphene.ID(required=False)
+ name = graphene.String(required=True)
+
+ folder = graphene.Field(SecretFolderType)
+
+ @classmethod
+ def mutate(cls, root, info, id, env_id, name, parent_folder_id=None):
+ user = info.context.user
+ if user_can_access_environment(user.id, env_id):
+ folder = SecretFolder.objects.create(
+ id=id, environment_id=env_id, parent_id=parent_folder_id, name=name)
+
+ return CreateSecretFolderMutation(folder=folder)
+
+
+class CreateSecretTagMutation(graphene.Mutation):
+ class Arguments:
+ org_id = graphene.ID(required=True)
+ name = graphene.String(required=True)
+ color = graphene.String(required=True)
+
+ tag = graphene.Field(SecretTagType)
+
+ @classmethod
+ def mutate(cls, root, info, org_id, name, color):
+
+ if not user_is_org_member(info.context.user.userId, org_id):
+ raise GraphQLError(
+ "You don't have permission to perform this action")
+
+ org = Organisation.objects.get(id=org_id)
+
+ if SecretTag.objects.filter(organisation=org, name=name).exists():
+ raise GraphQLError('This tag already exists!')
+
+ tag = SecretTag.objects.create(
+ organisation=org, name=name, color=color)
+
+ return CreateSecretTagMutation(tag=tag)
+
+
+class CreateSecretMutation(graphene.Mutation):
+ class Arguments:
+ secret_data = SecretInput(SecretInput)
+
+ secret = graphene.Field(SecretType)
+
+ @classmethod
+ def mutate(cls, root, info, secret_data):
+ env = Environment.objects.get(id=secret_data.env_id)
+ org = env.app.organisation
+ if not user_is_org_member(info.context.user.userId, org.id):
+ raise GraphQLError(
+ "You don't have permission to perform this action")
+
+ tags = SecretTag.objects.filter(
+ id__in=secret_data.tags)
+
+ secret_obj_data = {
+ 'environment_id': env.id,
+ 'folder_id': secret_data.folder_id,
+ 'key': secret_data.key,
+ 'key_digest': secret_data.key_digest,
+ 'value': secret_data.value,
+ 'version': 1,
+ 'comment': secret_data.comment
+ }
+
+ secret = Secret.objects.create(**secret_obj_data)
+ secret.tags.set(tags)
+
+ ip_address, user_agent = get_resolver_request_meta(info.context)
+
+ org_member = OrganisationMember.objects.get(
+ user=info.context.user, organisation=org, deleted_at=None)
+
+ event = SecretEvent.objects.create(
+ **{**secret_obj_data, **{
+ 'user': org_member,
+ 'secret': secret,
+ 'event_type': SecretEvent.CREATE,
+ 'ip_address': ip_address,
+ 'user_agent': user_agent
+ }})
+ event.tags.set(tags)
+
+ return CreateSecretMutation(secret=secret)
+
+
+class EditSecretMutation(graphene.Mutation):
+ class Arguments:
+ id = graphene.ID(required=True)
+ secret_data = SecretInput(SecretInput)
+
+ secret = graphene.Field(SecretType)
+
+ @classmethod
+ def mutate(cls, root, info, id, secret_data):
+ secret = Secret.objects.get(id=id)
+ env = secret.environment
+ org = env.app.organisation
+ if not user_is_org_member(info.context.user.userId, org.id):
+ raise GraphQLError(
+ "You don't have permission to perform this action")
+
+ tags = SecretTag.objects.filter(
+ id__in=secret_data.tags)
+
+ secret_obj_data = {
+ 'folder_id': secret_data.folder_id,
+ 'key': secret_data.key,
+ 'key_digest': secret_data.key_digest,
+ 'value': secret_data.value,
+ 'version': secret.version + 1,
+ 'comment': secret_data.comment
+ }
+
+ for key, value in secret_obj_data.items():
+ setattr(secret, key, value)
+
+ secret.updated_at = timezone.now()
+ secret.tags.set(tags)
+ secret.save()
+
+ ip_address, user_agent = get_resolver_request_meta(info.context)
+
+ org_member = OrganisationMember.objects.get(
+ user=info.context.user, organisation=org, deleted_at=None)
+
+ event = SecretEvent.objects.create(
+ **{**secret_obj_data, **{
+ 'user': org_member,
+ 'environment': env,
+ 'secret': secret,
+ 'event_type': SecretEvent.UPDATE,
+ 'ip_address': ip_address,
+ 'user_agent': user_agent
+ }})
+ event.tags.set(tags)
+
+ return EditSecretMutation(secret=secret)
+
+
+class DeleteSecretMutation(graphene.Mutation):
+ class Arguments:
+ id = graphene.ID(required=True)
+
+ secret = graphene.Field(SecretType)
+
+ @classmethod
+ def mutate(cls, root, info, id):
+ secret = Secret.objects.get(id=id)
+ env = secret.environment
+ org = env.app.organisation
+
+ if not user_is_org_member(info.context.user.userId, org.id):
+ raise GraphQLError(
+ "You don't have permission to perform this action")
+
+ secret.updated_at = timezone.now()
+ secret.deleted_at = timezone.now()
+ secret.save()
+
+ ip_address, user_agent = get_resolver_request_meta(info.context)
+
+ org_member = OrganisationMember.objects.get(
+ user=info.context.user, organisation=org, deleted_at=None)
+
+ most_recent_event_copy = SecretEvent.objects.filter(
+ secret=secret).order_by('version').last()
+
+ # setting the pk to None and then saving it creates a copy of the instance with updated fields
+ most_recent_event_copy.id = None
+ most_recent_event_copy.event_type = SecretEvent.DELETE
+ most_recent_event_copy.user = org_member
+ most_recent_event_copy.ip_address = ip_address
+ most_recent_event_copy.user_agent = user_agent
+ most_recent_event_copy.save()
+
+ return DeleteSecretMutation(secret=secret)
+
+
+class ReadSecretMutation(graphene.Mutation):
+ class Arguments:
+ id = graphene.ID(required=True)
+
+ ok = graphene.Boolean()
+
+ @classmethod
+ def mutate(cls, root, info, id):
+ secret = Secret.objects.get(id=id)
+ env = secret.environment
+ org = env.app.organisation
+ if not user_is_org_member(info.context.user.userId, org.id):
+ raise GraphQLError(
+ "You don't have permission to perform this action")
+ else:
+ ip_address, user_agent = get_resolver_request_meta(info.context)
+
+ org_member = OrganisationMember.objects.get(
+ user=info.context.user, organisation=org, deleted_at=None)
+
+ read_event = SecretEvent.objects.create(secret=secret, environment=secret.environment, user=org_member, key=secret.key, key_digest=secret.key_digest,
+ value=secret.value, comment=secret.comment, event_type=SecretEvent.READ, ip_address=ip_address, user_agent=user_agent)
+ read_event.tags.set(secret.tags.all())
+ return ReadSecretMutation(ok=True)
diff --git a/backend/backend/graphene/mutations/organisation.py b/backend/backend/graphene/mutations/organisation.py
new file mode 100644
index 000000000..3145e30b7
--- /dev/null
+++ b/backend/backend/graphene/mutations/organisation.py
@@ -0,0 +1,209 @@
+from api.emails import send_inite_email
+from backend.graphene.utils.permissions import user_is_admin, user_is_org_member
+import graphene
+from graphql import GraphQLError
+from api.models import App, Organisation, CustomUser, OrganisationMember, OrganisationMemberInvite
+from backend.graphene.types import OrganisationMemberInviteType, OrganisationMemberType, OrganisationType
+from datetime import datetime, timedelta
+from django.utils import timezone
+
+
+class CreateOrganisationMutation(graphene.Mutation):
+ class Arguments:
+ id = graphene.ID(required=True)
+ name = graphene.String(required=True)
+ identity_key = graphene.String(required=True)
+ wrapped_keyring = graphene.String(required=True)
+ wrapped_recovery = graphene.String(required=True)
+
+ organisation = graphene.Field(OrganisationType)
+
+ @classmethod
+ def mutate(cls, root, info, id, name, identity_key, wrapped_keyring, wrapped_recovery):
+ if Organisation.objects.filter(name__iexact=name).exists():
+ raise GraphQLError('This organisation name is not available.')
+ if OrganisationMember.objects.filter(user_id=info.context.user.userId, role=OrganisationMember.OWNER).exists():
+ raise GraphQLError(
+ 'Your current plan only supports one organisation.')
+
+ owner = CustomUser.objects.get(userId=info.context.user.userId)
+ org = Organisation.objects.create(
+ id=id, name=name, identity_key=identity_key)
+ OrganisationMember.objects.create(
+ user=owner, organisation=org, role=OrganisationMember.OWNER, identity_key=identity_key, wrapped_keyring=wrapped_keyring, wrapped_recovery=wrapped_recovery)
+
+ return CreateOrganisationMutation(organisation=org)
+
+
+class UpdateUserWrappedSecretsMutation(graphene.Mutation):
+ class Arguments:
+ org_id = graphene.ID(required=True)
+ wrapped_keyring = graphene.String(required=True)
+ wrapped_recovery = graphene.String(required=True)
+
+ org_member = graphene.Field(OrganisationMemberType)
+
+ @classmethod
+ def mutate(cls, root, info, org_id, wrapped_keyring, wrapped_recovery):
+
+ org_member = OrganisationMember.objects.get(
+ organisation_id=org_id, user=info.context.user, deleted_at=None)
+
+ org_member.wrapped_keyring = wrapped_keyring
+ org_member.wrapped_recovery = wrapped_recovery
+ org_member.save()
+
+ return UpdateUserWrappedSecretsMutation(org_member=org_member)
+
+
+class InviteOrganisationMemberMutation(graphene.Mutation):
+ class Arguments:
+ org_id = graphene.ID(required=True)
+ email = graphene.String(required=True)
+ apps = graphene.List(graphene.String)
+ role = graphene.String()
+
+ invite = graphene.Field(OrganisationMemberInviteType)
+
+ @classmethod
+ def mutate(cls, root, info, org_id, email, apps, role):
+ if user_is_org_member(info.context.user, org_id):
+ user_already_exists = OrganisationMember.objects.filter(
+ organisation_id=org_id, user__email=email, deleted_at=None).exists()
+ if user_already_exists:
+ raise GraphQLError(
+ "This user is already a member if your organisation")
+
+ if OrganisationMemberInvite.objects.filter(organisation_id=org_id, invitee_email=email, valid=True, expires_at__gte=timezone.now()).exists():
+ raise GraphQLError(
+ "An active invitiation already exists for this user.")
+
+ invited_by = OrganisationMember.objects.get(
+ user=info.context.user, organisation_id=org_id, deleted_at=None)
+
+ expiry = datetime.now() + timedelta(days=3)
+
+ app_scope = App.objects.filter(id__in=apps)
+
+ invite = OrganisationMemberInvite.objects.create(
+ organisation_id=org_id, invited_by=invited_by, role=role.lower(), invitee_email=email, expires_at=expiry)
+
+ invite.apps.set(app_scope)
+
+ try:
+ send_inite_email(invite)
+ except Exception as e:
+ print(f"Error sending invite email: {e}")
+
+ return InviteOrganisationMemberMutation(invite=invite)
+ else:
+ raise GraphQLError(
+ "You don't have permission to perform this action")
+
+
+class DeleteInviteMutation(graphene.Mutation):
+ class Arguments:
+ invite_id = graphene.ID(required=True)
+
+ ok = graphene.Boolean()
+
+ @classmethod
+ def mutate(cls, rooot, info, invite_id):
+ invite = OrganisationMemberInvite.objects.get(id=invite_id)
+
+ if user_is_org_member(info.context.user, invite.organisation.id):
+ invite.delete()
+
+ return DeleteInviteMutation(ok=True)
+
+ else:
+ raise GraphQLError(
+ "You don't have permission to perform this action")
+
+
+class CreateOrganisationMemberMutation(graphene.Mutation):
+ class Arguments:
+ org_id = graphene.ID(required=True)
+ identity_key = graphene.String(required=True)
+ wrapped_keyring = graphene.String(required=False)
+ wrapped_recovery = graphene.String(required=False)
+ invite_id = graphene.ID(required=True)
+
+ org_member = graphene.Field(OrganisationMemberType)
+
+ @classmethod
+ def mutate(cls, root, info, org_id, identity_key, wrapped_keyring, wrapped_recovery, invite_id):
+ if user_is_org_member(info.context.user.userId, org_id):
+ raise GraphQLError(
+ "You are already a member of this organisation")
+
+ if OrganisationMemberInvite.objects.filter(id=invite_id, valid=True, expires_at__gte=timezone.now()).exists():
+
+ invite = OrganisationMemberInvite.objects.get(
+ id=invite_id, valid=True, expires_at__gte=timezone.now())
+
+ org = Organisation.objects.get(id=org_id)
+
+ org_member = OrganisationMember.objects.create(
+ user_id=info.context.user.userId, organisation=org, role=invite.role, identity_key=identity_key, wrapped_keyring=wrapped_keyring, wrapped_recovery=wrapped_recovery)
+
+ org_member.apps.set(invite.apps.all()) # broken
+
+ invite.valid = False
+ invite.save()
+
+ return CreateOrganisationMemberMutation(org_member=org_member)
+ else:
+ raise GraphQLError(
+ "You need a valid invite to join this organisation")
+
+
+class DeleteOrganisationMemberMutation(graphene.Mutation):
+ class Arguments:
+ member_id = graphene.ID(required=True)
+
+ ok = graphene.Boolean()
+
+ @classmethod
+ def mutate(cls, root, info, member_id):
+ org_member = OrganisationMember.objects.get(
+ id=member_id, deleted_at=None)
+
+ if org_member.user == info.context.user:
+ raise GraphQLError(
+ "You can't remove yourself from an organisation")
+
+ if user_is_admin(info.context.user.userId, org_member.organisation.id):
+ org_member.delete()
+
+ return DeleteOrganisationMemberMutation(ok=True)
+ else:
+ raise GraphQLError(
+ "You don't have permission to perform that action")
+
+
+class UpdateOrganisationMemberRole(graphene.Mutation):
+ class Arguments:
+ member_id = graphene.ID(required=True)
+ role = graphene.String(required=True)
+
+ org_member = graphene.Field(OrganisationMemberType)
+
+ @classmethod
+ def mutate(cls, root, info, member_id, role):
+
+ org_member = OrganisationMember.objects.get(
+ id=member_id, deleted_at=None)
+
+ if user_is_admin(info.context.user.userId, org_member.organisation.id):
+ if role.lower() == OrganisationMember.OWNER.lower():
+ raise GraphQLError(
+ 'You cannot set this user as the organisation owner')
+
+ org_member.role = role.lower()
+ org_member.save()
+
+ return UpdateOrganisationMemberRole(org_member=org_member)
+ else:
+ raise GraphQLError(
+ "You don't have permission to perform this action")
diff --git a/backend/backend/graphene/types.py b/backend/backend/graphene/types.py
new file mode 100644
index 000000000..bcd865c14
--- /dev/null
+++ b/backend/backend/graphene/types.py
@@ -0,0 +1,206 @@
+import graphene
+from enum import Enum
+from graphene import ObjectType, relay
+from graphene_django import DjangoObjectType
+from api.models import CustomUser, Environment, EnvironmentKey, EnvironmentToken, Organisation, App, OrganisationMember, OrganisationMemberInvite, Secret, SecretEvent, SecretFolder, SecretTag, ServiceToken, UserToken
+from logs.dynamodb_models import KMSLog
+from allauth.socialaccount.models import SocialAccount
+
+
+class OrganisationType(DjangoObjectType):
+ role = graphene.String()
+ member_id = graphene.ID()
+ keyring = graphene.String()
+ recovery = graphene.String()
+
+ class Meta:
+ model = Organisation
+ fields = ('id', 'name', 'identity_key',
+ 'created_at', 'plan', 'role', 'member_id', 'keyring', 'recovery')
+
+ def resolve_role(self, info):
+ org_member = OrganisationMember.objects.get(
+ user=info.context.user, organisation=self, deleted_at=None)
+ return org_member.role
+
+ def resolve_member_id(self, info):
+ org_member = OrganisationMember.objects.get(
+ user=info.context.user, organisation=self, deleted_at=None)
+ return org_member.id
+
+ def resolve_keyring(self, info):
+ org_member = OrganisationMember.objects.get(
+ user=info.context.user, organisation=self, deleted_at=None)
+ return org_member.wrapped_keyring
+
+ def resolve_recovery(self, info):
+ org_member = OrganisationMember.objects.get(
+ user=info.context.user, organisation=self, deleted_at=None)
+ return org_member.wrapped_recovery
+
+ def resolve_idenity_key(self, info):
+ org_member = OrganisationMember.objects.get(
+ user=info.context.user, organisation=self, deleted_at=None)
+ return org_member.identity_key
+
+
+class OrganisationMemberType(DjangoObjectType):
+ email = graphene.String()
+ username = graphene.String()
+ full_name = graphene.String()
+ avatar_url = graphene.String()
+ self = graphene.Boolean()
+
+ class Meta:
+ model = OrganisationMember
+ fields = ('id', 'email', 'username', 'full_name', 'avatar_url', 'role',
+ 'identity_key', 'wrapped_keyring', 'created_at', 'updated_at')
+
+ def resolve_email(self, info):
+ return self.user.email
+
+ def resolve_username(self, info):
+ return self.user.username
+
+ def resolve_full_name(self, info):
+ social_acc = self.user.socialaccount_set.first()
+ if social_acc:
+ return social_acc.extra_data.get('name')
+ return None
+
+ def resolve_avatar_url(self, info):
+ social_acc = self.user.socialaccount_set.first()
+ if social_acc:
+ if social_acc.provider == 'google':
+ return social_acc.extra_data.get('picture')
+ return social_acc.extra_data.get('avatar_url')
+ return None
+
+ def resolve_self(self, info):
+ return self.user == info.context.user
+
+
+class OrganisationMemberInviteType(DjangoObjectType):
+ class Meta:
+ model = OrganisationMemberInvite
+ fields = ('id', 'invited_by', 'invitee_email', 'valid', 'organisation', 'apps', 'role',
+ 'created_at', 'updated_at', 'expires_at')
+
+
+class AppType(DjangoObjectType):
+ class Meta:
+ model = App
+ fields = ('id', 'name', 'identity_key',
+ 'wrapped_key_share', 'created_at', 'app_token', 'app_seed', 'app_version')
+
+
+class EnvironmentType(DjangoObjectType):
+ class Meta:
+ model = Environment
+ fields = ('id', 'name', 'env_type', 'identity_key',
+ 'wrapped_seed', 'wrapped_salt', 'created_at', 'updated_at')
+
+
+class EnvironmentKeyType(DjangoObjectType):
+ class Meta:
+ model = EnvironmentKey
+ fields = ('id', 'identity_key', 'wrapped_seed',
+ 'wrapped_salt', 'created_at', 'updated_at', 'environment')
+
+
+class EnvironmentTokenType(DjangoObjectType):
+ class Meta:
+ model = EnvironmentToken
+ fields = ('id', 'name', 'identity_key', 'token',
+ 'wrapped_key_share', 'created_at', 'updated_at')
+
+
+class UserTokenType(DjangoObjectType):
+ class Meta:
+ model = UserToken
+ fields = ('id', 'name', 'identity_key', 'token',
+ 'wrapped_key_share', 'created_at', 'updated_at', 'expires_at')
+
+
+class ServiceTokenType(DjangoObjectType):
+ class Meta:
+ model = ServiceToken
+ fields = ('id', 'keys', 'identity_key',
+ 'token', 'wrapped_key_share', 'name', 'created_by', 'created_at', 'updated_at', 'expires_at')
+
+
+class SecretFolderType(DjangoObjectType):
+ class Meta:
+ model = SecretFolder
+ fields = ('id', 'environment_id', 'parent_folder_id',
+ 'name', 'created_at', 'updated_at')
+
+
+class SecretTagType(DjangoObjectType):
+ class Meta:
+ model = SecretTag
+ fields = ('id', 'name', 'color')
+
+
+class SecretEventType(DjangoObjectType):
+ class Meta:
+ model = SecretEvent
+ fields = ('id', 'secret', 'key', 'value',
+ 'version', 'tags', 'comment', 'event_type', 'timestamp', 'user', 'ip_address', 'user_agent', 'environment')
+
+
+class SecretType(DjangoObjectType):
+
+ history = graphene.List(SecretEventType)
+
+ class Meta:
+ model = Secret
+ fields = ('id', 'key', 'value', 'folder', 'version', 'tags',
+ 'comment', 'created_at', 'updated_at', 'history')
+ # interfaces = (relay.Node, )
+
+ def resolve_history(self, info):
+ return SecretEvent.objects.filter(secret_id=self.id).order_by('timestamp')
+
+
+class KMSLogType(ObjectType):
+ class Meta:
+ model = KMSLog
+ fields = ('id', 'app_id', 'timestamp', 'phase_node',
+ 'event_type', 'ip_address', 'ph_size', 'edge_location', 'country', 'city', 'latitude', 'longitude')
+ interfaces = (relay.Node, )
+
+ id = graphene.ID(required=True)
+ timestamp = graphene.BigInt()
+ app_id = graphene.String()
+ phase_node = graphene.String()
+ event_type = graphene.String()
+ ip_address = graphene.String()
+ ph_size = graphene.Int()
+ asn = graphene.Int()
+ isp = graphene.String()
+ edge_location = graphene.String()
+ country = graphene.String()
+ city = graphene.String()
+ latitude = graphene.Float()
+ longitude = graphene.Float()
+
+
+class ChartDataPointType(graphene.ObjectType):
+ index = graphene.Int()
+ date = graphene.BigInt()
+ data = graphene.Int()
+
+
+class TimeRange(Enum):
+ HOUR = 'hour'
+ DAY = 'day'
+ WEEK = 'week'
+ MONTH = 'month'
+ YEAR = 'year'
+ ALL_TIME = 'allTime'
+
+
+class LogsResponseType(ObjectType):
+ kms = graphene.List(KMSLogType)
+ secrets = graphene.List(SecretEventType)
diff --git a/backend/backend/graphene/utils/permissions.py b/backend/backend/graphene/utils/permissions.py
new file mode 100644
index 000000000..12a8a43a8
--- /dev/null
+++ b/backend/backend/graphene/utils/permissions.py
@@ -0,0 +1,31 @@
+from api.models import App, Environment, EnvironmentKey, Organisation, OrganisationMember
+
+admin_roles = [OrganisationMember.OWNER, OrganisationMember.ADMIN]
+
+
+def user_is_admin(user_id, org_id):
+ member = OrganisationMember.objects.get(
+ user_id=user_id, organisation_id=org_id, deleted_at=None)
+ return member.role in admin_roles
+
+
+def user_is_org_member(user_id, org_id):
+ return OrganisationMember.objects.filter(user_id=user_id, organisation_id=org_id, deleted_at=None).exists()
+
+
+def user_can_access_app(user_id, app_id):
+ app = App.objects.get(id=app_id)
+ org_member = OrganisationMember.objects.get(
+ user_id=user_id, organisation=app.organisation, deleted_at=None)
+ return org_member in app.members.all()
+
+
+def user_can_access_environment(user_id, env_id):
+ env = Environment.objects.get(id=env_id)
+ org_member = OrganisationMember.objects.get(
+ organisation=env.app.organisation, user_id=user_id, deleted_at=None)
+ return EnvironmentKey.objects.filter(user_id=org_member, 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()
diff --git a/backend/backend/schema.py b/backend/backend/schema.py
index cc17a572d..e7f16695e 100644
--- a/backend/backend/schema.py
+++ b/backend/backend/schema.py
@@ -1,246 +1,295 @@
-from enum import Enum
+from .graphene.mutations.environment import CreateEnvironmentKeyMutation, CreateEnvironmentMutation, CreateEnvironmentTokenMutation, CreateSecretFolderMutation, CreateSecretMutation, CreateSecretTagMutation, CreateServiceTokenMutation, CreateUserTokenMutation, DeleteSecretMutation, DeleteServiceTokenMutation, DeleteUserTokenMutation, EditSecretMutation, ReadSecretMutation, UpdateMemberEnvScopeMutation
+from .graphene.utils.permissions import user_can_access_app, user_can_access_environment, user_is_admin, user_is_org_member
+from .graphene.mutations.app import AddAppMemberMutation, CreateAppMutation, DeleteAppMutation, RemoveAppMemberMutation, RotateAppKeysMutation
+from .graphene.mutations.organisation import CreateOrganisationMemberMutation, CreateOrganisationMutation, DeleteInviteMutation, DeleteOrganisationMemberMutation, InviteOrganisationMemberMutation, UpdateOrganisationMemberRole, UpdateUserWrappedSecretsMutation
+from .graphene.types import AppType, ChartDataPointType, EnvironmentKeyType, EnvironmentTokenType, EnvironmentType, KMSLogType, LogsResponseType, OrganisationMemberInviteType, OrganisationMemberType, OrganisationType, SecretEventType, SecretTagType, SecretType, ServiceTokenType, TimeRange, UserTokenType
import graphene
-from django.utils import timezone
-from graphene import ObjectType, relay
-from graphene_django import DjangoObjectType
from graphql import GraphQLError
-from api.models import CustomUser, Organisation, App
-from backend.api.kv import delete, purge
-from ee.feature_flags import allow_new_app
-from logs.dynamodb_models import KMSLog
+from api.models import Environment, EnvironmentKey, EnvironmentToken, Organisation, App, OrganisationMember, OrganisationMemberInvite, Secret, SecretEvent, SecretTag, ServiceToken, UserToken
from logs.queries import get_app_log_count, get_app_log_count_range, get_app_logs
from datetime import datetime, timedelta
from django.conf import settings
from logs.models import KMSDBLog
+from itertools import chain
+from django.utils import timezone
CLOUD_HOSTED = settings.APP_HOST == 'cloud'
-class OrganisationType(DjangoObjectType):
- class Meta:
- model = Organisation
- fields = ('id', 'name', 'identity_key', 'created_at', 'plan')
-
-
-class AppType(DjangoObjectType):
- class Meta:
- model = App
- fields = ('id', 'name', 'identity_key',
- 'wrapped_key_share', 'created_at', 'app_token', 'app_seed', 'app_version')
-
-
-class KMSLogType(ObjectType):
- class Meta:
- model = KMSLog
- fields = ('id', 'app_id', 'timestamp', 'phase_node',
- 'event_type', 'ip_address', 'ph_size', 'edge_location', 'country', 'city', 'latitude', 'longitude')
- interfaces = (relay.Node, )
-
- id = graphene.ID(required=True)
- timestamp = graphene.BigInt()
- app_id = graphene.String()
- phase_node = graphene.String()
- event_type = graphene.String()
- ip_address = graphene.String()
- ph_size = graphene.Int()
- asn = graphene.Int()
- isp = graphene.String()
- edge_location = graphene.String()
- country = graphene.String()
- city = graphene.String()
- latitude = graphene.Float()
- longitude = graphene.Float()
-
-
-class ChartDataPointType(graphene.ObjectType):
- index = graphene.Int()
- date = graphene.BigInt()
- data = graphene.Int()
-
-
-class TimeRange(Enum):
- HOUR = 'hour'
- DAY = 'day'
- WEEK = 'week'
- MONTH = 'month'
- YEAR = 'year'
- ALL_TIME = 'allTime'
-
-
-class CreateOrganisationMutation(graphene.Mutation):
- class Arguments:
- id = graphene.ID(required=True)
- name = graphene.String(required=True)
- identity_key = graphene.String(required=True)
-
- organisation = graphene.Field(OrganisationType)
-
- @classmethod
- def mutate(cls, root, info, id, name, identity_key):
- if Organisation.objects.filter(name__iexact=name).exists():
- raise GraphQLError('This organisation name is not available.')
- if Organisation.objects.filter(owner__userId=info.context.user.userId).exists():
- raise GraphQLError(
- 'Your current plan only supports one organisation.')
-
- owner = CustomUser.objects.get(userId=info.context.user.userId)
- org = Organisation.objects.create(
- id=id, name=name, identity_key=identity_key, owner=owner)
-
- return CreateOrganisationMutation(organisation=org)
-
-
-class CreateAppMutation(graphene.Mutation):
- class Arguments:
- id = graphene.ID(required=True)
- organisation_id = graphene.ID(required=True)
- name = graphene.String(required=True)
- identity_key = graphene.String(required=True)
- app_token = graphene.String(required=True)
- app_seed = graphene.String(required=True)
- wrapped_key_share = graphene.String(required=True)
- app_version = graphene.Int(required=True)
-
- app = graphene.Field(AppType)
-
- @classmethod
- def mutate(cls, root, info, id, organisation_id, name, identity_key, app_token, app_seed, wrapped_key_share, app_version):
- owner = info.context.user
- org = Organisation.objects.get(id=organisation_id)
- if not Organisation.objects.filter(id=organisation_id, owner__userId=owner.userId).exists():
- raise GraphQLError("You don't have access to this organisation")
-
- if allow_new_app(org) == False:
- raise GraphQLError(
- 'You have reached the App limit for your current plan. Please upgrade your account to add more.')
- if App.objects.filter(identity_key=identity_key).exists():
- raise GraphQLError("This app already exists")
-
- app = App.objects.create(id=id, organisation=org, name=name, identity_key=identity_key,
- app_token=app_token, app_seed=app_seed, wrapped_key_share=wrapped_key_share, app_version=app_version)
+class Query(graphene.ObjectType):
+ organisations = graphene.List(OrganisationType)
+ organisation_members = graphene.List(OrganisationMemberType, organisation_id=graphene.ID(
+ ), user_id=graphene.ID(), role=graphene.List(graphene.String))
+ organisation_admins_and_self = graphene.List(
+ OrganisationMemberType, organisation_id=graphene.ID())
+ organisation_invites = graphene.List(
+ OrganisationMemberInviteType, org_id=graphene.ID())
+ validate_invite = graphene.Field(
+ OrganisationMemberInviteType, invite_id=graphene.ID())
+ apps = graphene.List(
+ AppType, organisation_id=graphene.ID(), app_id=graphene.ID())
- return CreateAppMutation(app=app)
+ logs = graphene.Field(LogsResponseType, app_id=graphene.ID(),
+ start=graphene.BigInt(), end=graphene.BigInt())
+ kms_logs_count = graphene.Int(app_id=graphene.ID(),
+ this_month=graphene.Boolean())
-class RotateAppKeysMutation(graphene.Mutation):
- class Arguments:
- id = graphene.ID(required=True)
- app_token = graphene.String(required=True)
- wrapped_key_share = graphene.String(required=True)
+ secrets_logs_count = graphene.Int(app_id=graphene.ID())
- app = graphene.Field(AppType)
+ app_activity_chart = graphene.List(ChartDataPointType, app_id=graphene.ID(
+ ), period=graphene.Argument(graphene.Enum.from_enum(TimeRange)))
- @classmethod
- def mutate(cls, root, info, id, app_token, wrapped_key_share):
- owner = info.context.user
- org = Organisation.objects.filter(
- owner__userId=owner.userId).first()
- app = App.objects.get(id=id)
- if not app.organisation.id == org.id:
- raise GraphQLError("You don't have access to this app")
+ app_environments = graphene.List(EnvironmentType, app_id=graphene.ID(
+ ), environment_id=graphene.ID(required=False), member_id=graphene.ID(required=False))
+ app_users = graphene.List(OrganisationMemberType, app_id=graphene.ID())
+ secrets = graphene.List(SecretType, env_id=graphene.ID())
+ secret_history = graphene.List(SecretEventType, secret_id=graphene.ID())
+ secret_tags = graphene.List(SecretTagType, org_id=graphene.ID())
+ environment_keys = graphene.List(
+ EnvironmentKeyType, app_id=graphene.ID(required=False), environment_id=graphene.ID(required=False), member_id=graphene.ID(required=False))
+ environment_tokens = graphene.List(
+ EnvironmentTokenType, environment_id=graphene.ID())
+ user_tokens = graphene.List(UserTokenType, organisation_id=graphene.ID())
+ service_tokens = graphene.List(ServiceTokenType, app_id=graphene.ID())
- if CLOUD_HOSTED:
- # delete current keys from cloudflare KV
- deleted = delete(app.app_token)
+ def resolve_organisations(root, info):
+ memberships = OrganisationMember.objects.filter(
+ user=info.context.user, deleted_at=None)
- # purge keys from cloudflare cache
- purged = purge(
- f"phApp:v{app.app_version}:{app.identity_key}/{app.app_token}")
+ return [membership.organisation for membership in memberships]
- if not deleted or not purged:
- raise GraphQLError("Failed to delete app keys. Please try again.")
+ def resolve_organisation_members(root, info, organisation_id, role, user_id=None):
+ if not user_is_org_member(info.context.user.userId, organisation_id):
+ raise GraphQLError("You don't have access to this organisation")
- app.app_token = app_token
- app.wrapped_key_share = wrapped_key_share
- app.save()
+ filter = {
+ "organisation_id": organisation_id,
+ "deleted_at": None
+ }
- return RotateAppKeysMutation(app=app)
+ if role:
+ roles = [user_role.lower() for user_role in role]
+ filter["roles__in"] = roles
+ return OrganisationMember.objects.filter(**filter)
-class DeleteAppMutation(graphene.Mutation):
- class Arguments:
- id = graphene.ID(required=True)
+ def resolve_organisation_admins_and_self(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")
- app = graphene.Field(AppType)
+ roles = ['owner', 'admin']
- @classmethod
- def mutate(cls, root, info, id):
- owner = info.context.user
- org = Organisation.objects.filter(
- owner__userId=owner.userId).first()
- app = App.objects.get(id=id)
- if not app.organisation.id == org.id:
- raise GraphQLError("You don't have access to this app")
+ members = OrganisationMember.objects.filter(
+ organisation_id=organisation_id, role__in=roles, deleted_at=None)
- if CLOUD_HOSTED:
- # delete current keys from cloudflare KV
- deleted = delete(app.app_token)
+ 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))
- # purge keys from cloudflare cache
- purged = purge(
- f"phApp:v{app.app_version}:{app.identity_key}/{app.app_token}")
+ return members
- if not deleted or not purged:
- raise GraphQLError("Failed to delete app keys. Please try again.")
+ def resolve_organisation_invites(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")
- app.wrapped_key_share = ""
- app.is_deleted = True
- app.deleted_at = timezone.now()
- app.save()
+ invites = OrganisationMemberInvite.objects.filter(
+ organisation_id=org_id, valid=True)
- return DeleteAppMutation(app=app)
+ return invites
+ def resolve_validate_invite(root, info, invite_id):
+ try:
+ invite = OrganisationMemberInvite.objects.get(
+ id=invite_id, valid=True)
+ except:
+ raise GraphQLError("This invite is invalid")
-class Query(graphene.ObjectType):
- organisations = graphene.List(OrganisationType)
- apps = graphene.List(
- AppType, organisation_id=graphene.ID(), app_id=graphene.ID())
- logs = graphene.List(KMSLogType, app_id=graphene.ID(),
+ if invite.expires_at < timezone.now():
+ raise GraphQLError("This invite has expired")
- start=graphene.BigInt(), end=graphene.BigInt())
- logs_count = graphene.Int(app_id=graphene.ID(),
- this_month=graphene.Boolean())
-
- app_activity_chart = graphene.List(ChartDataPointType, app_id=graphene.ID(
- ), period=graphene.Argument(graphene.Enum.from_enum(TimeRange)))
-
- def resolve_organisations(root, info):
- return Organisation.objects.filter(owner__userId=info.context.user.userId)
+ if invite.invitee_email == info.context.user.email:
+ return invite
+ else:
+ raise GraphQLError("This invite is for another user")
def resolve_apps(root, info, organisation_id, app_id):
+ org_member = OrganisationMember.objects.get(
+ organisation_id=organisation_id, user_id=info.context.user.userId, deleted_at=None)
+
filter = {
'organisation_id': organisation_id,
+ 'id__in': org_member.apps.all(),
'is_deleted': False
}
+
if app_id != '':
filter['id'] = app_id
return App.objects.filter(**filter)
- def resolve_logs(root, info, app_id, start=0, end=0):
- owner = info.context.user
- org = Organisation.objects.filter(
- owner__userId=owner.userId).first()
+ def resolve_app_environments(root, info, app_id, environment_id, member_id=None):
+ if not user_can_access_app(info.context.user.userId, app_id):
+ raise GraphQLError("You don't have access to this app")
+
+ app = App.objects.get(id=app_id)
+
+ if member_id is not None:
+ org_member = OrganisationMember.objects.get(id=member_id)
+ else:
+ org_member = OrganisationMember.objects.get(
+ organisation=app.organisation, user_id=info.context.user.userId, deleted_at=None)
+
+ filter = {
+ 'app_id': app_id
+ }
+
+ if environment_id:
+ filter['id'] = environment_id
+
+ app_environments = Environment.objects.filter(**filter)
+ return [app_env for app_env in app_environments if EnvironmentKey.objects.filter(user=org_member, environment_id=app_env.id).exists()]
+
+ def resolve_app_users(root, info, app_id):
+ if not user_can_access_app(info.context.user.userId, app_id):
+ raise GraphQLError("You don't have access to this app")
+
+ app = App.objects.get(id=app_id)
+ return app.members.filter(deleted_at=None)
+
+ def resolve_secrets(root, info, env_id):
+ if not user_can_access_environment(info.context.user.userId, env_id):
+ raise GraphQLError("You don't have access to this environment")
+
+ return Secret.objects.filter(environment_id=env_id, deleted_at=None).order_by('created_at')
+
+ def resolve_secret_history(root, info, secret_id):
+ secret = Secret.objects.get(id=secret_id)
+ if not user_can_access_environment(info.context.user.userId, secret.environment.id):
+ raise GraphQLError("You don't have access to this secret")
+ return SecretEvent.objects.filter(secret_id=secret_id)
+
+ def resolve_secret_tags(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")
+
+ return SecretTag.objects.filter(organisation_id=org_id)
+
+ def resolve_environment_keys(root, info, app_id=None, environment_id=None, member_id=None):
+ if app_id is None and environment_id is None:
+ return None
+ elif app_id is not None:
+ app = App.objects.get(id=app_id)
+ else:
+ app = Environment.objects.get(id=environment_id).app
+
+ if not user_can_access_app(info.context.user.userId, app.id):
+ raise GraphQLError("You don't have access to this app")
+
+ filter = {
+ 'environment__app': app,
+ 'deleted_at': None
+ }
+
+ if environment_id:
+ filter['environment_id'] = environment_id
+
+ if member_id is not None:
+ org_member = OrganisationMember.objects.get(
+ id=member_id, deleted_at=None)
+ else:
+ org_member = OrganisationMember.objects.get(
+ user=info.context.user, organisation=app.organisation, deleted_at=None)
+
+ filter['user'] = org_member
+
+ return EnvironmentKey.objects.filter(**filter)
+
+ def resolve_environment_tokens(root, info, environment_id):
+ if not user_can_access_environment(info.context.user.userId, environment_id):
+ raise GraphQLError("You don't have access to this secret")
+
+ env = Environment.objects.get(id=environment_id)
+ org_member = OrganisationMember.objects.get(
+ user=info.context.user, organisation=env.app.organisation, deleted_at=None)
+ return EnvironmentToken.objects.filter(environment=env, user=org_member)
+
+ def resolve_user_tokens(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")
+
+ org_member = OrganisationMember.objects.get(
+ user=info.context.user, organisation_id=organisation_id, deleted_at=None)
+ return UserToken.objects.filter(user=org_member, deleted_at=None)
+
+ def resolve_service_tokens(root, info, app_id):
app = App.objects.get(id=app_id)
- if not app.organisation.id == org.id:
+ if not user_is_org_member(info.context.user.userId, app.organisation.id):
+ raise GraphQLError("You don't have access to this organisation")
+
+ return ServiceToken.objects.filter(app=app, deleted_at=None)
+
+ 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")
+
+ app = App.objects.get(id=app_id)
+
if end == 0:
end = datetime.now().timestamp() * 1000
+
if CLOUD_HOSTED:
- return get_app_logs(f"phApp:v{app.app_version}:{app.identity_key}", start, end, 25)
- logs = KMSDBLog.objects.filter(app_id=f"phApp:v{app.app_version}:{app.identity_key}",timestamp__lte=end, timestamp__gte=start).order_by('-timestamp')[:25]
- return list(logs.values())
-
- def resolve_logs_count(root, info, app_id):
- owner = info.context.user
- org = Organisation.objects.filter(
- owner__userId=owner.userId).first()
- app = App.objects.get(id=app_id)
- if not app.organisation.id == org.id:
+ kms_logs = get_app_logs(
+ f"phApp:v{app.app_version}:{app.identity_key}", start, end, 25)
+
+ else:
+ kms_logs = list(KMSDBLog.objects.filter(
+ app_id=f"phApp:v{app.app_version}:{app.identity_key}", timestamp__lte=end, timestamp__gte=start).order_by('-timestamp')[:25].values())
+
+ org_member = OrganisationMember.objects.get(
+ user=info.context.user, organisation=app.organisation, deleted_at=None)
+
+ env_keys = EnvironmentKey.objects.filter(
+ environment__app=app, user=org_member, deleted_at=None
+ ).select_related('environment')
+
+ envs = [env_key.environment for env_key in env_keys]
+
+ start_dt = datetime.fromtimestamp(start / 1000)
+ end_dt = datetime.fromtimestamp(end / 1000)
+
+ secret_events = SecretEvent.objects.filter(
+ environment__in=envs, timestamp__lte=end_dt, timestamp__gte=start_dt).order_by('-timestamp')[:25]
+
+ return LogsResponseType(kms=kms_logs, secrets=secret_events)
+
+ def resolve_kms_logs_count(root, info, app_id):
+ if not user_can_access_app(info.context.user.userId, app_id):
raise GraphQLError("You don't have access to this app")
+
+ app = App.objects.get(id=app_id)
+
if CLOUD_HOSTED:
return get_app_log_count(f"phApp:v{app.app_version}:{app.identity_key}")
return KMSDBLog.objects.filter(app_id=f"phApp:v{app.app_version}:{app.identity_key}").count()
+ def resolve_secrets_logs_count(root, info, app_id):
+ if not user_can_access_app(info.context.user.userId, app_id):
+ raise GraphQLError("You don't have access to this app")
+
+ app = App.objects.get(id=app_id)
+
+ org_member = OrganisationMember.objects.get(
+ user=info.context.user, organisation=app.organisation, deleted_at=None)
+
+ env_keys = EnvironmentKey.objects.filter(
+ environment__app=app, user=org_member, deleted_at=None
+ ).select_related('environment')
+
+ envs = [env_key.environment for env_key in env_keys]
+
+ return SecretEvent.objects.filter(environment__in=envs).count()
+
def resolve_app_activity_chart(root, info, app_id, period=TimeRange.DAY):
"""
Converts app log activity for the chosen time period into time series data that can be used to draw a chart
@@ -255,11 +304,9 @@ def resolve_app_activity_chart(root, info, app_id, period=TimeRange.DAY):
Returns:
List[ChartDataPointType]: Time series decrypt count data
"""
- owner = info.context.user
- org = Organisation.objects.filter(
- owner__userId=owner.userId).first()
+
app = App.objects.get(id=app_id)
- if not app.organisation.id == org.id:
+ if not user_can_access_app(info.context.user.userId, app_id):
raise GraphQLError("You don't have access to this app")
end_date = datetime.now() # current time
@@ -316,7 +363,8 @@ def resolve_app_activity_chart(root, info, app_id, period=TimeRange.DAY):
decrypts = get_app_log_count_range(
f"phApp:v{app.app_version}:{app.identity_key}", start_unix, end_unix)
else:
- decrypts = KMSDBLog.objects.filter(app_id=f"phApp:v{app.app_version}:{app.identity_key}",timestamp__lte=end_unix, timestamp__gte=start_unix).count()
+ decrypts = KMSDBLog.objects.filter(
+ app_id=f"phApp:v{app.app_version}:{app.identity_key}", timestamp__lte=end_unix, timestamp__gte=start_unix).count()
time_series_logs.append(ChartDataPointType(
index=str(index), date=end_unix, data=decrypts))
@@ -330,9 +378,37 @@ def resolve_app_activity_chart(root, info, app_id, period=TimeRange.DAY):
class Mutation(graphene.ObjectType):
create_organisation = CreateOrganisationMutation.Field()
+ invite_organisation_member = InviteOrganisationMemberMutation.Field()
+ create_organisation_member = CreateOrganisationMemberMutation.Field()
+ delete_organisation_member = DeleteOrganisationMemberMutation.Field()
+ update_organisation_member_role = UpdateOrganisationMemberRole.Field()
+ update_member_wrapped_secrets = UpdateUserWrappedSecretsMutation.Field()
+
+ delete_invitation = DeleteInviteMutation.Field()
+
create_app = CreateAppMutation.Field()
rotate_app_keys = RotateAppKeysMutation.Field()
delete_app = DeleteAppMutation.Field()
+ add_app_member = AddAppMemberMutation.Field()
+ remove_app_member = RemoveAppMemberMutation.Field()
+ update_member_environment_scope = UpdateMemberEnvScopeMutation.Field()
+
+ create_environment = CreateEnvironmentMutation.Field()
+ create_environment_key = CreateEnvironmentKeyMutation.Field()
+ create_environment_token = CreateEnvironmentTokenMutation.Field()
+
+ create_user_token = CreateUserTokenMutation.Field()
+ delete_user_token = DeleteUserTokenMutation.Field()
+
+ create_service_token = CreateServiceTokenMutation.Field()
+ delete_service_token = DeleteServiceTokenMutation.Field()
+
+ create_secret_folder = CreateSecretFolderMutation.Field()
+ create_secret_tag = CreateSecretTagMutation.Field()
+ create_secret = CreateSecretMutation.Field()
+ edit_secret = EditSecretMutation.Field()
+ delete_secret = DeleteSecretMutation.Field()
+ read_secret = ReadSecretMutation.Field()
schema = graphene.Schema(query=Query, mutation=Mutation)
diff --git a/backend/backend/settings.py b/backend/backend/settings.py
index 321257936..db939fea0 100644
--- a/backend/backend/settings.py
+++ b/backend/backend/settings.py
@@ -121,6 +121,17 @@
OAUTH_REDIRECT_URI = os.getenv('OAUTH_REDIRECT_URI')
+
+# Email configurations
+EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
+EMAIL_HOST = os.getenv('SMTP_SERVER')
+EMAIL_PORT = int(os.getenv('SMTP_PORT', 587))
+EMAIL_USE_TLS = True
+EMAIL_HOST_USER = os.getenv('SMTP_USERNAME')
+EMAIL_HOST_PASSWORD = os.getenv('SMTP_PASSWORD')
+DEFAULT_FROM_EMAIL = os.getenv('DEFAULT_FROM_EMAIL')
+
+
SITE_ID = 1
MIDDLEWARE = [
@@ -256,4 +267,3 @@
APP_HOST = os.getenv('APP_HOST')
except:
APP_HOST = 'self'
-
\ No newline at end of file
diff --git a/backend/backend/urls.py b/backend/backend/urls.py
index 872d7ed04..413acefed 100644
--- a/backend/backend/urls.py
+++ b/backend/backend/urls.py
@@ -3,7 +3,7 @@
from django.conf import settings
from graphene_django.views import GraphQLView
from django.views.decorators.csrf import csrf_exempt
-from api.views import PrivateGraphQLView, logout_view, health_check, kms
+from api.views import PrivateGraphQLView, logout_view, health_check, kms, SecretsView, user_token_kms, service_token_kms, secrets_tokens
CLOUD_HOSTED = settings.APP_HOST == 'cloud'
@@ -14,6 +14,8 @@
path('logout/', csrf_exempt(logout_view)),
path('graphql/', csrf_exempt(PrivateGraphQLView.as_view(graphiql=True))),
path('493c5048-99f9-4eac-ad0d-98c3740b491f/health', health_check),
+ path('secrets/', SecretsView.as_view()),
+ path('secrets/tokens/', secrets_tokens)
]
if not CLOUD_HOSTED:
diff --git a/backend/ee/LICENSE b/backend/ee/LICENSE
index 5e43b4ed4..6343eec5e 100644
--- a/backend/ee/LICENSE
+++ b/backend/ee/LICENSE
@@ -9,13 +9,13 @@ and are in compliance with, the Phase Subscription Terms of Service, available
at https://phase.dev/legal/terms, or other
agreement governing the use of the Software, as agreed by you and Phase,
and otherwise have a valid Phase Console Enterprise License for the
-correct number of applications & user seats. Subject to the foregoing sentence, you are free to
+correct number of user seats. Subject to the foregoing sentence, you are free to
modify this Software and publish patches to the Software. You agree that Phase
and/or its licensors (as applicable) retain all right, title and interest in and
to all such modifications and/or patches, and all such modifications and/or
patches may only be used, copied, modified, displayed, distributed, or otherwise
exploited with a valid Phase Console Enterprise subscription for the correct number of
-applications & user seats. Notwithstanding the foregoing, you may copy and modify
+user seats. Notwithstanding the foregoing, you may copy and modify
the Software for development and testing purposes, without requiring a
subscription. You agree that Phase and/or its licensors (as applicable) retain
all right, title and interest in and to all such modifications. You are not
diff --git a/backend/ee/feature_flags.py b/backend/ee/feature_flags.py
index df8d37284..c1ea1107d 100644
--- a/backend/ee/feature_flags.py
+++ b/backend/ee/feature_flags.py
@@ -11,13 +11,14 @@ def allow_new_app(organisation):
Returns:
bool: Whether or not to allow creating an app for the given org
"""
- FREE_APP_LIMIT = 1
+ FREE_APP_LIMIT = 5
PRO_APP_LIMIT = 10
- current_app_count = App.objects.filter(organisation=organisation, is_deleted=False).count()
-
+ current_app_count = App.objects.filter(
+ organisation=organisation, is_deleted=False).count()
+
if organisation.plan == Organisation.FREE_PLAN and current_app_count >= FREE_APP_LIMIT:
return False
elif organisation.plan == Organisation.PRO_PLAN and current_app_count >= PRO_APP_LIMIT:
return False
- return True
\ No newline at end of file
+ return True
diff --git a/docker-compose.yml b/docker-compose.yml
index 048658716..d327cff42 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -29,7 +29,7 @@ services:
NEXTAUTH_URL: "${HTTP_PROTOCOL}${HOST}"
OAUTH_REDIRECT_URI: "${HTTP_PROTOCOL}${HOST}"
BACKEND_API_BASE: "http://backend:8000"
- NEXT_PUBLIC_BACKEND_API_BASE: "${HTTP_PROTOCOL}${HOST}/ph-backend"
+ NEXT_PUBLIC_BACKEND_API_BASE: "${HTTP_PROTOCOL}${HOST}/service"
NEXT_PUBLIC_NEXTAUTH_PROVIDERS: "${SSO_PROVIDERS}"
networks:
- phase-net
diff --git a/frontend/.vscode/settings.json b/frontend/.vscode/settings.json
new file mode 100644
index 000000000..d0679104b
--- /dev/null
+++ b/frontend/.vscode/settings.json
@@ -0,0 +1,4 @@
+{
+ "typescript.tsdk": "node_modules/typescript/lib",
+ "typescript.enablePromptUseWorkspaceTsdk": true
+}
\ No newline at end of file
diff --git a/frontend/apollo/gql.ts b/frontend/apollo/gql.ts
index b04b67717..497ecb2ef 100644
--- a/frontend/apollo/gql.ts
+++ b/frontend/apollo/gql.ts
@@ -1,8 +1,65 @@
/* eslint-disable */
-import * as types from './graphql'
-import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'
+import * as types from './graphql';
+import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
+
+/**
+ * Map of all GraphQL operations in the project.
+ *
+ * This map has several performance disadvantages:
+ * 1. It is not tree-shakeable, so it will include all operations in the project.
+ * 2. It is not minifiable, so the string of a GraphQL query will be multiple times inside the bundle.
+ * 3. It does not support dead code elimination, so it will add unused operations.
+ *
+ * Therefore it is highly recommended to use the babel or swc plugin for production.
+ */
+const documents = {
+ "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 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 createdAt\n }\n }\n}": types.CreateOrgDocument,
+ "mutation DeleteApplication($id: ID!) {\n deleteApp(id: $id) {\n app {\n id\n }\n }\n}": types.DeleteApplicationDocument,
+ "mutation CreateEnv($input: EnvironmentInput!) {\n createEnvironment(environmentData: $input) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n}": types.CreateEnvDocument,
+ "mutation CreateEnvKey($envId: ID!, $userId: ID, $wrappedSeed: String!, $wrappedSalt: String!, $identityKey: String!) {\n createEnvironmentKey(\n envId: $envId\n userId: $userId\n wrappedSeed: $wrappedSeed\n wrappedSalt: $wrappedSalt\n identityKey: $identityKey\n ) {\n environmentKey {\n id\n createdAt\n }\n }\n}": types.CreateEnvKeyDocument,
+ "mutation CreateEnvToken($envId: ID!, $name: String!, $identityKey: String!, $token: String!, $wrappedKeyShare: String!) {\n createEnvironmentToken(\n envId: $envId\n name: $name\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n ) {\n environmentToken {\n id\n createdAt\n }\n }\n}": types.CreateEnvTokenDocument,
+ "mutation CreateNewSecret($newSecret: SecretInput!) {\n createSecret(secretData: $newSecret) {\n secret {\n id\n key\n value\n createdAt\n }\n }\n}": types.CreateNewSecretDocument,
+ "mutation CreateNewSecretTag($orgId: ID!, $name: String!, $color: String!) {\n createSecretTag(orgId: $orgId, name: $name, color: $color) {\n tag {\n id\n }\n }\n}": types.CreateNewSecretTagDocument,
+ "mutation CreateNewServiceToken($appId: ID!, $environmentKeys: [EnvironmentKeyInput], $identityKey: String!, $token: String!, $wrappedKeyShare: String!, $name: String!, $expiry: BigInt) {\n createServiceToken(\n appId: $appId\n environmentKeys: $environmentKeys\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n name: $name\n expiry: $expiry\n ) {\n serviceToken {\n id\n createdAt\n expiresAt\n }\n }\n}": types.CreateNewServiceTokenDocument,
+ "mutation DeleteSecretOp($id: ID!) {\n deleteSecret(id: $id) {\n secret {\n id\n }\n }\n}": types.DeleteSecretOpDocument,
+ "mutation RevokeServiceToken($tokenId: ID!) {\n deleteServiceToken(tokenId: $tokenId) {\n ok\n }\n}": types.RevokeServiceTokenDocument,
+ "mutation UpdateSecret($id: ID!, $secretData: SecretInput!) {\n editSecret(id: $id, secretData: $secretData) {\n secret {\n id\n updatedAt\n }\n }\n}": types.UpdateSecretDocument,
+ "mutation InitAppEnvironments($devEnv: EnvironmentInput!, $stagingEnv: EnvironmentInput!, $prodEnv: EnvironmentInput!, $devAdminKeys: [EnvironmentKeyInput], $stagAdminKeys: [EnvironmentKeyInput], $prodAdminKeys: [EnvironmentKeyInput]) {\n devEnvironment: createEnvironment(\n environmentData: $devEnv\n adminKeys: $devAdminKeys\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n stagingEnvironment: createEnvironment(\n environmentData: $stagingEnv\n adminKeys: $stagAdminKeys\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n prodEnvironment: createEnvironment(\n environmentData: $prodEnv\n adminKeys: $prodAdminKeys\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n}": types.InitAppEnvironmentsDocument,
+ "mutation LogSecretRead($id: ID!) {\n readSecret(id: $id) {\n ok\n }\n}": types.LogSecretReadDocument,
+ "mutation AcceptOrganisationInvite($orgId: ID!, $identityKey: String!, $wrappedKeyring: String!, $wrappedRecovery: String!, $inviteId: ID!) {\n createOrganisationMember(\n orgId: $orgId\n identityKey: $identityKey\n wrappedKeyring: $wrappedKeyring\n wrappedRecovery: $wrappedRecovery\n inviteId: $inviteId\n ) {\n orgMember {\n id\n email\n createdAt\n role\n }\n }\n}": types.AcceptOrganisationInviteDocument,
+ "mutation DeleteOrgInvite($inviteId: ID!) {\n deleteInvitation(inviteId: $inviteId) {\n ok\n }\n}": types.DeleteOrgInviteDocument,
+ "mutation RemoveMember($memberId: ID!) {\n deleteOrganisationMember(memberId: $memberId) {\n ok\n }\n}": types.RemoveMemberDocument,
+ "mutation InviteMember($orgId: ID!, $email: String!, $apps: [String], $role: String) {\n inviteOrganisationMember(orgId: $orgId, email: $email, apps: $apps, role: $role) {\n invite {\n id\n }\n }\n}": types.InviteMemberDocument,
+ "mutation UpdateMemberRole($memberId: ID!, $role: String!) {\n updateOrganisationMemberRole(memberId: $memberId, role: $role) {\n orgMember {\n id\n role\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 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 }\n}": types.GetAppMembersDocument,
+ "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 }\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 }\n}": types.GetAppsDocument,
+ "query GetOrganisations {\n organisations {\n id\n name\n identityKey\n createdAt\n plan\n role\n memberId\n keyring\n recovery\n }\n}": types.GetOrganisationsDocument,
+ "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 GetOrganisationAdminsAndSelf($organisationId: ID!) {\n organisationAdminsAndSelf(organisationId: $organisationId) {\n id\n role\n identityKey\n self\n }\n}": types.GetOrganisationAdminsAndSelfDocument,
+ "query GetOrganisationMembers($organisationId: ID!, $role: [String]) {\n organisationMembers(organisationId: $organisationId, role: $role) {\n id\n role\n identityKey\n email\n fullName\n avatarUrl\n createdAt\n self\n }\n}": types.GetOrganisationMembersDocument,
+ "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 }\n}": types.GetAppEnvironmentsDocument,
+ "query GetAppSecretsLogs($appId: ID!, $start: BigInt, $end: BigInt) {\n logs(appId: $appId, start: $start, end: $end) {\n secrets {\n id\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 eventType\n environment {\n id\n envType\n name\n }\n secret {\n id\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 GetEnvSecretsKV($envId: ID!) {\n secrets(envId: $envId) {\n id\n key\n value\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!) {\n secrets(envId: $envId) {\n id\n key\n value\n tags {\n id\n name\n color\n }\n comment\n createdAt\n history {\n id\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 eventType\n }\n }\n appEnvironments(appId: $appId, environmentId: $envId) {\n id\n name\n envType\n identityKey\n }\n environmentKeys(appId: $appId, environmentId: $envId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\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 }\n }\n}": types.GetServiceTokensDocument,
+ "query GetUserTokens($organisationId: ID!) {\n userTokens(organisationId: $organisationId) {\n id\n name\n wrappedKeyShare\n createdAt\n expiresAt\n }\n}": types.GetUserTokensDocument,
+};
-const documents: never[] = []
/**
* The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
*
@@ -15,11 +72,191 @@ const documents: never[] = []
* The query argument is unknown!
* Please regenerate the types.
*/
-export function graphql(source: string): unknown
+export function graphql(source: string): unknown;
+
+/**
+ * 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}"];
+/**
+ * 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}"];
+/**
+ * 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}"];
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function graphql(source: "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}"): (typeof documents)["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}"];
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function graphql(source: "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 createdAt\n }\n }\n}"): (typeof documents)["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 createdAt\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 DeleteApplication($id: ID!) {\n deleteApp(id: $id) {\n app {\n id\n }\n }\n}"): (typeof documents)["mutation DeleteApplication($id: ID!) {\n deleteApp(id: $id) {\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 CreateEnv($input: EnvironmentInput!) {\n createEnvironment(environmentData: $input) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n}"): (typeof documents)["mutation CreateEnv($input: EnvironmentInput!) {\n createEnvironment(environmentData: $input) {\n environment {\n id\n name\n createdAt\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: "mutation CreateEnvKey($envId: ID!, $userId: ID, $wrappedSeed: String!, $wrappedSalt: String!, $identityKey: String!) {\n createEnvironmentKey(\n envId: $envId\n userId: $userId\n wrappedSeed: $wrappedSeed\n wrappedSalt: $wrappedSalt\n identityKey: $identityKey\n ) {\n environmentKey {\n id\n createdAt\n }\n }\n}"): (typeof documents)["mutation CreateEnvKey($envId: ID!, $userId: ID, $wrappedSeed: String!, $wrappedSalt: String!, $identityKey: String!) {\n createEnvironmentKey(\n envId: $envId\n userId: $userId\n wrappedSeed: $wrappedSeed\n wrappedSalt: $wrappedSalt\n identityKey: $identityKey\n ) {\n environmentKey {\n id\n createdAt\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 CreateEnvToken($envId: ID!, $name: String!, $identityKey: String!, $token: String!, $wrappedKeyShare: String!) {\n createEnvironmentToken(\n envId: $envId\n name: $name\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n ) {\n environmentToken {\n id\n createdAt\n }\n }\n}"): (typeof documents)["mutation CreateEnvToken($envId: ID!, $name: String!, $identityKey: String!, $token: String!, $wrappedKeyShare: String!) {\n createEnvironmentToken(\n envId: $envId\n name: $name\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n ) {\n environmentToken {\n id\n createdAt\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 CreateNewSecret($newSecret: SecretInput!) {\n createSecret(secretData: $newSecret) {\n secret {\n id\n key\n value\n createdAt\n }\n }\n}"): (typeof documents)["mutation CreateNewSecret($newSecret: SecretInput!) {\n createSecret(secretData: $newSecret) {\n secret {\n id\n key\n value\n createdAt\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 CreateNewSecretTag($orgId: ID!, $name: String!, $color: String!) {\n createSecretTag(orgId: $orgId, name: $name, color: $color) {\n tag {\n id\n }\n }\n}"): (typeof documents)["mutation CreateNewSecretTag($orgId: ID!, $name: String!, $color: String!) {\n createSecretTag(orgId: $orgId, name: $name, color: $color) {\n tag {\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 CreateNewServiceToken($appId: ID!, $environmentKeys: [EnvironmentKeyInput], $identityKey: String!, $token: String!, $wrappedKeyShare: String!, $name: String!, $expiry: BigInt) {\n createServiceToken(\n appId: $appId\n environmentKeys: $environmentKeys\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n name: $name\n expiry: $expiry\n ) {\n serviceToken {\n id\n createdAt\n expiresAt\n }\n }\n}"): (typeof documents)["mutation CreateNewServiceToken($appId: ID!, $environmentKeys: [EnvironmentKeyInput], $identityKey: String!, $token: String!, $wrappedKeyShare: String!, $name: String!, $expiry: BigInt) {\n createServiceToken(\n appId: $appId\n environmentKeys: $environmentKeys\n identityKey: $identityKey\n token: $token\n wrappedKeyShare: $wrappedKeyShare\n name: $name\n expiry: $expiry\n ) {\n serviceToken {\n id\n createdAt\n expiresAt\n }\n }\n}"];
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function graphql(source: "mutation DeleteSecretOp($id: ID!) {\n deleteSecret(id: $id) {\n secret {\n id\n }\n }\n}"): (typeof documents)["mutation DeleteSecretOp($id: ID!) {\n deleteSecret(id: $id) {\n secret {\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 RevokeServiceToken($tokenId: ID!) {\n deleteServiceToken(tokenId: $tokenId) {\n ok\n }\n}"): (typeof documents)["mutation RevokeServiceToken($tokenId: ID!) {\n deleteServiceToken(tokenId: $tokenId) {\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 UpdateSecret($id: ID!, $secretData: SecretInput!) {\n editSecret(id: $id, secretData: $secretData) {\n secret {\n id\n updatedAt\n }\n }\n}"): (typeof documents)["mutation UpdateSecret($id: ID!, $secretData: SecretInput!) {\n editSecret(id: $id, secretData: $secretData) {\n secret {\n id\n updatedAt\n }\n }\n}"];
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function graphql(source: "mutation InitAppEnvironments($devEnv: EnvironmentInput!, $stagingEnv: EnvironmentInput!, $prodEnv: EnvironmentInput!, $devAdminKeys: [EnvironmentKeyInput], $stagAdminKeys: [EnvironmentKeyInput], $prodAdminKeys: [EnvironmentKeyInput]) {\n devEnvironment: createEnvironment(\n environmentData: $devEnv\n adminKeys: $devAdminKeys\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n stagingEnvironment: createEnvironment(\n environmentData: $stagingEnv\n adminKeys: $stagAdminKeys\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n prodEnvironment: createEnvironment(\n environmentData: $prodEnv\n adminKeys: $prodAdminKeys\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n}"): (typeof documents)["mutation InitAppEnvironments($devEnv: EnvironmentInput!, $stagingEnv: EnvironmentInput!, $prodEnv: EnvironmentInput!, $devAdminKeys: [EnvironmentKeyInput], $stagAdminKeys: [EnvironmentKeyInput], $prodAdminKeys: [EnvironmentKeyInput]) {\n devEnvironment: createEnvironment(\n environmentData: $devEnv\n adminKeys: $devAdminKeys\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n stagingEnvironment: createEnvironment(\n environmentData: $stagingEnv\n adminKeys: $stagAdminKeys\n ) {\n environment {\n id\n name\n createdAt\n identityKey\n }\n }\n prodEnvironment: createEnvironment(\n environmentData: $prodEnv\n adminKeys: $prodAdminKeys\n ) {\n environment {\n id\n name\n createdAt\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: "mutation LogSecretRead($id: ID!) {\n readSecret(id: $id) {\n ok\n }\n}"): (typeof documents)["mutation LogSecretRead($id: ID!) {\n readSecret(id: $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 AcceptOrganisationInvite($orgId: ID!, $identityKey: String!, $wrappedKeyring: String!, $wrappedRecovery: String!, $inviteId: ID!) {\n createOrganisationMember(\n orgId: $orgId\n identityKey: $identityKey\n wrappedKeyring: $wrappedKeyring\n wrappedRecovery: $wrappedRecovery\n inviteId: $inviteId\n ) {\n orgMember {\n id\n email\n createdAt\n role\n }\n }\n}"): (typeof documents)["mutation AcceptOrganisationInvite($orgId: ID!, $identityKey: String!, $wrappedKeyring: String!, $wrappedRecovery: String!, $inviteId: ID!) {\n createOrganisationMember(\n orgId: $orgId\n identityKey: $identityKey\n wrappedKeyring: $wrappedKeyring\n wrappedRecovery: $wrappedRecovery\n inviteId: $inviteId\n ) {\n orgMember {\n id\n email\n createdAt\n role\n }\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 DeleteOrgInvite($inviteId: ID!) {\n deleteInvitation(inviteId: $inviteId) {\n ok\n }\n}"): (typeof documents)["mutation DeleteOrgInvite($inviteId: ID!) {\n deleteInvitation(inviteId: $inviteId) {\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 RemoveMember($memberId: ID!) {\n deleteOrganisationMember(memberId: $memberId) {\n ok\n }\n}"): (typeof documents)["mutation RemoveMember($memberId: ID!) {\n deleteOrganisationMember(memberId: $memberId) {\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 InviteMember($orgId: ID!, $email: String!, $apps: [String], $role: String) {\n inviteOrganisationMember(orgId: $orgId, email: $email, apps: $apps, role: $role) {\n invite {\n id\n }\n }\n}"): (typeof documents)["mutation InviteMember($orgId: ID!, $email: String!, $apps: [String], $role: String) {\n inviteOrganisationMember(orgId: $orgId, email: $email, apps: $apps, role: $role) {\n invite {\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 UpdateMemberRole($memberId: ID!, $role: String!) {\n updateOrganisationMemberRole(memberId: $memberId, role: $role) {\n orgMember {\n id\n role\n }\n }\n}"): (typeof documents)["mutation UpdateMemberRole($memberId: ID!, $role: String!) {\n updateOrganisationMemberRole(memberId: $memberId, role: $role) {\n orgMember {\n id\n role\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 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}"): (typeof documents)["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}"];
+/**
+ * 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 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}"): (typeof 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}"];
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function graphql(source: "mutation RevokeUserToken($tokenId: ID!) {\n deleteUserToken(tokenId: $tokenId) {\n ok\n }\n}"): (typeof documents)["mutation RevokeUserToken($tokenId: ID!) {\n deleteUserToken(tokenId: $tokenId) {\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: "query GetAppMembers($appId: ID!) {\n appUsers(appId: $appId) {\n id\n identityKey\n email\n fullName\n avatarUrl\n createdAt\n role\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 }\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 GetAppActivityChart($appId: ID!, $period: TimeRange) {\n appActivityChart(appId: $appId, period: $period) {\n index\n date\n data\n }\n}"): (typeof documents)["query GetAppActivityChart($appId: ID!, $period: TimeRange) {\n appActivityChart(appId: $appId, period: $period) {\n index\n date\n data\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 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 }\n}"): (typeof documents)["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 }\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 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}"): (typeof documents)["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}"];
+/**
+ * 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 }\n}"): (typeof documents)["query GetApps($organisationId: ID!, $appId: ID!) {\n apps(organisationId: $organisationId, appId: $appId) {\n id\n name\n identityKey\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 GetOrganisations {\n organisations {\n id\n name\n identityKey\n createdAt\n plan\n role\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 role\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.
+ */
+export function graphql(source: "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}"): (typeof documents)["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}"];
+/**
+ * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
+ */
+export function graphql(source: "query GetOrganisationAdminsAndSelf($organisationId: ID!) {\n organisationAdminsAndSelf(organisationId: $organisationId) {\n id\n role\n identityKey\n self\n }\n}"): (typeof documents)["query GetOrganisationAdminsAndSelf($organisationId: ID!) {\n organisationAdminsAndSelf(organisationId: $organisationId) {\n id\n role\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 GetOrganisationMembers($organisationId: ID!, $role: [String]) {\n organisationMembers(organisationId: $organisationId, role: $role) {\n id\n role\n identityKey\n email\n fullName\n avatarUrl\n createdAt\n self\n }\n}"): (typeof documents)["query GetOrganisationMembers($organisationId: ID!, $role: [String]) {\n organisationMembers(organisationId: $organisationId, role: $role) {\n id\n role\n identityKey\n email\n fullName\n avatarUrl\n createdAt\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 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}"): (typeof documents)["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}"];
+/**
+ * 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 }\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 }\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 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 eventType\n environment {\n id\n envType\n name\n }\n secret {\n id\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 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 eventType\n environment {\n id\n envType\n name\n }\n secret {\n id\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.
+ */
+export function graphql(source: "query GetEnvironmentKey($envId: ID!, $appId: ID!) {\n environmentKeys(environmentId: $envId, appId: $appId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n}"): (typeof documents)["query GetEnvironmentKey($envId: ID!, $appId: ID!) {\n environmentKeys(environmentId: $envId, appId: $appId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\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 GetEnvironmentTokens($envId: ID!) {\n environmentTokens(environmentId: $envId) {\n id\n name\n wrappedKeyShare\n createdAt\n }\n}"): (typeof documents)["query GetEnvironmentTokens($envId: ID!) {\n environmentTokens(environmentId: $envId) {\n id\n name\n wrappedKeyShare\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 GetEnvSecretsKV($envId: ID!) {\n secrets(envId: $envId) {\n id\n key\n value\n }\n environmentKeys(environmentId: $envId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n}"): (typeof documents)["query GetEnvSecretsKV($envId: ID!) {\n secrets(envId: $envId) {\n id\n key\n value\n }\n environmentKeys(environmentId: $envId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\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 GetSecretTags($orgId: ID!) {\n secretTags(orgId: $orgId) {\n id\n name\n color\n }\n}"): (typeof documents)["query GetSecretTags($orgId: ID!) {\n secretTags(orgId: $orgId) {\n id\n name\n color\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 GetSecrets($appId: ID!, $envId: ID!) {\n secrets(envId: $envId) {\n id\n key\n value\n tags {\n id\n name\n color\n }\n comment\n createdAt\n history {\n id\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 eventType\n }\n }\n appEnvironments(appId: $appId, environmentId: $envId) {\n id\n name\n envType\n identityKey\n }\n environmentKeys(appId: $appId, environmentId: $envId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\n }\n}"): (typeof documents)["query GetSecrets($appId: ID!, $envId: ID!) {\n secrets(envId: $envId) {\n id\n key\n value\n tags {\n id\n name\n color\n }\n comment\n createdAt\n history {\n id\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 eventType\n }\n }\n appEnvironments(appId: $appId, environmentId: $envId) {\n id\n name\n envType\n identityKey\n }\n environmentKeys(appId: $appId, environmentId: $envId) {\n id\n identityKey\n wrappedSeed\n wrappedSalt\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 }\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 }\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 GetUserTokens($organisationId: ID!) {\n userTokens(organisationId: $organisationId) {\n id\n name\n wrappedKeyShare\n createdAt\n expiresAt\n }\n}"): (typeof documents)["query GetUserTokens($organisationId: ID!) {\n userTokens(organisationId: $organisationId) {\n id\n name\n wrappedKeyShare\n createdAt\n expiresAt\n }\n}"];
export function graphql(source: string) {
- return (documents as any)[source] ?? {}
+ return (documents as any)[source] ?? {};
}
-export type DocumentType> =
- TDocumentNode extends DocumentNode ? TType : never
+export type DocumentType> = TDocumentNode extends DocumentNode< infer TType, any> ? TType : never;
\ No newline at end of file
diff --git a/frontend/apollo/graphql.ts b/frontend/apollo/graphql.ts
index ac1822861..a6b000c98 100644
--- a/frontend/apollo/graphql.ts
+++ b/frontend/apollo/graphql.ts
@@ -1,28 +1,64 @@
/* eslint-disable */
-export type Maybe = T | null
-export type InputMaybe = Maybe
-export type Exact = { [K in keyof T]: T[K] }
-export type MakeOptional = Omit & { [SubKey in K]?: Maybe }
-export type MakeMaybe = Omit & { [SubKey in K]: Maybe }
+import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core';
+export type Maybe = T | null;
+export type InputMaybe = Maybe;
+export type Exact = { [K in keyof T]: T[K] };
+export type MakeOptional = Omit & { [SubKey in K]?: Maybe };
+export type MakeMaybe = Omit & { [SubKey in K]: Maybe };
/** All built-in and custom scalars, mapped to their actual values */
export type Scalars = {
- ID: string
- String: string
- Boolean: boolean
- Int: number
- Float: number
+ ID: string;
+ String: string;
+ Boolean: boolean;
+ Int: number;
+ Float: number;
/**
* 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.
*/
- BigInt: any
+ BigInt: any;
/**
* The `DateTime` scalar type represents a DateTime
* value as specified by
* [iso8601](https://en.wikipedia.org/wiki/ISO_8601).
*/
- DateTime: any
+ DateTime: any;
+};
+
+export type AddAppMemberMutation = {
+ __typename?: 'AddAppMemberMutation';
+ app?: Maybe;
+};
+
+/** An enumeration. */
+export enum ApiEnvironmentEnvTypeChoices {
+ /** Development */
+ Dev = 'DEV',
+ /** Production */
+ Prod = 'PROD',
+ /** Staging */
+ Staging = 'STAGING'
+}
+
+/** An enumeration. */
+export enum ApiOrganisationMemberInviteRoleChoices {
+ /** Admin */
+ Admin = 'ADMIN',
+ /** Developer */
+ Dev = 'DEV',
+ /** Owner */
+ Owner = 'OWNER'
+}
+
+/** An enumeration. */
+export enum ApiOrganisationMemberRoleChoices {
+ /** Admin */
+ Admin = 'ADMIN',
+ /** Developer */
+ Dev = 'DEV',
+ /** Owner */
+ Owner = 'OWNER'
}
/** An enumeration. */
@@ -32,145 +68,681 @@ export enum ApiOrganisationPlanChoices {
/** Free */
Fr = 'FR',
/** Pro */
- Pr = 'PR',
+ Pr = 'PR'
}
-export type AppType = {
- __typename?: 'AppType'
- appSeed: Scalars['String']
- appToken: Scalars['String']
- appVersion: Scalars['Int']
- createdAt?: Maybe
- id: Scalars['String']
- identityKey: Scalars['String']
- name: Scalars['String']
- wrappedKeyShare: Scalars['String']
+/** An enumeration. */
+export enum ApiSecretEventEventTypeChoices {
+ /** Create */
+ C = 'C',
+ /** Delete */
+ D = 'D',
+ /** Read */
+ R = 'R',
+ /** Update */
+ U = 'U'
}
+export type AppType = {
+ __typename?: 'AppType';
+ appSeed: Scalars['String'];
+ appToken: Scalars['String'];
+ appVersion: Scalars['Int'];
+ createdAt?: Maybe;
+ id: Scalars['String'];
+ identityKey: Scalars['String'];
+ name: Scalars['String'];
+ wrappedKeyShare: Scalars['String'];
+};
+
export type ChartDataPointType = {
- __typename?: 'ChartDataPointType'
- data?: Maybe
- date?: Maybe
- index?: Maybe
-}
+ __typename?: 'ChartDataPointType';
+ data?: Maybe;
+ date?: Maybe;
+ index?: Maybe;
+};
export type CreateAppMutation = {
- __typename?: 'CreateAppMutation'
- app?: Maybe
-}
+ __typename?: 'CreateAppMutation';
+ app?: Maybe;
+};
+
+export type CreateEnvironmentKeyMutation = {
+ __typename?: 'CreateEnvironmentKeyMutation';
+ environmentKey?: Maybe;
+};
+
+export type CreateEnvironmentMutation = {
+ __typename?: 'CreateEnvironmentMutation';
+ environment?: Maybe;
+};
+
+export type CreateEnvironmentTokenMutation = {
+ __typename?: 'CreateEnvironmentTokenMutation';
+ environmentToken?: Maybe;
+};
+
+export type CreateOrganisationMemberMutation = {
+ __typename?: 'CreateOrganisationMemberMutation';
+ orgMember?: Maybe;
+};
export type CreateOrganisationMutation = {
- __typename?: 'CreateOrganisationMutation'
- organisation?: Maybe
-}
+ __typename?: 'CreateOrganisationMutation';
+ organisation?: Maybe;
+};
+
+export type CreateSecretFolderMutation = {
+ __typename?: 'CreateSecretFolderMutation';
+ folder?: Maybe;
+};
+
+export type CreateSecretMutation = {
+ __typename?: 'CreateSecretMutation';
+ secret?: Maybe;
+};
+
+export type CreateSecretTagMutation = {
+ __typename?: 'CreateSecretTagMutation';
+ tag?: Maybe;
+};
+
+export type CreateServiceTokenMutation = {
+ __typename?: 'CreateServiceTokenMutation';
+ serviceToken?: Maybe;
+};
+
+export type CreateUserTokenMutation = {
+ __typename?: 'CreateUserTokenMutation';
+ ok?: Maybe;
+ userToken?: Maybe;
+};
export type DeleteAppMutation = {
- __typename?: 'DeleteAppMutation'
- app?: Maybe
-}
+ __typename?: 'DeleteAppMutation';
+ app?: Maybe;
+};
+
+export type DeleteInviteMutation = {
+ __typename?: 'DeleteInviteMutation';
+ ok?: Maybe;
+};
+
+export type DeleteOrganisationMemberMutation = {
+ __typename?: 'DeleteOrganisationMemberMutation';
+ ok?: Maybe;
+};
+
+export type DeleteSecretMutation = {
+ __typename?: 'DeleteSecretMutation';
+ secret?: Maybe;
+};
+
+export type DeleteServiceTokenMutation = {
+ __typename?: 'DeleteServiceTokenMutation';
+ ok?: Maybe;
+};
+
+export type DeleteUserTokenMutation = {
+ __typename?: 'DeleteUserTokenMutation';
+ ok?: Maybe;
+};
+
+export type EditSecretMutation = {
+ __typename?: 'EditSecretMutation';
+ secret?: Maybe;
+};
+
+export type EnvironmentInput = {
+ appId: Scalars['ID'];
+ envType: Scalars['String'];
+ identityKey: Scalars['String'];
+ name: Scalars['String'];
+ wrappedSalt: Scalars['String'];
+ wrappedSeed: Scalars['String'];
+};
+
+export type EnvironmentKeyInput = {
+ envId: Scalars['ID'];
+ identityKey: Scalars['String'];
+ userId?: InputMaybe;
+ wrappedSalt: Scalars['String'];
+ wrappedSeed: Scalars['String'];
+};
+
+export type EnvironmentKeyType = {
+ __typename?: 'EnvironmentKeyType';
+ createdAt?: Maybe;
+ environment: EnvironmentType;
+ id: Scalars['String'];
+ identityKey: Scalars['String'];
+ updatedAt: Scalars['DateTime'];
+ wrappedSalt: Scalars['String'];
+ wrappedSeed: Scalars['String'];
+};
+
+export type EnvironmentTokenType = {
+ __typename?: 'EnvironmentTokenType';
+ createdAt?: Maybe;
+ id: Scalars['String'];
+ identityKey: Scalars['String'];
+ name: Scalars['String'];
+ token: Scalars['String'];
+ updatedAt: Scalars['DateTime'];
+ wrappedKeyShare: Scalars['String'];
+};
+
+export type EnvironmentType = {
+ __typename?: 'EnvironmentType';
+ createdAt?: Maybe;
+ envType: ApiEnvironmentEnvTypeChoices;
+ id: Scalars['String'];
+ identityKey: Scalars['String'];
+ name: Scalars['String'];
+ updatedAt: Scalars['DateTime'];
+ wrappedSalt: Scalars['String'];
+ wrappedSeed: Scalars['String'];
+};
+
+export type InviteOrganisationMemberMutation = {
+ __typename?: 'InviteOrganisationMemberMutation';
+ invite?: Maybe;
+};
export type KmsLogType = Node & {
- __typename?: 'KMSLogType'
- appId?: Maybe
- asn?: Maybe
- city?: Maybe
- country?: Maybe
- edgeLocation?: Maybe
- eventType?: Maybe
- id: Scalars['ID']
- ipAddress?: Maybe
- isp?: Maybe
- latitude?: Maybe
- longitude?: Maybe
- phSize?: Maybe
- phaseNode?: Maybe
- timestamp?: Maybe
-}
+ __typename?: 'KMSLogType';
+ appId?: Maybe;
+ asn?: Maybe;
+ city?: Maybe;
+ country?: Maybe;
+ edgeLocation?: Maybe;
+ eventType?: Maybe;
+ id: Scalars['ID'];
+ ipAddress?: Maybe;
+ isp?: Maybe;
+ latitude?: Maybe;
+ longitude?: Maybe;
+ phSize?: Maybe;
+ phaseNode?: Maybe;
+ timestamp?: Maybe;
+};
+
+export type LogsResponseType = {
+ __typename?: 'LogsResponseType';
+ kms?: Maybe>>;
+ secrets?: Maybe>>;
+};
export type Mutation = {
- __typename?: 'Mutation'
- createApp?: Maybe
- createOrganisation?: Maybe
- deleteApp?: Maybe
- rotateAppKeys?: Maybe
-}
+ __typename?: 'Mutation';
+ addAppMember?: Maybe;
+ createApp?: Maybe;
+ createEnvironment?: Maybe;
+ createEnvironmentKey?: Maybe;
+ createEnvironmentToken?: Maybe;
+ createOrganisation?: Maybe;
+ createOrganisationMember?: Maybe;
+ createSecret?: Maybe;
+ createSecretFolder?: Maybe;
+ createSecretTag?: Maybe;
+ createServiceToken?: Maybe;
+ createUserToken?: Maybe;
+ deleteApp?: Maybe;
+ deleteInvitation?: Maybe;
+ deleteOrganisationMember?: Maybe;
+ deleteSecret?: Maybe;
+ deleteServiceToken?: Maybe;
+ deleteUserToken?: Maybe;
+ editSecret?: Maybe;
+ inviteOrganisationMember?: Maybe;
+ readSecret?: Maybe;
+ removeAppMember?: Maybe;
+ rotateAppKeys?: Maybe;
+ updateMemberEnvironmentScope?: Maybe;
+ updateMemberWrappedSecrets?: Maybe;
+ updateOrganisationMemberRole?: Maybe;
+};
+
+
+export type MutationAddAppMemberArgs = {
+ appId?: InputMaybe;
+ envKeys?: InputMaybe>>;
+ memberId?: InputMaybe;
+};
+
export type MutationCreateAppArgs = {
- appSeed: Scalars['String']
- appToken: Scalars['String']
- appVersion: Scalars['Int']
- id: Scalars['ID']
- identityKey: Scalars['String']
- name: Scalars['String']
- organisationId: Scalars['ID']
- wrappedKeyShare: Scalars['String']
-}
+ appSeed: Scalars['String'];
+ appToken: Scalars['String'];
+ appVersion: Scalars['Int'];
+ id: Scalars['ID'];
+ identityKey: Scalars['String'];
+ name: Scalars['String'];
+ organisationId: Scalars['ID'];
+ wrappedKeyShare: Scalars['String'];
+};
+
+
+export type MutationCreateEnvironmentArgs = {
+ adminKeys?: InputMaybe>>;
+ environmentData: EnvironmentInput;
+};
+
+
+export type MutationCreateEnvironmentKeyArgs = {
+ envId: Scalars['ID'];
+ identityKey: Scalars['String'];
+ userId?: InputMaybe;
+ wrappedSalt: Scalars['String'];
+ wrappedSeed: Scalars['String'];
+};
+
+
+export type MutationCreateEnvironmentTokenArgs = {
+ envId: Scalars['ID'];
+ identityKey: Scalars['String'];
+ name: Scalars['String'];
+ token: Scalars['String'];
+ wrappedKeyShare: Scalars['String'];
+};
+
export type MutationCreateOrganisationArgs = {
- id: Scalars['ID']
- identityKey: Scalars['String']
- name: Scalars['String']
-}
+ id: Scalars['ID'];
+ identityKey: Scalars['String'];
+ name: Scalars['String'];
+ wrappedKeyring: Scalars['String'];
+ wrappedRecovery: Scalars['String'];
+};
+
+
+export type MutationCreateOrganisationMemberArgs = {
+ identityKey: Scalars['String'];
+ inviteId: Scalars['ID'];
+ orgId: Scalars['ID'];
+ wrappedKeyring?: InputMaybe;
+ wrappedRecovery?: InputMaybe;
+};
+
+
+export type MutationCreateSecretArgs = {
+ secretData?: InputMaybe;
+};
+
+
+export type MutationCreateSecretFolderArgs = {
+ envId: Scalars['ID'];
+ id: Scalars['ID'];
+ name: Scalars['String'];
+ parentFolderId?: InputMaybe;
+};
+
+
+export type MutationCreateSecretTagArgs = {
+ color: Scalars['String'];
+ name: Scalars['String'];
+ orgId: Scalars['ID'];
+};
+
+
+export type MutationCreateServiceTokenArgs = {
+ appId: Scalars['ID'];
+ environmentKeys?: InputMaybe>>;
+ expiry?: InputMaybe;
+ identityKey: Scalars['String'];
+ name: Scalars['String'];
+ token: Scalars['String'];
+ wrappedKeyShare: Scalars['String'];
+};
+
+
+export type MutationCreateUserTokenArgs = {
+ expiry?: InputMaybe;
+ identityKey: Scalars['String'];
+ name: Scalars['String'];
+ orgId: Scalars['ID'];
+ token: Scalars['String'];
+ wrappedKeyShare: Scalars['String'];
+};
+
export type MutationDeleteAppArgs = {
- id: Scalars['ID']
-}
+ id: Scalars['ID'];
+};
+
+
+export type MutationDeleteInvitationArgs = {
+ inviteId: Scalars['ID'];
+};
+
+
+export type MutationDeleteOrganisationMemberArgs = {
+ memberId: Scalars['ID'];
+};
+
+
+export type MutationDeleteSecretArgs = {
+ id: Scalars['ID'];
+};
+
+
+export type MutationDeleteServiceTokenArgs = {
+ tokenId: Scalars['ID'];
+};
+
+
+export type MutationDeleteUserTokenArgs = {
+ tokenId: Scalars['ID'];
+};
+
+
+export type MutationEditSecretArgs = {
+ id: Scalars['ID'];
+ secretData?: InputMaybe;
+};
+
+
+export type MutationInviteOrganisationMemberArgs = {
+ apps?: InputMaybe>>;
+ email: Scalars['String'];
+ orgId: Scalars['ID'];
+ role?: InputMaybe;
+};
+
+
+export type MutationReadSecretArgs = {
+ id: Scalars['ID'];
+};
+
+
+export type MutationRemoveAppMemberArgs = {
+ appId?: InputMaybe;
+ memberId?: InputMaybe;
+};
+
export type MutationRotateAppKeysArgs = {
- appToken: Scalars['String']
- id: Scalars['ID']
- wrappedKeyShare: Scalars['String']
-}
+ appToken: Scalars['String'];
+ id: Scalars['ID'];
+ wrappedKeyShare: Scalars['String'];
+};
+
+
+export type MutationUpdateMemberEnvironmentScopeArgs = {
+ appId?: InputMaybe;
+ envKeys?: InputMaybe>>;
+ memberId?: InputMaybe;
+};
+
+
+export type MutationUpdateMemberWrappedSecretsArgs = {
+ orgId: Scalars['ID'];
+ wrappedKeyring: Scalars['String'];
+ wrappedRecovery: Scalars['String'];
+};
+
+
+export type MutationUpdateOrganisationMemberRoleArgs = {
+ memberId: Scalars['ID'];
+ role: Scalars['String'];
+};
/** An object with an ID */
export type Node = {
/** The ID of the object */
- id: Scalars['ID']
-}
+ id: Scalars['ID'];
+};
+
+export type OrganisationMemberInviteType = {
+ __typename?: 'OrganisationMemberInviteType';
+ apps: Array;
+ createdAt?: Maybe;
+ expiresAt: Scalars['DateTime'];
+ id: Scalars['String'];
+ invitedBy: OrganisationMemberType;
+ inviteeEmail: Scalars['String'];
+ organisation: OrganisationType;
+ role: ApiOrganisationMemberInviteRoleChoices;
+ updatedAt: Scalars['DateTime'];
+ valid: Scalars['Boolean'];
+};
+
+export type OrganisationMemberType = {
+ __typename?: 'OrganisationMemberType';
+ avatarUrl?: Maybe;
+ createdAt?: Maybe;
+ email?: Maybe;
+ fullName?: Maybe;
+ id: Scalars['String'];
+ identityKey?: Maybe;
+ role: ApiOrganisationMemberRoleChoices;
+ self?: Maybe;
+ updatedAt: Scalars['DateTime'];
+ username?: Maybe;
+ wrappedKeyring: Scalars['String'];
+};
export type OrganisationType = {
- __typename?: 'OrganisationType'
- createdAt?: Maybe
- id: Scalars['String']
- identityKey: Scalars['String']
- name: Scalars['String']
- plan: ApiOrganisationPlanChoices
-}
+ __typename?: 'OrganisationType';
+ createdAt?: Maybe;
+ id: Scalars['String'];
+ identityKey: Scalars['String'];
+ keyring?: Maybe;
+ memberId?: Maybe;
+ name: Scalars['String'];
+ plan: ApiOrganisationPlanChoices;
+ recovery?: Maybe;
+ role?: Maybe;
+};
export type Query = {
- __typename?: 'Query'
- appActivityChart?: Maybe>>
- apps?: Maybe>>
- logs?: Maybe>>
- logsCount?: Maybe
- organisations?: Maybe>>
-}
+ __typename?: 'Query';
+ appActivityChart?: Maybe>>;
+ appEnvironments?: Maybe>>;
+ appUsers?: Maybe>>;
+ apps?: Maybe>>;
+ environmentKeys?: Maybe>>;
+ environmentTokens?: Maybe>>;
+ kmsLogsCount?: Maybe;
+ logs?: Maybe;
+ organisationAdminsAndSelf?: Maybe>>;
+ organisationInvites?: Maybe>>;
+ organisationMembers?: Maybe>>;
+ organisations?: Maybe>>;
+ secretHistory?: Maybe>>;
+ secretTags?: Maybe>>;
+ secrets?: Maybe>>;
+ secretsLogsCount?: Maybe;
+ serviceTokens?: Maybe>>;
+ userTokens?: Maybe>>;
+ validateInvite?: Maybe;
+};
+
export type QueryAppActivityChartArgs = {
- appId?: InputMaybe
- period?: InputMaybe
-}
+ appId?: InputMaybe;
+ period?: InputMaybe;
+};
+
+
+export type QueryAppEnvironmentsArgs = {
+ appId?: InputMaybe;
+ environmentId?: InputMaybe;
+ memberId?: InputMaybe;
+};
+
+
+export type QueryAppUsersArgs = {
+ appId?: InputMaybe;
+};
+
export type QueryAppsArgs = {
- appId?: InputMaybe
- organisationId?: InputMaybe
-}
+ appId?: InputMaybe;
+ organisationId?: InputMaybe;
+};
+
+
+export type QueryEnvironmentKeysArgs = {
+ appId?: InputMaybe;
+ environmentId?: InputMaybe;
+ memberId?: InputMaybe;
+};
+
+
+export type QueryEnvironmentTokensArgs = {
+ environmentId?: InputMaybe;
+};
+
+
+export type QueryKmsLogsCountArgs = {
+ appId?: InputMaybe;
+ thisMonth?: InputMaybe;
+};
+
export type QueryLogsArgs = {
- appId?: InputMaybe
- end?: InputMaybe
- start?: InputMaybe
-}
+ appId?: InputMaybe;
+ end?: InputMaybe;
+ start?: InputMaybe;
+};
+
+
+export type QueryOrganisationAdminsAndSelfArgs = {
+ organisationId?: InputMaybe;
+};
+
+
+export type QueryOrganisationInvitesArgs = {
+ orgId?: InputMaybe;
+};
+
+
+export type QueryOrganisationMembersArgs = {
+ organisationId?: InputMaybe;
+ role?: InputMaybe>>;
+ userId?: InputMaybe;
+};
+
+
+export type QuerySecretHistoryArgs = {
+ secretId?: InputMaybe;
+};
+
+
+export type QuerySecretTagsArgs = {
+ orgId?: InputMaybe;
+};
-export type QueryLogsCountArgs = {
- appId?: InputMaybe
- thisMonth?: InputMaybe
-}
+
+export type QuerySecretsArgs = {
+ envId?: InputMaybe;
+};
+
+
+export type QuerySecretsLogsCountArgs = {
+ appId?: InputMaybe;
+};
+
+
+export type QueryServiceTokensArgs = {
+ appId?: InputMaybe;
+};
+
+
+export type QueryUserTokensArgs = {
+ organisationId?: InputMaybe;
+};
+
+
+export type QueryValidateInviteArgs = {
+ inviteId?: InputMaybe;
+};
+
+export type ReadSecretMutation = {
+ __typename?: 'ReadSecretMutation';
+ ok?: Maybe;
+};
+
+export type RemoveAppMemberMutation = {
+ __typename?: 'RemoveAppMemberMutation';
+ app?: Maybe;
+};
export type RotateAppKeysMutation = {
- __typename?: 'RotateAppKeysMutation'
- app?: Maybe
-}
+ __typename?: 'RotateAppKeysMutation';
+ app?: Maybe;
+};
+
+export type SecretEventType = {
+ __typename?: 'SecretEventType';
+ comment: Scalars['String'];
+ environment: EnvironmentType;
+ eventType: ApiSecretEventEventTypeChoices;
+ id: Scalars['String'];
+ ipAddress?: Maybe;
+ key: Scalars['String'];
+ secret: SecretType;
+ tags: Array;
+ timestamp: Scalars['DateTime'];
+ user?: Maybe;
+ userAgent?: Maybe;
+ value: Scalars['String'];
+ version: Scalars['Int'];
+};
+
+export type SecretFolderType = {
+ __typename?: 'SecretFolderType';
+ createdAt?: Maybe;
+ id: Scalars['String'];
+ name: Scalars['String'];
+ updatedAt: Scalars['DateTime'];
+};
+
+export type SecretInput = {
+ comment?: InputMaybe;
+ envId?: InputMaybe;
+ folderId?: InputMaybe;
+ key: Scalars['String'];
+ keyDigest: Scalars['String'];
+ tags?: InputMaybe>>;
+ value: Scalars['String'];
+};
+
+export type SecretTagType = {
+ __typename?: 'SecretTagType';
+ color: Scalars['String'];
+ id: Scalars['String'];
+ name: Scalars['String'];
+};
+
+export type SecretType = {
+ __typename?: 'SecretType';
+ comment: Scalars['String'];
+ createdAt?: Maybe;
+ folder?: Maybe;
+ history?: Maybe>>;
+ id: Scalars['String'];
+ key: Scalars['String'];
+ tags: Array;
+ updatedAt: Scalars['DateTime'];
+ value: Scalars['String'];
+ version: Scalars['Int'];
+};
+
+export type ServiceTokenType = {
+ __typename?: 'ServiceTokenType';
+ createdAt?: Maybe;
+ createdBy?: Maybe;
+ expiresAt?: Maybe;
+ id: Scalars['String'];
+ identityKey: Scalars['String'];
+ keys: Array;
+ name: Scalars['String'];
+ token: Scalars['String'];
+ updatedAt: Scalars['DateTime'];
+ wrappedKeyShare: Scalars['String'];
+};
/** An enumeration. */
export enum TimeRange {
@@ -179,5 +751,458 @@ export enum TimeRange {
Hour = 'HOUR',
Month = 'MONTH',
Week = 'WEEK',
- Year = 'YEAR',
+ Year = 'YEAR'
}
+
+export type UpdateMemberEnvScopeMutation = {
+ __typename?: 'UpdateMemberEnvScopeMutation';
+ app?: Maybe;
+};
+
+export type UpdateOrganisationMemberRole = {
+ __typename?: 'UpdateOrganisationMemberRole';
+ orgMember?: Maybe;
+};
+
+export type UpdateUserWrappedSecretsMutation = {
+ __typename?: 'UpdateUserWrappedSecretsMutation';
+ orgMember?: Maybe;
+};
+
+export type UserTokenType = {
+ __typename?: 'UserTokenType';
+ createdAt?: Maybe;
+ expiresAt?: Maybe;
+ id: Scalars['String'];
+ identityKey: Scalars['String'];
+ name: Scalars['String'];
+ token: Scalars['String'];
+ updatedAt: Scalars['DateTime'];
+ wrappedKeyShare: Scalars['String'];
+};
+
+export type AddMemberToAppMutationVariables = Exact<{
+ memberId: Scalars['ID'];
+ appId: Scalars['ID'];
+ envKeys?: InputMaybe> | InputMaybe>;
+}>;
+
+
+export type AddMemberToAppMutation = { __typename?: 'Mutation', addAppMember?: { __typename?: 'AddAppMemberMutation', app?: { __typename?: 'AppType', id: string } | null } | null };
+
+export type RemoveMemberFromAppMutationVariables = Exact<{
+ memberId: Scalars['ID'];
+ appId: Scalars['ID'];
+}>;
+
+
+export type RemoveMemberFromAppMutation = { __typename?: 'Mutation', removeAppMember?: { __typename?: 'RemoveAppMemberMutation', app?: { __typename?: 'AppType', id: string } | null } | null };
+
+export type UpdateEnvScopeMutationVariables = Exact<{
+ memberId: Scalars['ID'];
+ appId: Scalars['ID'];
+ envKeys?: InputMaybe> | InputMaybe>;
+}>;
+
+
+export type UpdateEnvScopeMutation = { __typename?: 'Mutation', updateMemberEnvironmentScope?: { __typename?: 'UpdateMemberEnvScopeMutation', app?: { __typename?: 'AppType', id: string } | null } | null };
+
+export type CreateApplicationMutationVariables = Exact<{
+ id: Scalars['ID'];
+ organisationId: Scalars['ID'];
+ name: Scalars['String'];
+ identityKey: Scalars['String'];
+ appToken: Scalars['String'];
+ appSeed: Scalars['String'];
+ wrappedKeyShare: Scalars['String'];
+ appVersion: Scalars['Int'];
+}>;
+
+
+export type CreateApplicationMutation = { __typename?: 'Mutation', createApp?: { __typename?: 'CreateAppMutation', app?: { __typename?: 'AppType', id: string, name: string, identityKey: string } | null } | null };
+
+export type CreateOrgMutationVariables = Exact<{
+ id: Scalars['ID'];
+ name: Scalars['String'];
+ identityKey: Scalars['String'];
+ wrappedKeyring: Scalars['String'];
+ wrappedRecovery: Scalars['String'];
+}>;
+
+
+export type CreateOrgMutation = { __typename?: 'Mutation', createOrganisation?: { __typename?: 'CreateOrganisationMutation', organisation?: { __typename?: 'OrganisationType', id: string, name: string, createdAt?: any | null } | null } | null };
+
+export type DeleteApplicationMutationVariables = Exact<{
+ id: Scalars['ID'];
+}>;
+
+
+export type DeleteApplicationMutation = { __typename?: 'Mutation', deleteApp?: { __typename?: 'DeleteAppMutation', app?: { __typename?: 'AppType', id: string } | null } | null };
+
+export type CreateEnvMutationVariables = Exact<{
+ input: EnvironmentInput;
+}>;
+
+
+export type CreateEnvMutation = { __typename?: 'Mutation', createEnvironment?: { __typename?: 'CreateEnvironmentMutation', environment?: { __typename?: 'EnvironmentType', id: string, name: string, createdAt?: any | null, identityKey: string } | null } | null };
+
+export type CreateEnvKeyMutationVariables = Exact<{
+ envId: Scalars['ID'];
+ userId?: InputMaybe;
+ wrappedSeed: Scalars['String'];
+ wrappedSalt: Scalars['String'];
+ identityKey: Scalars['String'];
+}>;
+
+
+export type CreateEnvKeyMutation = { __typename?: 'Mutation', createEnvironmentKey?: { __typename?: 'CreateEnvironmentKeyMutation', environmentKey?: { __typename?: 'EnvironmentKeyType', id: string, createdAt?: any | null } | null } | null };
+
+export type CreateEnvTokenMutationVariables = Exact<{
+ envId: Scalars['ID'];
+ name: Scalars['String'];
+ identityKey: Scalars['String'];
+ token: Scalars['String'];
+ wrappedKeyShare: Scalars['String'];
+}>;
+
+
+export type CreateEnvTokenMutation = { __typename?: 'Mutation', createEnvironmentToken?: { __typename?: 'CreateEnvironmentTokenMutation', environmentToken?: { __typename?: 'EnvironmentTokenType', id: string, createdAt?: any | null } | null } | null };
+
+export type CreateNewSecretMutationVariables = Exact<{
+ newSecret: SecretInput;
+}>;
+
+
+export type CreateNewSecretMutation = { __typename?: 'Mutation', createSecret?: { __typename?: 'CreateSecretMutation', secret?: { __typename?: 'SecretType', id: string, key: string, value: string, createdAt?: any | null } | null } | null };
+
+export type CreateNewSecretTagMutationVariables = Exact<{
+ orgId: Scalars['ID'];
+ name: Scalars['String'];
+ color: Scalars['String'];
+}>;
+
+
+export type CreateNewSecretTagMutation = { __typename?: 'Mutation', createSecretTag?: { __typename?: 'CreateSecretTagMutation', tag?: { __typename?: 'SecretTagType', id: string } | null } | null };
+
+export type CreateNewServiceTokenMutationVariables = Exact<{
+ appId: Scalars['ID'];
+ environmentKeys?: InputMaybe> | InputMaybe>;
+ identityKey: Scalars['String'];
+ token: Scalars['String'];
+ wrappedKeyShare: Scalars['String'];
+ name: Scalars['String'];
+ expiry?: InputMaybe;
+}>;
+
+
+export type CreateNewServiceTokenMutation = { __typename?: 'Mutation', createServiceToken?: { __typename?: 'CreateServiceTokenMutation', serviceToken?: { __typename?: 'ServiceTokenType', id: string, createdAt?: any | null, expiresAt?: any | null } | null } | null };
+
+export type DeleteSecretOpMutationVariables = Exact<{
+ id: Scalars['ID'];
+}>;
+
+
+export type DeleteSecretOpMutation = { __typename?: 'Mutation', deleteSecret?: { __typename?: 'DeleteSecretMutation', secret?: { __typename?: 'SecretType', id: string } | null } | null };
+
+export type RevokeServiceTokenMutationVariables = Exact<{
+ tokenId: Scalars['ID'];
+}>;
+
+
+export type RevokeServiceTokenMutation = { __typename?: 'Mutation', deleteServiceToken?: { __typename?: 'DeleteServiceTokenMutation', ok?: boolean | null } | null };
+
+export type UpdateSecretMutationVariables = Exact<{
+ id: Scalars['ID'];
+ secretData: SecretInput;
+}>;
+
+
+export type UpdateSecretMutation = { __typename?: 'Mutation', editSecret?: { __typename?: 'EditSecretMutation', secret?: { __typename?: 'SecretType', id: string, updatedAt: any } | null } | null };
+
+export type InitAppEnvironmentsMutationVariables = Exact<{
+ devEnv: EnvironmentInput;
+ stagingEnv: EnvironmentInput;
+ prodEnv: EnvironmentInput;
+ devAdminKeys?: InputMaybe> | InputMaybe>;
+ stagAdminKeys?: InputMaybe> | InputMaybe>;
+ prodAdminKeys?: InputMaybe> | InputMaybe