Add/Remove endorsements

pull/3/head
db0 2023-06-22 11:30:51 +02:00
parent c21caa6d08
commit 51bd6e83bf
7 changed files with 217 additions and 13 deletions

View File

@ -25,6 +25,7 @@ class Models:
'csv': fields.String(description="The suspicious domains as a csv."), 'csv': fields.String(description="The suspicious domains as a csv."),
}) })
self.response_model_instances = api.model('InstanceDetails', { self.response_model_instances = api.model('InstanceDetails', {
'id': fields.Integer(description="The instance id"),
'domain': fields.String(description="The instance domain"), 'domain': fields.String(description="The instance domain"),
'open_registrations': fields.Boolean(description="The instance uptime pct. 100% and thousand of users is unlikely"), 'open_registrations': fields.Boolean(description="The instance uptime pct. 100% and thousand of users is unlikely"),
'email_verify': fields.Boolean(description="The amount of local posts in that instance"), 'email_verify': fields.Boolean(description="The amount of local posts in that instance"),
@ -32,7 +33,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_Whitelist_get = api.model('Instances', { self.response_model_model_Whitelist_get = api.model('WhitelistedInstances', {
'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,5 +1,9 @@
import overseer.apis.v1.base as base import overseer.apis.v1.base as base
import overseer.apis.v1.endorsements as endorsements
from overseer.apis.v1.base import api from overseer.apis.v1.base import api
api.add_resource(base.Suspicions, "/instances") api.add_resource(base.Suspicions, "/instances")
api.add_resource(base.Whitelist, "/whitelist") api.add_resource(base.Whitelist, "/whitelist")
api.add_resource(base.WhitelistDomain, "/whitelist/<string:domain>")
api.add_resource(endorsements.Endorsements, "/endorsements/<string:domain>")
api.add_resource(endorsements.Approvals, "/approvals/<string:domain>")

View File

@ -54,8 +54,8 @@ class Suspicions(Resource):
class Whitelist(Resource): class Whitelist(Resource):
get_parser = reqparse.RequestParser() get_parser = reqparse.RequestParser()
get_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers") get_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
get_parser.add_argument("endorsements", required=False, default=1, type=int, help="Limit to this amount of endorsements of more", location="args") get_parser.add_argument("endorsements", required=False, default=0, type=int, help="Limit to this amount of endorsements of more", location="args")
get_parser.add_argument("domain", required=False, type=str, help="Filter by instance domain", location="args") get_parser.add_argument("guarantors", required=False, default=1, type=int, help="Limit to this amount of guarantors of more", location="args")
get_parser.add_argument("csv", required=False, type=bool, help="Set to true to return just the domains as a csv. Mutually exclusive with domains", location="args") get_parser.add_argument("csv", required=False, type=bool, help="Set to true to return just the domains as a csv. Mutually exclusive with domains", 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") 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")
@ -67,7 +67,7 @@ class Whitelist(Resource):
''' '''
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(self.args.endorsements,self.args.guarantors):
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
@ -84,6 +84,7 @@ class Whitelist(Resource):
@api.expect(put_parser) @api.expect(put_parser)
@api.marshal_with(models.response_model_instances, code=200, description='Instances') @api.marshal_with(models.response_model_instances, code=200, description='Instances')
@api.response(400, 'Bad Request', models.response_model_error)
def put(self): def put(self):
'''Register a new instance to the overseer '''Register a new instance to the overseer
An instance account has to exist in the overseer lemmy instance An instance account has to exist in the overseer lemmy instance
@ -117,15 +118,17 @@ class Whitelist(Resource):
@api.expect(patch_parser) @api.expect(patch_parser)
@api.marshal_with(models.response_model_instances, code=200, description='Instances', skip_none=True) @api.marshal_with(models.response_model_instances, code=200, description='Instances', skip_none=True)
@api.response(401, 'Invalid API Key', models.response_model_error)
@api.response(403, 'Instance Not Registered', models.response_model_error)
def patch(self): def patch(self):
'''Regenerate API key for instance '''Regenerate API key for instance
''' '''
self.args = self.patch_parser.parse_args() self.args = self.patch_parser.parse_args()
if not self.args.apikey: if not self.args.apikey:
raise e.Unauthorized("You must provide the API key that was PM'd to your overctrl.dbzer0.com account") raise e.Unauthorized("You must provide the API key that was PM'd to your overctrl.dbzer0.com account")
instance = database.find_authenticated_instance(self.args.domain, self.args.apikey) instance = database.find_instance_by_api_key(self.args.apikey)
if not instance: if not instance:
raise e.BadRequest(f"No Instance found matching provided API key and domain. Have you remembered to register it?") raise e.Forbidden(f"No Instance found matching provided API key and domain. Have you remembered to register it?")
if self.args.regenerate_key: if self.args.regenerate_key:
new_key = pm_new_api_key(self.args.domain) new_key = pm_new_api_key(self.args.domain)
instance.api_key = hash_api_key(new_key) instance.api_key = hash_api_key(new_key)
@ -140,6 +143,9 @@ class Whitelist(Resource):
@api.expect(delete_parser) @api.expect(delete_parser)
@api.marshal_with(models.response_model_simple_response, code=200, description='Instances', skip_none=True) @api.marshal_with(models.response_model_simple_response, code=200, description='Instances', skip_none=True)
@api.response(400, 'Bad Request', models.response_model_error)
@api.response(401, 'Invalid API Key', models.response_model_error)
@api.response(403, 'Forbidden', models.response_model_error)
def delete(self): def delete(self):
'''Delete instance from overseer '''Delete instance from overseer
''' '''
@ -154,4 +160,22 @@ class Whitelist(Resource):
db.session.delete(instance) db.session.delete(instance)
db.session.commit() db.session.commit()
logger.warning(f"{self.args.domain} deleted") logger.warning(f"{self.args.domain} deleted")
return {"message":'OK'}, 200 return {"message":'Changed'}, 200
class WhitelistDomain(Resource):
get_parser = reqparse.RequestParser()
get_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
@api.expect(get_parser)
@cache.cached(timeout=10, query_string=True)
@api.marshal_with(models.response_model_instances, code=200, description='Instances')
def get(self, domain):
'''Display info about a specific instance
'''
self.args = self.get_parser.parse_args()
instance = database.find_instance_by_domain(domain)
if not instance:
raise e.NotFound(f"No Instance found matching provided domain. Have you remembered to register it?")
return instance.get_details(),200

View File

@ -0,0 +1,122 @@
from overseer.apis.v1.base import *
from overseer.classes.instance import Endorsement
class Approvals(Resource):
get_parser = reqparse.RequestParser()
get_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
get_parser.add_argument("csv", required=False, type=bool, help="Set to true to return just the domains as a csv. Mutually exclusive with domains", 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)
@cache.cached(timeout=10, query_string=True)
@api.marshal_with(models.response_model_model_Whitelist_get, code=200, description='Instances', skip_none=True)
@api.response(404, 'Instance not registered', models.response_model_error)
def get(self, domain):
'''Display all endorsements given by a specific domain
'''
self.args = self.get_parser.parse_args()
instance = database.find_instance_by_domain(domain)
if not instance:
raise e.NotFound(f"No Instance found matching provided domain. Have you remembered to register it?")
instance_details = []
for instance in database.get_all_endorsed_instances_by_approving_id(instance.id):
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
return {"instances": instance_details},200
class Endorsements(Resource):
get_parser = reqparse.RequestParser()
get_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
get_parser.add_argument("csv", required=False, type=bool, help="Set to true to return just the domains as a csv. Mutually exclusive with domains", 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)
@cache.cached(timeout=10, query_string=True)
@api.marshal_with(models.response_model_model_Whitelist_get, code=200, description='Instances', skip_none=True)
@api.response(404, 'Instance not registered', models.response_model_error)
def get(self, domain):
'''Display all endorsements given by a specific domain
'''
self.args = self.get_parser.parse_args()
instance = database.find_instance_by_domain(domain)
if not instance:
raise e.NotFound(f"No Instance found matching provided domain. Have you remembered to register it?")
instance_details = []
for instance in database.get_all_approving_instances_by_endorsed_id(instance.id):
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
return {"instances": instance_details},200
put_parser = reqparse.RequestParser()
put_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers')
put_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
@api.expect(put_parser)
@api.marshal_with(models.response_model_simple_response, code=200, description='Endorse Instance')
@api.response(400, 'Bad Request', models.response_model_error)
@api.response(401, 'Invalid API Key', models.response_model_error)
@api.response(403, 'Not Guaranteed', models.response_model_error)
@api.response(404, 'Instance not registered', models.response_model_error)
def put(self, domain):
'''Endorse an instance
'''
self.args = self.put_parser.parse_args()
if not self.args.apikey:
raise e.Unauthorized("You must provide the API key that was PM'd to your overctrl.dbzer0.com account")
instance = database.find_instance_by_api_key(self.args.apikey)
if not instance:
raise e.NotFound(f"No Instance found matching provided API key and domain. Have you remembered to register it?")
if len(instance.guarantors) == 0:
raise e.Forbidden("Only guaranteed instances can endorse others.")
if instance.domain == domain:
raise e.BadRequest("Nice try, but you can't endorse yourself.")
target_instance = database.find_instance_by_domain(domain=domain)
if not target_instance:
raise e.BadRequest("Instance to endorse not found")
if database.get_endorsement(target_instance.id,instance.id):
return {"message":'OK'}, 200
new_endorsement = Endorsement(
approving_id=instance.id,
endorsed_id=target_instance.id,
)
db.session.add(new_endorsement)
db.session.commit()
logger.info(f"{instance.domain} Endorsed {domain}")
return {"message":'Changed'}, 200
delete_parser = reqparse.RequestParser()
delete_parser.add_argument("apikey", type=str, required=True, help="The sending instance's API key.", location='headers')
delete_parser.add_argument("Client-Agent", default="unknown:0:unknown", type=str, required=False, help="The client name and version.", location="headers")
@api.expect(delete_parser)
@api.marshal_with(models.response_model_simple_response, code=200, description='Withdraw Instance Endorsement')
@api.response(400, 'Bad Request', models.response_model_error)
@api.response(401, 'Invalid API Key', models.response_model_error)
@api.response(404, 'Instance not registered', models.response_model_error)
def delete(self,domain):
'''Withdraw an instance endorsement
'''
self.args = self.delete_parser.parse_args()
if not self.args.apikey:
raise e.Unauthorized("You must provide the API key that was PM'd to your overctrl.dbzer0.com account")
instance = database.find_instance_by_api_key(self.args.apikey)
if not instance:
raise e.NotFound(f"No Instance found matching provided API key and domain. Have you remembered to register it?")
target_instance = database.find_instance_by_domain(domain=domain)
if not target_instance:
raise e.BadRequest("Instance from which to withdraw endorsement not found")
endorsement = database.get_endorsement(target_instance.id,instance.id)
if not endorsement:
return {"message":'OK'}, 200
db.session.delete(endorsement)
db.session.commit()
logger.info(f"{instance.domain} Withdrew endorsement from {domain}")
return {"message":'Changed'}, 200

View File

@ -56,6 +56,7 @@ class Instance(db.Model):
def get_details(self): def get_details(self):
guarantor = self.get_guarantor() guarantor = self.get_guarantor()
ret_dict = { ret_dict = {
"id": self.id,
"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,

View File

@ -1,16 +1,61 @@
import time import time
import uuid import uuid
import json import json
from loguru import logger
from datetime import datetime, timedelta from datetime import datetime, timedelta
from sqlalchemy import func, or_, and_, not_, Boolean from sqlalchemy import func, or_, and_, not_, Boolean
from sqlalchemy.orm import noload from sqlalchemy.orm import noload
from overseer.flask import db, SQLITE_MODE from overseer.flask import db, SQLITE_MODE
from overseer.utils import hash_api_key from overseer.utils import hash_api_key
from sqlalchemy.orm import joinedload
from overseer.classes.instance import Instance, Endorsement
from overseer.classes.instance import Instance def get_all_instances(min_endorsements = 0, min_guarantors = 1):
query = db.session.query(
Instance
).outerjoin(
Instance.endorsements,
Instance.guarantors,
).options(
joinedload(Instance.guarantors),
joinedload(Instance.endorsements),
).group_by(
Instance.id
).having(
db.func.count(Instance.endorsements) >= min_endorsements,
).having(
db.func.count(Instance.guarantors) >= min_guarantors,
)
return query.all()
def get_all_instances():
return db.session.query(Instance).all() def get_all_endorsed_instances_by_approving_id(approving_id):
query = db.session.query(
Instance
).outerjoin(
Instance.endorsements,
).options(
joinedload(Instance.endorsements),
).filter(
Endorsement.approving_id == approving_id
).group_by(
Instance.id
)
return query.all()
def get_all_approving_instances_by_endorsed_id(endorsed_id):
query = db.session.query(
Instance
).outerjoin(
Instance.approvals,
).options(
joinedload(Instance.approvals),
).filter(
Endorsement.endorsed_id == endorsed_id
).group_by(
Instance.id
)
return query.all()
def find_instance_by_api_key(api_key): def find_instance_by_api_key(api_key):
@ -24,3 +69,10 @@ def find_instance_by_domain(domain):
def find_authenticated_instance(domain,api_key): def find_authenticated_instance(domain,api_key):
instance = Instance.query.filter_by(domain=domain, api_key=hash_api_key(api_key)).first() instance = Instance.query.filter_by(domain=domain, api_key=hash_api_key(api_key)).first()
return instance return instance
def get_endorsement(instance_id, endorsing_instance_id):
query = Endorsement.query.filter_by(
endorsed_id=instance_id,
approving_id=endorsing_instance_id,
)
return query.first()

View File

@ -12,7 +12,7 @@ overseer_lemmy_user = overctrl_lemmy.user.get(username=os.getenv('OVERSEER_LEMMY
def pm_new_api_key(domain: str): def pm_new_api_key(domain: str):
api_key = secrets.token_urlsafe(16) 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." pm_content = f"The API Key for domain {domain} is\n\n{api_key}\n\nUse this to perform operations on the overseer."
domain_username = domain.replace(".", "_") domain_username = domain.replace(".", "_")
domain_user = overctrl_lemmy.user.get(username=domain_username) domain_user = overctrl_lemmy.user.get(username=domain_username)
if not domain_user: if not domain_user: