Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Ability to invite users to organizations #604

Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
c64933d
setup db schema for organisations
devgenix Sep 13, 2023
f406233
fix requested changes
devgenix Sep 13, 2023
1dc844a
Update agenta-backend/agenta_backend/models/db_models.py
devgenix Sep 13, 2023
204e007
fix members field
devgenix Sep 13, 2023
2736ad1
Merge branch '#58-oss-ability-to-invite-users-to-organizations' of ht…
devgenix Sep 13, 2023
43b334a
add owners and members
devgenix Sep 13, 2023
320b796
modify get_user_objectid to return list of organization ids
devgenix Sep 13, 2023
b8a99fc
format with black
devgenix Sep 13, 2023
0b73e9e
updated get user's organisations
devgenix Sep 13, 2023
180d1c8
switch to odmantic
devgenix Sep 13, 2023
c905c7e
return ids in get_user_and_org_id
devgenix Sep 13, 2023
0e288d6
added forward ref
devgenix Sep 13, 2023
041cbf2
update get_user_object to use organisations
devgenix Sep 13, 2023
ee85443
change organizations to use objectid to avoid circular import
devgenix Sep 13, 2023
99903f1
modify get_organisation & get apps fromorg and user
devgenix Sep 13, 2023
e06d9b1
format black
devgenix Sep 13, 2023
5bf5fc8
Merge branch 'Agenta-AI:main' into #58-oss-ability-to-invite-users-to…
devgenix Sep 14, 2023
bd32422
remove debugging
devgenix Sep 14, 2023
2d83f22
Merge branch '#58-oss-ability-to-invite-users-to-organizations' of ht…
devgenix Sep 14, 2023
c769ffc
change get organisation format
devgenix Sep 14, 2023
71e3f32
installed sendgrid
devgenix Sep 14, 2023
c0e9d9a
built func to generate token by length
devgenix Sep 15, 2023
89874ba
created organisation router
devgenix Sep 15, 2023
864c072
create db schema for org invites
devgenix Sep 15, 2023
358a0b9
update model referenc to follow new schema
devgenix Sep 15, 2023
1c9b59e
remove org_instance to comply with ee
devgenix Sep 15, 2023
e6e0ad0
defined Organizational types
devgenix Sep 15, 2023
8ff61f3
exclude file from black
devgenix Sep 15, 2023
20abb0f
run black
devgenix Sep 15, 2023
14d808f
feat: allow users to invite and join organizations
devgenix Sep 15, 2023
6858850
switch to using payload data.
devgenix Sep 15, 2023
235cf67
undo change to workflow
devgenix Sep 15, 2023
59d25d2
fix resolve pr comments
devgenix Sep 15, 2023
338b248
global organization name refactor & optmised perf with sets
devgenix Sep 15, 2023
80d8502
format black
devgenix Sep 15, 2023
be83f4e
move org funcs to ee
devgenix Sep 15, 2023
94331a2
add type to organizationDB
devgenix Sep 18, 2023
16c82ae
add organization_id to AppVariant type
devgenix Sep 18, 2023
a9d7610
move dbegine to common to avoid circular import
devgenix Sep 18, 2023
01febad
improve functions
devgenix Sep 18, 2023
a4e37f7
use try except & create user based on feature flag
devgenix Sep 18, 2023
3f7e639
define get_user_own_org
devgenix Sep 18, 2023
638cfdf
save AppVariant with organization based on condition
devgenix Sep 18, 2023
208353c
fix requested changes
devgenix Sep 18, 2023
3f73b57
fix minor issues
devgenix Sep 18, 2023
5f12960
Merge branch 'main' into #58-oss-email-invitations-to-join-an-organiz…
devgenix Sep 19, 2023
83a8274
remove router
devgenix Sep 19, 2023
20e5565
ran format black
devgenix Sep 19, 2023
b8b5962
update db schema
devgenix Sep 19, 2023
ea60381
use BaseModel and add fields
devgenix Sep 19, 2023
c37dd73
fix get org apps & create user, org; use org in add app from variant …
devgenix Sep 19, 2023
9e149f0
use user.id for irg owner
devgenix Sep 19, 2023
a9cec29
move token function to ee
devgenix Sep 19, 2023
c0afa59
update local with remote
devgenix Sep 19, 2023
c59315c
ran format black
devgenix Sep 19, 2023
cefddcb
resolved poetry
devgenix Sep 19, 2023
867be6b
Merge branch 'main' into #58-oss-email-invitations-to-join-an-organiz…
devgenix Sep 20, 2023
f7cf526
return list_apps by org_id if present
devgenix Sep 20, 2023
620e078
update local with remote
devgenix Sep 20, 2023
20e6345
oss sync with frontend request improvements
devgenix Sep 20, 2023
fd2f092
sync with frontend enpoints request
devgenix Sep 20, 2023
f7b1068
create user profile router and endpoint
devgenix Sep 20, 2023
4753be2
remove org ee router
devgenix Sep 21, 2023
b73d727
ran format black
devgenix Sep 21, 2023
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
2 changes: 2 additions & 0 deletions agenta-backend/agenta_backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from fastapi.middleware.cors import CORSMiddleware
from agenta_backend.routers import container_router
from agenta_backend.routers import evaluation_router
from agenta_backend.routers import organization_router
from agenta_backend.services.db_manager import (
add_template,
remove_old_template_from_db,
Expand Down Expand Up @@ -89,6 +90,7 @@ async def lifespan(application: FastAPI, cache=True):
app.include_router(evaluation_router.router, prefix="/evaluations")
app.include_router(testset_router.router, prefix="/testsets")
app.include_router(container_router.router, prefix="/containers")
app.include_router(organizsation_router.router, prefix="/organizations")

allow_headers = ["Content-Type"]

Expand Down
10 changes: 10 additions & 0 deletions agenta-backend/agenta_backend/models/api/api_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,13 @@ class CreateAppVariant(BaseModel):
image_id: str
image_tag: str
env_vars: Dict[str, str]


class OrganizationInvite(BaseModel):
organization_id: str
email_address: str


class OrganizationToken(BaseModel):
organization_id: str
token: str
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Optional
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, Field
from agenta_backend.models.api.user_models import User


class TimestampModel(BaseModel):
Expand All @@ -11,8 +12,14 @@ class TimestampModel(BaseModel):
class Organization(TimestampModel):
name: str
description: Optional[str]
owner: User
members: Optional[List]
devgenix marked this conversation as resolved.
Show resolved Hide resolved
invitations: Optional[List]


class OrganizationUpdate(BaseModel):
name: Optional[str]
description: Optional[str]
owner: User
members: Optional[List]
invitations: Optional[List]
4 changes: 2 additions & 2 deletions agenta-backend/agenta_backend/models/api/user_models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from typing import Optional
from datetime import datetime
from typing import Optional, List
from pydantic import BaseModel, Field


Expand All @@ -12,7 +12,7 @@ class User(TimestampModel):
uid: str
username: str
email: str # switch to EmailStr when langchain support pydantic>=2.1
devgenix marked this conversation as resolved.
Show resolved Hide resolved
organization_id: str
organizations: Optional[List]
devgenix marked this conversation as resolved.
Show resolved Hide resolved


class UserUpdate(BaseModel):
Expand Down
22 changes: 20 additions & 2 deletions agenta-backend/agenta_backend/models/db_models.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
from datetime import datetime
from bson import ObjectId
from datetime import datetime, timedelta
from typing import Optional, Dict, Any, List
from odmantic import Field, Model, Reference, EmbeddedModel


class InvitationDB(EmbeddedModel):
token: str = Field(unique=True)
email: str
expiration_date: datetime = Field(default="0")
used: bool = False


class OrganizationDB(Model):
name: str = Field(default="agenta")
description: str = Field(default="")
owner: "UserDB"
members: Optional[List["UserDB"]]
invitations: Optional[List[InvitationDB]] = []

class Config:
collection = "organizations"
Expand All @@ -15,7 +26,7 @@ class UserDB(Model):
uid: str = Field(default="0", unique=True, index=True)
username: str = Field(default="agenta")
email: str = Field(default="[email protected]", unique=True)
organization_id: OrganizationDB = Reference(key_name="org")
organizations: Optional[List[ObjectId]] = []

class Config:
collection = "users"
Expand All @@ -41,6 +52,7 @@ class AppVariantDB(Model):
user_id: UserDB = Reference(key_name="user")
parameters: Dict[str, Any] = Field(default=dict)
previous_variant_name: Optional[str]
organization: Optional[OrganizationDB]
is_deleted: bool = Field(
default=False
) # soft deletion for using the template variants
Expand Down Expand Up @@ -92,6 +104,7 @@ class EvaluationDB(Model):
app_name: str
testset: Dict[str, str]
user: UserDB = Reference(key_name="user")
organization: Optional[OrganizationDB]
created_at: Optional[datetime] = Field(default=datetime.utcnow())
updated_at: Optional[datetime] = Field(default=datetime.utcnow())

Expand All @@ -107,6 +120,7 @@ class EvaluationScenarioDB(Model):
evaluation: Optional[str]
evaluation_id: str
user: UserDB = Reference(key_name="user")
organization: Optional[OrganizationDB]
correct_answer: Optional[str]
created_at: Optional[datetime] = Field(default=datetime.utcnow())
updated_at: Optional[datetime] = Field(default=datetime.utcnow())
Expand All @@ -120,8 +134,12 @@ class TestSetDB(Model):
app_name: str
csvdata: List[Dict[str, str]]
user: UserDB = Reference(key_name="user")
organization: Optional[OrganizationDB]
created_at: Optional[datetime] = Field(default=datetime.utcnow())
updated_at: Optional[datetime] = Field(default=datetime.utcnow())

class Config:
collection = "testsets"


OrganizationDB.update_forward_refs()
150 changes: 150 additions & 0 deletions agenta-backend/agenta_backend/routers/organization_router.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import os
from bson import ObjectId
from datetime import datetime, timedelta
from fastapi.responses import JSONResponse
from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks
from agenta_backend.services.organization_service import (
get_organization,
send_invitation_email,
accept_org_invitation,
check_user_org_access,
notify_org_admin_invitation,
)
from agenta_backend.services.db_manager import engine
from agenta_backend.models.db_models import InvitationDB, UserDB
from agenta_backend.utills.common import generate_invitation_token
from agenta_backend.models.api.api_models import (
OrganizationInvite,
OrganizationToken,
)

if os.environ["FEATURE_FLAG"] in ["cloud", "ee", "demo"]:
from agenta_backend.ee.services.auth_helper import (
SessionContainer,
verify_session,
)
from agenta_backend.ee.services.selectors import get_user_and_org_id
else:
from agenta_backend.services.auth_helper import (
SessionContainer,
verify_session,
)
from agenta_backend.services.selectors import get_user_and_org_id


router = APIRouter()


@router.post("/add/{organization_id}/invite/")
async def invite_to_org(
payload: OrganizationInvite,
stoken_session: SessionContainer = Depends(verify_session()),
):
if os.environ["FEATURE_FLAG"] not in ["cloud", "ee", "demo"]:
raise HTTPException(
status_code=500,
detail="This feature is not available in the Open Source version",
)

try:
devgenix marked this conversation as resolved.
Show resolved Hide resolved
organization_id, email_address = (
payload.organization_id,
payload.email_address,
)

kwargs: dict = await get_user_and_org_id(stoken_session)
organization_access = await check_user_org_access(
kwargs, ObjectId(organization_id)
)

if organization_access:
organization = await get_organization((organization_id))
user = await engine.find_one(UserDB, UserDB.uid == kwargs["uid"])
if user.email == email_address:
return JSONResponse(
{"message": "You cannot invite yourself to your own organization"},
status_code=400,
)

token = generate_invitation_token()
expiration_date = datetime.utcnow() + timedelta(days=7)

send_email = send_invitation_email(email_address, token, organization, user)

if send_email:
created_invitation = InvitationDB(
token=token,
email=email_address,
expiration_date=expiration_date,
used=False,
)

organization.invitations.append(created_invitation)
await engine.save(organization)

return JSONResponse(
{"message": "Invited user to organization"}, status_code=200
)
else:
return JSONResponse(
{"message": "Failed to invite user to organization"},
status_code=400,
)

else:
return JSONResponse(
{"message": "You do not have permission to access this organization"},
status_code=403,
)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))


@router.post("/accept/")
async def add_user_to_org(
payload: OrganizationToken,
background_tasks: BackgroundTasks,
stoken_session: SessionContainer = Depends(verify_session()),
):
if not (os.environ["FEATURE_FLAG"] in ["cloud", "ee", "demo"]):
raise HTTPException(
status_code=500,
detail="This feature is not available in the Open Source version",
)

try:
devgenix marked this conversation as resolved.
Show resolved Hide resolved
organization_id, token = (payload.organization_id, payload.token)

kwargs: dict = await get_user_and_org_id(stoken_session)
organization_access = await check_user_org_access(
kwargs, ObjectId(organization_id)
)

if not organization_access:
organization = await get_organization(organization_id)
user = await engine.find_one(UserDB, UserDB.uid == kwargs["uid"])

join_organization = accept_org_invitation(user, organization, token)

if join_organization:
background_tasks.add_task(
notify_org_admin_invitation, organization, user
)

return JSONResponse(
{"message": "Added user to organization"}, status_code=200
)
else:
return JSONResponse(
{"message": "Invitation not found or has expired"},
status_code=400,
)
else:
return JSONResponse(
{"message": "You already belong to this organization"}, status_code=400
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=str(e),
)
Loading