feat(jira): per-user auth, lifecycle hooks, admin config endpoints
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
- Add jira_api_token field to User model + migration b042 - Per-user Jira client: user's corporate email + personal Atlassian token - Admin-configurable Jira URL/project via system_configs (GET/PATCH /system/jira-config + POST /system/jira-test) - Auto-create Jira ticket when a test is created (non-fatal) - Push lifecycle comments on every state transition: draft→red_executing→blue_evaluating→in_review→validated/rejected→draft - Rich ticket descriptions with technique, MITRE ID, priority from severity, labels - UserOut.jira_token_set (bool) instead of exposing raw token - PATCH /users/me/preferences now accepts jira_api_token Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -201,6 +201,109 @@ def scheduler_status(
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Jira config endpoints (admin only)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class JiraConfigOut(BaseModel):
|
||||
enabled: bool
|
||||
url: str
|
||||
project_key: str
|
||||
# Credentials are never returned
|
||||
|
||||
|
||||
class JiraConfigUpdate(BaseModel):
|
||||
enabled: Optional[bool] = None
|
||||
url: Optional[str] = None
|
||||
project_key: Optional[str] = None
|
||||
|
||||
|
||||
_JIRA_KEYS = {
|
||||
"enabled": "jira.enabled",
|
||||
"url": "jira.url",
|
||||
"project_key": "jira.project_key",
|
||||
}
|
||||
|
||||
|
||||
@router.get("/jira-config", response_model=JiraConfigOut)
|
||||
def get_jira_config(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_role("admin")),
|
||||
):
|
||||
"""Return current Jira configuration (merged DB + env).
|
||||
|
||||
**Requires** the ``admin`` role. Credentials are never returned.
|
||||
"""
|
||||
from app.services.jira_service import get_jira_url, get_jira_project_key, is_jira_enabled
|
||||
|
||||
return JiraConfigOut(
|
||||
enabled=is_jira_enabled(db),
|
||||
url=get_jira_url(db) or "",
|
||||
project_key=get_jira_project_key(db) or "",
|
||||
)
|
||||
|
||||
|
||||
@router.patch("/jira-config", response_model=JiraConfigOut)
|
||||
def update_jira_config(
|
||||
payload: JiraConfigUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_role("admin")),
|
||||
):
|
||||
"""Update Jira configuration and persist to DB.
|
||||
|
||||
**Requires** the ``admin`` role. Only provided fields are updated.
|
||||
"""
|
||||
from app.services.jira_service import (
|
||||
upsert_jira_config, get_jira_url, get_jira_project_key, is_jira_enabled,
|
||||
)
|
||||
|
||||
update_data = payload.model_dump(exclude_unset=True)
|
||||
for field, val in update_data.items():
|
||||
db_key = _JIRA_KEYS.get(field)
|
||||
if db_key:
|
||||
upsert_jira_config(db, db_key, str(val))
|
||||
db.commit()
|
||||
|
||||
return JiraConfigOut(
|
||||
enabled=is_jira_enabled(db),
|
||||
url=get_jira_url(db) or "",
|
||||
project_key=get_jira_project_key(db) or "",
|
||||
)
|
||||
|
||||
|
||||
@router.post("/jira-test")
|
||||
def test_jira_connection(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(require_role("admin")),
|
||||
):
|
||||
"""Test the Jira connection using the current user's credentials.
|
||||
|
||||
Requires the admin to have a personal Jira API token configured in their
|
||||
profile settings.
|
||||
"""
|
||||
from app.services.jira_service import get_user_jira_client, get_jira_url
|
||||
|
||||
jira_url = get_jira_url(db)
|
||||
if not jira_url:
|
||||
raise HTTPException(status_code=400, detail="Jira URL not configured.")
|
||||
|
||||
try:
|
||||
jira = get_user_jira_client(current_user, db)
|
||||
# Lightweight call: get current user info
|
||||
myself = jira.myself()
|
||||
return {
|
||||
"status": "ok",
|
||||
"connected_as": myself.get("displayName") or myself.get("emailAddress", "unknown"),
|
||||
"jira_url": jira_url,
|
||||
}
|
||||
except Exception as exc:
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail=f"Jira connection failed: {exc}",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# GET /system/email-config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -145,6 +145,14 @@ def create_test(
|
||||
uow.commit()
|
||||
db.refresh(test)
|
||||
|
||||
# Auto-create Jira ticket (non-fatal — any failure is logged, not raised)
|
||||
try:
|
||||
from app.services.jira_service import auto_create_test_issue
|
||||
auto_create_test_issue(db, test, current_user)
|
||||
db.commit()
|
||||
except Exception:
|
||||
pass # jira_service already logs warnings internally
|
||||
|
||||
return test
|
||||
|
||||
|
||||
@@ -191,6 +199,14 @@ def create_test_from_template(
|
||||
uow.commit()
|
||||
db.refresh(test)
|
||||
|
||||
# Auto-create Jira ticket (non-fatal)
|
||||
try:
|
||||
from app.services.jira_service import auto_create_test_issue
|
||||
auto_create_test_issue(db, test, current_user)
|
||||
db.commit()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return test
|
||||
|
||||
|
||||
|
||||
@@ -33,10 +33,18 @@ def update_my_preferences(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Update the current user's notification preferences and Jira account ID."""
|
||||
"""Update the current user's notification preferences, Jira account ID and Jira API token.
|
||||
|
||||
Send ``jira_api_token: ""`` to clear a previously stored token.
|
||||
The token is never returned in any response.
|
||||
"""
|
||||
update_data = payload.model_dump(exclude_unset=True)
|
||||
for field, value in update_data.items():
|
||||
setattr(current_user, field, value)
|
||||
if field == "jira_api_token":
|
||||
# Empty string means "clear token"
|
||||
setattr(current_user, field, value if value else None)
|
||||
else:
|
||||
setattr(current_user, field, value)
|
||||
db.commit()
|
||||
db.refresh(current_user)
|
||||
return current_user
|
||||
|
||||
Reference in New Issue
Block a user