Skip to content

Commit 0819fa8

Browse files
committed
Merge branch 'main' into 52-fix-archlinux-image-script
2 parents 05d55ce + a39ead2 commit 0819fa8

90 files changed

Lines changed: 4271 additions & 691 deletions

Some content is hidden

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

.env.example

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,28 @@
11
# Postgres
2-
POSTGRES_NAME = distribox
3-
POSTGRES_USER = distribox_user
4-
POSTGRES_PORT = 5432
5-
POSTGRES_PASSWORD = distribox_password
2+
POSTGRES_NAME=distribox
3+
POSTGRES_USER=distribox_user
4+
POSTGRES_PORT=5432
5+
POSTGRES_PASSWORD=distribox_password
66
# This should be `database` for docker compose usage, localhost for independant use
7-
POSTGRES_HOST = database
7+
POSTGRES_HOST=database
88

99
# Admin
10-
ADMIN_USERNAME = admin
11-
# You should change this
12-
ADMIN_PASSWORD = admin
10+
ADMIN_USERNAME=admin
11+
# You should change this, we recommend a strong password
12+
ADMIN_PASSWORD=admin
13+
14+
# Ports
15+
VITE_PORT=3000
16+
BACKEND_PORT=8080
1317

14-
# Frontend
15-
VITE_PORT = 3000
1618
# This should be your backend url, By default the backend is deployed on port 8080, if you have a reverse proxy, ensure you have the right address
17-
VITE_API_DOMAIN = http://localhost:8080
19+
VITE_API_DOMAIN=http://localhost:${BACKEND_PORT}
1820

1921
# Backend
2022
# This should be your frontend url, if the application is deployed on https://example.com, the frontend url should be the same. This will be used inside of the CORS
21-
FRONTEND_URL = http://localhost:3000
22-
BACKEND_PORT = 8080
23+
FRONTEND_URL=http://localhost:${VITE_PORT}
24+
# You should make this a strong secret, this is used to encrypt sensitive data
25+
DISTRIBOX_SECRET=secret
26+
# This is the name of the bucket where the images will be uploaded, the public one is called distribox-images
27+
DISTRIBOX_BUCKET_REGISTRY=distribox-images
28+
AWS_REGION=eu-west-3

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,15 @@ docker compose up -d
8383

8484
Once the application started, you will find your application portal at `localhost:3000`. For further use and deployment we recommend applying a reverse proxy for convenience of your users.
8585

86+
## Permissions
87+
88+
Distribox uses a simple permission system to control what each user can see and do.
89+
90+
- Every account has one or more policies.
91+
- Policies grant access to specific areas or actions (for example viewing hosts, listing VMs, or managing users).
92+
- If a policy is missing, the related feature is hidden or access is refused.
93+
- The default admin account has full access.
94+
8695
## Get involved
8796

8897
You're invited to join this project ! Check out the [contributing guide](./CONTRIBUTING.md).

atlas/main.tf

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ resource "aws_s3_bucket_policy" "public_read" {
5454
Principal = "*"
5555
Action = "s3:GetObject"
5656
Resource = "${aws_s3_bucket.images.arn}/*"
57+
},
58+
{
59+
Sid = "PublicListBucket"
60+
Effect = "Allow"
61+
Principal = "*"
62+
Action = "s3:ListBucket"
63+
Resource = aws_s3_bucket.images.arn
5764
}
5865
]
5966
})

backend/Dockerfile

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
FROM python:3.9 as base
1+
FROM python:3.13 as base
22

