Skip to content

Commit c8450b5

Browse files
Merge branch 'main' into feat--better-titles
2 parents 8272bee + 242dbb9 commit c8450b5

File tree

155 files changed

+11887
-742
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

155 files changed

+11887
-742
lines changed

.vscode/settings.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,5 +32,7 @@
3232
"[typescriptreact]": {
3333
"editor.defaultFormatter": "esbenp.prettier-vscode"
3434
},
35-
"cSpell.words": ["organisation"] // Don't run prettier for files listed in .gitignore
35+
"[typescript]": {
36+
"editor.defaultFormatter": "esbenp.prettier-vscode"
37+
} // Don't run prettier for files listed in .gitignore
3638
}

CONTRIBUTING.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,12 +57,12 @@ Any third-party components incorporated into our code are licensed under the ori
5757

5858
3. Install the dependencies:
5959
```bash
60-
docker compose -f dev-docker-compose.yml build
60+
docker compose --env-file .env.dev -f dev-docker-compose.yml build
6161
```
6262

6363
4. Start the containers in dev mode using:
6464
```bash
65-
docker compose -f dev-docker-compose.yml up
65+
docker compose --env-file .env.dev -f dev-docker-compose.yml up -d
6666
```
6767

6868
5. The Console is now running at <https://localhost> with [HMR (Hot Module Replacement)](https://webpack.js.org/concepts/hot-module-replacement) and a self-signed certificate.

backend/Dockerfile.dev

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.12.1-alpine3.19@sha256:28230397c48cf4e2619822beb834ae7e46ebcd255b8f7cef58eff932fd75d2ce
1+
FROM python:3.12.1-alpine3.19
22

33
ENV PYTHONUNBUFFERED 1
44

backend/api/auth.py

