mirror of
https://framagit.org/framasoft/framaspace/argos.git
synced 2025-05-06 05:31:51 +02:00
Compare commits
9 commits
da221b856b
...
a1600cb08e
Author | SHA1 | Date | |
---|---|---|---|
![]() |
a1600cb08e | ||
![]() |
0da1f4986e | ||
![]() |
1853b4fead | ||
![]() |
bb4db3ca84 | ||
![]() |
7d21d8d271 | ||
![]() |
868e91b866 | ||
![]() |
ffd24173e5 | ||
![]() |
594fbd6881 | ||
![]() |
04e33a8d24 |
18 changed files with 155 additions and 31 deletions
|
@ -1,3 +1,4 @@
|
||||||
|
---
|
||||||
image: python:3.11
|
image: python:3.11
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
|
@ -18,6 +19,9 @@ default:
|
||||||
|
|
||||||
install:
|
install:
|
||||||
stage: install
|
stage: install
|
||||||
|
before_script:
|
||||||
|
- apt-get update
|
||||||
|
- apt-get install -y build-essential libldap-dev libsasl2-dev
|
||||||
script:
|
script:
|
||||||
- make venv
|
- make venv
|
||||||
- make develop
|
- make develop
|
||||||
|
|
12
CHANGELOG.md
12
CHANGELOG.md
|
@ -2,6 +2,17 @@
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## 0.6.1
|
||||||
|
|
||||||
|
Date: 2024-11-28
|
||||||
|
|
||||||
|
- 🐛 - Fix database migrations without default values
|
||||||
|
- 🐛 - Fix domain status selector’s bug on page refresh
|
||||||
|
|
||||||
|
## 0.6.0
|
||||||
|
|
||||||
|
Date: 2024-11-28
|
||||||
|
|
||||||
- 💄 — Show only not-OK domains by default in domains list, to reduce the load on browser
|
- 💄 — Show only not-OK domains by default in domains list, to reduce the load on browser
|
||||||
- ♿️ — Fix not-OK domains display if javascript is disabled
|
- ♿️ — Fix not-OK domains display if javascript is disabled
|
||||||
- ✨ — Retry check right after a httpx.ReadError
|
- ✨ — Retry check right after a httpx.ReadError
|
||||||
|
@ -14,6 +25,7 @@
|
||||||
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)
|
- ✨🛂 — Allow partial or total anonymous access to web interface (#63)
|
||||||
|
- ✨🛂 — Allow to use a LDAP server for authentication (#64)
|
||||||
|
|
||||||
## 0.5.0
|
## 0.5.0
|
||||||
|
|
||||||
|
|
2
Makefile
2
Makefile
|
@ -10,7 +10,7 @@ NC=\033[0m # No Color
|
||||||
venv: ## Create the venv
|
venv: ## Create the venv
|
||||||
python3 -m venv venv
|
python3 -m venv venv
|
||||||
develop: venv ## Install the dev dependencies
|
develop: venv ## Install the dev dependencies
|
||||||
venv/bin/pip install -e ".[dev,docs]"
|
venv/bin/pip install -e ".[dev,docs,ldap]"
|
||||||
docs: cog ## Build the docs
|
docs: cog ## Build the docs
|
||||||
venv/bin/sphinx-build docs public
|
venv/bin/sphinx-build docs public
|
||||||
if [ ! -e "public/mermaid.min.js" ]; then curl -sL $$(grep mermaid.min.js public/search.html | cut -f 2 -d '"') --output public/mermaid.min.js; fi
|
if [ ! -e "public/mermaid.min.js" ]; then curl -sL $$(grep mermaid.min.js public/search.html | cut -f 2 -d '"') --output public/mermaid.min.js; fi
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
VERSION = "0.5.0"
|
VERSION = "0.6.1"
|
||||||
|
|
|
@ -36,6 +36,23 @@ general:
|
||||||
# If not present, all pages needs authentication
|
# If not present, all pages needs authentication
|
||||||
# unauthenticated_access: "all"
|
# unauthenticated_access: "all"
|
||||||
|
|
||||||
|
# LDAP authentication
|
||||||
|
# Instead of relying on Argos’ users, use a LDAP server to authenticate users.
|
||||||
|
# If not present, Argos’ native user system is used.
|
||||||
|
# ldap:
|
||||||
|
# # Server URI
|
||||||
|
# uri: "ldaps://ldap.example.org"
|
||||||
|
# # Search base DN
|
||||||
|
# user_tree: "ou=users,dc=example,dc=org"
|
||||||
|
# # Search bind DN
|
||||||
|
# bind_dn: "uid=ldap_user,ou=users,dc=example,dc=org"
|
||||||
|
# # Search bind password
|
||||||
|
# bind_pwd: "secr3t"
|
||||||
|
# # User attribute (uid, mail, sAMAccountName, etc.)
|
||||||
|
# user_attr: "uid"
|
||||||
|
# # User filter (to exclude some users, etc.)
|
||||||
|
# user_filter: "(!(uid=ldap_user))"
|
||||||
|
|
||||||
# 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:
|
||||||
|
|
|
@ -183,6 +183,15 @@ class DbSettings(BaseModel):
|
||||||
max_overflow: int = 20
|
max_overflow: int = 20
|
||||||
|
|
||||||
|
|
||||||
|
class LdapSettings(BaseModel):
|
||||||
|
uri: str
|
||||||
|
user_tree: str
|
||||||
|
bind_dn: str | None = None
|
||||||
|
bind_pwd: str | None = None
|
||||||
|
user_attr: str
|
||||||
|
user_filter: str | None = None
|
||||||
|
|
||||||
|
|
||||||
class General(BaseModel):
|
class General(BaseModel):
|
||||||
"""Frequency for the checks and alerts"""
|
"""Frequency for the checks and alerts"""
|
||||||
|
|
||||||
|
@ -192,6 +201,7 @@ class General(BaseModel):
|
||||||
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
|
unauthenticated_access: Unauthenticated | None = None
|
||||||
|
ldap: LdapSettings | None = None
|
||||||
frequency: float
|
frequency: float
|
||||||
recheck_delay: float | None = None
|
recheck_delay: float | None = None
|
||||||
root_path: str = ""
|
root_path: str = ""
|
||||||
|
|
|
@ -25,7 +25,7 @@ def get_icon_from_severity(severity: str) -> str:
|
||||||
return icon
|
return icon
|
||||||
|
|
||||||
|
|
||||||
def handle_alert(config: Config, result, task, severity, old_severity, request):
|
def handle_alert(config: Config, result, task, severity, old_severity, request): # pylint: disable-msg=too-many-positional-arguments
|
||||||
"""Dispatch alert through configured alert channels"""
|
"""Dispatch alert through configured alert channels"""
|
||||||
|
|
||||||
if "local" in getattr(config.general.alerts, severity):
|
if "local" in getattr(config.general.alerts, severity):
|
||||||
|
@ -64,7 +64,7 @@ def handle_alert(config: Config, result, task, severity, old_severity, request):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def notify_with_apprise(
|
def notify_with_apprise( # pylint: disable-msg=too-many-positional-arguments
|
||||||
result, task, severity: str, old_severity: str, group: List[str], request
|
result, task, severity: str, old_severity: str, group: List[str], request
|
||||||
) -> None:
|
) -> None:
|
||||||
logger.debug("Will send apprise notification")
|
logger.debug("Will send apprise notification")
|
||||||
|
@ -90,7 +90,7 @@ See results of task on {request.url_for('get_task_results_view', task_id=task.id
|
||||||
apobj.notify(title=title, body=msg)
|
apobj.notify(title=title, body=msg)
|
||||||
|
|
||||||
|
|
||||||
def notify_by_mail(
|
def notify_by_mail( # pylint: disable-msg=too-many-positional-arguments
|
||||||
result, task, severity: str, old_severity: str, config: Mail, request
|
result, task, severity: str, old_severity: str, config: Mail, request
|
||||||
) -> None:
|
) -> None:
|
||||||
logger.debug("Will send mail notification")
|
logger.debug("Will send mail notification")
|
||||||
|
@ -137,7 +137,7 @@ See results of task on {request.url_for('get_task_results_view', task_id=task.id
|
||||||
smtp.send_message(mail, to_addrs=address)
|
smtp.send_message(mail, to_addrs=address)
|
||||||
|
|
||||||
|
|
||||||
def notify_with_gotify(
|
def notify_with_gotify( # pylint: disable-msg=too-many-positional-arguments
|
||||||
result, task, severity: str, old_severity: str, config: List[GotifyUrl], request
|
result, task, severity: str, old_severity: str, config: List[GotifyUrl], request
|
||||||
) -> None:
|
) -> None:
|
||||||
logger.debug("Will send gotify notification")
|
logger.debug("Will send gotify notification")
|
||||||
|
|
|
@ -36,13 +36,25 @@ def get_application() -> FastAPI:
|
||||||
appli.add_exception_handler(NotAuthenticatedException, auth_exception_handler)
|
appli.add_exception_handler(NotAuthenticatedException, auth_exception_handler)
|
||||||
appli.state.manager = create_manager(config.general.cookie_secret)
|
appli.state.manager = create_manager(config.general.cookie_secret)
|
||||||
|
|
||||||
|
if config.general.ldap is not None:
|
||||||
|
import ldap
|
||||||
|
|
||||||
|
l = ldap.initialize(config.general.ldap.uri)
|
||||||
|
l.simple_bind_s(config.general.ldap.bind_dn, config.general.ldap.bind_pwd)
|
||||||
|
appli.state.ldap = l
|
||||||
|
|
||||||
@appli.state.manager.user_loader()
|
@appli.state.manager.user_loader()
|
||||||
async def query_user(user: str) -> None | models.User:
|
async def query_user(user: str) -> None | str | models.User:
|
||||||
"""
|
"""
|
||||||
Get a user from the db
|
Get a user from the db or LDAP
|
||||||
:param user: name of the user
|
:param user: name of the user
|
||||||
:return: None or the user object
|
:return: None or the user object
|
||||||
"""
|
"""
|
||||||
|
if appli.state.config.general.ldap is not None:
|
||||||
|
from argos.server.routes.dependencies import find_ldap_user
|
||||||
|
|
||||||
|
return await find_ldap_user(appli.state.config, appli.state.ldap, user)
|
||||||
|
|
||||||
return await queries.get_user(appli.state.db, user)
|
return await queries.get_user(appli.state.db, user)
|
||||||
|
|
||||||
appli.include_router(routes.api, prefix="/api")
|
appli.include_router(routes.api, prefix="/api")
|
||||||
|
|
|
@ -21,7 +21,14 @@ depends_on: Union[str, Sequence[str], None] = None
|
||||||
def upgrade() -> None:
|
def upgrade() -> None:
|
||||||
with op.batch_alter_table("tasks", schema=None) as batch_op:
|
with op.batch_alter_table("tasks", schema=None) as batch_op:
|
||||||
batch_op.add_column(sa.Column("recheck_delay", sa.Float(), nullable=True))
|
batch_op.add_column(sa.Column("recheck_delay", sa.Float(), nullable=True))
|
||||||
batch_op.add_column(sa.Column("already_retried", sa.Boolean(), nullable=False))
|
batch_op.add_column(
|
||||||
|
sa.Column(
|
||||||
|
"already_retried",
|
||||||
|
sa.Boolean(),
|
||||||
|
nullable=False,
|
||||||
|
server_default=sa.sql.false(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def downgrade() -> None:
|
def downgrade() -> None:
|
||||||
|
|
|
@ -36,6 +36,7 @@ def upgrade() -> None:
|
||||||
name="method",
|
name="method",
|
||||||
),
|
),
|
||||||
nullable=False,
|
nullable=False,
|
||||||
|
server_default="GET",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ async def read_tasks(
|
||||||
|
|
||||||
|
|
||||||
@route.post("/results", status_code=201, dependencies=[Depends(verify_token)])
|
@route.post("/results", status_code=201, dependencies=[Depends(verify_token)])
|
||||||
async def create_results(
|
async def create_results( # pylint: disable-msg=too-many-positional-arguments
|
||||||
request: Request,
|
request: Request,
|
||||||
results: List[AgentResult],
|
results: List[AgentResult],
|
||||||
background_tasks: BackgroundTasks,
|
background_tasks: BackgroundTasks,
|
||||||
|
|
|
@ -31,3 +31,28 @@ async def verify_token(
|
||||||
if token.credentials not in request.app.state.config.service.secrets:
|
if token.credentials not in request.app.state.config.service.secrets:
|
||||||
raise HTTPException(status_code=401, detail="Unauthorized")
|
raise HTTPException(status_code=401, detail="Unauthorized")
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
async def find_ldap_user(config, ldap, user: str) -> str | None:
|
||||||
|
"""Do a LDAP search for user and return its dn"""
|
||||||
|
import ldap.filter as ldap_filter
|
||||||
|
from ldapurl import LDAP_SCOPE_SUBTREE
|
||||||
|
|
||||||
|
result = ldap.search_s(
|
||||||
|
config.general.ldap.user_tree,
|
||||||
|
LDAP_SCOPE_SUBTREE,
|
||||||
|
filterstr=ldap_filter.filter_format(
|
||||||
|
f"(&(%s=%s){config.general.ldap.user_filter})",
|
||||||
|
[
|
||||||
|
config.general.ldap.user_attr,
|
||||||
|
user,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
attrlist=[config.general.ldap.user_attr],
|
||||||
|
)
|
||||||
|
|
||||||
|
# If there is a result, there should, logically, be only one entry
|
||||||
|
if len(result) > 0:
|
||||||
|
return result[0][0]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
|
@ -80,11 +80,25 @@ async def post_login(
|
||||||
)
|
)
|
||||||
|
|
||||||
username = data.username
|
username = data.username
|
||||||
user = await queries.get_user(db, username)
|
|
||||||
invalid_credentials = templates.TemplateResponse(
|
invalid_credentials = templates.TemplateResponse(
|
||||||
"login.html",
|
"login.html",
|
||||||
{"request": request, "msg": "Sorry, invalid username or bad password."},
|
{"request": request, "msg": "Sorry, invalid username or bad password."},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if config.general.ldap is not None:
|
||||||
|
from ldap import INVALID_CREDENTIALS # pylint: disable-msg=no-name-in-module
|
||||||
|
from argos.server.routes.dependencies import find_ldap_user
|
||||||
|
|
||||||
|
ldap_dn = await find_ldap_user(config, request.app.state.ldap, username)
|
||||||
|
if ldap_dn is None:
|
||||||
|
return invalid_credentials
|
||||||
|
try:
|
||||||
|
request.app.state.ldap.simple_bind_s(ldap_dn, data.password)
|
||||||
|
except INVALID_CREDENTIALS:
|
||||||
|
return invalid_credentials
|
||||||
|
else:
|
||||||
|
user = await queries.get_user(db, username)
|
||||||
if user is None:
|
if user is None:
|
||||||
return invalid_credentials
|
return invalid_credentials
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,7 @@
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
function filterDomains(e) {
|
function filterDomains() {
|
||||||
let status = document.getElementById('select-status');
|
let status = document.getElementById('select-status');
|
||||||
let filter = document.getElementById('domain-search').value;
|
let filter = document.getElementById('domain-search').value;
|
||||||
console.log(filter)
|
console.log(filter)
|
||||||
|
@ -111,13 +111,7 @@
|
||||||
}
|
}
|
||||||
document.getElementById('select-status').addEventListener('change', filterDomains);
|
document.getElementById('select-status').addEventListener('change', filterDomains);
|
||||||
document.getElementById('domain-search').addEventListener('input', filterDomains);
|
document.getElementById('domain-search').addEventListener('input', filterDomains);
|
||||||
document.querySelectorAll('[data-status]').forEach((item) => {
|
filterDomains()
|
||||||
if (item.dataset.status !== 'ok') {
|
|
||||||
item.style.display = null;
|
|
||||||
} else {
|
|
||||||
item.style.display = 'none';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
document.getElementById('js-only').style.display = null;
|
document.getElementById('js-only').style.display = null;
|
||||||
</script>
|
</script>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|
|
@ -280,7 +280,11 @@ You can choose to protect Argos’ web interface with a user system, in which ca
|
||||||
|
|
||||||
See [`unauthenticated_access` in the configuration file](configuration.md) to allow partial or total unauthenticated access to Argos.
|
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.
|
See [`ldap` in the configuration file](configuration.md) to authenticate users against a LDAP server instead of Argos’ database.
|
||||||
|
|
||||||
|
You can manage Argos’ users only through CLI.
|
||||||
|
|
||||||
|
NB: you can’t manage the LDAP users with Argos.
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
.. [[[cog
|
.. [[[cog
|
||||||
|
|
|
@ -10,6 +10,14 @@ NB: if you want a quick-installation guide, we [got you covered](tl-dr.md).
|
||||||
- Python 3.11+
|
- Python 3.11+
|
||||||
- PostgreSQL 13+ (for production)
|
- PostgreSQL 13+ (for production)
|
||||||
|
|
||||||
|
### Optional dependencies
|
||||||
|
|
||||||
|
If you want to use LDAP authentication, you will need to install some packages (here for a Debian-based system):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
apt-get install build-essential python3-dev libldap-dev libsasl2-dev
|
||||||
|
```
|
||||||
|
|
||||||
## Recommendation
|
## Recommendation
|
||||||
|
|
||||||
Create a dedicated user for argos:
|
Create a dedicated user for argos:
|
||||||
|
@ -45,6 +53,18 @@ For production, we recommend the use of [Gunicorn](https://gunicorn.org/), which
|
||||||
pip install "argos-monitoring[gunicorn]"
|
pip install "argos-monitoring[gunicorn]"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If you want to use LDAP authentication, you’ll need to install Argos this way:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install "argos-monitoring[ldap]"
|
||||||
|
```
|
||||||
|
|
||||||
|
And for an installation with Gunicorn and LDAP authentication:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install "argos-monitoring[gunicorn,ldap]"
|
||||||
|
```
|
||||||
|
|
||||||
## Install from sources
|
## Install from sources
|
||||||
|
|
||||||
Once you got the source locally, create a virtualenv and install the dependencies:
|
Once you got the source locally, create a virtualenv and install the dependencies:
|
||||||
|
|
|
@ -28,7 +28,7 @@ dependencies = [
|
||||||
"durations-nlp>=1.0.1,<2",
|
"durations-nlp>=1.0.1,<2",
|
||||||
"fastapi>=0.103,<0.104",
|
"fastapi>=0.103,<0.104",
|
||||||
"fastapi-login>=1.10.0,<2",
|
"fastapi-login>=1.10.0,<2",
|
||||||
"httpx>=0.27.2,<1",
|
"httpx>=0.27.2,<0.28.0",
|
||||||
"Jinja2>=3.0,<4",
|
"Jinja2>=3.0,<4",
|
||||||
"jsonpointer>=3.0,<4",
|
"jsonpointer>=3.0,<4",
|
||||||
"passlib>=1.7.4,<2",
|
"passlib>=1.7.4,<2",
|
||||||
|
@ -48,12 +48,12 @@ dependencies = [
|
||||||
dev = [
|
dev = [
|
||||||
"black==23.3.0",
|
"black==23.3.0",
|
||||||
"djlint>=1.34.0",
|
"djlint>=1.34.0",
|
||||||
"hatch==1.9.4",
|
"hatch==1.13.0",
|
||||||
"ipdb>=0.13,<0.14",
|
"ipdb>=0.13,<0.14",
|
||||||
"ipython>=8.16,<9",
|
"ipython>=8.16,<9",
|
||||||
"isort==5.11.5",
|
"isort==5.11.5",
|
||||||
"mypy>=1.10.0,<2",
|
"mypy>=1.10.0,<2",
|
||||||
"pylint>=3.0.2",
|
"pylint>=3.2.5",
|
||||||
"pytest-asyncio>=0.21,<1",
|
"pytest-asyncio>=0.21,<1",
|
||||||
"pytest>=6.2.5",
|
"pytest>=6.2.5",
|
||||||
"respx>=0.20,<1",
|
"respx>=0.20,<1",
|
||||||
|
@ -72,6 +72,9 @@ docs = [
|
||||||
gunicorn = [
|
gunicorn = [
|
||||||
"gunicorn>=21.2,<22",
|
"gunicorn>=21.2,<22",
|
||||||
]
|
]
|
||||||
|
ldap = [
|
||||||
|
"python-ldap>=3.4.4,<4",
|
||||||
|
]
|
||||||
|
|
||||||
[project.urls]
|
[project.urls]
|
||||||
homepage = "https://argos-monitoring.framasoft.org/"
|
homepage = "https://argos-monitoring.framasoft.org/"
|
||||||
|
|
|
@ -60,6 +60,7 @@ def ssl_task(db):
|
||||||
task = models.Task(
|
task = models.Task(
|
||||||
url="https://exemple.com/",
|
url="https://exemple.com/",
|
||||||
domain="https://exemple.com/",
|
domain="https://exemple.com/",
|
||||||
|
method="GET",
|
||||||
check="ssl-certificate-expiration",
|
check="ssl-certificate-expiration",
|
||||||
expected="on-check",
|
expected="on-check",
|
||||||
frequency=1,
|
frequency=1,
|
||||||
|
|
Loading…
Reference in a new issue