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"), '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."), '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)), 'instances': fields.List(fields.Nested(self.response_model_instances)),
'domains': fields.List(fields.String(description="The instance domains as a list.")), 'domains': fields.List(fields.String(description="The instance domains as a list.")),
'csv': fields.String(description="The instance domains as a csv."), 'csv': fields.String(description="The instance domains as a csv."),

View File

@ -1,10 +1,15 @@
import os
from flask import request from flask import request
from flask_restx import Namespace, Resource, reqparse 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 overseer.observer import retrieve_suspicious_instances
from loguru import logger from loguru import logger
from overseer.classes.instance import Instance from overseer.classes.instance import Instance
from overseer.database import functions as database 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' ) api = Namespace('v1', 'API Version 1' )
@ -12,6 +17,9 @@ from overseer.apis.models.v1 import Models
models = Models(api) 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 # Used to for the flask limiter, to limit requests per url paths
def get_request_path(): def get_request_path():
# logger.info(dir(request)) # 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") 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) @api.expect(get_parser)
@logger.catch(reraise=True)
@cache.cached(timeout=10, query_string=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): def get(self):
'''A List with the details of all instances and their endorsements '''A List with the details of all instances and their endorsements
''' '''
self.args = self.get_parser.parse_args() self.args = self.get_parser.parse_args()
instance_details = [] instance_details = []
for instance in database.get_all_instances(): for instance in database.get_all_instances():
logger.debug(instance)
instance_details.append(instance.get_details()) instance_details.append(instance.get_details())
if self.args.csv: if self.args.csv:
return {"csv": ",".join([instance["domain"] for instance in instance_details])},200 return {"csv": ",".join([instance["domain"] for instance in instance_details])},200
if self.args.domains: if self.args.domains:
return {"domains": [instance["domain"] for instance in instance_details]},200 return {"domains": [instance["domain"] for instance in instance_details]},200
logger.debug(instance_details)
return {"instances": instance_details},200 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() 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() admin = db.session.query(Instance).filter_by(domain=admin_domain).first()
if not admin: if not admin:
admin = Instance( admin = Instance(
id=0, id=0,
domain=admin_domain, 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, open_registrations=False,
email_verify=False, email_verify=False,
software="lemmy",
) )
admin.create() admin.create()
guarantee = Guarantee( guarantee = Guarantee(

View File

@ -35,13 +35,14 @@ class Instance(db.Model):
__tablename__ = "instances" __tablename__ = "instances"
id = db.Column(db.Integer, primary_key=True) 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) api_key = db.Column(db.String(100), unique=True, nullable=False, index=True)
created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False) created = db.Column(db.DateTime, default=datetime.utcnow, nullable=False)
updated = 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) open_registrations = db.Column(db.Boolean, unique=False, nullable=False, index=True)
email_verify = 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]) 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]) 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() db.session.commit()
def get_details(self): def get_details(self):
guarantor = self.get_guarantor()
ret_dict = { ret_dict = {
"domain": self.domain, "domain": self.domain,
"open_registrations": self.open_registrations, "open_registrations": self.open_registrations,
"email_verify": self.email_verify, "email_verify": self.email_verify,
"endorsements": len(self.endorsements), "endorsements": len(self.endorsements),
"approvals": len(self.approvals), "approvals": len(self.approvals),
"guarantor": self.get_guarantor().domain, "guarantor": guarantor.domain if guarantor else None,
} }
return ret_dict return ret_dict
def get_guarantor(self): def get_guarantor(self):
if len(self.guarantors) == 0:
return None
guarantee = self.guarantors[0] guarantee = self.guarantors[0]
return Instance.query.filter_by(id=guarantee.guarantor_id).first() 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 waitress~=2.1.2
requests >= 2.27 requests >= 2.27
Markdown~=3.4.1 Markdown~=3.4.1
flask-dance[sqla]
blinker
python-dotenv python-dotenv
loguru loguru
python-dateutil~=2.8.2 python-dateutil~=2.8.2
redis~=4.3.5
flask_sqlalchemy==3.0.2 flask_sqlalchemy==3.0.2
SQLAlchemy~=1.4.44 SQLAlchemy~=1.4.44
psycopg2-binary psycopg2-binary
regex regex
pythorhead>=0.6.0