Lines changed: 71 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
get_token_type,
77
token_is_expired_or_deleted,
88
)
9-
from api.models import Environment
9+
from api.models import DynamicSecret, Environment, Secret
1010
from api.utils.access.permissions import (
1111
service_account_can_access_environment,
1212
user_can_access_environment,
@@ -17,6 +17,18 @@
1717
logger = logging.getLogger(__name__)
1818

1919

20+
class ServiceAccountUser:
21+
"""Mock ServiceAccount user"""
22+
23+
def __init__(self, service_account):
24+
self.userId = service_account.id
25+
self.id = service_account.id
26+
self.is_authenticated = True
27+
self.is_active = True
28+
self.username = service_account.name
29+
self.service_account = service_account
30+
31+
2032
class PhaseTokenAuthentication(authentication.BaseAuthentication):
2133
def authenticate(self, request):
2234

@@ -44,34 +56,59 @@ def authenticate(self, request):
4456
if token_is_expired_or_deleted(auth_token):
4557
raise exceptions.AuthenticationFailed("Token expired or deleted")
4658

47-
env_id = request.headers.get("environment")
59+
env = None
4860

49-
# Try resolving env from header
50-
if env_id:
61+
# Try resolving secret_id from header OR query params (supports Secret or DynamicSecret)
62+
secret_id = request.headers.get("secret_id") or request.GET.get("secret_id")
63+
if secret_id:
64+
found = False
5165
try:
52-
env = Environment.objects.get(id=env_id)
53-
except Environment.DoesNotExist:
54-
raise exceptions.AuthenticationFailed("Environment not found")
66+
secret = Secret.objects.get(id=secret_id)
67+
env = secret.environment
68+
found = True
69+
except Secret.DoesNotExist:
70+
pass
71+
if not found:
72+
try:
73+
dyn_secret = DynamicSecret.objects.get(id=secret_id)
74+
env = dyn_secret.environment
75+
found = True
76+
except DynamicSecret.DoesNotExist:
77+
pass
78+
if not found:
79+
raise exceptions.NotFound("Secret not found")
80+
81+
# If env is still None, try resolving from header or query params
82+
if env is None:
83+
env_id = request.headers.get("environment")
84+
# Try resolving env from header
85+
if env_id:
86+
try:
87+
env = Environment.objects.get(id=env_id)
88+
except Environment.DoesNotExist:
89+
raise exceptions.AuthenticationFailed("Environment not found")
5590

56-
# Try resolving env from query params
57-
else:
58-
try:
59-
app_id = request.GET.get("app_id")
60-
env_name = request.GET.get("env")
61-
if not app_id:
62-
raise exceptions.AuthenticationFailed("Missing app_id parameter")
63-
if not env_name:
64-
raise exceptions.AuthenticationFailed("Missing env parameter")
65-
env = Environment.objects.get(app_id=app_id, name__iexact=env_name)
66-
except Environment.DoesNotExist:
67-
# Check if the app exists to give a more specific error
68-
App = apps.get_model("api", "App")
69-
if not App.objects.filter(id=app_id).exists():
70-
raise exceptions.NotFound(f"App with ID {app_id} not found")
71-
else:
72-
raise exceptions.NotFound(
73-
f"Environment '{env_name}' not found in App {app_id}"
74-
)
91+
# Try resolving env from query params
92+
else:
93+
try:
94+
app_id = request.GET.get("app_id")
95+
env_name = request.GET.get("env")
96+
if not app_id:
97+
raise exceptions.AuthenticationFailed(
98+
"Missing app_id parameter"
99+
)
100+
if not env_name:
101+
raise exceptions.AuthenticationFailed("Missing env parameter")
102+
env = Environment.objects.get(app_id=app_id, name__iexact=env_name)
103+
except Environment.DoesNotExist:
104+
# Check if the app exists to give a more specific error
105+
App = apps.get_model("api", "App")
106+
if not App.objects.filter(id=app_id).exists():
107+
raise exceptions.NotFound(f"App with ID {app_id} not found")
108+
else:
109+
raise exceptions.NotFound(
110+
f"Environment '{env_name}' not found in App {app_id}"
111+
)
75112

76113
auth["environment"] = env
77114

@@ -97,8 +134,14 @@ def authenticate(self, request):
97134

98135
try:
99136
service_token = get_service_token(auth_token)
100-
service_account = get_service_account_from_token(auth_token)
101-
user = service_token.created_by.user
137+
service_account = get_service_account_from_token(auth_token)
138+
139+
creator = getattr(service_token, "created_by", None)
140+
if creator:
141+
user = creator.user
142+
else:
143+
user = ServiceAccountUser(service_account)
144+
102145
auth["service_account"] = service_account
103146
auth["service_account_token"] = service_token
104147

backend/api/identity_providers.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
class IdentityProviders:
2+
"""
3+
Configuration for supported identity providers.
4+
Similar to Providers in services.py but specifically for identity authentication.
5+
"""
6+
7+
AWS_IAM = {
8+
"id": "aws_iam",
9+
"name": "AWS IAM",
10+
"description": "Use AWS STS GetCallerIdentity to authenticate.",
11+
"icon_id": "aws", # Maps to ProviderIcon component
12+
"supported": True,
13+
}
14+
15+
# Future identity providers can be added here:
16+
# GitHub OIDC
17+
# "id": "github_oidc",
18+
# "name": "GitHub OIDC",
19+
# "description": "Use GitHub OIDC for authentication.",
20+
# "icon_id": "github",
21+
# "supported": False,
22+
# }
23+
#
24+
# Kubernetes OIDC
25+
# "id": "kubernetes_oidc",
26+
# "name": "Kubernetes OIDC",
27+
# "description": "Use Kubernetes OIDC for authentication.",
28+
# "icon_id": "kubernetes",
29+
# "supported": False,
30+
# }
31+
32+
@classmethod
33+
def get_all_providers(cls):
34+
"""Get all identity providers, including unsupported ones for future roadmap display."""
35+
return [
36+
provider
37+
for provider in cls.__dict__.values()
38+
if isinstance(provider, dict)
39+
]
40+
41+
@classmethod
42+
def get_supported_providers(cls):
43+
"""Get only currently supported identity providers."""
44+
return [
45+
provider
46+
for provider in cls.__dict__.values()
47+
if isinstance(provider, dict) and provider.get("supported", False)
48+
]
49+
50+
@classmethod
51+
def get_provider_config(cls, provider_id):
52+
"""Get configuration for a specific provider by ID."""
53+
for provider in cls.__dict__.values():
54+
if isinstance(provider, dict) and provider["id"] == provider_id:
55+
return provider
56+
raise ValueError(f"Identity provider '{provider_id}' not found")
57+
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Generated by Django 4.2.22 on 2025-08-20 08:11
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
import uuid
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('api', '0105_environmentkey_unique_envkey_user_and_more'),
12+
]
13+
14+
operations = [
15+
migrations.CreateModel(
16+
name='DynamicSecret',
17+
fields=[
18+
('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
19+
('name', models.TextField()),
20+
('description', models.TextField(blank=True)),
21+
('path', models.TextField(default='/')),
22+
('default_ttl', models.DurationField(help_text='Default TTL for leases (must be <= max_ttl).')),
23+
('max_ttl', models.DurationField(help_text='Maximum allowed TTL for leases.')),
24+
('provider', models.CharField(choices=[('aws', 'AWS')], help_text='Which provider this secret is associated with.', max_length=50)),
25+
('config', models.JSONField()),
26+
('created_at', models.DateTimeField(auto_now_add=True, null=True)),
27+
('updated_at', models.DateTimeField(auto_now=True)),
28+
('deleted_at', models.DateTimeField(blank=True, null=True)),
29+
('authentication', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='api.providercredentials')),
30+
('environment', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.environment')),
31+
('folder', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='api.secretfolder')),
32+
],
33+
),
34+
migrations.CreateModel(
35+
name='DynamicSecretLease',
36+
fields=[
37+
('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
38+
('name', models.TextField()),
39+
('description', models.TextField(blank=True)),
40+
('ttl', models.DurationField()),
41+
('status', models.CharField(choices=[('active', 'Active'), ('renewed', 'Renewed'), ('revoked', 'Revoked'), ('expired', 'Expired')], default='active', help_text='Current status of the lease', max_length=50)),
42+
('created_at', models.DateTimeField(auto_now_add=True, null=True)),
43+
('renewed_at', models.DateTimeField(null=True)),
44+
('expires_at', models.DateTimeField(null=True)),
45+
('revoked_at', models.DateTimeField(null=True)),
46+
('deleted_at', models.DateTimeField(null=True)),
47+
('organisation_member', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.organisationmember')),
48+
('secret', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leases', to='api.dynamicsecret')),
49+
('service_account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='api.serviceaccount')),
50+
],
51+
),
52+
migrations.CreateModel(
53+
name='DynamicSecretLeaseEvent',
54+
fields=[
55+
('id', models.TextField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
56+
('lease', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='api.dynamicsecretlease')),
57+
],
58+
),
59+
]
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# Generated by Django 4.2.22 on 2025-08-26 09:16
2+
3+
from django.db import migrations, models
4+
import django.db.models.deletion
5+
import django.utils.timezone
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
('api', '0106_dynamicsecret_dynamicsecretlease_and_more'),
12+
]
13+
14+
operations = [
15+
migrations.AddField(
16+
model_name='dynamicsecret',
17+
name='key_map',
18+
field=models.JSONField(default=list, help_text="Provider-agnostic mapping of keys: [{'id': '<key_id>', 'name': '<key_name>'}, ...]"),
19+
),
20+
migrations.AddField(
21+
model_name='dynamicsecretlease',
22+
name='credentials',
23+
field=models.JSONField(default=list),
24+
),
25+
migrations.AddField(
26+
model_name='dynamicsecretleaseevent',
27+
name='created_at',
28+
field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now),
29+
preserve_default=False,
30+
),
31+
migrations.AddField(
32+
model_name='dynamicsecretleaseevent',
33+
name='event_type',
34+
field=models.CharField(choices=[('created', 'Created'), ('active', 'Active'), ('renewed', 'Renewed'), ('revoked', 'Revoked'), ('expired', 'Expired')], default='created', max_length=50),
35+
),
36+
migrations.AddField(
37+
model_name='dynamicsecretleaseevent',
38+
name='ip_address',
39+
field=models.GenericIPAddressField(blank=True, null=True),
40+
),
41+
migrations.AddField(
42+
model_name='dynamicsecretleaseevent',
43+
name='metadata',
44+
field=models.JSONField(blank=True, default=dict),
45+
),
46+
migrations.AddField(
47+
model_name='dynamicsecretleaseevent',
48+
name='organisation_member',
49+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lease_events', to='api.organisationmember'),
50+
),
51+
migrations.AddField(
52+
model_name='dynamicsecretleaseevent',
53+
name='service_account',
54+
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='lease_events', to='api.serviceaccount'),
55+
),
56+
migrations.AddField(
57+
model_name='dynamicsecretleaseevent',
58+
name='user_agent',
59+
field=models.TextField(blank=True, null=True),
60+
),
61+
migrations.AlterField(
62+
model_name='dynamicsecretlease',
63+
name='status',
64+
field=models.CharField(choices=[('created', 'Created'), ('active', 'Active'), ('renewed', 'Renewed'), ('revoked', 'Revoked'), ('expired', 'Expired')], default='active', help_text='Current status of the lease', max_length=50),
65+
),
66+
migrations.AlterField(
67+
model_name='dynamicsecretleaseevent',
68+
name='id',
69+
field=models.BigAutoField(primary_key=True, serialize=False),
70+
),
71+
migrations.AlterField(
72+
model_name='dynamicsecretleaseevent',
73+
name='lease',
74+
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='events', to='api.dynamicsecretlease'),
75+
),
76+
]
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Generated by Django 4.2.22 on 2025-08-28 08:49
2+
3+
from django.db import migrations, models
4+
import uuid
5+
6+
7+
class Migration(migrations.Migration):
8+
9+
dependencies = [
10+
('api', '0107_dynamicsecret_key_map_dynamicsecretlease_credentials_and_more'),
11+
]
12+
13+
operations = [
14+
migrations.AddField(
15+
model_name='dynamicsecretlease',
16+
name='cleanup_job_id',
17+
field=models.TextField(default=uuid.uuid4),
18+
),
19+
migrations.AlterField(
20+
model_name='dynamicsecretlease',
21+
name='credentials',
22+
field=models.JSONField(default=dict),
23+
),
24+
]
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 4.2.22 on 2025-09-12 05:18
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('api', '0108_dynamicsecretlease_cleanup_job_id_and_more'),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name='dynamicsecret',
15+
name='key_map',
16+
field=models.JSONField(default=list, help_text="Provider-agnostic mapping of keys: [{'id': '<key_id>', 'key_name': '<encrypted_key_name>', 'key_digest': '<key_digest>'}, ...]"),
17+
),
18+
]

0 commit comments

Comments
 (0)