Skip to content
This repository was archived by the owner on Aug 29, 2024. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 25 additions & 8 deletions api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,29 @@
from importlib import import_module
from flask_sqlalchemy import SQLAlchemy
from flask_openai import OpenAI

from api.config import Config
from api.models.db.cycle import Cycle
from api.models.db.key_result_check_mark import KeyResultCheckMark
from api.models.db.team import Team
from api.models.db.types.cycle_cadence_enum import CycleCadenceEnum
from api.models.db.user import User
from api.models.db.key_result import KeyResult
from api.models.db.objective import Objective

from authlib.integrations.flask_client import OAuth


core_db = SQLAlchemy()
core_openai = OpenAI()
core_oauth = OAuth()


def create_auth(config: Config):
core_oauth.register(
'auth0',
client_id=config.AUTH0_CLIENT_ID,
client_secret=config.AUTH0_CLIENT_SECRET,
client_kwargs={
'scope': 'openid profile email offline_access',
},
server_metadata_url=f'https://{
config.AUTH0_DOMAIN}/.well-known/openid-configuration'
)


dictConfig({
'version': 1,
Expand Down Expand Up @@ -43,8 +55,13 @@ def create_app(config=Config()):
app.config.from_object(config)
core_db.init_app(app)
core_openai.init_app(app)
for module_name in ('llm',):
core_oauth.init_app(app)
create_auth(config)

for module_name in ('llm', 'auth'):
module = import_module(
'api.services.{}.routes'.format(module_name))
app.register_blueprint(module.blueprint)
print(app.url_map)

return app
7 changes: 7 additions & 0 deletions api/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@


class Config(object):
SECRET_KEY = os.environ['SECRET_KEY']
SESSION_TYPE = 'cachelib'

basedir = os.path.abspath(os.path.dirname(__file__))

Expand All @@ -17,6 +19,11 @@ class Config(object):

OPENAI_API_KEY = os.environ['OPENAI_API_KEY']

AUTH0_CLIENT_ID = os.environ['AUTH0_CLIENT_ID']
AUTH0_CLIENT_SECRET = os.environ['AUTH0_CLIENT_SECRET']
AUTH0_DOMAIN = os.environ['AUTH0_DOMAIN']
AUTH0_AUDIENCE = os.environ['AUTH0_AUDIENCE']

DEBUG = (os.getenv('DEBUG', 'False') == 'True')
if not DEBUG:
# Security
Expand Down
20 changes: 20 additions & 0 deletions api/models/db/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from api.models.db.cycle import Cycle
from api.models.db.key_result_check_in import KeyResultCheckIn
from api.models.db.key_result_check_mark import KeyResultCheckMark
from api.models.db.key_result_comment import KeyResultComment
from api.models.db.team import Team
from api.models.db.user import User
from api.models.db.key_result import KeyResult
from api.models.db.objective import Objective


DB_MAP = {
'team': Team,
'user': User,
'cycle': Cycle,
'objective': Objective,
'key-result': KeyResult,
'key-result-check-mark': KeyResultCheckMark,
'key-result-check_in': KeyResultCheckIn,
'key-result-comment': KeyResultComment,
}
Empty file.
10 changes: 10 additions & 0 deletions api/models/db/associations/association_team_user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from sqlalchemy import Column, ForeignKey, Table

from api.models.db.base import Base

association_team_user = Table(
'team_users_user',
Base.metadata,
Column('user_id', ForeignKey('user.id')),
Column('team_id', ForeignKey('team.id')),
)
4 changes: 4 additions & 0 deletions api/models/db/cycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,7 @@ class Cycle(Base):

objectives: Mapped[List['Objective']] = relationship(
back_populates='cycle')

@property
def owner_id(self):
return self.team.owner_id
8 changes: 8 additions & 0 deletions api/models/db/key_result_check_in.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,11 @@ class KeyResultCheckIn(Base):
key_result_id: Mapped[str] = mapped_column(ForeignKey('key_result.id'))
key_result: Mapped['KeyResult'] = relationship(
back_populates='key_result_check_ins')

@property
def team_id(self):
return self.key_result.team_id

@property
def team(self):
return self.key_result.team
12 changes: 12 additions & 0 deletions api/models/db/key_result_check_mark.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,15 @@ class KeyResultCheckMark(Base):
ForeignKey('key_result.id'))
key_result: Mapped['KeyResult'] = relationship(
back_populates='key_result_check_marks')

@property
def team_id(self):
return self.key_result.team_id

@property
def team(self):
return self.key_result.team

@property
def owner_id(self):
return self.assigned_user_id
8 changes: 8 additions & 0 deletions api/models/db/key_result_comment.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,11 @@ class KeyResultComment(Base):
ForeignKey('key_result.id'))
key_result: Mapped['KeyResult'] = relationship(
back_populates='key_result_comments')

