Skip to content

Commit

Permalink
Organizations endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
bsatoriu committed Aug 26, 2024
1 parent 601b897 commit 563a9dd
Show file tree
Hide file tree
Showing 10 changed files with 468 additions and 0 deletions.
348 changes: 348 additions & 0 deletions api/endpoints/organizations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
import logging
from flask_restx import Resource
from flask import request
from flask_api import status

from api.models.job_queue import JobQueue
from api.models.organization import Organization as Organization_db
from api.models.organization_job_queue import OrganizationJobQueue
from api.models.organization_membership import OrganizationMembership as OrganizationMembership_db
from api.models.member import Member
from api.models.role import Role
from api.restplus import api
from api.auth.security import login_required, get_authorized_user
from api.maap_database import db
from api.schemas.organization_job_queue_schema import OrganizationJobQueueSchema
from api.schemas.organization_membership_schema import OrganizationMembershipSchema
from api.schemas.organization_schema import OrganizationSchema
from api.schemas.member_schema import MemberSchema
from datetime import datetime
import json
from api.utils.http_util import err_response

log = logging.getLogger(__name__)
ns = api.namespace('organizations', description='Operations related to the MAAP organizations')

@ns.route('')
class Organizations(Resource):

@api.doc(security='ApiKeyAuth')
@login_required()
def get(self):
"""
Lists the hierarchy of organizations using MAAP
:return:
"""
top_query = db.session.query(Organization_db)
top_query = top_query.filter(Organization_db.parent_org_id is not None)
top_query = top_query.cte('cte', recursive=True)

bot_query = db.session.query(Organization_db)
bot_query = bot_query.join(top_query, Organization_db.parent_org_id == top_query.c.id)

recursive_q = top_query.union(bot_query)
orgs = db.session.query(recursive_q)

organization_schema = OrganizationSchema()
result = [json.loads(organization_schema.dumps(p)) for p in orgs]
return result

@api.doc(security='ApiKeyAuth')
@login_required()
def post(self):
"""
Create new organization
:return:
"""

req_data = request.get_json()
if not isinstance(req_data, dict):
return err_response("Valid JSON body object required.")

name = req_data.get("name", "")
if not isinstance(name, str) or not name:
return err_response("Valid org name is required.")

parent_org_id = req_data.get("parent_org_id", None)
default_job_limit_count = req_data.get("default_job_limit_count", None)
default_job_limit_hours = req_data.get("default_job_limit_hours", None)

new_org = Organization_db(name=name, parent_org_id=parent_org_id, default_job_limit_count=default_job_limit_count,
default_job_limit_hours=default_job_limit_hours, creation_date=datetime.utcnow())

db.session.add(new_org)
db.session.commit()

org_schema = OrganizationSchema()
return json.loads(org_schema.dumps(new_org))


@ns.route('/<int:org_id>')
class Organization(Resource):

@api.doc(security='ApiKeyAuth')
@login_required()
def get(self, org_id):
"""
Retrieve organization
"""

org = db.session \
.query(Organization_db) \
.filter_by(id=org_id) \
.first()

if org is None:
return err_response(msg="No organization found with id " + org_id, code=status.HTTP_404_NOT_FOUND)

org_schema = OrganizationSchema()
result = json.loads(org_schema.dumps(org))

return result

@api.doc(security='ApiKeyAuth')
@login_required()
def put(self, org_id):

"""
Update organization. Only supplied fields are updated.
"""

if not org_id:
return err_response("Org id is required.")

req_data = request.get_json()
if not isinstance(req_data, dict):
return err_response("Valid JSON body object required.")

org = db.session.query(Organization_db).filter_by(id=org_id).first()

if org is None:
return err_response(msg="No org found with id " + org_id)

org.name = req_data.get("name", org.name)
org.parent_id = req_data.get("parent_id", org.parent_id)
org.default_job_limit_count = req_data.get("default_job_limit_count", org.default_job_limit_count)
org.default_job_limit_hours = req_data.get("default_job_limit_hours", org.default_job_limit_hours)
db.session.commit()

org_schema = OrganizationSchema()
return json.loads(org_schema.dumps(org))

@api.doc(security='ApiKeyAuth')
@login_required()
def delete(self, org_id):
"""
Delete organization
"""

