Remove public internet features and fix upstream source UI (#107)

This commit is contained in:
Mondo Diaz
2026-01-29 13:26:28 -06:00
parent e93e7e7021
commit 82f67539bd
13 changed files with 194 additions and 292 deletions

View File

@@ -6,7 +6,22 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### Changed
- Upstream source connectivity test no longer follows redirects, fixing "Exceeded maximum allowed redirects" error with Artifactory proxies (#107)
- Upstream sources table now has dedicated "Test" column with OK/Error status badges (#107)
- Test runs automatically after saving a new or updated upstream source (#107)
- Error states in upstream sources table are now clickable to show full error details in a modal (#107)
- Source name column no longer wraps text for better table layout (#107)
### Removed
- Removed `is_public` field from upstream sources - all sources are now treated as internal/private (#107)
- Removed `allow_public_internet` (air-gap mode) setting from cache settings - not needed for enterprise proxy use case (#107)
- Removed seeding of public registry URLs (npm-public, pypi-public, maven-central, docker-hub) (#107)
- Removed "Public" badge and checkbox from upstream sources UI (#107)
- Removed "Allow Public Internet" toggle from cache settings UI (#107)
### Added ### Added
- Added `ORCHARD_PURGE_SEED_DATA` environment variable support to stage helm values to remove seed data from long-running deployments (#107)
- Added frontend system projects visual distinction (#105) - Added frontend system projects visual distinction (#105)
- "Cache" badge for system projects in project list - "Cache" badge for system projects in project list
- "System Cache" badge on project detail page - "System Cache" badge on project detail page

View File

@@ -61,8 +61,7 @@ class Settings(BaseSettings):
# Cache settings # Cache settings
cache_encryption_key: str = "" # Fernet key for encrypting upstream credentials (auto-generated if empty) cache_encryption_key: str = "" # Fernet key for encrypting upstream credentials (auto-generated if empty)
# Global cache settings overrides (None = use DB value, True/False = override DB) # Global cache settings override (None = use DB value, True/False = override DB)
cache_allow_public_internet: Optional[bool] = None # Override allow_public_internet (air-gap mode)
cache_auto_create_system_projects: Optional[bool] = None # Override auto_create_system_projects cache_auto_create_system_projects: Optional[bool] = None # Override auto_create_system_projects
# JWT Authentication settings (optional, for external identity providers) # JWT Authentication settings (optional, for external identity providers)
@@ -108,7 +107,6 @@ class EnvUpstreamSource:
url: str, url: str,
source_type: str = "generic", source_type: str = "generic",
enabled: bool = True, enabled: bool = True,
is_public: bool = True,
auth_type: str = "none", auth_type: str = "none",
username: Optional[str] = None, username: Optional[str] = None,
password: Optional[str] = None, password: Optional[str] = None,
@@ -118,7 +116,6 @@ class EnvUpstreamSource:
self.url = url self.url = url
self.source_type = source_type self.source_type = source_type
self.enabled = enabled self.enabled = enabled
self.is_public = is_public
self.auth_type = auth_type self.auth_type = auth_type
self.username = username self.username = username
self.password = password self.password = password
@@ -188,7 +185,6 @@ def parse_upstream_sources_from_env() -> list[EnvUpstreamSource]:
url=url, url=url,
source_type=data.get("TYPE", "generic").lower(), source_type=data.get("TYPE", "generic").lower(),
enabled=parse_bool(data.get("ENABLED"), True), enabled=parse_bool(data.get("ENABLED"), True),
is_public=parse_bool(data.get("IS_PUBLIC"), True),
auth_type=data.get("AUTH_TYPE", "none").lower(), auth_type=data.get("AUTH_TYPE", "none").lower(),
username=data.get("USERNAME"), username=data.get("USERNAME"),
password=data.get("PASSWORD"), password=data.get("PASSWORD"),

View File

@@ -462,7 +462,6 @@ def _run_migrations():
source_type VARCHAR(50) NOT NULL DEFAULT 'generic', source_type VARCHAR(50) NOT NULL DEFAULT 'generic',
url VARCHAR(2048) NOT NULL, url VARCHAR(2048) NOT NULL,
enabled BOOLEAN NOT NULL DEFAULT FALSE, enabled BOOLEAN NOT NULL DEFAULT FALSE,
is_public BOOLEAN NOT NULL DEFAULT TRUE,
auth_type VARCHAR(20) NOT NULL DEFAULT 'none', auth_type VARCHAR(20) NOT NULL DEFAULT 'none',
username VARCHAR(255), username VARCHAR(255),
password_encrypted BYTEA, password_encrypted BYTEA,
@@ -480,7 +479,6 @@ def _run_migrations():
); );
CREATE INDEX IF NOT EXISTS idx_upstream_sources_enabled ON upstream_sources(enabled); CREATE INDEX IF NOT EXISTS idx_upstream_sources_enabled ON upstream_sources(enabled);
CREATE INDEX IF NOT EXISTS idx_upstream_sources_source_type ON upstream_sources(source_type); CREATE INDEX IF NOT EXISTS idx_upstream_sources_source_type ON upstream_sources(source_type);
CREATE INDEX IF NOT EXISTS idx_upstream_sources_is_public ON upstream_sources(is_public);
CREATE INDEX IF NOT EXISTS idx_upstream_sources_priority ON upstream_sources(priority); CREATE INDEX IF NOT EXISTS idx_upstream_sources_priority ON upstream_sources(priority);
""", """,
), ),
@@ -489,14 +487,13 @@ def _run_migrations():
sql=""" sql="""
CREATE TABLE IF NOT EXISTS cache_settings ( CREATE TABLE IF NOT EXISTS cache_settings (
id INTEGER PRIMARY KEY DEFAULT 1, id INTEGER PRIMARY KEY DEFAULT 1,
allow_public_internet BOOLEAN NOT NULL DEFAULT TRUE,
auto_create_system_projects BOOLEAN NOT NULL DEFAULT TRUE, auto_create_system_projects BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
CONSTRAINT check_cache_settings_singleton CHECK (id = 1) CONSTRAINT check_cache_settings_singleton CHECK (id = 1)
); );
INSERT INTO cache_settings (id, allow_public_internet, auto_create_system_projects) INSERT INTO cache_settings (id, auto_create_system_projects)
VALUES (1, TRUE, TRUE) VALUES (1, TRUE)
ON CONFLICT (id) DO NOTHING; ON CONFLICT (id) DO NOTHING;
""", """,
), ),
@@ -522,13 +519,50 @@ def _run_migrations():
Migration( Migration(
name="020_seed_default_upstream_sources", name="020_seed_default_upstream_sources",
sql=""" sql="""
INSERT INTO upstream_sources (id, name, source_type, url, enabled, is_public, auth_type, priority) -- Originally seeded public sources, but these are no longer used.
VALUES -- Migration 023 deletes any previously seeded sources.
(gen_random_uuid(), 'npm-public', 'npm', 'https://registry.npmjs.org', FALSE, TRUE, 'none', 100), -- This migration is now a no-op for fresh installs.
(gen_random_uuid(), 'pypi-public', 'pypi', 'https://pypi.org/simple', FALSE, TRUE, 'none', 100), SELECT 1;
(gen_random_uuid(), 'maven-central', 'maven', 'https://repo1.maven.org/maven2', FALSE, TRUE, 'none', 100), """,
(gen_random_uuid(), 'docker-hub', 'docker', 'https://registry-1.docker.io', FALSE, TRUE, 'none', 100) ),
ON CONFLICT (name) DO NOTHING; Migration(
name="021_remove_is_public_from_upstream_sources",
sql="""
DO $$
BEGIN
-- Drop the index if it exists
DROP INDEX IF EXISTS idx_upstream_sources_is_public;
-- Drop the column if it exists
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'upstream_sources' AND column_name = 'is_public'
) THEN
ALTER TABLE upstream_sources DROP COLUMN is_public;
END IF;
END $$;
""",
),
Migration(
name="022_remove_allow_public_internet_from_cache_settings",
sql="""
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'cache_settings' AND column_name = 'allow_public_internet'
) THEN
ALTER TABLE cache_settings DROP COLUMN allow_public_internet;
END IF;
END $$;
""",
),
Migration(
name="023_delete_seeded_public_sources",
sql="""
-- Delete the seeded public sources that were added by migration 020
DELETE FROM upstream_sources
WHERE name IN ('npm-public', 'pypi-public', 'maven-central', 'docker-hub');
""", """,
), ),
] ]

View File

@@ -667,7 +667,6 @@ class UpstreamSource(Base):
source_type = Column(String(50), default="generic", nullable=False) source_type = Column(String(50), default="generic", nullable=False)
url = Column(String(2048), nullable=False) url = Column(String(2048), nullable=False)
enabled = Column(Boolean, default=False, nullable=False) enabled = Column(Boolean, default=False, nullable=False)
is_public = Column(Boolean, default=True, nullable=False)
auth_type = Column(String(20), default="none", nullable=False) auth_type = Column(String(20), default="none", nullable=False)
username = Column(String(255)) username = Column(String(255))
password_encrypted = Column(LargeBinary) password_encrypted = Column(LargeBinary)
@@ -684,7 +683,6 @@ class UpstreamSource(Base):
__table_args__ = ( __table_args__ = (
Index("idx_upstream_sources_enabled", "enabled"), Index("idx_upstream_sources_enabled", "enabled"),
Index("idx_upstream_sources_source_type", "source_type"), Index("idx_upstream_sources_source_type", "source_type"),
Index("idx_upstream_sources_is_public", "is_public"),
Index("idx_upstream_sources_priority", "priority"), Index("idx_upstream_sources_priority", "priority"),
CheckConstraint( CheckConstraint(
"source_type IN ('npm', 'pypi', 'maven', 'docker', 'helm', 'nuget', 'deb', 'rpm', 'generic')", "source_type IN ('npm', 'pypi', 'maven', 'docker', 'helm', 'nuget', 'deb', 'rpm', 'generic')",
@@ -747,13 +745,12 @@ class UpstreamSource(Base):
class CacheSettings(Base): class CacheSettings(Base):
"""Global cache settings (singleton table). """Global cache settings (singleton table).
Controls behavior of the upstream caching system including air-gap mode. Controls behavior of the upstream caching system.
""" """
__tablename__ = "cache_settings" __tablename__ = "cache_settings"
id = Column(Integer, primary_key=True, default=1) id = Column(Integer, primary_key=True, default=1)
allow_public_internet = Column(Boolean, default=True, nullable=False)
auto_create_system_projects = Column(Boolean, default=True, nullable=False) auto_create_system_projects = Column(Boolean, default=True, nullable=False)
created_at = Column(DateTime(timezone=True), default=datetime.utcnow) created_at = Column(DateTime(timezone=True), default=datetime.utcnow)
updated_at = Column( updated_at = Column(

View File

@@ -7866,7 +7866,6 @@ from .upstream import (
UpstreamTimeoutError, UpstreamTimeoutError,
UpstreamHTTPError, UpstreamHTTPError,
UpstreamSSLError, UpstreamSSLError,
AirGapError,
FileSizeExceededError as UpstreamFileSizeExceededError, FileSizeExceededError as UpstreamFileSizeExceededError,
SourceNotFoundError, SourceNotFoundError,
SourceDisabledError, SourceDisabledError,
@@ -8021,10 +8020,6 @@ def cache_artifact(
- Optionally creates tag in user project - Optionally creates tag in user project
- Records URL mapping for provenance - Records URL mapping for provenance
**Air-Gap Mode:**
When `allow_public_internet` is false, only URLs matching private
(non-public) upstream sources are allowed.
**Example (curl):** **Example (curl):**
```bash ```bash
curl -X POST "http://localhost:8080/api/v1/cache" \\ curl -X POST "http://localhost:8080/api/v1/cache" \\
@@ -8118,8 +8113,6 @@ def cache_artifact(
cache_request.url, cache_request.url,
expected_hash=cache_request.expected_hash, expected_hash=cache_request.expected_hash,
) )
except AirGapError as e:
raise HTTPException(status_code=403, detail=str(e))
except SourceDisabledError as e: except SourceDisabledError as e:
raise HTTPException(status_code=503, detail=str(e)) raise HTTPException(status_code=503, detail=str(e))
except UpstreamHTTPError as e: except UpstreamHTTPError as e:
@@ -8333,7 +8326,6 @@ def _env_source_to_response(env_source) -> UpstreamSourceResponse:
source_type=env_source.source_type, source_type=env_source.source_type,
url=env_source.url, url=env_source.url,
enabled=env_source.enabled, enabled=env_source.enabled,
is_public=env_source.is_public,
auth_type=env_source.auth_type, auth_type=env_source.auth_type,
username=env_source.username, username=env_source.username,
has_password=bool(env_source.password), has_password=bool(env_source.password),
@@ -8417,7 +8409,6 @@ def list_upstream_sources(
source_type=s.source_type, source_type=s.source_type,
url=s.url, url=s.url,
enabled=s.enabled, enabled=s.enabled,
is_public=s.is_public,
auth_type=s.auth_type, auth_type=s.auth_type,
username=s.username, username=s.username,
has_password=s.has_password(), has_password=s.has_password(),
@@ -8466,7 +8457,6 @@ def create_upstream_source(
"source_type": "npm", "source_type": "npm",
"url": "https://npm.internal.corp", "url": "https://npm.internal.corp",
"enabled": true, "enabled": true,
"is_public": false,
"auth_type": "basic", "auth_type": "basic",
"username": "reader", "username": "reader",
"password": "secret123", "password": "secret123",
@@ -8488,7 +8478,6 @@ def create_upstream_source(
source_type=source_create.source_type, source_type=source_create.source_type,
url=source_create.url, url=source_create.url,
enabled=source_create.enabled, enabled=source_create.enabled,
is_public=source_create.is_public,
auth_type=source_create.auth_type, auth_type=source_create.auth_type,
username=source_create.username, username=source_create.username,
priority=source_create.priority, priority=source_create.priority,
@@ -8528,7 +8517,6 @@ def create_upstream_source(
source_type=source.source_type, source_type=source.source_type,
url=source.url, url=source.url,
enabled=source.enabled, enabled=source.enabled,
is_public=source.is_public,
auth_type=source.auth_type, auth_type=source.auth_type,
username=source.username, username=source.username,
has_password=source.has_password(), has_password=source.has_password(),
@@ -8576,7 +8564,6 @@ def get_upstream_source(
source_type=source.source_type, source_type=source.source_type,
url=source.url, url=source.url,
enabled=source.enabled, enabled=source.enabled,
is_public=source.is_public,
auth_type=source.auth_type, auth_type=source.auth_type,
username=source.username, username=source.username,
has_password=source.has_password(), has_password=source.has_password(),
@@ -8663,10 +8650,6 @@ def update_upstream_source(
changes["enabled"] = {"old": source.enabled, "new": source_update.enabled} changes["enabled"] = {"old": source.enabled, "new": source_update.enabled}
source.enabled = source_update.enabled source.enabled = source_update.enabled
if source_update.is_public is not None and source_update.is_public != source.is_public:
changes["is_public"] = {"old": source.is_public, "new": source_update.is_public}
source.is_public = source_update.is_public
if source_update.auth_type is not None and source_update.auth_type != source.auth_type: if source_update.auth_type is not None and source_update.auth_type != source.auth_type:
changes["auth_type"] = {"old": source.auth_type, "new": source_update.auth_type} changes["auth_type"] = {"old": source.auth_type, "new": source_update.auth_type}
source.auth_type = source_update.auth_type source.auth_type = source_update.auth_type
@@ -8719,7 +8702,6 @@ def update_upstream_source(
source_type=source.source_type, source_type=source.source_type,
url=source.url, url=source.url,
enabled=source.enabled, enabled=source.enabled,
is_public=source.is_public,
auth_type=source.auth_type, auth_type=source.auth_type,
username=source.username, username=source.username,
has_password=source.has_password(), has_password=source.has_password(),
@@ -8860,12 +8842,10 @@ def get_cache_settings(
Admin-only endpoint for viewing cache configuration. Admin-only endpoint for viewing cache configuration.
**Settings:** **Settings:**
- `allow_public_internet`: When false, blocks all requests to sources marked `is_public=true` (air-gap mode)
- `auto_create_system_projects`: When true, system projects (`_npm`, etc.) are created automatically on first cache - `auto_create_system_projects`: When true, system projects (`_npm`, etc.) are created automatically on first cache
**Environment variable overrides:** **Environment variable overrides:**
Settings can be overridden via environment variables: Settings can be overridden via environment variables:
- `ORCHARD_CACHE_ALLOW_PUBLIC_INTERNET`: Overrides `allow_public_internet`
- `ORCHARD_CACHE_AUTO_CREATE_SYSTEM_PROJECTS`: Overrides `auto_create_system_projects` - `ORCHARD_CACHE_AUTO_CREATE_SYSTEM_PROJECTS`: Overrides `auto_create_system_projects`
When an env var override is active, the `*_env_override` field will contain the override value. When an env var override is active, the `*_env_override` field will contain the override value.
@@ -8874,12 +8854,6 @@ def get_cache_settings(
db_settings = _get_cache_settings(db) db_settings = _get_cache_settings(db)
# Apply env var overrides # Apply env var overrides
allow_public_internet = db_settings.allow_public_internet
allow_public_internet_env_override = None
if app_settings.cache_allow_public_internet is not None:
allow_public_internet = app_settings.cache_allow_public_internet
allow_public_internet_env_override = app_settings.cache_allow_public_internet
auto_create_system_projects = db_settings.auto_create_system_projects auto_create_system_projects = db_settings.auto_create_system_projects
auto_create_system_projects_env_override = None auto_create_system_projects_env_override = None
if app_settings.cache_auto_create_system_projects is not None: if app_settings.cache_auto_create_system_projects is not None:
@@ -8887,9 +8861,7 @@ def get_cache_settings(
auto_create_system_projects_env_override = app_settings.cache_auto_create_system_projects auto_create_system_projects_env_override = app_settings.cache_auto_create_system_projects
return CacheSettingsResponse( return CacheSettingsResponse(
allow_public_internet=allow_public_internet,
auto_create_system_projects=auto_create_system_projects, auto_create_system_projects=auto_create_system_projects,
allow_public_internet_env_override=allow_public_internet_env_override,
auto_create_system_projects_env_override=auto_create_system_projects_env_override, auto_create_system_projects_env_override=auto_create_system_projects_env_override,
created_at=db_settings.created_at, created_at=db_settings.created_at,
updated_at=db_settings.updated_at, updated_at=db_settings.updated_at,
@@ -8915,16 +8887,11 @@ def update_cache_settings(
Supports partial updates - only provided fields are updated. Supports partial updates - only provided fields are updated.
**Settings:** **Settings:**
- `allow_public_internet`: When false, enables air-gap mode (blocks public sources)
- `auto_create_system_projects`: When false, system projects must be created manually - `auto_create_system_projects`: When false, system projects must be created manually
**Note:** Environment variables can override these settings. When overridden, **Note:** Environment variables can override these settings. When overridden,
the `*_env_override` fields in the response indicate the effective value. the `*_env_override` fields in the response indicate the effective value.
Updates to the database will be saved but won't take effect until the env var is removed. Updates to the database will be saved but won't take effect until the env var is removed.
**Warning:** Changing `allow_public_internet` to false will immediately block
all cache requests to public sources. This is a security-sensitive setting
and is logged prominently.
""" """
app_settings = get_settings() app_settings = get_settings()
settings = _get_cache_settings(db) settings = _get_cache_settings(db)
@@ -8932,26 +8899,6 @@ def update_cache_settings(
# Track changes for audit log # Track changes for audit log
changes = {} changes = {}
if settings_update.allow_public_internet is not None:
if settings_update.allow_public_internet != settings.allow_public_internet:
changes["allow_public_internet"] = {
"old": settings.allow_public_internet,
"new": settings_update.allow_public_internet,
}
settings.allow_public_internet = settings_update.allow_public_internet
# Log prominently for security audit
if not settings_update.allow_public_internet:
logger.warning(
f"AIR-GAP MODE ENABLED by {current_user.username} - "
f"all public internet access is now blocked"
)
else:
logger.warning(
f"AIR-GAP MODE DISABLED by {current_user.username} - "
f"public internet access is now allowed"
)
if settings_update.auto_create_system_projects is not None: if settings_update.auto_create_system_projects is not None:
if settings_update.auto_create_system_projects != settings.auto_create_system_projects: if settings_update.auto_create_system_projects != settings.auto_create_system_projects:
changes["auto_create_system_projects"] = { changes["auto_create_system_projects"] = {
@@ -8961,11 +8908,9 @@ def update_cache_settings(
settings.auto_create_system_projects = settings_update.auto_create_system_projects settings.auto_create_system_projects = settings_update.auto_create_system_projects
if changes: if changes:
# Audit log with security flag for air-gap changes
is_security_change = "allow_public_internet" in changes
_log_audit( _log_audit(
db, db,
action="cache_settings.update" if not is_security_change else "cache_settings.security_update", action="cache_settings.update",
resource="cache-settings", resource="cache-settings",
user_id=current_user.username, user_id=current_user.username,
source_ip=request.client.host if request.client else None, source_ip=request.client.host if request.client else None,
@@ -8976,12 +8921,6 @@ def update_cache_settings(
db.refresh(settings) db.refresh(settings)
# Apply env var overrides for the response # Apply env var overrides for the response
allow_public_internet = settings.allow_public_internet
allow_public_internet_env_override = None
if app_settings.cache_allow_public_internet is not None:
allow_public_internet = app_settings.cache_allow_public_internet
allow_public_internet_env_override = app_settings.cache_allow_public_internet
auto_create_system_projects = settings.auto_create_system_projects auto_create_system_projects = settings.auto_create_system_projects
auto_create_system_projects_env_override = None auto_create_system_projects_env_override = None
if app_settings.cache_auto_create_system_projects is not None: if app_settings.cache_auto_create_system_projects is not None:
@@ -8989,9 +8928,7 @@ def update_cache_settings(
auto_create_system_projects_env_override = app_settings.cache_auto_create_system_projects auto_create_system_projects_env_override = app_settings.cache_auto_create_system_projects
return CacheSettingsResponse( return CacheSettingsResponse(
allow_public_internet=allow_public_internet,
auto_create_system_projects=auto_create_system_projects, auto_create_system_projects=auto_create_system_projects,
allow_public_internet_env_override=allow_public_internet_env_override,
auto_create_system_projects_env_override=auto_create_system_projects_env_override, auto_create_system_projects_env_override=auto_create_system_projects_env_override,
created_at=settings.created_at, created_at=settings.created_at,
updated_at=settings.updated_at, updated_at=settings.updated_at,

View File

@@ -1214,7 +1214,6 @@ class UpstreamSourceCreate(BaseModel):
source_type: str = "generic" source_type: str = "generic"
url: str url: str
enabled: bool = False enabled: bool = False
is_public: bool = True
auth_type: str = "none" auth_type: str = "none"
username: Optional[str] = None username: Optional[str] = None
password: Optional[str] = None # Write-only password: Optional[str] = None # Write-only
@@ -1271,7 +1270,6 @@ class UpstreamSourceUpdate(BaseModel):
source_type: Optional[str] = None source_type: Optional[str] = None
url: Optional[str] = None url: Optional[str] = None
enabled: Optional[bool] = None enabled: Optional[bool] = None
is_public: Optional[bool] = None
auth_type: Optional[str] = None auth_type: Optional[str] = None
username: Optional[str] = None username: Optional[str] = None
password: Optional[str] = None # Write-only, None = keep existing, empty string = clear password: Optional[str] = None # Write-only, None = keep existing, empty string = clear
@@ -1331,7 +1329,6 @@ class UpstreamSourceResponse(BaseModel):
source_type: str source_type: str
url: str url: str
enabled: bool enabled: bool
is_public: bool
auth_type: str auth_type: str
username: Optional[str] username: Optional[str]
has_password: bool # True if password is set has_password: bool # True if password is set
@@ -1347,9 +1344,7 @@ class UpstreamSourceResponse(BaseModel):
class CacheSettingsResponse(BaseModel): class CacheSettingsResponse(BaseModel):
"""Global cache settings response""" """Global cache settings response"""
allow_public_internet: bool
auto_create_system_projects: bool auto_create_system_projects: bool
allow_public_internet_env_override: Optional[bool] = None # Set if overridden by env var
auto_create_system_projects_env_override: Optional[bool] = None # Set if overridden by env var auto_create_system_projects_env_override: Optional[bool] = None # Set if overridden by env var
created_at: Optional[datetime] = None # May be None for legacy data created_at: Optional[datetime] = None # May be None for legacy data
updated_at: Optional[datetime] = None # May be None for legacy data updated_at: Optional[datetime] = None # May be None for legacy data
@@ -1360,7 +1355,6 @@ class CacheSettingsResponse(BaseModel):
class CacheSettingsUpdate(BaseModel): class CacheSettingsUpdate(BaseModel):
"""Update cache settings (partial)""" """Update cache settings (partial)"""
allow_public_internet: Optional[bool] = None
auto_create_system_projects: Optional[bool] = None auto_create_system_projects: Optional[bool] = None

View File

@@ -57,10 +57,6 @@ class UpstreamSSLError(UpstreamError):
pass pass
class AirGapError(UpstreamError):
"""Request blocked due to air-gap mode."""
pass
class FileSizeExceededError(UpstreamError): class FileSizeExceededError(UpstreamError):
@@ -156,12 +152,6 @@ class UpstreamClient:
# Sort sources by priority (lower = higher priority) # Sort sources by priority (lower = higher priority)
self.sources = sorted(self.sources, key=lambda s: s.priority) self.sources = sorted(self.sources, key=lambda s: s.priority)
def _get_allow_public_internet(self) -> bool:
"""Get the allow_public_internet setting."""
if self.cache_settings is None:
return True # Default to allowing if no settings provided
return self.cache_settings.allow_public_internet
def _match_source(self, url: str) -> Optional[UpstreamSource]: def _match_source(self, url: str) -> Optional[UpstreamSource]:
""" """
Find the upstream source that matches the given URL. Find the upstream source that matches the given URL.
@@ -288,7 +278,6 @@ class UpstreamClient:
FetchResult with content, hash, size, and headers. FetchResult with content, hash, size, and headers.
Raises: Raises:
AirGapError: If air-gap mode blocks the request.
SourceDisabledError: If the matching source is disabled. SourceDisabledError: If the matching source is disabled.
UpstreamConnectionError: On connection failures. UpstreamConnectionError: On connection failures.
UpstreamTimeoutError: On timeout. UpstreamTimeoutError: On timeout.
@@ -301,19 +290,6 @@ class UpstreamClient:
# Match URL to source # Match URL to source
source = self._match_source(url) source = self._match_source(url)
# Check air-gap mode
allow_public = self._get_allow_public_internet()
if not allow_public:
if source is None:
raise AirGapError(
f"Air-gap mode enabled: URL does not match any configured upstream source: {url}"
)
if source.is_public:
raise AirGapError(
f"Air-gap mode enabled: Cannot fetch from public source '{source.name}'"
)
# Check if source is enabled (if we have a match) # Check if source is enabled (if we have a match)
if source is not None and not source.enabled: if source is not None and not source.enabled:
raise SourceDisabledError( raise SourceDisabledError(
@@ -536,7 +512,8 @@ class UpstreamClient:
Test connectivity to an upstream source. Test connectivity to an upstream source.
Performs a HEAD request to the source URL to verify connectivity Performs a HEAD request to the source URL to verify connectivity
and authentication. and authentication. Does not follow redirects - a 3xx response
is considered successful since it proves the server is reachable.
Args: Args:
source: The upstream source to test. source: The upstream source to test.
@@ -564,7 +541,7 @@ class UpstreamClient:
source.url, source.url,
headers=headers, headers=headers,
auth=auth, auth=auth,
follow_redirects=True, follow_redirects=False,
) )
# Consider 2xx and 3xx as success, also 405 (Method Not Allowed) # Consider 2xx and 3xx as success, also 405 (Method Not Allowed)
# since some servers don't support HEAD # since some servers don't support HEAD
@@ -582,5 +559,7 @@ class UpstreamClient:
return (False, f"Connection timed out: {e}", None) return (False, f"Connection timed out: {e}", None)
except httpx.ReadTimeout as e: except httpx.ReadTimeout as e:
return (False, f"Read timed out: {e}", None) return (False, f"Read timed out: {e}", None)
except httpx.TooManyRedirects as e:
return (False, f"Too many redirects: {e}", None)
except Exception as e: except Exception as e:
return (False, f"Error: {e}", None) return (False, f"Error: {e}", None)

View File

@@ -91,7 +91,6 @@ class TestUpstreamSourceModel:
assert hasattr(source, 'source_type') assert hasattr(source, 'source_type')
assert hasattr(source, 'url') assert hasattr(source, 'url')
assert hasattr(source, 'enabled') assert hasattr(source, 'enabled')
assert hasattr(source, 'is_public')
assert hasattr(source, 'auth_type') assert hasattr(source, 'auth_type')
assert hasattr(source, 'username') assert hasattr(source, 'username')
assert hasattr(source, 'password_encrypted') assert hasattr(source, 'password_encrypted')
@@ -107,7 +106,6 @@ class TestUpstreamSourceModel:
source_type="npm", source_type="npm",
url="https://npm.example.com", url="https://npm.example.com",
enabled=True, enabled=True,
is_public=False,
auth_type="basic", auth_type="basic",
username="admin", username="admin",
priority=50, priority=50,
@@ -116,7 +114,6 @@ class TestUpstreamSourceModel:
assert source.source_type == "npm" assert source.source_type == "npm"
assert source.url == "https://npm.example.com" assert source.url == "https://npm.example.com"
assert source.enabled is True assert source.enabled is True
assert source.is_public is False
assert source.auth_type == "basic" assert source.auth_type == "basic"
assert source.username == "admin" assert source.username == "admin"
assert source.priority == 50 assert source.priority == 50
@@ -260,7 +257,6 @@ class TestUpstreamSourceSchemas:
source_type="npm", source_type="npm",
url="https://npm.example.com", url="https://npm.example.com",
enabled=True, enabled=True,
is_public=False,
auth_type="basic", auth_type="basic",
username="admin", username="admin",
password="secret", password="secret",
@@ -281,7 +277,6 @@ class TestUpstreamSourceSchemas:
) )
assert source.source_type == "generic" assert source.source_type == "generic"
assert source.enabled is False assert source.enabled is False
assert source.is_public is True
assert source.auth_type == "none" assert source.auth_type == "none"
assert source.priority == 100 assert source.priority == 100
@@ -578,7 +573,6 @@ class TestUpstreamClientSourceMatching:
name="npm-public", name="npm-public",
url="https://registry.npmjs.org", url="https://registry.npmjs.org",
enabled=True, enabled=True,
is_public=True,
auth_type="none", auth_type="none",
priority=100, priority=100,
) )
@@ -603,7 +597,6 @@ class TestUpstreamClientSourceMatching:
name="npm-private", name="npm-private",
url="https://registry.npmjs.org", url="https://registry.npmjs.org",
enabled=True, enabled=True,
is_public=False,
auth_type="basic", auth_type="basic",
priority=50, priority=50,
) )
@@ -611,7 +604,6 @@ class TestUpstreamClientSourceMatching:
name="npm-public", name="npm-public",
url="https://registry.npmjs.org", url="https://registry.npmjs.org",
enabled=True, enabled=True,
is_public=True,
auth_type="none", auth_type="none",
priority=100, priority=100,
) )
@@ -711,89 +703,6 @@ class TestUpstreamClientAuthHeaders:
assert auth is None assert auth is None
class TestUpstreamClientAirGapMode:
"""Tests for air-gap mode enforcement."""
def test_airgap_blocks_public_source(self):
"""Test that air-gap mode blocks public sources."""
from app.models import UpstreamSource, CacheSettings
from app.upstream import UpstreamClient, AirGapError
source = UpstreamSource(
name="npm-public",
url="https://registry.npmjs.org",
enabled=True,
is_public=True,
auth_type="none",
priority=100,
)
settings = CacheSettings(allow_public_internet=False)
client = UpstreamClient(sources=[source], cache_settings=settings)
with pytest.raises(AirGapError) as exc_info:
client.fetch("https://registry.npmjs.org/lodash")
assert "Air-gap mode enabled" in str(exc_info.value)
assert "public source" in str(exc_info.value)
def test_airgap_blocks_unmatched_url(self):
"""Test that air-gap mode blocks URLs not matching any source."""
from app.models import CacheSettings
from app.upstream import UpstreamClient, AirGapError
settings = CacheSettings(allow_public_internet=False)
client = UpstreamClient(sources=[], cache_settings=settings)
with pytest.raises(AirGapError) as exc_info:
client.fetch("https://example.com/file.tgz")
assert "Air-gap mode enabled" in str(exc_info.value)
assert "does not match any configured" in str(exc_info.value)
def test_airgap_allows_private_source(self):
"""Test that air-gap mode allows private sources."""
from app.models import UpstreamSource, CacheSettings
from app.upstream import UpstreamClient, SourceDisabledError
source = UpstreamSource(
name="npm-private",
url="https://npm.internal.corp",
enabled=False, # Disabled, but would pass air-gap check
is_public=False,
auth_type="none",
priority=100,
)
settings = CacheSettings(allow_public_internet=False)
client = UpstreamClient(sources=[source], cache_settings=settings)
# Should fail due to disabled source, not air-gap
with pytest.raises(SourceDisabledError):
client.fetch("https://npm.internal.corp/package.tgz")
def test_allow_public_internet_true(self):
"""Test that public internet is allowed when setting is true."""
from app.models import UpstreamSource, CacheSettings
from app.upstream import UpstreamClient, SourceDisabledError
source = UpstreamSource(
name="npm-public",
url="https://registry.npmjs.org",
enabled=False, # Disabled
is_public=True,
auth_type="none",
priority=100,
)
settings = CacheSettings(allow_public_internet=True)
client = UpstreamClient(sources=[source], cache_settings=settings)
# Should fail due to disabled source, not air-gap
with pytest.raises(SourceDisabledError):
client.fetch("https://registry.npmjs.org/lodash")
class TestUpstreamClientSourceDisabled: class TestUpstreamClientSourceDisabled:
"""Tests for disabled source handling.""" """Tests for disabled source handling."""
@@ -806,7 +715,6 @@ class TestUpstreamClientSourceDisabled:
name="npm-public", name="npm-public",
url="https://registry.npmjs.org", url="https://registry.npmjs.org",
enabled=False, enabled=False,
is_public=True,
auth_type="none", auth_type="none",
priority=100, priority=100,
) )
@@ -979,13 +887,6 @@ class TestUpstreamExceptions:
assert error.status_code == 404 assert error.status_code == 404
assert error.response_headers == {"x-custom": "value"} assert error.response_headers == {"x-custom": "value"}
def test_airgap_error(self):
"""Test AirGapError."""
from app.upstream import AirGapError
error = AirGapError("Blocked by air-gap")
assert "Blocked by air-gap" in str(error)
def test_source_not_found_error(self): def test_source_not_found_error(self):
"""Test SourceNotFoundError.""" """Test SourceNotFoundError."""
from app.upstream import SourceNotFoundError from app.upstream import SourceNotFoundError
@@ -1420,7 +1321,6 @@ class TestUpstreamSourcesAdminAPI:
"source_type": "generic", "source_type": "generic",
"url": "https://example.com/packages", "url": "https://example.com/packages",
"enabled": False, "enabled": False,
"is_public": False,
"auth_type": "none", "auth_type": "none",
"priority": 200, "priority": 200,
}, },
@@ -1432,7 +1332,6 @@ class TestUpstreamSourcesAdminAPI:
assert data["source_type"] == "generic" assert data["source_type"] == "generic"
assert data["url"] == "https://example.com/packages" assert data["url"] == "https://example.com/packages"
assert data["enabled"] is False assert data["enabled"] is False
assert data["is_public"] is False
assert data["priority"] == 200 assert data["priority"] == 200
assert "id" in data assert "id" in data
@@ -1452,7 +1351,6 @@ class TestUpstreamSourcesAdminAPI:
"source_type": "npm", "source_type": "npm",
"url": "https://npm.internal.corp", "url": "https://npm.internal.corp",
"enabled": False, "enabled": False,
"is_public": False,
"auth_type": "basic", "auth_type": "basic",
"username": "reader", "username": "reader",
"password": "secret123", "password": "secret123",
@@ -1958,7 +1856,6 @@ class TestEnvVarUpstreamSourcesParsing:
# Check defaults # Check defaults
assert test_source.source_type == "generic" assert test_source.source_type == "generic"
assert test_source.enabled is True assert test_source.enabled is True
assert test_source.is_public is True
assert test_source.auth_type == "none" assert test_source.auth_type == "none"
assert test_source.priority == 100 assert test_source.priority == 100
finally: finally:
@@ -1981,7 +1878,6 @@ class TestEnvSourceToResponse:
url="https://example.com", url="https://example.com",
source_type="npm", source_type="npm",
enabled=True, enabled=True,
is_public=False,
auth_type="basic", auth_type="basic",
username="user", username="user",
password="pass", password="pass",
@@ -1992,7 +1888,6 @@ class TestEnvSourceToResponse:
assert source.url == "https://example.com" assert source.url == "https://example.com"
assert source.source_type == "npm" assert source.source_type == "npm"
assert source.enabled is True assert source.enabled is True
assert source.is_public is False
assert source.auth_type == "basic" assert source.auth_type == "basic"
assert source.username == "user" assert source.username == "user"
assert source.password == "pass" assert source.password == "pass"

View File

@@ -156,6 +156,7 @@
.source-name { .source-name {
font-weight: 500; font-weight: 500;
color: var(--text-primary); color: var(--text-primary);
white-space: nowrap;
} }
.url-cell { .url-cell {
@@ -168,7 +169,6 @@
} }
/* Badges */ /* Badges */
.public-badge,
.env-badge, .env-badge,
.status-badge { .status-badge {
display: inline-block; display: inline-block;
@@ -179,11 +179,6 @@
margin-left: 0.5rem; margin-left: 0.5rem;
} }
.public-badge {
background-color: #e3f2fd;
color: #1976d2;
}
.env-badge { .env-badge {
background-color: #fff3e0; background-color: #fff3e0;
color: #e65100; color: #e65100;
@@ -213,17 +208,64 @@
} }
.test-result { .test-result {
display: inline-block; display: inline-flex;
margin-left: 0.5rem; align-items: center;
font-size: 0.85rem; gap: 0.25rem;
padding: 0.2rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
white-space: nowrap;
} }
.test-result.success { .test-result.success {
background-color: #e8f5e9;
color: #2e7d32; color: #2e7d32;
} }
.test-result.failure { .test-result.failure {
background-color: #ffebee;
color: #c62828; color: #c62828;
cursor: pointer;
}
.test-result.failure:hover {
background-color: #ffcdd2;
}
.test-result.testing {
background-color: #e3f2fd;
color: #1976d2;
}
/* Error Modal */
.error-modal-content {
background: var(--bg-primary);
border-radius: 8px;
padding: 2rem;
width: 100%;
max-width: 500px;
}
.error-modal-content h3 {
margin-top: 0;
color: #c62828;
}
.error-modal-content .error-details {
background: var(--bg-tertiary);
padding: 1rem;
border-radius: 4px;
font-family: monospace;
font-size: 0.9rem;
word-break: break-word;
white-space: pre-wrap;
}
.error-modal-content .modal-actions {
display: flex;
justify-content: flex-end;
margin-top: 1.5rem;
} }
/* Buttons */ /* Buttons */

View File

@@ -38,7 +38,6 @@ function AdminCachePage() {
source_type: 'generic' as SourceType, source_type: 'generic' as SourceType,
url: '', url: '',
enabled: true, enabled: true,
is_public: true,
auth_type: 'none' as AuthType, auth_type: 'none' as AuthType,
username: '', username: '',
password: '', password: '',
@@ -60,6 +59,10 @@ function AdminCachePage() {
// Success message // Success message
const [successMessage, setSuccessMessage] = useState<string | null>(null); const [successMessage, setSuccessMessage] = useState<string | null>(null);
// Error modal state
const [showErrorModal, setShowErrorModal] = useState(false);
const [selectedError, setSelectedError] = useState<{ sourceName: string; error: string } | null>(null);
useEffect(() => { useEffect(() => {
if (!authLoading && !user) { if (!authLoading && !user) {
navigate('/login', { state: { from: '/admin/cache' } }); navigate('/login', { state: { from: '/admin/cache' } });
@@ -113,7 +116,6 @@ function AdminCachePage() {
source_type: 'generic', source_type: 'generic',
url: '', url: '',
enabled: true, enabled: true,
is_public: true,
auth_type: 'none', auth_type: 'none',
username: '', username: '',
password: '', password: '',
@@ -130,7 +132,6 @@ function AdminCachePage() {
source_type: source.source_type, source_type: source.source_type,
url: source.url, url: source.url,
enabled: source.enabled, enabled: source.enabled,
is_public: source.is_public,
auth_type: source.auth_type, auth_type: source.auth_type,
username: source.username || '', username: source.username || '',
password: '', password: '',
@@ -155,6 +156,8 @@ function AdminCachePage() {
setFormError(null); setFormError(null);
try { try {
let savedSourceId: string | null = null;
if (editingSource) { if (editingSource) {
// Update existing source // Update existing source
await updateUpstreamSource(editingSource.id, { await updateUpstreamSource(editingSource.id, {
@@ -162,30 +165,35 @@ function AdminCachePage() {
source_type: formData.source_type, source_type: formData.source_type,
url: formData.url.trim(), url: formData.url.trim(),
enabled: formData.enabled, enabled: formData.enabled,
is_public: formData.is_public,
auth_type: formData.auth_type, auth_type: formData.auth_type,
username: formData.username.trim() || undefined, username: formData.username.trim() || undefined,
password: formData.password || undefined, password: formData.password || undefined,
priority: formData.priority, priority: formData.priority,
}); });
savedSourceId = editingSource.id;
setSuccessMessage('Source updated successfully'); setSuccessMessage('Source updated successfully');
} else { } else {
// Create new source // Create new source
await createUpstreamSource({ const newSource = await createUpstreamSource({
name: formData.name.trim(), name: formData.name.trim(),
source_type: formData.source_type, source_type: formData.source_type,
url: formData.url.trim(), url: formData.url.trim(),
enabled: formData.enabled, enabled: formData.enabled,
is_public: formData.is_public,
auth_type: formData.auth_type, auth_type: formData.auth_type,
username: formData.username.trim() || undefined, username: formData.username.trim() || undefined,
password: formData.password || undefined, password: formData.password || undefined,
priority: formData.priority, priority: formData.priority,
}); });
savedSourceId = newSource.id;
setSuccessMessage('Source created successfully'); setSuccessMessage('Source created successfully');
} }
setShowForm(false); setShowForm(false);
await loadSources(); await loadSources();
// Auto-test the source after save
if (savedSourceId) {
testSourceById(savedSourceId);
}
} catch (err) { } catch (err) {
setFormError(err instanceof Error ? err.message : 'Failed to save source'); setFormError(err instanceof Error ? err.message : 'Failed to save source');
} finally { } finally {
@@ -211,24 +219,28 @@ function AdminCachePage() {
} }
async function handleTest(source: UpstreamSource) { async function handleTest(source: UpstreamSource) {
setTestingId(source.id); testSourceById(source.id);
setTestResults((prev) => ({ ...prev, [source.id]: { success: true, message: 'Testing...' } })); }
async function testSourceById(sourceId: string) {
setTestingId(sourceId);
setTestResults((prev) => ({ ...prev, [sourceId]: { success: true, message: 'Testing...' } }));
try { try {
const result = await testUpstreamSource(source.id); const result = await testUpstreamSource(sourceId);
setTestResults((prev) => ({ setTestResults((prev) => ({
...prev, ...prev,
[source.id]: { [sourceId]: {
success: result.success, success: result.success,
message: result.success message: result.success
? `Connected (${result.elapsed_ms}ms)` ? `OK (${result.elapsed_ms}ms)`
: result.error || `HTTP ${result.status_code}`, : result.error || `HTTP ${result.status_code}`,
}, },
})); }));
} catch (err) { } catch (err) {
setTestResults((prev) => ({ setTestResults((prev) => ({
...prev, ...prev,
[source.id]: { [sourceId]: {
success: false, success: false,
message: err instanceof Error ? err.message : 'Test failed', message: err instanceof Error ? err.message : 'Test failed',
}, },
@@ -238,13 +250,16 @@ function AdminCachePage() {
} }
} }
async function handleSettingsToggle(field: 'allow_public_internet' | 'auto_create_system_projects') { function showError(sourceName: string, error: string) {
setSelectedError({ sourceName, error });
setShowErrorModal(true);
}
async function handleSettingsToggle(field: 'auto_create_system_projects') {
if (!settings) return; if (!settings) return;
// Check if env override is active // Check if env override is active
const isOverridden = const isOverridden = field === 'auto_create_system_projects' && settings.auto_create_system_projects_env_override !== null;
(field === 'allow_public_internet' && settings.allow_public_internet_env_override !== null) ||
(field === 'auto_create_system_projects' && settings.auto_create_system_projects_env_override !== null);
if (isOverridden) { if (isOverridden) {
alert('This setting is overridden by an environment variable and cannot be changed via UI.'); alert('This setting is overridden by an environment variable and cannot be changed via UI.');
@@ -291,28 +306,6 @@ function AdminCachePage() {
<div className="error-message">{settingsError}</div> <div className="error-message">{settingsError}</div>
) : settings ? ( ) : settings ? (
<div className="settings-grid"> <div className="settings-grid">
<div className="setting-item">
<label className="toggle-label">
<span className="setting-name">
Allow Public Internet
{settings.allow_public_internet_env_override !== null && (
<span className="env-badge" title="Overridden by environment variable">
ENV
</span>
)}
</span>
<span className="setting-description">
When disabled (air-gap mode), requests to public sources are blocked.
</span>
</label>
<button
className={`toggle-button ${settings.allow_public_internet ? 'on' : 'off'}`}
onClick={() => handleSettingsToggle('allow_public_internet')}
disabled={updatingSettings || settings.allow_public_internet_env_override !== null}
>
{settings.allow_public_internet ? 'Enabled' : 'Disabled'}
</button>
</div>
<div className="setting-item"> <div className="setting-item">
<label className="toggle-label"> <label className="toggle-label">
<span className="setting-name"> <span className="setting-name">
@@ -364,6 +357,7 @@ function AdminCachePage() {
<th>Priority</th> <th>Priority</th>
<th>Status</th> <th>Status</th>
<th>Source</th> <th>Source</th>
<th>Test</th>
<th>Actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
@@ -372,7 +366,6 @@ function AdminCachePage() {
<tr key={source.id} className={source.enabled ? '' : 'disabled-row'}> <tr key={source.id} className={source.enabled ? '' : 'disabled-row'}>
<td> <td>
<span className="source-name">{source.name}</span> <span className="source-name">{source.name}</span>
{source.is_public && <span className="public-badge">Public</span>}
</td> </td>
<td>{source.source_type}</td> <td>{source.source_type}</td>
<td className="url-cell">{source.url}</td> <td className="url-cell">{source.url}</td>
@@ -391,13 +384,34 @@ function AdminCachePage() {
'Database' 'Database'
)} )}
</td> </td>
<td>
{testingId === source.id ? (
<span className="test-result testing">Testing...</span>
) : testResults[source.id] ? (
testResults[source.id].success ? (
<span className="test-result success" title={testResults[source.id].message}>
OK
</span>
) : (
<span
className="test-result failure"
title="Click to see details"
onClick={() => showError(source.name, testResults[source.id].message)}
>
Error
</span>
)
) : (
<span className="test-result" style={{ opacity: 0.5 }}></span>
)}
</td>
<td className="actions-cell"> <td className="actions-cell">
<button <button
className="btn btn-sm" className="btn btn-sm"
onClick={() => handleTest(source)} onClick={() => handleTest(source)}
disabled={testingId === source.id} disabled={testingId === source.id}
> >
{testingId === source.id ? 'Testing...' : 'Test'} Test
</button> </button>
{source.source !== 'env' && ( {source.source !== 'env' && (
<> <>
@@ -413,11 +427,6 @@ function AdminCachePage() {
</button> </button>
</> </>
)} )}
{testResults[source.id] && (
<span className={`test-result ${testResults[source.id].success ? 'success' : 'failure'}`}>
{testResults[source.id].message}
</span>
)}
</td> </td>
</tr> </tr>
))} ))}
@@ -498,16 +507,6 @@ function AdminCachePage() {
Enabled Enabled
</label> </label>
</div> </div>
<div className="form-group checkbox-group">
<label>
<input
type="checkbox"
checked={formData.is_public}
onChange={(e) => setFormData({ ...formData, is_public: e.target.checked })}
/>
Public Internet Source
</label>
</div>
</div> </div>
<div className="form-group"> <div className="form-group">
@@ -573,6 +572,21 @@ function AdminCachePage() {
</div> </div>
</div> </div>
)} )}
{/* Error Details Modal */}
{showErrorModal && selectedError && (
<div className="modal-overlay" onClick={() => setShowErrorModal(false)}>
<div className="error-modal-content" onClick={(e) => e.stopPropagation()}>
<h3>Connection Error: {selectedError.sourceName}</h3>
<div className="error-details">{selectedError.error}</div>
<div className="modal-actions">
<button className="btn" onClick={() => setShowErrorModal(false)}>
Close
</button>
</div>
</div>
</div>
)}
</div> </div>
); );
} }

View File

@@ -515,7 +515,6 @@ export interface UpstreamSource {
source_type: SourceType; source_type: SourceType;
url: string; url: string;
enabled: boolean; enabled: boolean;
is_public: boolean;
auth_type: AuthType; auth_type: AuthType;
username: string | null; username: string | null;
has_password: boolean; has_password: boolean;
@@ -531,7 +530,6 @@ export interface UpstreamSourceCreate {
source_type: SourceType; source_type: SourceType;
url: string; url: string;
enabled?: boolean; enabled?: boolean;
is_public?: boolean;
auth_type?: AuthType; auth_type?: AuthType;
username?: string; username?: string;
password?: string; password?: string;
@@ -544,7 +542,6 @@ export interface UpstreamSourceUpdate {
source_type?: SourceType; source_type?: SourceType;
url?: string; url?: string;
enabled?: boolean; enabled?: boolean;
is_public?: boolean;
auth_type?: AuthType; auth_type?: AuthType;
username?: string; username?: string;
password?: string; password?: string;
@@ -563,15 +560,12 @@ export interface UpstreamSourceTestResult {
// Cache Settings types // Cache Settings types
export interface CacheSettings { export interface CacheSettings {
allow_public_internet: boolean;
auto_create_system_projects: boolean; auto_create_system_projects: boolean;
allow_public_internet_env_override: boolean | null;
auto_create_system_projects_env_override: boolean | null; auto_create_system_projects_env_override: boolean | null;
created_at: string | null; created_at: string | null;
updated_at: string | null; updated_at: string | null;
} }
export interface CacheSettingsUpdate { export interface CacheSettingsUpdate {
allow_public_internet?: boolean;
auto_create_system_projects?: boolean; auto_create_system_projects?: boolean;
} }

View File

@@ -128,6 +128,10 @@ spec:
value: {{ .Values.orchard.rateLimit.login | quote }} value: {{ .Values.orchard.rateLimit.login | quote }}
{{- end }} {{- end }}
{{- end }} {{- end }}
{{- if .Values.orchard.purgeSeedData }}
- name: ORCHARD_PURGE_SEED_DATA
value: "true"
{{- end }}
{{- if .Values.orchard.database.poolSize }} {{- if .Values.orchard.database.poolSize }}
- name: ORCHARD_DATABASE_POOL_SIZE - name: ORCHARD_DATABASE_POOL_SIZE
value: {{ .Values.orchard.database.poolSize | quote }} value: {{ .Values.orchard.database.poolSize | quote }}

View File

@@ -91,6 +91,7 @@ affinity: {}
# Orchard server configuration # Orchard server configuration
orchard: orchard:
env: "development" # Allows seed data for testing env: "development" # Allows seed data for testing
purgeSeedData: true # Remove public seed data (npm-public, pypi-public, etc.)
server: server:
host: "0.0.0.0" host: "0.0.0.0"
port: 8080 port: 8080