A corporation manager and dashboard for EVE Online
Nevar pievienot vairāk kā 25 tēmas Tēmai ir jāsākas ar burtu vai ciparu, tā var saturēt domu zīmes ('-') un var būt līdz 35 simboliem gara.
 
 
 
 
 

241 rinda
8.1 KiB

  1. # -*- coding: utf-8 -*-
  2. from datetime import datetime, timedelta
  3. from flask import g, session, url_for
  4. from itsdangerous import BadSignature, URLSafeSerializer
  5. from .exceptions import AccessDeniedError
  6. __all__ = ["AuthManager"]
  7. _SCOPES = ["publicData", "characterAssetsRead"] # ...
  8. class AuthManager:
  9. """Authentication manager. Handles user access and management."""
  10. EXPIRY_THRESHOLD = 30
  11. def __init__(self, config, eve):
  12. self._config = config
  13. self._eve = eve
  14. def _get_session_id(self):
  15. """Return the current session ID, allocating a new one if necessary."""
  16. if "id" not in session:
  17. session["id"] = g.db.new_session()
  18. g._session_checked = True
  19. g._session_expired = False
  20. return session["id"]
  21. def _invalidate_session(self):
  22. """Mark the current session as invalid.
  23. Remove it from the database and from the user's cookies.
  24. """
  25. if "id" in session:
  26. g.db.drop_session(session["id"])
  27. del session["id"]
  28. def _check_session(self):
  29. """Return whether the user has a valid, non-expired session.
  30. This checks for the session existing in the database, but does not
  31. check that the user is logged in or has any particular access roles.
  32. """
  33. if "id" not in session:
  34. return False
  35. if hasattr(g, "_session_checked"):
  36. return g._session_checked
  37. g._session_checked = check = g.db.has_session(session["id"])
  38. if not check:
  39. g._session_expired = True
  40. self._invalidate_session()
  41. return check
  42. def _get_state_hash(self):
  43. """Return a hash of the user's session ID suitable for OAuth2 state.
  44. Allocates a new session ID if necessary.
  45. """
  46. key = self._config.get("auth.session_key")
  47. serializer = URLSafeSerializer(key)
  48. return serializer.dumps(self._get_session_id())
  49. def _verify_state_hash(self, state):
  50. """Confirm that a state hash is correct for the user's session.
  51. Assumes we've already checked the session ID. If the state is invalid,
  52. the session will be invalidated.
  53. """
  54. key = self._config.get("auth.session_key")
  55. serializer = URLSafeSerializer(key)
  56. try:
  57. value = serializer.loads(state)
  58. except BadSignature:
  59. self._invalidate_session()
  60. return False
  61. if value != session["id"]:
  62. self._invalidate_session()
  63. return False
  64. return True
  65. def _fetch_new_token(self, code, refresh=False):
  66. """Given an auth code or refresh token, get a new token and other data.
  67. If refresh is True, code should be a refresh token, otherwise an auth
  68. code. If successful, we'll return a 5-tuple of (access_token,
  69. token_expiry, refresh_token, char_id, char_name). If the token was
  70. invalid, we'll return None. We may also raise EVEAPIError if there was
  71. an internal API error.
  72. """
  73. cid = self._config.get("auth.client_id")
  74. secret = self._config.get("auth.client_secret")
  75. result = self._eve.sso.get_access_token(cid, secret, code, refresh)
  76. if not result:
  77. return None
  78. token, expiry, refresh = result
  79. expires = datetime.utcnow() + timedelta(seconds=expiry)
  80. result = self._eve.sso.get_character_info(token)
  81. if not result:
  82. return None
  83. char_id, char_name = result
  84. return token, expires, refresh, char_id, char_name
  85. def _get_token(self, cid):
  86. """Return a valid access token for the given character, or None.
  87. If the database doesn't have an auth entry for this character, return
  88. None. If the database's token is expired but the refresh token is
  89. valid, then refresh it, update the database, and return the new token.
  90. If the token has become invalid and couldn't be refreshed, drop the
  91. auth information from the database and return None.
  92. """
  93. result = g.db.get_auth(cid)
  94. if not result:
  95. return None
  96. token, expires, refresh = result
  97. seconds_til_expiry = (expires - datetime.utcnow()).total_seconds()
  98. if seconds_til_expiry >= self.EXPIRY_THRESHOLD:
  99. return token
  100. result = self._fetch_new_token(refresh, refresh=True)
  101. if not result:
  102. g.db.drop_auth(cid)
  103. return None
  104. token, expires, refresh, char_id, char_name = result
  105. if char_id != cid:
  106. g.db.drop_auth(cid)
  107. return None
  108. g.db.put_character(cid, char_name)
  109. g.db.update_auth(cid, token, expires, refresh)
  110. return token
  111. def _check_access(self, token, char_id):
  112. """"Check whether the given character is allowed to access this site.
  113. If allowed, do nothing. If not, raise AccessDeniedError.
  114. """
  115. resp = self._eve.esi(token).v3.characters(char_id).get()
  116. if resp.get("corporation_id") != self._config.get("corp.id"):
  117. g.db.drop_auth(char_id)
  118. self._invalidate_session()
  119. raise AccessDeniedError()
  120. def get_character_id(self):
  121. """Return the character ID associated with the current session.
  122. Returns None if the session is invalid or is not associated with a
  123. character.
  124. """
  125. if not self._check_session():
  126. return None
  127. if not hasattr(g, "_character_id"):
  128. g._character_id = g.db.read_session(session["id"])
  129. return g._character_id
  130. def get_character_prop(self, prop):
  131. """Look up a property for the current session's character.
  132. Returns None if the session is invalid, is not associated with a
  133. character, or the property has no non-default value.
  134. """
  135. cid = self.get_character_id()
  136. if not cid:
  137. return None
  138. if not hasattr(g, "_character_props"):
  139. g._character_props = g.db.read_character(cid)
  140. return g._character_props.get(prop)
  141. def is_authenticated(self):
  142. """Return whether the user has permission to access this site.
  143. We confirm that they have a valid, non-expired session that is
  144. associated with a character that is permitted to be here.
  145. EVEAPIError or AccessDeniedError may be raised.
  146. """
  147. cid = self.get_character_id()
  148. if not cid:
  149. return False
  150. token = self._get_token(cid)
  151. if not token:
  152. self._invalidate_session()
  153. return False
  154. self._check_access(token, cid)
  155. g.db.touch_session(session["id"])
  156. return True
  157. def make_login_link(self):
  158. """Return a complete EVE SSO link that the user can use to log in."""
  159. cid = self._config.get("auth.client_id")
  160. target = url_for("login", _external=True, _scheme=self._config.scheme)
  161. scopes = _SCOPES
  162. state = self._get_state_hash()
  163. return self._eve.sso.get_authorize_url(cid, target, scopes, state)
  164. def handle_login(self, code, state):
  165. """Given an OAuth2 code and state, try to authenticate the user.
  166. If the user has a legitimate session and the state is valid, we'll
  167. check the code with EVE SSO to fetch an authentication token. If the
  168. token corresponds to a character that is allowed to access the site,
  169. we'll update their session to indicate so.
  170. Return whether authentication was successful. EVEAPIError or
  171. AccessDeniedError may be raised.
  172. """
  173. if not code or not state:
  174. return False
  175. if not self._check_session():
  176. return False
  177. if not self._verify_state_hash(state):
  178. return False
  179. result = self._fetch_new_token(code)
  180. if not result:
  181. self._invalidate_session()
  182. return False
  183. token, expires, refresh, char_id, char_name = result
  184. self._check_access(token, char_id)
  185. sid = session["id"]
  186. g.db.put_character(char_id, char_name)
  187. g.db.set_auth(char_id, token, expires, refresh)
  188. g.db.attach_session(sid, char_id)
  189. g.db.touch_session(sid)
  190. return True