org = db.session.query(Organization_db).filter_by(id=org_id).first()

if org is None:
return err_response(msg="Organization does not exist")

db.session.query(Organization_db).filter_by(id=org_id).delete()
db.session.commit()

return {"code": status.HTTP_200_OK, "message": "Successfully deleted {}.".format(org)}


@ns.route('/<int:org_id>/membership')
class OrganizationMemberships(Resource):

@api.doc(security='ApiKeyAuth')
@login_required()
def get(self, org_id):
"""
Retrieve organization members
"""

org_members = db.session.query(
OrganizationMembership_db, Member, Organization_db,
).filter(
OrganizationMembership_db.member_id == Member.id,
).filter(
OrganizationMembership_db.org_id == Organization_db.id,
).filter(
OrganizationMembership_db.org_id == org_id,
).order_by(Member.username).all()

result = [{
'org_id': om.organization.id
} for om in org_members]

return result


@ns.route('/<int:org_id>/membership/<string:username>')
class OrganizationMembership(Resource):

@api.doc(security='ApiKeyAuth')
@login_required()
def post(self, org_id, username):
"""
Add organization member
:return:
"""

req_data = request.get_json()
if not isinstance(req_data, dict):
return err_response("Valid JSON body object required.")

member = get_authorized_user()
membership = db.session.query(OrganizationMembership_db).filter_by(member_id=member.id, org_id=org_id).first()

if member.role_id != Role.ROLE_ADMIN and not membership.org_maintainer:
return err_response("Must be an org maintainer to add members.", status.HTTP_403_FORBIDDEN)

org_member = db.session.query(Member).filter_by(username=username).first()

if org_member is None:
return err_response("Valid username is required.")

membership_dup = db.session.query(OrganizationMembership_db).filter_by(member_id=org_member.id, org_id=org_id).first()

if membership_dup is not None:
return err_response("Member {} already exists in org {}".format(username, org_id))

job_limit_count = req_data.get("job_limit_count", None)
job_limit_hours = req_data.get("job_limit_hours", None)
org_maintainer = req_data.get("org_maintainer", False)

new_org_membership = OrganizationMembership_db(org_id=org_id, member_id=org_member.id, job_limit_count=job_limit_count,
job_limit_hours=job_limit_hours, org_maintainer=org_maintainer, creation_date=datetime.utcnow())

db.session.add(new_org_membership)
db.session.commit()

org_schema = OrganizationMembershipSchema()
return json.loads(org_schema.dumps(new_org_membership))

@api.doc(security='ApiKeyAuth')
@login_required()
def delete(self, org_id, username):
"""
Delete organization member
"""

member = get_authorized_user()
membership = db.session.query(OrganizationMembership_db).filter_by(member_id=member.id, org_id=org_id).first()

if membership is None:
return err_response("Org id {} for user {} was not found.".format(org_id, member.username))

if not membership.org_maintainer and member.role_id != Role.ROLE_ADMIN:
return err_response("Must be an org maintainer to remove members.", status.HTTP_403_FORBIDDEN)

member_to_delete = db.session.query(Member).filter_by(username=username).first()

if member_to_delete is None:
return err_response("Member {} was not found.".format(username))

membership_to_delete = db.session.query(OrganizationMembership_db).filter_by(member_id=member_to_delete.id, org_id=org_id).first()

if membership_to_delete is None:
return err_response("Org id {} for user {} was not found.".format(org_id, member_to_delete.username))

db.session.query(OrganizationMembership_db).filter_by(member_id=member_to_delete.id, org_id=org_id).delete()
db.session.commit()

return {"code": status.HTTP_200_OK, "message": "Successfully removed {} from org {}.".format(member_to_delete.username, org_id)}


@ns.route('/<int:org_id>/job_queues')
class OrganizationJobQueues(Resource):

@api.doc(security='ApiKeyAuth')
@login_required()
def get(self, org_id):
"""
Retrieve organization members
"""

org_queues = db.session.query(
OrganizationJobQueue, JobQueue, Organization_db,
).filter(
OrganizationJobQueue.job_queue_id == JobQueue.id,
).filter(
OrganizationJobQueue.org_id == Organization_db.id,
).filter(
OrganizationJobQueue.org_id == org_id,
).order_by(JobQueue.queue_name).all()

