def __init__(self, api_url, api_token, verify_ssl=True): self.api_url = api_url self.api_token = api_token self.zones = [] self.verify_ssl = verify_ssl if not self.verify_ssl: logger.warning( "__init__@dns_server.py - Disabling SSL Warnings. I hope this was intentional" ) requests.packages.urllib3.disable_warnings( category=InsecureRequestWarning) payload = jwt.decode(api_token, verify=False) # do not trust if not "dns_server_name" in payload.keys( ) or not payload["dns_server_name"]: logger.critical( f"__init__@api_client.py - No dns_server_name on api token payload: {str(payload)}" ) self.dns_server_name = payload.get("dns_server_name", "") if not "http_server_name" in payload.keys( ) or not payload["http_server_name"]: logger.critical( f"__init__@api_client.py - No http_server_name on api token payload: {str(payload)}" ) self.http_server_name = payload.get("http_server_name", "")
async def on_after_insert(mapper, connection, target): logger.debug("on_after_insert@DnsRequest: Publishing message") try: publisher = await make_redis() res = await publisher.publish_json( "channel:auth", { "type": "MESSAGE", "name": "DNS_REQUEST_CREATED", "payload": "" }, ) except Exception as e: logger.warning(f"on_after_insert error: {str(e)}")
async def store( form: ApiTokenCreateForm, api_token_repo: ApiTokenRepo = Depends(ApiTokenRepo()), dns_server_repo: DnsServerRepo = Depends(DnsServerRepo()), http_server_repo: HttpServerRepo = Depends(HttpServerRepo()), token: TokenPayload = Depends(ScopedTo("api-token:create")), user: User = Depends(current_user), ): if form.dns_server_id and form.dns_server_id > 0: dns_server_name = (dns_server_repo.first_or_fail( id=form.dns_server_id).results().name) if form.http_server_id and form.http_server_id > 0: http_server_name = (http_server_repo.first_or_fail( id=form.http_server_id).results().name) scopes = [] for requested_scope in form.scopes.split(" "): request_scope_satisfied = False for user_token in token.scopes: # TODO: double check this, pretty lenient # if a:b in a:b:c if user_token in requested_scope: request_scope_satisfied = True if not request_scope_satisfied: logger.warning( f"[email protected]: Attempt to create unauthorized scope {requested_scope}" ) raise HTTPException(403, detail="unauthorized") else: scopes.append(requested_scope) # TODO: use better randomness token = create_bearer_token( data={ "sub": user.id, "scopes": " ".join(scopes), "dns_server_name": dns_server_name, "http_server_name": http_server_name, }) data = { "scopes": " ".join(scopes), "token": str(token), "expires_at": form.expires_at, "dns_server_id": form.dns_server_id, "http_server_id": form.http_server_id, } api_token = api_token_repo.create(data).data() return ApiTokenResponse(api_token=api_token)
def refresh_zones_if_needed(self): logger.info( "refresh_zones_if_needed@api_client.py - Checking for New Zones and Records..." ) old_zones = self.zones new_zones = self.get_zones() # TODO: fix this mess if len(old_zones) != len(new_zones): logger.warning( f"refresh_zones_if_needed@api_client.py - Zone Length mistmatch. New or Changed Zone Found: {str(old_zones)} != {str(new_zones)}. Reloading zones." ) self.load_zones() return True for nz in new_zones: # make sure new zones are in old zones is_nz_exists = False for oz in old_zones: if oz.domain == nz.domain and oz.ip == nz.ip: nz_dns_records = nz.dns_records or [] for nrec in nz_dns_records: is_rec_satisfied = False oz_dns_records = oz.dns_records or [] if len(nz_dns_records) != len(oz_dns_records): logger.warning( f"refresh_zones_if_needed@api_client.py - Zone Record Length mistmatch {str(len(nz_dns_records))} != {str(len(oz_dns_records))}. New or Changed Zone Record Found: {str(nz_dns_records)} != {str(oz_dns_records)}. Reloading zones." ) self.load_zones() return True for orec in oz_dns_records: if orec.record == nrec.record and orec.sort == nrec.sort: is_rec_satisfied = True if not is_rec_satisfied: logger.warning( f"refresh_zones_if_needed@api_client.py - New or Changed Zone Record {str(nz)}: {str(nrec)} found for server. Reloading zones." ) self.load_zones() return True is_nz_exists = True if not is_nz_exists: logger.warning( f"refresh_zones_if_needed@api_client.py - New or Changed Zone {str(nz)} found for server. Reloading zones." ) self.load_zones() return True logger.info( "refresh_zones_if_needed@api_client.py - No New Zones or Records Found. All is well" ) return False
def wait_for_up(self): attempts = 0 while True: if attempts > 60: logger.warning("could not connect to api. api not up") return False logger.info( f"wait_for_up@api_client.py - checking for api status : {self.url('/status')}" ) try: sleep(1) self.get_status() sleep(3) return True except Exception as e: logger.info( "wait_for_up@api_client.py - api check not ready after {} attempts: {}" .format(str(attempts), str(e.__class__.__name__))) attempts = attempts + 1 sleep(1)
def verify_jwt_token(token: str, bl_token_repo=None, leeway=0) -> TokenPayload: from boucanpy.api import config # environment must be loaded if bl_token_repo: if bl_token_repo.exists(token=token): raise HTTPException(status_code=403, detail="Forbidden") else: logger.warning( "verifying token without checking the blacklist. dangerous!") try: payload = jwt.decode(token, config.API_SECRET_KEY, algorithms=config.JWT_ALGORITHM, leeway=leeway) except jwt.PyJWTError: raise HTTPException(status_code=403, detail="Forbidden") return TokenPayload( payload=payload, scopes=payload.get("scopes", "").split(" "), token=token, sub=payload.get("sub", ""), exp=payload.get("exp", ""), )
async def __call__( self, request: Request, bl_token_repo: BlackListedTokenRepo = Depends( BlackListedTokenRepo()), token: str = Security(oauth2), ) -> TokenPayload: token = verify_jwt_token(token, bl_token_repo, self._leeway) # proper validation goes here if self._satisfy not in ["all", "one"]: logger.warning(f"Invalid satisfy value: {self._satisfy}") if self._satisfy == "one": if not token_has_one_required_scopes(token, self._scopes): vmsg = f"Token does not have one of the required scopes: {str(self._scopes)}" logger.error(vmsg) abort(code=403, msg="Forbidden", debug=vmsg) else: if not token_has_required_scopes(token, self._scopes): vmsg = f"Token does not have all required scopes: {str(self._scopes)}" logger.error(vmsg) abort(code=403, msg="Forbidden", debug=vmsg) return token
async def run(self): self.db_register() failed = [] if self.option("confirm"): for class_name, model in models.items(): for item in self.session().query(model).all(): logger.warning(f"run@db_truncate.py - Deleting {item}") try: self.session().delete(item) self.session().commit() except Exception as e: failed.append((item, e)) else: logger.warning("run@db_truncate.py - You must confirm to drop data") if len(failed) > 0: logger.warning("run@db_truncate.py - Encountered errors") for f in failed: print("Failed:", item[0]) print("Error", item[1])
async def login( ws_access_token: bool = False, db: Session = Depends(async_session), form: OAuth2PasswordRequestForm = Depends(), ): username = form.username.lower() if form.username else "" user = db.query(User).filter_by(email=username).first() if not user or not user.hashed_password: logger.warning(f"user exists failed for {username}") raise HTTPException(status_code=401, detail="Incorrect email or password") if not verify_password(form.password, user.hashed_password): logger.warning(f"hash verification failed for {username}") raise HTTPException(status_code=401, detail="Incorrect email or password") if user.mfa_secret: # mfa is enabled scopes = "profile mfa_required" elif user.is_superuser: scopes = SUPER_SCOPES # grant access to super routes else: scopes = NORMAL_SCOPES logger.warning(f"creating token with scopes {scopes}") token = create_bearer_token(data={"sub": user.id, "scopes": scopes}) data = {"token_type": "bearer", "access_token": str(token)} if ws_access_token and int(environ.get("BROADCAST_ENABLED", 0)) == 1: # TODO: make it so that you cannot get publish access without base scope data["ws_access_token"] = create_bearer_token(data={ "sub": user.id, "scopes": PUBLISH_SCOPES }) return PasswordAuthResponse(**data)
async def run(self): app = "boucanpy.api.main:api" kwargs = self.get_kwargs() if self.should_import_check(): logger.info("run@api_server.py - Performing import check") from boucanpy.api.main import api logger.critical( "run@api_server.py - Starting api server with options: {}".format( str(kwargs))) from boucanpy.db.checks import is_db_up, is_db_setup # alembic just destroys the loggers, it's annoying if self.should_db_check(): logger.info( "run@api_server.py - Waiting for database service to be up") db_wait_options = self._args_to_dict(self.options) await DbWait(db_wait_options).run() if self.option("db_setup"): logger.critical("run@api_server.py - Running database migration") db_setup_options = self._args_to_dict(self.options) if self.option("db_seed"): db_setup_options["seed"] = True await DbSetup(db_setup_options).run() if self.should_db_check(): logger.info( "run@api_server.py - Checking if application database is setup and configured" ) db_setup = is_db_setup() if not db_setup: logger.critical( "run@api_server.py - Database not setup error. please check logs" ) return self.exit(1) from boucanpy.broadcast import is_broadcast_up if self.should_bcast_check(): bcast_up = await is_broadcast_up() if not bcast_up: logger.critical( "run@api_server.py - Broadcast (queue) not up error. please check logs" ) return self.exit(1) if self.option("db_seed_env", False): self.seed_from_env() # taken from uvicorn/main.py:run logger.debug("run@api_server.py - Building Uvicorn Config and Server") config = UvicornConfig(app, log_config=self.get_uvicorn_logging(), **kwargs) server = UvicornServer(config=config) if self.option("force_exit"): server.force_exit = True if isinstance(app, str) and (config.debug or config.reload): logger.warning( f"run@api_server.py - Running boucanpy api in dev mode...") sock = config.bind_socket() supervisor = StatReload(config) return supervisor.run(server.run, sockets=[sock]) elif config.workers > 1: sock = config.bind_socket() supervisor = Multiprocess(config) logger.warning( f"run@api_server.py - Running boucanpy api in worker mode...") return supervisor.run(server.run, sockets=[sock]) else: sockets = None logger.warning( f"run@api_server.py - Running boucanpy api in standard mode..." ) return await server.serve(sockets=sockets)