Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 24 additions & 89 deletions clients/python/coflux/__main__.py

Large diffs are not rendered by default.

5 changes: 1 addition & 4 deletions clients/python/coflux/worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,6 @@ class SessionExpiredError(Exception):
class Worker:
def __init__(
self,
project_id: str,
workspace_name: str,
server_host: str,
secure: bool,
Expand All @@ -54,7 +53,6 @@ def __init__(
session_id: str,
targets: dict[str, dict[str, tuple[models.Target, t.Callable]]],
):
self._project_id = project_id
self._workspace_name = workspace_name
self._server_host = server_host
self._secure = secure
Expand Down Expand Up @@ -96,7 +94,6 @@ def _url(self, scheme: str, path: str, params: dict[str, str]) -> str:

def _params(self):
params = {
"project": self._project_id,
"workspace": self._workspace_name,
}
if API_VERSION:
Expand All @@ -112,7 +109,7 @@ async def run(self) -> None:
"""Run the worker. Raises SessionExpiredError if session expires."""
check_server(self._server_host, self._secure)
while True:
print(f"Connecting ({self._server_host}, {self._project_id}, {self._workspace_name})...")
print(f"Connecting ({self._server_host}, {self._workspace_name})...")
scheme = "wss" if self._secure else "ws"
url = self._url(scheme, "worker", self._params())
try:
Expand Down
8 changes: 2 additions & 6 deletions server/lib/coflux/application.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
defmodule Coflux.Application do
use Application

alias Coflux.{Config, Projects, Orchestration, Topics}
alias Coflux.Auth.TokenStore
alias Coflux.{Config, ProjectStore, Orchestration, Topics}

@impl true
def start(_type, _args) do
Expand All @@ -12,15 +11,13 @@ defmodule Coflux.Application do

children =
[
TokenStore,
{Projects, name: Coflux.ProjectsServer},
ProjectStore,
# TODO: separate launch supervisor per project? (and specify max_children?)
{Task.Supervisor, name: Coflux.LauncherSupervisor},
Orchestration.Supervisor,
{Topical, name: Coflux.TopicalRegistry, topics: topics()},
{Coflux.Web, port: port}
]
|> Enum.filter(& &1)

opts = [strategy: :one_for_one, name: Coflux.Supervisor]

Expand All @@ -33,7 +30,6 @@ defmodule Coflux.Application do
defp topics() do
[
Topics.Sessions,
Topics.Projects,
Topics.Workspaces,
Topics.Modules,
Topics.Run,
Expand Down
65 changes: 65 additions & 0 deletions server/lib/coflux/auth.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
defmodule Coflux.Auth do
@moduledoc """
Handles token authentication.

Token access is configured per-project in $COFLUX_DATA_DIR/projects.json:
```json
{
"acme": {
"tokens": ["<sha256-hash-1>", "<sha256-hash-2>"]
},
"demo": {}
}
```

The `tokens` field controls access:
- missing/null: open access (no token required)
- [] (empty array): locked (no valid tokens, access denied)
- ["hash1", ...]: restricted to listed token hashes
"""

alias Coflux.ProjectStore

@doc """
Checks if the given token is authorized for the project.

Returns `:ok` when access is allowed.
Returns `{:error, :unauthorized}` otherwise.
"""
def check(token, project_id) do
case ProjectStore.get_tokens(project_id) do
{:ok, nil} ->
# No token auth configured - open access
:ok

{:ok, []} ->
# Empty tokens list - locked, no valid tokens
{:error, :unauthorized}

{:ok, tokens} ->
# Token list configured - must provide valid token
validate_token(token, tokens)

:error ->
# Project not found (shouldn't happen if validate_project passed)
{:error, :unauthorized}
end
end

defp validate_token(nil, _tokens), do: {:error, :unauthorized}

defp validate_token(token, tokens) do
token_hash = hash_token(token)

if token_hash in tokens do
:ok
else
{:error, :unauthorized}
end
end

defp hash_token(token) do
:crypto.hash(:sha256, token)
|> Base.encode16(case: :lower)
end
end
67 changes: 0 additions & 67 deletions server/lib/coflux/auth/token_store.ex

This file was deleted.

36 changes: 23 additions & 13 deletions server/lib/coflux/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,28 @@ defmodule Coflux.Config do
Caches configuration values read from environment variables at startup.

Uses persistent_term for fast reads without copying.

## Project Configuration

At least one of the following must be configured:

- **COFLUX_PROJECT**: Restricts the server to a single project. All requests
are routed to this project. Supports any access method including IP addresses.

- **COFLUX_BASE_DOMAIN**: Enables subdomain-based project routing. The project
is extracted from the subdomain (e.g., `acme.example.com` → project "acme").
Requires subdomain access - direct IP or base domain access is not allowed.

When both are set, subdomain routing is used but only the configured project
is allowed.
"""

@doc """
Loads all config from environment variables. Call once at startup.
"""
def init do
:persistent_term.put(:coflux_data_dir, parse_data_dir())
:persistent_term.put(:coflux_auth_mode, parse_auth_mode())
:persistent_term.put(:coflux_project, System.get_env("COFLUX_PROJECT"))
:persistent_term.put(:coflux_base_domain, System.get_env("COFLUX_BASE_DOMAIN"))
:persistent_term.put(:coflux_allowed_origins, parse_allowed_origins())
:ok
Expand All @@ -24,14 +38,18 @@ defmodule Coflux.Config do
end

@doc """
Returns the auth mode: `:none` (default) or `:token`.
Returns the configured project name, or nil if not set.

When set, restricts the server to only serve this project.
"""
def auth_mode do
:persistent_term.get(:coflux_auth_mode)
def project do
:persistent_term.get(:coflux_project)
end

@doc """
Returns the base domain for namespace resolution, or nil if not set.
Returns the base domain for subdomain-based routing, or nil if not set.

When set, the project is extracted from the request's subdomain.
"""
def base_domain do
:persistent_term.get(:coflux_base_domain)
Expand All @@ -48,14 +66,6 @@ defmodule Coflux.Config do
System.get_env("COFLUX_DATA_DIR", Path.join(File.cwd!(), "data"))
end

defp parse_auth_mode do
case System.get_env("COFLUX_AUTH_MODE", "none") do
"none" -> :none
"token" -> :token
other -> raise "Invalid COFLUX_AUTH_MODE: #{inspect(other)}. Must be \"none\" or \"token\"."
end
end

@default_allowed_origins ["https://studio.coflux.com"]

defp parse_allowed_origins do
Expand Down
Loading