Coverage for .venv/lib/python3.13/site-packages/litellm/proxy/common_utils/html_forms/native_client_consent.py: 33%

14 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 12:01 +0000

1from collections.abc import Sequence 

2from html import escape 

3from typing import Final 

4 

5from litellm.constants import CLI_JWT_EXPIRATION_HOURS 

6 

7 

8def render_native_client_consent_page( 

9 *, 

10 client_origin: str, 

11 user_id: str, 

12 teams: Sequence[tuple[str, str]], 

13 flow_handle: str, 

14 complete_url: str, 

15) -> str: 

16 """The consent page a native client's sign-in lands on: who is signed in, which 

17 loopback client asked, which team the credential is attributed to, and an explicit 

18 Approve or Deny that POSTs back to ``complete_url``. Every value is client- or 

19 user-influenced and HTML-escaped; the flow handle travels only in the form body.""" 

20 return f"""<!DOCTYPE html> 

21<html lang="en"> 

22<head> 

23<meta charset="UTF-8"> 

24<meta name="viewport" content="width=device-width, initial-scale=1.0"> 

25<meta name="referrer" content="no-referrer"> 

26<title>Authorize CLI access - LiteLLM</title> 

27<style> 

28body {{ 

29 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; 

30 background-color: #f8fafc; 

31 margin: 0; 

32 padding: 20px; 

33 display: flex; 

34 justify-content: center; 

35 align-items: center; 

36 min-height: 100vh; 

37 color: #1e293b; 

38}} 

39.container {{ 

40 background-color: #fff; 

41 padding: 40px; 

42 border-radius: 8px; 

43 box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); 

44 width: 450px; 

45 max-width: 100%; 

46}} 

47h1 {{ margin: 0 0 16px; font-size: 24px; font-weight: 600; }} 

48p {{ margin: 0 0 12px; line-height: 1.5; }} 

49code {{ background: #f1f5f9; padding: 2px 6px; border-radius: 4px; }} 

50label {{ display: block; margin: 16px 0 6px; font-weight: 600; }} 

51select {{ width: 100%; padding: 8px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 14px; }} 

52.actions {{ display: flex; gap: 12px; margin-top: 24px; }} 

53button {{ flex: 1; padding: 10px; border-radius: 6px; font-size: 15px; cursor: pointer; border: 1px solid #cbd5e1; }} 

54.approve {{ background: #2563eb; color: #fff; border-color: #2563eb; }} 

55.deny {{ background: #fff; color: #1e293b; }} 

56</style> 

57</head> 

58<body> 

59<div class="container"> 

60<h1>Authorize CLI access</h1> 

61<p>A command-line client at <code>{escape(client_origin)}</code> wants to call LiteLLM as <strong>{escape(user_id)}</strong>.</p> 

62<p>Approving issues it a personal credential that expires within {CLI_JWT_EXPIRATION_HOURS} hours. <code>lite logout</code> stops it from being renewed. Only approve if you started this sign-in yourself.</p> 

63<form method="post" action="{escape(complete_url)}"> 

64<input type="hidden" name="flow" value="{escape(flow_handle)}"> 

65{_team_field(teams)} 

66<div class="actions"> 

67<button type="submit" name="decision" value="deny" class="deny">Deny</button> 

68<button type="submit" name="decision" value="approve" class="approve">Approve</button> 

69</div> 

70</form> 

71</div> 

72</body> 

73</html> 

74""" 

75 

76 

77def _team_field(teams: Sequence[tuple[str, str]]) -> str: 

78 if not teams: 

79 return "" 

80 if len(teams) == 1: 

81 team_id, team_label = teams[0] 

82 return ( 

83 f'<input type="hidden" name="team_id" value="{escape(team_id)}">' 

84 f"<p>Requests are attributed to team <strong>{escape(team_label)}</strong>.</p>" 

85 ) 

86 options: Final = "".join( 

87 f'<option value="{escape(team_id)}">{escape(team_label)}</option>' for team_id, team_label in teams 

88 ) 

89 return ( 

90 f'<label for="team_id">Attribute requests to team</label><select id="team_id" name="team_id">{options}</select>' 

91 )