mirror of
https://framagit.org/framasoft/framaspace/argos.git
synced 2025-04-28 18:02:41 +02:00
✨🛂 — Allow partial or total anonymous access to web interface (fix #63)
This commit is contained in:
parent
841f8638de
commit
da221b856b
8 changed files with 103 additions and 7 deletions
|
@ -13,6 +13,7 @@
|
||||||
BREAKING CHANGE: `mo` is no longer accepted for declaring a duration in month in the configuration
|
BREAKING CHANGE: `mo` is no longer accepted for declaring a duration in month in the configuration
|
||||||
You need to use `M`, `month` or `months`
|
You need to use `M`, `month` or `months`
|
||||||
- ✨ - Allow to choose a frequency smaller than a minute
|
- ✨ - Allow to choose a frequency smaller than a minute
|
||||||
|
- ✨🛂 — Allow partial or total anonymous access to web interface (#63)
|
||||||
|
|
||||||
## 0.5.0
|
## 0.5.0
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
---
|
---
|
||||||
general:
|
general:
|
||||||
|
# Except for frequency and recheck_delay settings, changes in general
|
||||||
|
# section of the configuration will need a restart of argos server.
|
||||||
db:
|
db:
|
||||||
# The database URL, as defined in SQLAlchemy docs :
|
# The database URL, as defined in SQLAlchemy docs :
|
||||||
# https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls
|
# https://docs.sqlalchemy.org/en/20/core/engines.html#database-urls
|
||||||
|
@ -28,6 +30,12 @@ general:
|
||||||
# If not present, the "Remember me" feature is not available
|
# If not present, the "Remember me" feature is not available
|
||||||
# remember_me_duration: "1M"
|
# remember_me_duration: "1M"
|
||||||
|
|
||||||
|
# Unauthenticated access
|
||||||
|
# If can grant an unauthenticated access to the dashboard or to all pages
|
||||||
|
# To do so, choose either "dashboard", or "all"
|
||||||
|
# If not present, all pages needs authentication
|
||||||
|
# unauthenticated_access: "all"
|
||||||
|
|
||||||
# Default delay for checks.
|
# Default delay for checks.
|
||||||
# Can be superseeded in domain configuration.
|
# Can be superseeded in domain configuration.
|
||||||
# For ex., to run checks every 5 minutes:
|
# For ex., to run checks every 5 minutes:
|
||||||
|
|
|
@ -27,6 +27,7 @@ from argos.schemas.utils import Method
|
||||||
|
|
||||||
Severity = Literal["warning", "error", "critical", "unknown"]
|
Severity = Literal["warning", "error", "critical", "unknown"]
|
||||||
Environment = Literal["dev", "test", "production"]
|
Environment = Literal["dev", "test", "production"]
|
||||||
|
Unauthenticated = Literal["dashboard", "all"]
|
||||||
SQLiteDsn = Annotated[
|
SQLiteDsn = Annotated[
|
||||||
Url,
|
Url,
|
||||||
UrlConstraints(
|
UrlConstraints(
|
||||||
|
@ -190,6 +191,7 @@ class General(BaseModel):
|
||||||
cookie_secret: str
|
cookie_secret: str
|
||||||
session_duration: int = 10080 # 7 days
|
session_duration: int = 10080 # 7 days
|
||||||
remember_me_duration: int | None = None
|
remember_me_duration: int | None = None
|
||||||
|
unauthenticated_access: Unauthenticated | None = None
|
||||||
frequency: float
|
frequency: float
|
||||||
recheck_delay: float | None = None
|
recheck_delay: float | None = None
|
||||||
root_path: str = ""
|
root_path: str = ""
|
||||||
|
|
|
@ -100,7 +100,7 @@ def setup_database(appli):
|
||||||
models.Base.metadata.create_all(bind=engine)
|
models.Base.metadata.create_all(bind=engine)
|
||||||
|
|
||||||
|
|
||||||
def create_manager(cookie_secret):
|
def create_manager(cookie_secret: str) -> LoginManager:
|
||||||
if cookie_secret == "foo_bar_baz":
|
if cookie_secret == "foo_bar_baz":
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"You should change the cookie_secret secret in your configuration file."
|
"You should change the cookie_secret secret in your configuration file."
|
||||||
|
|
|
@ -18,6 +18,9 @@ def get_config(request: Request):
|
||||||
|
|
||||||
|
|
||||||
async def get_manager(request: Request) -> LoginManager:
|
async def get_manager(request: Request) -> LoginManager:
|
||||||
|
if request.app.state.config.general.unauthenticated_access is not None:
|
||||||
|
return await request.app.state.manager.optional(request)
|
||||||
|
|
||||||
return await request.app.state.manager(request)
|
return await request.app.state.manager(request)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,7 @@ from sqlalchemy.orm import Session
|
||||||
from argos.checks.base import Status
|
from argos.checks.base import Status
|
||||||
from argos.schemas import Config
|
from argos.schemas import Config
|
||||||
from argos.server import queries
|
from argos.server import queries
|
||||||
|
from argos.server.exceptions import NotAuthenticatedException
|
||||||
from argos.server.models import Result, Task, User
|
from argos.server.models import Result, Task, User
|
||||||
from argos.server.routes.dependencies import get_config, get_db, get_manager
|
from argos.server.routes.dependencies import get_config, get_db, get_manager
|
||||||
|
|
||||||
|
@ -33,6 +34,12 @@ async def login_view(
|
||||||
msg: str | None = None,
|
msg: str | None = None,
|
||||||
config: Config = Depends(get_config),
|
config: Config = Depends(get_config),
|
||||||
):
|
):
|
||||||
|
if config.general.unauthenticated_access == "all":
|
||||||
|
return RedirectResponse(
|
||||||
|
request.url_for("get_severity_counts_view"),
|
||||||
|
status_code=status.HTTP_303_SEE_OTHER,
|
||||||
|
)
|
||||||
|
|
||||||
token = request.cookies.get("access-token")
|
token = request.cookies.get("access-token")
|
||||||
if token is not None and token != "":
|
if token is not None and token != "":
|
||||||
manager = request.app.state.manager
|
manager = request.app.state.manager
|
||||||
|
@ -66,6 +73,12 @@ async def post_login(
|
||||||
rememberme: Annotated[str | None, Form()] = None,
|
rememberme: Annotated[str | None, Form()] = None,
|
||||||
config: Config = Depends(get_config),
|
config: Config = Depends(get_config),
|
||||||
):
|
):
|
||||||
|
if config.general.unauthenticated_access == "all":
|
||||||
|
return RedirectResponse(
|
||||||
|
request.url_for("get_severity_counts_view"),
|
||||||
|
status_code=status.HTTP_303_SEE_OTHER,
|
||||||
|
)
|
||||||
|
|
||||||
username = data.username
|
username = data.username
|
||||||
user = await queries.get_user(db, username)
|
user = await queries.get_user(db, username)
|
||||||
invalid_credentials = templates.TemplateResponse(
|
invalid_credentials = templates.TemplateResponse(
|
||||||
|
@ -103,7 +116,17 @@ async def post_login(
|
||||||
|
|
||||||
|
|
||||||
@route.get("/logout")
|
@route.get("/logout")
|
||||||
async def logout_view(request: Request, user: User | None = Depends(get_manager)):
|
async def logout_view(
|
||||||
|
request: Request,
|
||||||
|
config: Config = Depends(get_config),
|
||||||
|
user: User | None = Depends(get_manager),
|
||||||
|
):
|
||||||
|
if config.general.unauthenticated_access == "all":
|
||||||
|
return RedirectResponse(
|
||||||
|
request.url_for("get_severity_counts_view"),
|
||||||
|
status_code=status.HTTP_303_SEE_OTHER,
|
||||||
|
)
|
||||||
|
|
||||||
response = RedirectResponse(
|
response = RedirectResponse(
|
||||||
request.url_for("login_view").include_query_params(msg="logout"),
|
request.url_for("login_view").include_query_params(msg="logout"),
|
||||||
status_code=status.HTTP_303_SEE_OTHER,
|
status_code=status.HTTP_303_SEE_OTHER,
|
||||||
|
@ -133,6 +156,7 @@ async def get_severity_counts_view(
|
||||||
"agents": agents,
|
"agents": agents,
|
||||||
"auto_refresh_enabled": auto_refresh_enabled,
|
"auto_refresh_enabled": auto_refresh_enabled,
|
||||||
"auto_refresh_seconds": auto_refresh_seconds,
|
"auto_refresh_seconds": auto_refresh_seconds,
|
||||||
|
"user": user,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -141,9 +165,14 @@ async def get_severity_counts_view(
|
||||||
async def get_domains_view(
|
async def get_domains_view(
|
||||||
request: Request,
|
request: Request,
|
||||||
user: User | None = Depends(get_manager),
|
user: User | None = Depends(get_manager),
|
||||||
|
config: Config = Depends(get_config),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Show all tasks and their current state"""
|
"""Show all tasks and their current state"""
|
||||||
|
if config.general.unauthenticated_access == "dashboard":
|
||||||
|
if user is None:
|
||||||
|
raise NotAuthenticatedException
|
||||||
|
|
||||||
tasks = db.query(Task).all()
|
tasks = db.query(Task).all()
|
||||||
|
|
||||||
domains_severities = defaultdict(list)
|
domains_severities = defaultdict(list)
|
||||||
|
@ -184,6 +213,7 @@ async def get_domains_view(
|
||||||
"last_checks": domains_last_checks,
|
"last_checks": domains_last_checks,
|
||||||
"total_task_count": len(tasks),
|
"total_task_count": len(tasks),
|
||||||
"agents": agents,
|
"agents": agents,
|
||||||
|
"user": user,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -193,12 +223,23 @@ async def get_domain_tasks_view(
|
||||||
request: Request,
|
request: Request,
|
||||||
domain: str,
|
domain: str,
|
||||||
user: User | None = Depends(get_manager),
|
user: User | None = Depends(get_manager),
|
||||||
|
config: Config = Depends(get_config),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Show all tasks attached to a domain"""
|
"""Show all tasks attached to a domain"""
|
||||||
|
if config.general.unauthenticated_access == "dashboard":
|
||||||
|
if user is None:
|
||||||
|
raise NotAuthenticatedException
|
||||||
|
|
||||||
tasks = db.query(Task).filter(Task.domain.contains(f"//{domain}")).all()
|
tasks = db.query(Task).filter(Task.domain.contains(f"//{domain}")).all()
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"domain.html", {"request": request, "domain": domain, "tasks": tasks}
|
"domain.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"domain": domain,
|
||||||
|
"tasks": tasks,
|
||||||
|
"user": user,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -207,12 +248,23 @@ async def get_result_view(
|
||||||
request: Request,
|
request: Request,
|
||||||
result_id: int,
|
result_id: int,
|
||||||
user: User | None = Depends(get_manager),
|
user: User | None = Depends(get_manager),
|
||||||
|
config: Config = Depends(get_config),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Show the details of a result"""
|
"""Show the details of a result"""
|
||||||
|
if config.general.unauthenticated_access == "dashboard":
|
||||||
|
if user is None:
|
||||||
|
raise NotAuthenticatedException
|
||||||
|
|
||||||
result = db.query(Result).get(result_id)
|
result = db.query(Result).get(result_id)
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"result.html", {"request": request, "result": result, "error": Status.ERROR}
|
"result.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"result": result,
|
||||||
|
"error": Status.ERROR,
|
||||||
|
"user": user,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@ -225,6 +277,10 @@ async def get_task_results_view(
|
||||||
config: Config = Depends(get_config),
|
config: Config = Depends(get_config),
|
||||||
):
|
):
|
||||||
"""Show history of a task’s results"""
|
"""Show history of a task’s results"""
|
||||||
|
if config.general.unauthenticated_access == "dashboard":
|
||||||
|
if user is None:
|
||||||
|
raise NotAuthenticatedException
|
||||||
|
|
||||||
results = (
|
results = (
|
||||||
db.query(Result)
|
db.query(Result)
|
||||||
.filter(Result.task_id == task_id)
|
.filter(Result.task_id == task_id)
|
||||||
|
@ -243,6 +299,7 @@ async def get_task_results_view(
|
||||||
"task": task,
|
"task": task,
|
||||||
"description": description,
|
"description": description,
|
||||||
"error": Status.ERROR,
|
"error": Status.ERROR,
|
||||||
|
"user": user,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -251,9 +308,14 @@ async def get_task_results_view(
|
||||||
async def get_agents_view(
|
async def get_agents_view(
|
||||||
request: Request,
|
request: Request,
|
||||||
user: User | None = Depends(get_manager),
|
user: User | None = Depends(get_manager),
|
||||||
|
config: Config = Depends(get_config),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Show argos agents and the last time the server saw them"""
|
"""Show argos agents and the last time the server saw them"""
|
||||||
|
if config.general.unauthenticated_access == "dashboard":
|
||||||
|
if user is None:
|
||||||
|
raise NotAuthenticatedException
|
||||||
|
|
||||||
last_seen = (
|
last_seen = (
|
||||||
db.query(Result.agent_id, func.max(Result.submitted_at).label("submitted_at"))
|
db.query(Result.agent_id, func.max(Result.submitted_at).label("submitted_at"))
|
||||||
.group_by(Result.agent_id)
|
.group_by(Result.agent_id)
|
||||||
|
@ -261,7 +323,12 @@ async def get_agents_view(
|
||||||
)
|
)
|
||||||
|
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
"agents.html", {"request": request, "last_seen": last_seen}
|
"agents.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"last_seen": last_seen,
|
||||||
|
"user": user,
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -63,6 +63,8 @@
|
||||||
Agents
|
Agents
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% set unauthenticated_access = request.app.state.config.general.unauthenticated_access %}
|
||||||
|
{% if (user is defined and user is not none) or unauthenticated_access == "all" %}
|
||||||
<li>
|
<li>
|
||||||
<a href="#"
|
<a href="#"
|
||||||
id="reschedule-all"
|
id="reschedule-all"
|
||||||
|
@ -72,13 +74,24 @@
|
||||||
Reschedule non-ok checks
|
Reschedule non-ok checks
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
{% if user is defined and user is not none %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ url_for('logout_view') }}"
|
<a href="{{ url_for('logout_view') }}"
|
||||||
class="outline {{ 'contrast' if request.url == url_for('get_agents_view') }}"
|
class="outline }}"
|
||||||
role="button">
|
role="button">
|
||||||
Logout
|
Logout
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% elif unauthenticated_access != "all" %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ url_for('login_view') }}"
|
||||||
|
class="outline }}"
|
||||||
|
role="button">
|
||||||
|
Login
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
</li>
|
</li>
|
||||||
|
|
|
@ -276,7 +276,9 @@ Options:
|
||||||
|
|
||||||
### Server user management
|
### Server user management
|
||||||
|
|
||||||
To access Argos’ web interface, you need to create at least one user.
|
You can choose to protect Argos’ web interface with a user system, in which case you’ll need to create at least one user.
|
||||||
|
|
||||||
|
See [`unauthenticated_access` in the configuration file](configuration.md) to allow partial or total unauthenticated access to Argos.
|
||||||
|
|
||||||
You can manage users only through CLI.
|
You can manage users only through CLI.
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue