Skip to content

Commit d7772be

Browse files
Merge pull request #264 from ChrispyBacon-dev/stable
merge from stable
2 parents 17ccd4b + 99bbbcf commit d7772be

16 files changed

Lines changed: 1268 additions & 325 deletions

CHANGELOG.md

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
---
99

10+
## [v3.0.2] - 2025-09-30
11+
12+
### Added
13+
- **Enhanced API Key Management**
14+
- **Revoked Key Visibility:** Revoked API keys are now displayed in a separate "Revoked Keys" section with full key visibility for verification and audit purposes.
15+
- **Permanent Deletion:** Added "Delete Permanently" functionality for individual revoked keys and "Clear All" for bulk removal.
16+
- **Auto-Cleanup System:** Implemented automatic cleanup of revoked keys after 30 days with manual trigger option.
17+
- **Improved UX:** Revoked keys are visually distinguished (grayed out, full key shown) with countdown to auto-deletion.
18+
- **Copy Functionality:** Users can copy full revoked API keys for record-keeping before permanent deletion.
19+
20+
### Fixed
21+
- **API Key Revocation Display Bug:** Fixed issue where revoked API keys remained visible in the frontend as if they were active, even though backend authentication correctly rejected them.
22+
23+
---
24+
1025
## [v3.0.1] (Hotfixes) - 2025-09-27
1126