result = [{
'org_id': om.organization.id
} for om in org_queues]

return result


@ns.route('/<int:org_id>/job_queues/<string:queue_name>')
class OrganizationJobQueueCls(Resource):

@api.doc(security='ApiKeyAuth')
@login_required()
def post(self, org_id, queue_name):
"""
Add organization member
:return:
"""

req_data = request.get_json()
if not isinstance(req_data, dict):
return err_response("Valid JSON body object required.")

member = get_authorized_user()
membership = db.session.query(OrganizationMembership_db).filter_by(member_id=member.id, org_id=org_id).first()

if member.role_id != Role.ROLE_ADMIN and not membership.org_maintainer:
return err_response("Must be an org maintainer to add queues.", status.HTTP_403_FORBIDDEN)

org_queue = db.session.query(JobQueue).filter_by(queue_name=queue_name).first()

if org_queue is None:
return err_response("Valid job queue is required.")

org_queue_dup = db.session.query(OrganizationJobQueue).filter_by(job_queue_id=org_queue.id, org_id=org_id).first()

if org_queue_dup is not None:
return err_response("Job queue {} already exists in org {}".format(queue_name, org_id))

new_org_queue = OrganizationJobQueue(org_id=org_id, job_queue_id=org_queue.id, creation_date=datetime.utcnow())

db.session.add(new_org_queue)
db.session.commit()

org_schema = OrganizationJobQueueSchema()
return json.loads(org_schema.dumps(new_org_queue))

@api.doc(security='ApiKeyAuth')
@login_required()
def delete(self, org_id, queue_name):
"""
Delete organization member
"""

member = get_authorized_user()
membership = db.session.query(OrganizationMembership_db).filter_by(member_id=member.id, org_id=org_id).first()

if membership is None:
return err_response("Org id {} for user {} was not found.".format(org_id, member.username))

if not membership.org_maintainer and member.role_id != Role.ROLE_ADMIN:
return err_response("Must be an org maintainer to remove members.", status.HTTP_403_FORBIDDEN)

queue_to_delete = db.session.query(JobQueue).filter_by(queue_name=queue_name).first()

if queue_to_delete is None:
return err_response("Job queue {} was not found.".format(queue_name))

org_queue_to_delete = db.session.query(OrganizationJobQueue).filter_by(job_queue_id=queue_to_delete.id, org_id=org_id).first()

if org_queue_to_delete is None:
return err_response("Org id {} for job queue {} was not found.".format(org_id, queue_name))

db.session.query(OrganizationJobQueue).filter_by(job_queue_id=queue_to_delete.id, org_id=org_id).delete()
db.session.commit()

return {"code": status.HTTP_200_OK, "message": "Successfully removed {} from org {}.".format(queue_name, org_id)}
2 changes: 2 additions & 0 deletions api/maapapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from api.endpoints.wms import ns as wms_namespace
from api.endpoints.members import ns as members_namespace
from api.endpoints.environment import ns as environment_namespace
from api.endpoints.organizations import ns as organizations_namespace
from api.endpoints.admin import ns as admin_namespace
from api.restplus import api
from api.maap_database import db
Expand Down Expand Up @@ -103,6 +104,7 @@ def initialize_app(flask_app):
api.add_namespace(wms_namespace)
api.add_namespace(members_namespace)
api.add_namespace(environment_namespace)
api.add_namespace(organizations_namespace)
api.add_namespace(admin_namespace)
flask_app.register_blueprint(blueprint)

Expand Down
17 changes: 17 additions & 0 deletions api/models/job_queue.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from api.models import Base
from api.maap_database import db

class JobQueue(Base):
__tablename__ = 'job_queue'

id = db.Column(db.Integer, primary_key=True)
queue_name = db.Column(db.String())
queue_description = db.Column(db.String())
# Whether the queue is available to public 'Guest' users
guest_tier = db.Column(db.Boolean())
creation_date = db.Column(db.DateTime())

def __repr__(self):
return "<JobQueue(queue_name={self.queue_name!r})>".format(self=self)


Loading

0 comments on commit 563a9dd

Please sign in to comment.