async def on_after_insert(mapper, connection, target): logger.warning("on_after_insert: Zone") print("on_after_insert", mapper, connection, target) publisher = await make_redis() res = await publisher.publish_json("channel:auth", { "type": "MESSAGE", "name": "ZONE_CREATED", "payload": "" })
def from_zone(cls, zone): records = [] logger.info( f"[email protected] - Loading zone: {zone.domain}/{zone.ip} ({zone.id})" ) dns_records = zone.dns_records or [] # if the zone has no records, create some default ones if not dns_records: logger.warning( f"[email protected] - Zone has no dns_records. loading defaults: {zone.domain}/{zone.ip} ({zone.id})" ) rrs = RR.fromZone( ZONE_TEMPLATE.format(domain_name=zone.domain, domain_ip=zone.ip) ) zone_records = [Record.make(zone, rr) for rr in rrs] for zr in zone_records: # TODO: make this clean on output rrstr = str(dedent(str(zr.rr))) logger.debug(f"[email protected] - Loading record entry: {rrstr}") logger.debug( "[email protected] - Loaded record details - name: {} | rtype: {} | rr: {}".format( str(zr.rr.rname), str(QTYPE[zr.rr.rtype]), str(zr.rr) ) ) else: # loop over each dns_record of the zone and convert it to RR record dns_records = sorted(dns_records, key=lambda x: x.sort) zone_records = [] for dns_record in dns_records: try: rrs = RR.fromZone(dns_record.record) _zone_records = [Record.make(zone, rr) for rr in rrs] for zr in _zone_records: rrstr = str(dedent(str(zr.rr))) logger.debug( f"[email protected] - Loading record: {str(dns_record.record)}" ) logger.debug( f"[email protected] - Loading record entry: {rrstr}" ) logger.debug( "[email protected] - Loaded record details - name: {} | rtype: {} | rr: {}".format( str(zr.rr.rname), str(QTYPE[zr.rr.rtype]), str(zr.rr) ) ) zone_records = zone_records + _zone_records except Exception as e: logger.critical( f'[email protected] - Error processing line ({e.__class__.__name__}: {e}) "{dns_record.id}:{dns_record.record}" ' ) raise e # add the records for the zone to the rest of the records records = records + zone_records return cls(records)
async def on_after_insert(mapper, connection, target): 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)}")
def resolve(self, request, handler): reply = request.reply() logger.warning(request.__class__.__name__) qname = request.q.qname qtype = QTYPE[request.q.qtype] for record in self.zones: # Check if label & type match if record.match(request.q): a = copy.copy(record.rr) reply.add_answer(a) if reply.rr: return reply if not reply.rr: reply.header.rcode = RCODE.NXDOMAIN return reply
async def index( form: DnsRequestCreateForm, dns_request_repo: DnsRequestRepo = Depends(DnsRequestRepo), zone_repo: ZoneRepo = Depends(ZoneRepo), token: str = ScopedTo("dns-request:create"), ): data = dict(form) domain_name = data["name"] if zone_repo.filter( literal(domain_name).contains( zone_repo.model_column("domain"))).exists(): data["zone_id"] = zone_repo.data().id else: logger.warning(f"No zone found for dns request {domain_name}") dns_request = dns_request_repo.create(data).data() return DnsRequestResponse(dns_request=dns_request)
def wait_for_up(self): seconds = 0 while True: if seconds > 60: logger.warning("could not connect to api. api not up") return False logger.info("checking for api status") try: sleep(1) self.get_status() sleep(3) return True except Exception as e: logger.info("api check not ready after {} seconds: {}".format( str(seconds), str(e.__class__.__name__))) seconds = seconds + 1 sleep(1)
async def store( form: ApiTokenCreateForm, api_token_repo: ApiTokenRepo = Depends(ApiTokenRepo()), dns_server_repo: DnsServerRepo = Depends(DnsServerRepo()), token: TokenPayload = Depends(ScopedTo("api-token:create")), user: User = Depends(current_user), ): dns_server = dns_server_repo.first_or_fail(id=form.dns_server_id).results() 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, }) data = { "scopes": " ".join(scopes), "token": str(token), "expires_at": form.expires_at, "dns_server_id": form.dns_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 __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)}" ) raise Exception("no dns_server_name on api token") self.dns_server_name = payload["dns_server_name"]
def zones_to_records(self, zones): records = [] for zone in zones: try: logger.warning(f"loading zone: {zone.domain} ({zone.id})") for rr in self.zone_to_rr(zone): logger.debug( "registering zone rr name {} and type {}".format( rr.rname, QTYPE[rr.rtype] ) ) record = Record(zone, rr) records.append(record) logger.debug(" %2d: %s", len(records), record) except Exception as e: raise RuntimeError( f'Error processing line ({e.__class__.__name__}: {e}) "{zone.domain}"' ) from e logger.debug("zone map generated {}".format(str(zone))) logger.debug("%d zone resource records generated", len(records)) return records
async def run(self): self.load_env("api") self.db_register() failed = [] if self.option("confirm"): for model in models: for item in self.session().query(model).all(): logger.info(f"deleting {item}") try: self.session().delete(item) self.session().commit() except Exception as e: failed.append((item, e)) else: logger.warning("You must confirm to drop data") if len(failed) > 0: logger.critical("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(session), form: OAuth2PasswordRequestForm = Depends(), ): user = db.query(User).filter_by(email=form.username).first() if not user or not user.hashed_password: logger.warning(f"user exists failed for {form.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 {form.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 = "profile super zone user dns-request api-token refresh dns-record dns-server" # grant access to super routes else: scopes = "profile zone user:list dns-request api-token:list api-token:create api-token:destroy refresh dns-record:list dns-record:show dns-server:list dns-server:show" 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.decode())} if ws_access_token: data["ws_access_token"] = create_bearer_token( data={ "sub": user.id, "scopes": "zone:publish dns-request:publish refresh" }) return PasswordAuthResponse(**data)
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
def verify_jwt_token(token: str, bl_token_repo=None, leeway=0) -> TokenPayload: from bountydns.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 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): env = self.option("env") self.load_env(f"api.{env}") 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 run(self): app = "bountydns.api.main:api" kwargs = self.get_kwargs() env = self.option("env") self.load_env(f"api.{env}") if self.should_import_check(): logger.info("run@api_server.py - Performing import check") from bountydns.api.main import api logger.critical( "run@api_server.py - Starting api server with options: {}".format( str(kwargs))) from bountydns.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 bountydns.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 bountydns 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 bountydns api in worker mode...") return supervisor.run(server.run, sockets=[sock]) else: sockets = None logger.warning( f"run@api_server.py - Running bountydns api in standard mode..." ) return await server.serve(sockets=sockets)
async def run(self): app = "bountydns.api.main:api" kwargs = self.get_kwargs() env = self.option("env") self.load_env(f"api.{env}") if self.should_import_check(): logger.info("performing import check") from bountydns.api.main import api logger.critical("starting api server with options: {}".format( str(kwargs))) from bountydns.db.checks import is_db_up, is_db_setup if self.should_db_check(): self.db_register() db_up = is_db_up() if not db_up: logger.critical("database not up error. please check logs") return self.exit(1) if self.option("db_setup"): logger.critical("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(): db_setup = is_db_setup() if not db_setup: logger.critical("database not setup error. please check logs") return self.exit(1) from bountydns.broadcast import is_broadcast_up if self.should_bcast_check(): bcast_up = await is_broadcast_up() if not bcast_up: logger.critical( "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 config = UvicornConfig(app, **kwargs) server = UvicornServer(config=config) if isinstance(app, str) and (config.debug or config.reload): sock = config.bind_socket() supervisor = StatReload(config) logger.warning(f"running bountydns api in dev mode...") return supervisor.run(server.run, sockets=[sock]) elif config.workers > 1: sock = config.bind_socket() supervisor = Multiprocess(config) logger.warning(f"running bountydns api in worker mode...") return supervisor.run(server.run, sockets=[sock]) else: sockets = None logger.warning(f"running bountydns api in standard mode...") return await server.serve(sockets=sockets)