@property
def team_id(self):
return self.key_result.team_id

@property
def team(self):
return self.key_result.team
11 changes: 11 additions & 0 deletions api/models/db/team.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
from sqlalchemy.orm import relationship
from typing import TYPE_CHECKING, Optional, List

from api.models.db.associations.association_team_user import association_team_user
from api.models.db.views.team_company import association_team_company
from api.models.db.base import Base
if TYPE_CHECKING:
from api.models.db.key_result import KeyResult
Expand Down Expand Up @@ -30,3 +32,12 @@ class Team(Base):
objectives: Mapped[List['Objective']] = relationship(back_populates='team')
key_results: Mapped[List['KeyResult']
] = relationship(back_populates='team')

users: Mapped[List['User']] = relationship(
secondary=association_team_user, back_populates='teams')

company: Mapped['Team'] = relationship(
secondary=association_team_company,
primaryjoin=id == association_team_company.c.team_id,
secondaryjoin=id == association_team_company.c.company_id,
)
7 changes: 7 additions & 0 deletions api/models/db/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy.orm import relationship

from api.models.db.associations.association_team_user import association_team_user
from api.models.db.base import Base
if TYPE_CHECKING:
from api.models.db.team import Team
from api.models.db.key_result import KeyResult
from api.models.db.objective import Objective
from api.models.db.key_result_check_in import KeyResultCheckIn
Expand All @@ -17,6 +19,8 @@ class User(Base):
first_name: Mapped[str] = mapped_column()
last_name: Mapped[str] = mapped_column()

authz_sub: Mapped[str] = mapped_column()

objectives: Mapped[List['Objective']] = relationship(
back_populates='owner')
key_results: Mapped[List['KeyResult']
Expand All @@ -27,3 +31,6 @@ class User(Base):
back_populates='assigned_user')
key_result_comments: Mapped[List['KeyResultComment']] = relationship(
back_populates='user')

teams: Mapped[List['Team']] = relationship(
secondary=association_team_user, back_populates='users')
Empty file added api/models/db/views/__init__.py
Empty file.
12 changes: 12 additions & 0 deletions api/models/db/views/team_company.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Define the TeamCompany model representing the view
from sqlalchemy import Column, ForeignKey, Table
from api.models.db.base import Base


association_team_company = Table(
'team_company',
Base.metadata,
Column('team_id', ForeignKey('team.id')),
Column('company_id', ForeignKey('team.id')),
Column('depth'),
)
9 changes: 9 additions & 0 deletions api/services/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from flask import Blueprint

blueprint = Blueprint(
'auth',
__name__,
url_prefix='/core/auth',
template_folder='templates',
static_folder='static'
)
10 changes: 10 additions & 0 deletions api/services/auth/routes.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from flask import session, redirect
from api import core_oauth
from . import blueprint


@blueprint.route('/callback', methods=['GET', 'POST'])
def callback():
token = core_oauth.auth0.authorize_access_token() # type: ignore
session['user'] = token
return redirect('/')
15 changes: 9 additions & 6 deletions api/services/llm/routes.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from api.services.llm.logic.index import LlmIndex
from api.services.llm.logic.summary import LlmSummary
from api.utils.auth import requires_auth
from api.utils.openai import OpenAI
from . import blueprint
from flask import render_template