33
RUN apt-get update && apt-get install -y \
44
qemu-utils \
55
libvirt-dev \
6+
genisoimage \
67
&& rm -rf /var/lib/apt/lists/*
78

89
FROM base as builder
@@ -12,8 +13,8 @@ RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
1213

1314
FROM builder as dev-stage
1415
COPY ./app /code/app
15-
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080", "--reload"]
16+
CMD uvicorn app.main:app --host 0.0.0.0 --port ${BACKEND_PORT} --reload
1617

1718
FROM builder as production-stage
1819
COPY ./app /code/app
19-
CMD ["fastapi", "run", "app/main.py", "--port", "8080"]
20+
CMD fastapi run app/main.py --port ${BACKEND_PORT}
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
#cloud-config
22

3-
# Nom d'hôte (peut aussi être mis dans meta-data, mais fonctionne ici)
43
hostname: distribox
54
fqdn: distribox.local
65

7-
# Configuration des utilisateurs
86
users:
97
- name: user
108
groups: sudo
119
shell: /bin/bash
1210
sudo: ["ALL=(ALL) NOPASSWD:ALL"]
1311

14-
# Définition des mots de passe
1512
chpasswd:
1613
list: |
1714
user:password
@@ -20,8 +17,3 @@ ssh_pwauth: true
2017

2118
package_update: true
2219
package_upgrade: true
23-
# packages:
24-
# - qemu-guest-agent
25-
# - xubuntu-desktop
26-
# - vim
27-
# package_reboot_if_required: false

backend/app/core/config.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import libvirt
2+
import boto3
3+
from botocore import UNSIGNED
4+
from botocore.config import Config
25
from dotenv import load_dotenv
36
from os import getenv
7+
from sqlalchemy import inspect, text
48
from sqlmodel import create_engine, SQLModel
59
from app.telemetry.monitor import SystemMonitor
6-
710
load_dotenv()
811

912

@@ -13,6 +16,10 @@ def get_env_or_default(key: str, default: str) -> str:
1316
return value if value else default
1417

1518

19+
distribox_bucket_registry = get_env_or_default(
20+
"DISTRIBOX_BUCKET_REGISTRY", "distribox-images")
21+
aws_region = get_env_or_default("AWS_REGION", "eu-west-3")
22+
1623
db_name = get_env_or_default("POSTGRES_NAME", "distribox")
1724
db_user = get_env_or_default("POSTGRES_USER", "distribox_user")
1825
db_pass = get_env_or_default("POSTGRES_PASSWORD", "distribox_password")
@@ -22,11 +29,39 @@ def get_env_or_default(key: str, default: str) -> str:
2229
database_url = f"postgresql+psycopg2://{db_user}:{db_pass}@{db_host}:{db_port}/{db_name}"
2330
engine = create_engine(database_url, echo=True)
2431

32+
s3 = boto3.client(
33+
"s3",
34+
config=Config(signature_version=UNSIGNED),
35+
region_name=aws_region
36+
)
37+
2538

2639
def init_db():
2740
"""Initialize database tables."""
2841
SQLModel.metadata.create_all(engine)
2942

43+
with engine.begin() as conn:
44+
inspector = inspect(conn)
45+
if "users" not in inspector.get_table_names():
46+
return
47+
48+
columns = {column["name"] for column in inspector.get_columns("users")}
49+
if "created_by" not in columns:
50+
conn.execute(
51+
text("ALTER TABLE users ADD COLUMN created_by VARCHAR"))
52+
if "last_activity" not in columns:
53+
conn.execute(
54+
text("ALTER TABLE users ADD COLUMN last_activity TIMESTAMP"))
55+
if "password" not in columns:
56+
conn.execute(text("ALTER TABLE users ADD COLUMN password VARCHAR"))
57+
if "policies" not in columns:
58+
conn.execute(
59+
text(
60+
"ALTER TABLE users "
61+
"ADD COLUMN policies JSON NOT NULL DEFAULT '[]'::json"
62+
)
63+
)
64+
3065

3166
class QEMUConfig:
3267
qemu_conn = None

backend/app/core/policies.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
from collections.abc import Iterable
2+
3+
# When this changes, please update the frontend as well here frontend/app/lib/types/policies.ts
4+
DISTRIBOX_ADMIN_POLICY = "distribox:admin"
5+
6+
POLICIES: list[dict[str, str]] = [
7+
{
8+
"policy": DISTRIBOX_ADMIN_POLICY,
9+
"description": "This policy gives full access to the Distribox dashboard application.",
10+
},
11+
{
12+
"policy": "auth:me:get",
13+
"description": "Allows a user to fetch their own authenticated profile.",
14+
},
15+
{
16+
"policy": "auth:changePassword",
17+
"description": "Allows a user to change their own password.",
18+
},
19+
{
20+
"policy": "host:get",
21+
"description": "Allows the user to fetch the host resources.",
22+
},
23+
{
24+
"policy": "images:get",
25+
"description": "Allows the user to fetch images metadata from the registry.",
26+
},
27+
{
28+
"policy": "policies:get",
29+
"description": "Allows the user to fetch policies.",
30+
},
31+
{
32+
"policy": "users:get",
33+
"description": "Allows the user to fetch users.",
34+
},
35+
{
36+
"policy": "users:create",
37+
"description": "Allows the user to create users.",
38+
},
39+
{
40+
"policy": "users:updatePolicies",
41+
"description": "Allows the user to update user policies.",
42+
},
43+
{
44+
"policy": "users:delete",
45+
"description": "Allows the user to delete users.",
46+
},
47+
{
48+
"policy": "users:getPassword",
49+
"description": "Allows the user to fetch user passwords.",
50+
},
51+
{
52+
"policy": "vms:get",
53+
"description": "Allows the user to list virtual machines.",
54+
},
55+
{
56+
"policy": "vms:getById",
57+
"description": "Allows the user to fetch a virtual machine by id.",
58+
},
59+
{
60+
"policy": "vms:create",
61+
"description": "Allows the user to create virtual machines.",
62+
},
63+
{
64+
"policy": "vms:start",
65+
"description": "Allows the user to start virtual machines.",
66+
},
67+
{
68+
"policy": "vms:stop",
69+
"description": "Allows the user to stop virtual machines.",
70+
},
71+
{
72+
"policy": "vms:credentials:create",
73+
"description": "Allows the user to create virtual machine credentials.",
74+
},
75+
{
76+
"policy": "vms:delete",
77+
"description": "Allows the user to remove virtual machines.",
78+
},
79+
{
80+
"policy": "vms:credentials:revoke",
81+
"description": "Allows the user to revoke virtual machine credentials.",
82+
},
83+
{
84+
"policy": "vms:credentials:list",
85+
"description": "Allows the user to list virtual machine credentials.",
86+
},
87+
{
88+
"policy": "vms:credentials:getById",
89+
"description": "Allows the user to fetch a virtual machine credential by id.",
90+
},
91+
]
92+
93+
VALID_POLICIES = {entry["policy"] for entry in POLICIES}
94+
POLICY_BY_NAME = {entry["policy"]: entry for entry in POLICIES}
95+
96+
97+
def normalize_policies(policies: Iterable[str]) -> list[str]:
98+
"""Deduplicate while preserving first-seen order."""
99+
normalized: list[str] = []
100+
seen: set[str] = set()
101+
102+
for policy in policies:
103+
if policy not in seen:
104+
normalized.append(policy)
105+
seen.add(policy)
106+
107+
return normalized
108+
109+
110+
def invalid_policies(policies: Iterable[str]) -> list[str]:
111+
return [policy for policy in policies if policy not in VALID_POLICIES]
112+
113+
114+
def expand_policies(policies: Iterable[str]) -> list[dict[str, str]]:
115+
expanded: list[dict[str, str]] = []
116+
for policy in policies:
117+
policy_entry = POLICY_BY_NAME.get(policy)
118+
if policy_entry is not None:
119+
expanded.append(policy_entry)
120+
else:
121+
expanded.append(
122+
{
123+
"policy": policy,
124+
"description": "Custom policy",
125+
}
126+
)
127+
return expanded

backend/app/core/xml_builder.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ def build_xml(vm_read: VmRead):
88
domain = etree.Element("domain", type="kvm")
99

1010
etree.SubElement(domain, "name").text = str(vm_read.id)
11-
etree.SubElement(domain, "memory", unit="MiB").text = str(vm_read.mem)
11+
# Frontend sends memory in GiB; libvirt XML expects MiB here.
12+
memory_mib = vm_read.mem * 1024
13+
etree.SubElement(domain, "memory", unit="MiB").text = str(memory_mib)
1214
etree.SubElement(domain, "vcpu", placement="static").text = str(
1315
vm_read.vcpus)
1416

0 commit comments

Comments
 (0)