def auto_auth_check(self): apikey = request.environ.get('HTTP_X_APIKEY', None) uname = request.environ.get('HTTP_X_USER', None) if apikey is not None and uname is not None: ip = request.headers.get("X-Forwarded-For", request.remote_addr) with elasticapm.capture_span(name="@api_login:auto_auth_check()", span_type="authentication"): try: # TODO: apikey_handler is slow to verify the password (bcrypt's fault) # We could fix this by saving the hash of the combinaison of the # APIkey and the username in an ExpiringSet and looking it up for # sub-sequent calls... validated_user, priv = validate_apikey( uname, apikey, STORAGE) except AuthenticationException: msg = "Invalid user or APIKey" LOGGER.warning( f"Authentication failure. (U:{uname} - IP:{ip}) [{msg}]" ) abort(401, msg) return if validated_user: LOGGER.info(f"Login successful. (U:{uname} - IP:{ip})") if not set(self.required_priv).intersection(set(priv)): abort( 403, "The method you've used to login does not give you access to this API" ) return return validated_user return None
def check_submission_quota(user, num=1) -> Optional[str]: quota_user = user['uname'] quota = user.get('submission_quota', 5) count = num + Hash('submissions-' + quota_user, **persistent).length() if count > quota: LOGGER.info("User %s exceeded their submission quota. [%s/%s]", quota_user, count, quota) return "You've exceeded your maximum submission quota of %s " % quota return None
def check_submission_quota(user) -> Optional[str]: quota_user = user['uname'] max_quota = user.get('submission_quota', 5) if config.ui.enforce_quota and not SUBMISSION_TRACKER.begin( quota_user, max_quota): LOGGER.info("User %s exceeded their submission quota of %s.", quota_user, max_quota) return "You've exceeded your maximum submission quota of %s " % max_quota return None
def base(*args, **kwargs): if 'user' in kwargs: if kwargs['user'].get('authenticated', False): return func(*args, **kwargs) else: abort(403, "Invalid pre-authenticated user") return self.test_readonly("API") logged_in_uname = self.get_logged_in_user() impersonator = None # Impersonate authorization = request.environ.get("HTTP_AUTHORIZATION", None) if authorization: # noinspection PyBroadException try: bearer_token = authorization.split(" ")[-1] headers = jwt.get_unverified_header(bearer_token) decoded = jwt.decode( bearer_token, hashlib.sha256(f"{SECRET_KEY}_{headers['token_id']}". encode()).hexdigest(), algorithms=[headers.get('alg', "HS256")]) except Exception: abort(400, "Malformed bearer token") return target_user = STORAGE.user.get(headers['user'], as_obj=False) if target_user: target_token = target_user.get('apps', {}).get( headers['token_id'], {}) if target_token == decoded and target_token[ 'client_id'] == logged_in_uname: impersonator = logged_in_uname logged_in_uname = headers['user'] LOGGER.info( f"{impersonator} is impersonating {logged_in_uname} for query: {request.path}" ) if not set(self.required_priv).intersection( set(SCOPES[decoded["scope"]])): abort( 403, "The method you've used to login does not give you access to this API" ) return else: abort(403, "Invalid bearer token") return else: abort(404, "User not found") return user = login(logged_in_uname) # Terms of Service if request.path not in ["/api/v4/help/tos/", "/api/v4/user/whoami/", f"/api/v4/user/tos/{logged_in_uname}/", "/api/v4/auth/logout/"] \ and not user.get('agrees_with_tos', False) and config.ui.tos is not None: abort( 403, "Agree to Terms of Service before you can make any API calls" ) return self.test_require_type(user, "API") ############################################# # Special username api query validation # # If an API call requests a username, the username as to match # the logged in user or the user has to be ADMIN # # API that needs this special validation need to make sure their # variable name for the username is as an optional parameter # inside 'username_key'. Default: 'username' if self.username_key in kwargs: if kwargs[self.username_key] != user['uname'] \ and not kwargs[self.username_key] == "__global__" \ and not kwargs[self.username_key] == "__workflow__" \ and not kwargs[self.username_key].lower() == "__current__" \ and 'admin' not in user['type']: return make_api_response( {}, "Your username does not match requested username", 403) self.audit_if_required(args, kwargs, logged_in_uname, user, func, impersonator=impersonator) # Save user credential in user kwarg for future reference kwargs['user'] = user if config.core.metrics.apm_server.server_url is not None: elasticapm.set_user_context(username=user.get('name', None), email=user.get('email', None), user_id=user.get('uname', None)) # Check current user quota quota_user = user['uname'] flsk_session['quota_user'] = quota_user flsk_session['quota_set'] = True quota = user.get('api_quota', 10) if not QUOTA_TRACKER.begin(quota_user, quota): if config.ui.enforce_quota: LOGGER.info( f"User {quota_user} was prevented from using the api due to exceeded quota." ) return make_api_response( "", f"You've exceeded your maximum quota of {quota}", 503) else: LOGGER.debug( f"Quota of {quota} exceeded for user {quota_user}.") else: LOGGER.debug( f"{quota_user}'s quota is under or equal its limit of {quota}" ) return func(*args, **kwargs)
def login(**_): """ Login the user onto the system Variables: None Arguments: None Data Block: { "user": <UID>, "password": <ENCRYPTED_PASSWORD>, "otp": <OTP_TOKEN>, "apikey": <ENCRYPTED_APIKEY>, "webauthn_auth_resp": <RESPONSE_TO_CHALLENGE_FROM_WEBAUTHN> } Result example: { "username": <Logged in user>, # Username for the logged in user "privileges": ["R", "W"], # Different privileges that the user will get for this session "session_duration": 60 # Time after which this session becomes invalid # Note: The timer reset after each call } """ data = request.json if not data: data = request.values user = data.get('user', None) password = data.get('password', None) apikey = data.get('apikey', None) webauthn_auth_resp = data.get('webauthn_auth_resp', None) oauth_provider = data.get('oauth_provider', None) oauth_token = data.get('oauth_token', None) if config.auth.oauth.enabled and oauth_provider: oauth = current_app.extensions.get('authlib.integrations.flask_client') provider = oauth.create_client(oauth_provider) if provider: redirect_uri = f'https://{request.host}/login.html?provider={oauth_provider}' return provider.authorize_redirect(redirect_uri=redirect_uri) try: otp = int(data.get('otp', 0) or 0) except Exception: raise AuthenticationException('Invalid OTP token') if (user and password) or (user and apikey) or (user and oauth_token): auth = { 'username': user, 'password': password, 'otp': otp, 'webauthn_auth_resp': webauthn_auth_resp, 'apikey': apikey, 'oauth_token': oauth_token } logged_in_uname = None ip = request.headers.get("X-Forwarded-For", request.remote_addr) try: logged_in_uname, priv = default_authenticator( auth, request, flsk_session, STORAGE) session_duration = config.ui.session_duration cur_time = now() xsrf_token = generate_random_secret() current_session = { 'duration': session_duration, 'ip': ip, 'privileges': priv, 'time': int(cur_time) - (int(cur_time) % session_duration), 'user_agent': request.headers.get("User-Agent", None), 'username': logged_in_uname, 'xsrf_token': xsrf_token } session_id = hashlib.sha512( str(current_session).encode("UTF-8")).hexdigest() current_session['expire_at'] = cur_time + session_duration flsk_session['session_id'] = session_id KV_SESSION.add(session_id, current_session) return make_api_response( { "username": logged_in_uname, "privileges": priv, "session_duration": session_duration }, cookies={'XSRF-TOKEN': xsrf_token}) except AuthenticationException as wpe: uname = auth.get('username', '(None)') LOGGER.warning( f"Authentication failure. (U:{uname} - IP:{ip}) [{wpe}]") return make_api_response("", err=str(wpe), status_code=401) finally: if logged_in_uname: LOGGER.info( f"Login successful. (U:{logged_in_uname} - IP:{ip})") return make_api_response( "", "Not enough information to proceed with authentication", 401)
def base(*args, **kwargs): if 'user' in kwargs: if kwargs['user'].get('authenticated', False): return func(*args, **kwargs) else: abort(403, "Invalid pre-authenticated user") return self.test_readonly("API") logged_in_uname = self.get_logged_in_user() user = login(logged_in_uname) # Terms of Service if request.path not in ["/api/v4/help/tos/", "/api/v4/user/whoami/", f"/api/v4/user/tos/{logged_in_uname}/", "/api/v4/auth/logout/"] \ and not user.get('agrees_with_tos', False) and config.ui.tos is not None: abort( 403, "Agree to Terms of Service before you can make any API calls" ) return self.test_require_type(user, "API") ############################################# # Special username api query validation # # If an API call requests a username, the username as to match # the logged in user or the user has to be ADMIN # # API that needs this special validation need to make sure their # variable name for the username is as an optional parameter # inside 'username_key'. Default: 'username' if self.username_key in kwargs: if kwargs[self.username_key] != user['uname'] \ and not kwargs[self.username_key] == "__global__" \ and not kwargs[self.username_key] == "__workflow__" \ and not kwargs[self.username_key].lower() == "__current__" \ and 'admin' not in user['type']: return make_api_response( {}, "Your username does not match requested username", 403) self.audit_if_required(args, kwargs, logged_in_uname, user, func) # Save user credential in user kwarg for future reference kwargs['user'] = user if config.core.metrics.apm_server.server_url is not None: elasticapm.set_user_context(username=user.get('name', None), email=user.get('email', None), user_id=user.get('uname', None)) # Check current user quota quota_user = user['uname'] flsk_session['quota_user'] = quota_user flsk_session['quota_set'] = True quota = user.get('api_quota', 10) if not QUOTA_TRACKER.begin(quota_user, quota): if config.ui.enforce_quota: LOGGER.info( f"User {quota_user} was prevented from using the api due to exceeded quota." ) return make_api_response( "", f"You've exceeded your maximum quota of {quota}", 503) else: LOGGER.debug( f"Quota of {quota} exceeded for user {quota_user}.") else: LOGGER.debug( f"{quota_user}'s quota is under or equal its limit of {quota}" ) return func(*args, **kwargs)