Skip to content

Commit

Permalink
feat(asm): update the ATO support with the new specifications (#11932)
Browse files Browse the repository at this point in the history
- Add support for ATO V3 RFC by adding missing tags
- Update unit threat tests accordingly

APPSEC-56315

Once merged, this will also be tested with ATO V3 system tests.
DataDog/system-tests#3828

## Checklist
- [x] PR author has checked that all the criteria below are met
- The PR description includes an overview of the change
- The PR description articulates the motivation for the change
- The change includes tests OR the PR description describes a testing
strategy
- The PR description notes risks associated with the change, if any
- Newly-added code is easy to change
- The change follows the [library release note
guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html)
- The change includes or references documentation updates if necessary
- Backport labels are set (if
[applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting))

## Reviewer Checklist
- [x] Reviewer has checked that all the criteria below are met 
- Title is accurate
- All changes are related to the pull request's stated goal
- Avoids breaking
[API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces)
changes
- Testing strategy adequately addresses listed risks
- Newly-added code is easy to change
- Release note makes sense to a user of the library
- If necessary, author has acknowledged and discussed the performance
implications of this PR as reported in the benchmarks PR comment
- Backport labels are set in a manner that is consistent with the
[release branch maintenance
policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)
  • Loading branch information
christophe-papazian authored Jan 16, 2025
1 parent 87a74ae commit d696c67
Show file tree
Hide file tree
Showing 6 changed files with 56 additions and 38 deletions.
2 changes: 2 additions & 0 deletions ddtrace/appsec/_constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ class APPSEC(metaclass=Constant_Class):
CUSTOM_EVENT_PREFIX: Literal["appsec.events"] = "appsec.events"
USER_LOGIN_EVENT_PREFIX: Literal["_dd.appsec.events.users.login"] = "_dd.appsec.events.users.login"
USER_LOGIN_EVENT_PREFIX_PUBLIC: Literal["appsec.events.users.login"] = "appsec.events.users.login"
USER_LOGIN_USERID: Literal["_dd.appsec.usr.id"] = "_dd.appsec.usr.id"
USER_LOGIN_USERNAME: Literal["_dd.appsec.usr.login"] = "_dd.appsec.usr.login"
USER_LOGIN_EVENT_SUCCESS_TRACK: Literal[
"appsec.events.users.login.success.track"
] = "appsec.events.users.login.success.track"
Expand Down
59 changes: 31 additions & 28 deletions ddtrace/appsec/_trace_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ def _track_user_login_common(
span.set_tag_str("%s.%s" % (tag_metadata_prefix, k), str(v))

if login:
span.set_tag_str(f"{APPSEC.USER_LOGIN_EVENT_PREFIX_PUBLIC}.{success_str}.usr.login", login)
if login_events_mode != LOGIN_EVENTS_MODE.SDK:
span.set_tag_str(APPSEC.USER_LOGIN_USERNAME, login)
span.set_tag_str("%s.login" % tag_prefix, login)

if email:
Expand Down Expand Up @@ -130,6 +133,8 @@ def track_user_login_success_event(
if in_asm_context():
call_waf_callback(custom_data={"REQUEST_USER_ID": str(user_id), "LOGIN_SUCCESS": real_mode})

if login_events_mode != LOGIN_EVENTS_MODE.SDK:
span.set_tag_str(APPSEC.USER_LOGIN_USERID, str(user_id))
set_user(tracer, user_id, name, email, scope, role, session_id, propagate, span)


Expand All @@ -154,7 +159,7 @@ def track_user_login_failure_event(
real_mode = login_events_mode if login_events_mode != LOGIN_EVENTS_MODE.AUTO else asm_config._user_event_mode
if real_mode == LOGIN_EVENTS_MODE.DISABLED:
return
span = _track_user_login_common(tracer, False, metadata, login_events_mode)
span = _track_user_login_common(tracer, False, metadata, login_events_mode, login)
if not span:
return
if exists is not None:
Expand All @@ -163,6 +168,8 @@ def track_user_login_failure_event(
if user_id:
if real_mode == LOGIN_EVENTS_MODE.ANON and isinstance(user_id, str):
user_id = _hash_user_id(user_id)
if login_events_mode != LOGIN_EVENTS_MODE.SDK:
span.set_tag_str(APPSEC.USER_LOGIN_USERID, str(user_id))
span.set_tag_str("%s.failure.%s" % (APPSEC.USER_LOGIN_EVENT_PREFIX_PUBLIC, user.ID), str(user_id))
# if called from the SDK, set the login, email and name
if login_events_mode in (LOGIN_EVENTS_MODE.SDK, LOGIN_EVENTS_MODE.AUTO):
Expand All @@ -183,7 +190,7 @@ def track_user_signup_event(
if span:
success_str = "true" if success else "false"
span.set_tag_str(APPSEC.USER_SIGNUP_EVENT, success_str)
span.set_tag_str(user.ID, user_id)
span.set_tag_str(user.ID, str(user_id))
_asm_manual_keep(span)

# This is used to mark if the call was done from the SDK of the automatic login events
Expand Down Expand Up @@ -295,23 +302,16 @@ def block_request_if_user_blocked(tracer: Tracer, userid: str) -> None:
_asm_request_context.block_request()


def _on_django_login(
pin,
request,
user,
mode,
info_retriever,
django_config,
):
def _on_django_login(pin, request, user, mode, info_retriever, django_config):
if user:
from ddtrace.contrib.internal.django.compat import user_is_authenticated

user_id, user_extra = info_retriever.get_user_info(
login=django_config.include_user_login,
email=django_config.include_user_email,
name=django_config.include_user_realname,
)
if user_is_authenticated(user):
user_id, user_extra = info_retriever.get_user_info(
login=django_config.include_user_login,
email=django_config.include_user_email,
name=django_config.include_user_realname,
)
with pin.tracer.trace("django.contrib.auth.login", span_type=SpanTypes.AUTH):
session_key = getattr(request, "session_key", None)
track_user_login_success_event(
Expand All @@ -324,8 +324,10 @@ def _on_django_login(
)
else:
# Login failed and the user is unknown (may exist or not)
user_id = info_retriever.get_userid()
track_user_login_failure_event(pin.tracer, user_id=user_id, login_events_mode=mode)
# DEV: DEAD CODE?
track_user_login_failure_event(
pin.tracer, user_id=user_id, login_events_mode=mode, login=user_extra.get("login", None)
)


def _on_django_auth(result_user, mode, kwargs, pin, info_retriever, django_config):
Expand All @@ -344,17 +346,18 @@ def _on_django_auth(result_user, mode, kwargs, pin, info_retriever, django_confi
if not result_user:
with pin.tracer.trace("django.contrib.auth.login", span_type=SpanTypes.AUTH):
exists = info_retriever.user_exists()
if exists:
user_id, user_extra = info_retriever.get_user_info(
login=django_config.include_user_login,
email=django_config.include_user_email,
name=django_config.include_user_realname,
)
track_user_login_failure_event(
pin.tracer, user_id=user_id, login_events_mode=mode, exists=True, **user_extra
)
else:
track_user_login_failure_event(pin.tracer, user_id=user_id, login_events_mode=mode, exists=False)
user_id_found, user_extra = info_retriever.get_user_info(
login=django_config.include_user_login,
email=django_config.include_user_email,
name=django_config.include_user_realname,
)
if user_extra.get("login") is None:
user_extra["login"] = user_id
user_id = user_id_found or user_id

track_user_login_failure_event(
pin.tracer, user_id=user_id, login_events_mode=mode, exists=exists, **user_extra
)

return False, None

Expand Down
4 changes: 4 additions & 0 deletions releasenotes/notes/ATO_V3-e7f73ecf00d1474b.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
features:
- |
ASM: This introduces full support for Automated user lifecycle tracking for login events (success and failure)
12 changes: 7 additions & 5 deletions tests/appsec/contrib_appsec/fastapi_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -235,23 +235,25 @@ def authenticate(username: str, password: str) -> Optional[str]:
return USERS[username]["id"]
else:
appsec_trace_utils.track_user_login_failure_event(
tracer, user_id=USERS[username]["id"], exists=True, login_events_mode="auto"
tracer, user_id=USERS[username]["id"], exists=True, login_events_mode="auto", login=username
)
return None
appsec_trace_utils.track_user_login_failure_event(
tracer, user_id=username, exists=False, login_events_mode="auto"
tracer, user_id=username, exists=False, login_events_mode="auto", login=username
)
return None

def login(user_id: str) -> None:
def login(user_id: str, username: str) -> None:
"""login user"""
appsec_trace_utils.track_user_login_success_event(tracer, user_id=user_id, login_events_mode="auto")
appsec_trace_utils.track_user_login_success_event(
tracer, user_id=user_id, login_events_mode="auto", login=username
)

username = request.query_params.get("username")
password = request.query_params.get("password")
user_id = authenticate(username=username, password=password)
if user_id is not None:
login(user_id)
login(user_id, username)
return HTMLResponse("OK")
return HTMLResponse("login failure", status_code=401)

Expand Down
12 changes: 7 additions & 5 deletions tests/appsec/contrib_appsec/flask_app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,22 +188,24 @@ def authenticate(username: str, password: str) -> Optional[str]:
return USERS[username]["id"]
else:
appsec_trace_utils.track_user_login_failure_event(
tracer, user_id=USERS[username]["id"], exists=True, login_events_mode="auto"
tracer, user_id=USERS[username]["id"], exists=True, login_events_mode="auto", login=username
)
return None
appsec_trace_utils.track_user_login_failure_event(
tracer, user_id=username, exists=False, login_events_mode="auto"
tracer, user_id=username, exists=False, login_events_mode="auto", login=username
)
return None

def login(user_id: str) -> None:
def login(user_id: str, login: str) -> None:
"""login user"""
appsec_trace_utils.track_user_login_success_event(tracer, user_id=user_id, login_events_mode="auto")
appsec_trace_utils.track_user_login_success_event(
tracer, user_id=user_id, login_events_mode="auto", login=login
)

username = request.args.get("username")
password = request.args.get("password")
user_id = authenticate(username=username, password=password)
if user_id is not None:
login(user_id)
login(user_id, username)
return "OK"
return "login failure", 401
5 changes: 5 additions & 0 deletions tests/appsec/contrib_appsec/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1478,9 +1478,14 @@ def test_auto_user_events(
assert get_tag("_dd.appsec.events.users.login.failure.sdk") == "true"
else:
assert get_tag("_dd.appsec.events.users.login.success.sdk") is None
if mode == "identification":
assert get_tag("_dd.appsec.usr.login") == user
else:
assert get_tag("appsec.events.users.login.success.track") == "true"
assert get_tag("usr.id") == user_id_hash
assert get_tag("_dd.appsec.usr.id") == user_id_hash
if mode == "identification":
assert get_tag("_dd.appsec.usr.login") == user
# check for manual instrumentation tag in manual instrumented frameworks
if interface.name in ["flask", "fastapi"]:
assert get_tag("_dd.appsec.events.users.login.success.sdk") == "true"
Expand Down

0 comments on commit d696c67

Please sign in to comment.