27+
### Added
28+
- **Enhanced Country Selection UX**
29+
- **Bulk Selection Controls:** Added "Select All," "Select None," and "Invert Selection" buttons for more efficient country management.
30+
- **Quick Templates:** Implemented one-click presets such as "Block All Except US," "Block All Except EU," and "Block High Risk Countries."
31+
- **Regional Selection:** Users can now select entire continents (e.g., Africa, Asia, Europe) with a single click.
32+
- **Visual Feedback:** A dynamic counter now shows "X of 245 countries selected" to provide immediate feedback.
33+
1234
### Fixed
13-
- **IP Whitelist Access Policies:** Corrected an issue where IP-based access policies were not working as expected. DockFlare now correctly creates a `bypass` rule for IP whitelists and a separate `allow` rule for email-based authentication, ensuring whitelisted IPs can access services without an additional authentication step.
14-
- **Access Policy Updates:** Fixed a bug where updating an existing ingress rule's Access Policy would fail with an "application already exists" error. DockFlare now correctly updates the existing Cloudflare Access application instead of trying to create a new one.
15-
- **API Error Logging:** Reduced the severity of the log message for a `403 Forbidden` error when fetching a user's email. This is an expected and non-critical error when using a scoped API token.
16-
(raised by @durzo issue tracker #216 #217)
35+
- **Tedious Manual Selection:** Resolved an issue where "Allow US Only" required manually selecting over 194 countries; it now requires only one click (resolves #240).
36+
- **IP Whitelist Access Policies:** Corrected a bug where IP-based access policies were not functioning as intended. DockFlare now properly creates a `bypass` rule for whitelisted IPs.
37+
- **Access Policy Updates:** Addressed a failure where updating an Access Policy on an existing ingress rule would result in an "application already exists" error.
38+
- **API Error Logging:** The severity of the log message for a `403 Forbidden` error during user email fetches has been reduced, as this is expected behavior with a scoped API token (related to issues #216, #217 raised by @durzo).
39+
- **OAuth Provider Visibility:** Fixed the login screen to respect disabled providers immediately after changes through the API or UI, keeping password-disable overrides intact.
1740

1841
---
1942
## [v3.0.1] - 2025-09-26

docker-compose.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ services:
3030
restart: "no"
3131

3232
dockflare:
33-
build: ./dockflare
34-
#image: alplat/dockflare:stable
33+
#build: ./dockflare
34+
image: alplat/dockflare:stable
3535
container_name: dockflare
3636
restart: unless-stopped
3737
ports:
38-
- "5001:5000"
38+
- "5000:5000"
3939
#labels:
4040
# -- Cloudflare Tunnel Configuration (via DockFlare) OPTIONAL --
4141
# Main DockFlare interface with access policy

dockflare/app/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ def create_app():
131131
@login_manager.unauthorized_handler
132132
def unauthorized():
133133
from flask import request, jsonify, redirect, url_for
134+
135+
if app_instance.config.get('DISABLE_PASSWORD_LOGIN', False):
136+
from flask_login import login_user
137+
from app.core.user import User
138+
user = User('anonymous', auth_method='disabled')
139+
login_user(user)
140+
return redirect(request.url)
141+
134142
if request.path.startswith('/api/'):
135143
return jsonify({"status": "error", "message": "authentication_required"}), 401
136144

dockflare/app/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
import logging
2121

2222
# --- DockFlare Version ---
23-
APP_VERSION = "v3.0.1"
23+
APP_VERSION = "v3.0.2"
2424
# --- web: https://dockflare.app ---
2525
# --- github: https://github.com/ChrispyBacon-dev/DockFlare ---
2626

dockflare/app/core/access_manager.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,7 @@ def get_cloudflare_account_email():
9090
logging.warning(f"Failed to fetch Cloudflare account email, API call unsuccessful. Response: {response_data}")
9191
return None
9292
except requests.exceptions.RequestException as e:
93-
if hasattr(e, 'response') and e.response is not None and e.response.status_code == 403:
94-
logging.info("Could not fetch Cloudflare account email due to permissions. This is expected if using a scoped API token without User.Read permissions. The TLD policy feature will be disabled.")
95-
else:
96-
logging.error(f"API error fetching Cloudflare account email: {e}")
93+
logging.error(f"API error fetching Cloudflare account email: {e}")
9794
return None
9895
except Exception as e:
9996
logging.error(f"Unexpected error fetching Cloudflare account email: {e}", exc_info=True)

dockflare/app/core/state_manager.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,110 @@ def list_agent_keys():
323323
"""Return a shallow copy of the agent key metadata from the encrypted store."""
324324
return agent_key_store.list_keys()
325325

326+
def cleanup_expired_revoked_keys(retention_days=30):
327+
"""
328+
Auto-cleanup revoked keys older than retention_days.
329+
Returns dict with cleanup results.
330+
"""
331+
if retention_days <= 0:
332+
return {"status": "skipped", "message": "Auto-cleanup disabled"}
333+
334+
all_keys = agent_key_store.list_keys()
335+
now = datetime.utcnow().replace(tzinfo=timezone.utc)
336+
337+
expired_keys = []
338+
cleaned_count = 0
339+
340+
for key_id, key_info in all_keys.items():
341+
if key_info.get("status") != "revoked":
342+
continue
343+
344+
revoked_at_str = key_info.get("revoked_at")
345+
if not revoked_at_str:
346+
continue
347+
348+
try:
349+
# Parse revocation timestamp
350+
if revoked_at_str.endswith('Z'):
351+
revoked_at = datetime.fromisoformat(revoked_at_str.replace('Z', '+00:00'))
352+
else:
353+
revoked_at = datetime.fromisoformat(revoked_at_str)
354+
revoked_at = revoked_at.replace(tzinfo=timezone.utc) if revoked_at.tzinfo is None else revoked_at.astimezone(timezone.utc)
355+
356+
# Check if key is expired
357+
days_since_revoked = (now - revoked_at).days
358+
if days_since_revoked >= retention_days:
359+
owner = key_info.get("owner", "unknown")
360+
expired_keys.append({
361+
"key_id": key_id,
362+
"owner": owner,
363+
"revoked_at": revoked_at_str,
364+
"days_old": days_since_revoked
365+
})
366+
367+
# Remove the expired key
368+
agent_key_store.remove_key(key_id)
369+
cleaned_count += 1
370+
logging.info(f"AUTO_CLEANUP: Removed expired revoked key {key_id[:8]}... (owner: {owner}, revoked {days_since_revoked} days ago)")
371+
372+
except Exception as e:
373+
logging.warning(f"AUTO_CLEANUP: Failed to process revoked key {key_id[:8]}: {e}")
374+
375+
result = {
376+
"status": "completed",
377+
"cleaned_count": cleaned_count,
378+
"retention_days": retention_days,
379+
"expired_keys": expired_keys
380+
}
381+
382+
if cleaned_count > 0:
383+
logging.info(f"AUTO_CLEANUP: Removed {cleaned_count} expired revoked keys (retention: {retention_days} days)")
384+
385+
return result
386+
387+
def get_revoked_keys_summary():
388+
"""
389+
Get summary information about revoked keys for display.
390+
Returns dict with revoked key counts and aging info.
391+
"""
392+
all_keys = agent_key_store.list_keys()
393+
now = datetime.utcnow().replace(tzinfo=timezone.utc)
394+
395+
revoked_keys = []
396+
for key_id, key_info in all_keys.items():
397+
if key_info.get("status") != "revoked":
398+
continue
399+
400+
revoked_at_str = key_info.get("revoked_at")
401+
days_until_cleanup = None
402+
403+
if revoked_at_str:
404+
try:
405+
if revoked_at_str.endswith('Z'):
406+
revoked_at = datetime.fromisoformat(revoked_at_str.replace('Z', '+00:00'))
407+
else:
408+
revoked_at = datetime.fromisoformat(revoked_at_str)
409+
revoked_at = revoked_at.replace(tzinfo=timezone.utc) if revoked_at.tzinfo is None else revoked_at.astimezone(timezone.utc)
410+
411+
# Calculate days until auto-cleanup (assuming 30 day retention)
412+
days_since_revoked = (now - revoked_at).days
413+
days_until_cleanup = max(0, 30 - days_since_revoked)
414+
415+
except Exception:
416+
pass
417+
418+
revoked_keys.append({
419+
"key_id": key_id,
420+
"owner": key_info.get("owner", "unknown"),
421+
"revoked_at": revoked_at_str,
422+
"days_until_cleanup": days_until_cleanup
423+
})
424+
425+
return {
426+
"revoked_count": len(revoked_keys),
427+
"revoked_keys": revoked_keys
428+
}
429+
326430
def get_agent_rules(agent_id):
327431
"""Return all active rules for a specific agent."""
328432
with state_lock:

dockflare/app/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ def main_application_entrypoint():
236236
def register_oauth_providers(flask_app, oauth_instance):
237237
providers = flask_app.config.get('OAUTH_PROVIDERS', [])
238238
for provider in providers:
239-
if not provider.get('enabled'):
239+
if not provider.get('enabled', True):
240240
continue
241241

242242
try:

dockflare/app/static/css/output.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dockflare/app/static/js/main.js

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,7 @@ function initializeAllTomSelects() {
6969
manualTunnelTomSelect = new TomSelect(manualTunnelSelect, singleSelectOptions);
7070
}
7171

72-
// Initialize select on the Access Policies page (Access Group modal)
73-
const countrySelect = document.getElementById('group_countries');
74-
if (countrySelect) {
75-
new TomSelect(countrySelect, countrySelectOptions);
76-
}
72+
// Note: country selector is now initialized in access_policies.html with enhanced features
7773
}
7874

7975
const themeManager = (function() {
@@ -1061,6 +1057,9 @@ function openCreateAccessGroupModal() {
10611057
if (countrySelect && countrySelect.tomselect) {
10621058
countrySelect.tomselect.clear();
10631059
countrySelect.tomselect.sync();
1060+
if (window.enhancedCountrySelector && window.enhancedCountrySelector.updateSelectionCounter) {
1061+
window.enhancedCountrySelector.updateSelectionCounter();
1062+
}
10641063
}
10651064

10661065
modal.showModal();
@@ -1130,7 +1129,10 @@ function openEditAccessGroupModal(groupId, details) {
11301129
const countrySelect = document.getElementById('group_countries');
11311130
if (countrySelect && countrySelect.tomselect) {
11321131
countrySelect.tomselect.setValue(selectedCountries);
1133-
} else if (countrySelect) { // Fallback for when TomSelect isn't initialized
1132+
if (window.enhancedCountrySelector && window.enhancedCountrySelector.updateSelectionCounter) {
1133+
window.enhancedCountrySelector.updateSelectionCounter();
1134+
}
1135+
} else if (countrySelect) {
11341136
Array.from(countrySelect.options).forEach(option => {
11351137
option.selected = selectedCountries.includes(option.value);
11361138
});
@@ -1307,8 +1309,25 @@ document.addEventListener('DOMContentLoaded', function() {
13071309
}
13081310
};
13091311

1312+
const clearAccessGroupOnPolicyChange = () => {
1313+
if (select.value && select.value !== 'none') {
1314+
const accessGroupSelect = container.querySelector('#manual_access_group, #edit_manual_access_group');
1315+
if (accessGroupSelect) {
1316+
if (accessGroupSelect.tomselect) {
1317+
accessGroupSelect.tomselect.clear();
1318+
} else {
1319+
accessGroupSelect.value = '';
1320+
}
1321+
accessGroupSelect.dispatchEvent(new Event('change'));
1322+
}
1323+
}
1324+
};
1325+
13101326
// Add the event listener for user interactions
1311-
select.addEventListener('change', toggleEmailField);
1327+
select.addEventListener('change', () => {
1328+
toggleEmailField();
1329+
clearAccessGroupOnPolicyChange();
1330+
});
13121331

13131332
});
13141333

0 commit comments

Comments
 (0)