@blueprint.route('/<okr_id>')
def index(okr_id: str):
return render_template('index.html', opts=LlmIndex(okr_id))
@blueprint.route('/<key_result_id>')
@requires_auth('key-result:create')
def index(key_result_id: str):
return render_template('index.html', opts=LlmIndex(key_result_id))


@blueprint.route('/summary/<okr_id>')
def summary(okr_id: str):
return render_template('summary.html', opts=LlmSummary(OpenAI(), okr_id))
@blueprint.route('/summary/<key_result_id>')
@requires_auth('key-result:create')
def summary(key_result_id: str):
return render_template('summary.html', opts=LlmSummary(OpenAI(), key_result_id))
135 changes: 135 additions & 0 deletions api/utils/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
from functools import wraps
import jwt
import base64

from flask import abort, request, session, url_for
from sqlalchemy.orm import joinedload
from api import core_oauth, core_db
from api.models.db import DB_MAP
from api.models.db.team import Team
from api.models.db.user import User


SCOPES = ['company', 'team', 'owns']


class PermissionController:
'''Controller to check if user has minimum permissions to selected entity

Attributes:
sub: authz_sub as on database
model: entity model as defined on models.DB_MAP
entity_id: id to search for on table column `id`
'''

def __init__(self, sub: str, entity: str, entity_id: str):
'''Initializes controller with user sub and entity

Args:
sub: authz_sub as on database
entity: entity table name
entity_id: id to search for on table column `id`
'''
self.entity_id = entity_id
self.model = DB_MAP[entity]
self.sub = sub

def verify(self, scope: str) -> bool:
'''Verifies if current user has permission to access entity
based on scope

Args:
scope: auth scope in 'entity:action:scope'

Returns:
True if user has permission
'''
if scope == 'owns':
return self._user_owns_entity()
if scope == 'team':
return self._user_team_owns_entity()
if scope == 'company':
return self._user_company_owns_entity()
return False

def _user_owns_entity(self) -> bool:
'''Verifies if current entity has owner_id as the user id'''
cur_user: User | None = core_db.session.query(
User
).filter_by(
authz_sub=self.sub
).one()
cur_obj = core_db.session.query(
self.model).filter_by(id=self.entity_id).one()

return cur_user.id == cur_obj.owner_id

def _user_team_owns_entity(self) -> bool:
'''Verifies if current entity has team_id as the user team id'''
cur_user: User | None = core_db.session.query(User).filter_by(
authz_sub=self.sub
).options(
joinedload(User.teams),
).one()
cur_obj = core_db.session.query(
self.model).filter_by(id=self.entity_id).one()

for team in cur_user.teams:
if team.id == cur_obj.team_id:
return True
return False

def _user_company_owns_entity(self) -> bool:
'''Verifies if current entity has team.company_id
as the user company id'''
cur_user: User | None = core_db.session.query(User).filter_by(
authz_sub=self.sub
).options(
joinedload(User.teams),
joinedload(User.teams).joinedload(Team.company),
).one()
cur_obj = core_db.session.query(self.model).filter_by(
id=self.entity_id
).one()

for team in cur_user.teams:
if team.company.id == cur_obj.team.company.id:
return True
return False


def requires_auth(permission=None):
'''
Use on routes that require a valid session, otherwise it aborts with a 403
'''
def _decorator(f):
@wraps(f)
def wrapper(*args, **kwargs):
user = session.get('user')
print(user)
if user is None:
return core_oauth.auth0.authorize_redirect( # type: ignore
redirect_uri=url_for('auth.callback', _external=True)
)
if permission is not None:
# Get user permissions
granted_permissions = []
for p in user['userinfo']["'https://api.getbud.co'/permissions"]:
if p.startswith(permission):
granted_permissions.append(p)
granted_scope = ''
for scope in SCOPES:
if f'{permission}:{scope}' in granted_permissions:
granted_scope = scope
break
entity, _ = permission.split(':')
entity_id_param = f'{entity.replace('-', '_')}_id'

controller = PermissionController(
user['userinfo']['sub'], entity, kwargs[entity_id_param])
if not controller.verify(granted_scope):
return abort(403)

return f(*args, **kwargs)
return wrapper
return _decorator
Loading