PM API key

pull/3/head
db0 2023-06-22 02:04:45 +02:00
parent 39c5e5c423
commit 40aa422656
8 changed files with 126 additions and 13 deletions

7
.env_template 100644
View File

@ -0,0 +1,7 @@
POSTGRES_URI="postgresql://postgres:ChangeMe@postgres.example.tld/overseer"
USE_SQLITE=0
OVERSEER_LEMMY_DOMAIN="overctrl.example.tld"
OVERSEER_LEMMY_USERNAME="overseer"
OVERSEER_LEMMY_PASSWORD="LemmyPassword"
ADMIN_API_KEY="Password"
secret_key="VerySecretKey"

View File

@ -29,7 +29,7 @@ class Models:
'endorsements': fields.Integer(description="The amount of endorsements this instance has received"),
'guarantor': fields.String(description="The domain of the instance which guaranteed this instance."),
})
self.response_model_model_Instances_get = api.model('Instances', {
self.response_model_model_Whitelist_get = api.model('Instances', {
'instances': fields.List(fields.Nested(self.response_model_instances)),
'domains': fields.List(fields.String(description="The instance domains as a list.")),
'csv': fields.String(description="The instance domains as a csv."),

View File

@ -1,10 +1,15 @@
import os
from flask import request
from flask_restx import Namespace, Resource, reqparse
from overseer.flask import cache
from overseer.flask import cache, db
from overseer.observer import retrieve_suspicious_instances
from loguru import logger
from overseer.classes.instance import Instance
from overseer.database import functions as database
from overseer import exceptions as e
from overseer.utils import hash_api_key
from overseer.lemmy import pm_new_api_key
from pythorhead import Lemmy
api = Namespace('v1', 'API Version 1' )
@ -12,6 +17,9 @@ from overseer.apis.models.v1 import Models
models = Models(api)
handle_bad_request = api.errorhandler(e.BadRequest)(e.handle_bad_requests)
handle_forbidden = api.errorhandler(e.Forbidden)(e.handle_bad_requests)
# Used to for the flask limiter, to limit requests per url paths
def get_request_path():
# logger.info(dir(request))
@ -50,21 +58,69 @@ class Whitelist(Resource):
get_parser.add_argument("domains", required=False, type=bool, help="Set to true to return just the domains as a list. Mutually exclusive with csv", location="args")
@api.expect(get_parser)
@logger.catch(reraise=True)
@cache.cached(timeout=10, query_string=True)
@api.marshal_with(models.response_model_model_Instances_get, code=200, description='Instances', skip_none=True)
@api.marshal_with(models.response_model_model_Whitelist_get, code=200, description='Instances', skip_none=True)
def get(self):
'''A List with the details of all instances and their endorsements
'''
self.args = self.get_parser.parse_args()
instance_details = []
for instance in database.get_all_instances():
logger.debug(instance)
instance_details.append(instance.get_details())
if self.args.csv:
return {"csv": ",".join([instance["domain"] for instance in instance_details])},200
if self.args.domains:
return {"domains": [instance["domain"] for instance in instance_details]},200
logger.debug(instance_details)
return {"instances": instance_details},200
put_parser = reqparse.RequestParser()
put_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
put_parser.add_argument("domain", required=False, type=str, help="The instance domain. It MUST be alredy registered in https://overctrl.dbzer0.com", location="json")
put_parser.add_argument("guarantor", required=False, type=str, help="(Optiona) The domain of the guaranteeing instance. They will receive a PM to validate you", location="json")
@api.expect(put_parser)
@api.marshal_with(models.response_model_instances, code=200, description='Instances')
def put(self):
'''A List with the details of all instances and their endorsements
'''
self.args = self.put_parser.parse_args()
existing_instance = Instance.query.filter_by(domain=self.args.domain).first()
if existing_instance:
return existing_instance.get_details(),200
requested_lemmy = Lemmy(f"https://{self.args.domain}")
site = requested_lemmy.site.get()
api_key = pm_new_api_key(self.args.domain)
if not api_key:
raise e.BadRequest("Failed to generate API Key")
new_instance = Instance(
domain=self.args.domain,
api_key=hash_api_key(api_key),
open_registrations=site["site_view"]["local_site"]["registration_mode"] == "open",
email_verify=site["site_view"]["local_site"]["require_email_verification"],
software=requested_lemmy.nodeinfo['software']['name'],
)
new_instance.create()
return new_instance.get_details(),200
patch_parser = reqparse.RequestParser()
patch_parser.add_argument("Authorization: apikey", type=str, required=True, help="The sending instance's API key.", location='headers')
patch_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
patch_parser.add_argument("domain", required=False, type=str, help="The instance domain. It MUST be alredy registered in https://overctrl.dbzer0.com", location="json")
patch_parser.add_argument("guarantor", required=False, type=str, help="(Optiona) The domain of the guaranteeing instance. They will receive a PM to validate you", location="json")
@api.expect(put_parser)
@logger.catch(reraise=True)
@api.marshal_with(models.response_model_instances, code=200, description='Instances', skip_none=True)
def patch(self):
'''A List with the details of all instances and their endorsements
'''
self.args = self.patch_parser.parse_args()
self.apikey = self.args["Authorization: apikey"]
if not self.apikey:
raise e.BadRequest("You must provide the API key that was PM'd to your overctrl.dbzer0.com account")
existing_instance = Instance.query.filter_by(domain=self.args.domain).first()
if existing_instance:
return existing_instance.get_details,200

View File

@ -12,15 +12,16 @@ with OVERSEER.app_context():
db.create_all()
admin_domain = os.getenv("ADMIN_DOMAIN")
admin_domain = os.getenv("OVERSEER_LEMMY_DOMAIN")
admin = db.session.query(Instance).filter_by(domain=admin_domain).first()
if not admin:
admin = Instance(
id=0,
domain=admin_domain,
api_key=hash_api_key(os.getenv("ADMIN_PASSWORD")),
api_key=hash_api_key(os.getenv("ADMIN_API_KEY")),
open_registrations=False,
email_verify=False,
software="lemmy",
)
admin.create()
guarantee = Guarantee(

View File

@ -35,13 +35,14 @@ class Instance(db.Model):
__tablename__ = "instances"
id = db.Column(db.Integer, primary_key=True)
domain = db.Column(db.String(255), unique=True, nullable=False)
domain = db.Column(db.String(255), unique=True, nullable=False, index=True)
api_key = db.Column(db.String(100), unique=True, nullable=False, index=True)
created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
open_registrations = db.Column(db.Boolean, unique=False, nullable=False, index=True)
email_verify = db.Column(db.Boolean, unique=False, nullable=False, index=True)
software = db.Column(db.String(50), unique=False, nullable=False, index=True)
approvals = db.relationship("Endorsement", back_populates="approving_instance", cascade="all, delete-orphan", foreign_keys=[Endorsement.approving_id])
endorsements = db.relationship("Endorsement", back_populates="endorsed_instance", cascade="all, delete-orphan", foreign_keys=[Endorsement.endorsed_id])
@ -53,16 +54,19 @@ class Instance(db.Model):
db.session.commit()
def get_details(self):
guarantor = self.get_guarantor()
ret_dict = {
"domain": self.domain,
"open_registrations": self.open_registrations,
"email_verify": self.email_verify,
"endorsements": len(self.endorsements),
"approvals": len(self.approvals),
"guarantor": self.get_guarantor().domain,
"guarantor": guarantor.domain if guarantor else None,
}
return ret_dict
def get_guarantor(self):
if len(self.guarantors) == 0:
return None
guarantee = self.guarantors[0]
return Instance.query.filter_by(id=guarantee.guarantor_id).first()

View File

@ -0,0 +1,23 @@
from werkzeug import exceptions as wze
from loguru import logger
class BadRequest(wze.BadRequest):
def __init__(self, message, log=None):
self.specific = message
self.log = log
class Forbidden(wze.Forbidden):
def __init__(self, message, log=None):
self.specific = message
self.log = log
class Locked(wze.Locked):
def __init__(self, message, log=None):
self.specific = message
self.log = log
def handle_bad_requests(error):
'''Namespace error handler'''
if error.log:
logger.warning(error.log)
return({'message': error.specific}, error.code)

24
overseer/lemmy.py 100644
View File

@ -0,0 +1,24 @@
from pythorhead import Lemmy
from loguru import logger
import os
import secrets
import overseer.exceptions as e
overctrl_lemmy = Lemmy(f"https://{os.getenv('OVERSEER_LEMMY_DOMAIN')}")
_login = overctrl_lemmy.log_in(os.getenv('OVERSEER_LEMMY_USERNAME'),os.getenv('OVERSEER_LEMMY_PASSWORD'))
if not _login:
raise Exception("Failed to login to overctrl")
overseer_lemmy_user = overctrl_lemmy.user.get(username=os.getenv('OVERSEER_LEMMY_USERNAME'))
def pm_new_api_key(domain: str):
api_key = secrets.token_urlsafe(16)
pm_content = f"The API Key for domain {domain} is {api_key}.\n\nUse this to perform operations on the overseer."
domain_username = domain.replace(".", "_")
domain_user = overctrl_lemmy.user.get(username=domain_username)
if not domain_user:
raise e.BadRequest(f"Could not find domain user '{domain_username}'")
pm = overctrl_lemmy.private_message(pm_content,domain_user["person_view"]["person"]["id"])
if not pm:
raise e.BadRequest("API Key PM failed")
return api_key

View File

@ -6,13 +6,11 @@ Flask-Caching
waitress~=2.1.2
requests >= 2.27
Markdown~=3.4.1
flask-dance[sqla]
blinker
python-dotenv
loguru
python-dateutil~=2.8.2
redis~=4.3.5
flask_sqlalchemy==3.0.2
SQLAlchemy~=1.4.44
psycopg2-binary
regex
pythorhead>=0.6.0