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(platform): OAuth support + API key management + GitHub blocks #8044

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
efdd71b
Add Github blocks + "DEVELOPER_TOOLS" category
Bentlybro Sep 10, 2024
57c22e0
Merge branch 'master' into reinier/open-1806-implement-front-to-back-…
Pwuts Sep 11, 2024
4cec829
move github_blocks.py -> github.py
Pwuts Sep 11, 2024
e7fa682
remove duplicate OAuth handler files
Pwuts Sep 11, 2024
8ae5378
feat(server): Add Supabase support to `AppService`
Pwuts Sep 11, 2024
20c5137
Add `**kwargs` to `Block.run(..)` signature to support additional kwargs
Pwuts Sep 11, 2024
08666f4
feat(libs/supabase_integration_credentials_store): Add `CredentialsTy…
Pwuts Sep 11, 2024
14c8ca4
Add strict support for `credentials` fields on blocks
Pwuts Sep 11, 2024
da6d443
feat(executor): Fetch credentials for graph execution and pass them d…
Pwuts Sep 12, 2024
56d379f
feat(server/blocks): Integrate GitHub blocks with new credentials inf…
Pwuts Sep 12, 2024
d7e34b1
Merge branch 'master' into reinier/open-1806-implement-front-to-back-…
Pwuts Sep 12, 2024
c623d61
Merge branch 'master' into reinier/open-1806-implement-front-to-back-…
Pwuts Sep 12, 2024
26a837a
Merge branch 'master' into reinier/open-1806-implement-front-to-back-…
Pwuts Sep 16, 2024
c8f0110
Fix schema output with extra properties on complex input fields
Pwuts Sep 16, 2024
4a56de0
Merge branch 'master' into reinier/open-1806-implement-front-to-back-…
Pwuts Sep 17, 2024
57d543a
feat(server): Server endpoints to add `APIKeyCredentials` and remove …
kcze Sep 18, 2024
03fe5a1
Merge branch 'master' into reinier/open-1806-implement-front-to-back-…
Pwuts Sep 18, 2024
f4a92dc
fix conflict resolve in integrations API router
Pwuts Sep 18, 2024
c8115b1
Merge branch 'master' into reinier/open-1806-implement-front-to-back-…
Pwuts Sep 18, 2024
1b54bd3
feat(builder): Authorization UI on for integration blocks (#8067)
kcze Sep 19, 2024
d9b2ccc
fix field description on API key dialog
Pwuts Sep 19, 2024
e260c39
parametrize `test_available_blocks`
Pwuts Sep 19, 2024
a5264d6
Update .env.example and docker-compose.yml with new config variables
Pwuts Sep 19, 2024
74138d7
add docs for adding authenticated blocks
Pwuts Sep 19, 2024
57afc85
add comment to .env.example about GitHub app type
Pwuts Sep 19, 2024
6aea79b
fix block tests with credentials
Pwuts Sep 19, 2024
e213de6
Merge branch 'master' into reinier/open-1806-implement-front-to-back-…
Pwuts Sep 20, 2024
34a3dd9
filter OAuth credentials in dropdown by required scopes for block
Pwuts Sep 20, 2024
1b81215
Merge branch 'master' into reinier/open-1806-implement-front-to-back-…
Pwuts Sep 23, 2024
dd731f0
resolve block category comment
Pwuts Sep 23, 2024
a64055b
Break up `blocks/github.py`
Pwuts Sep 23, 2024
7cbcdb9
Merge branch 'master' into reinier/open-1806-implement-front-to-back-…
Pwuts Sep 23, 2024
3ee7939
regenerate GitHub block IDs
Pwuts Sep 23, 2024
a2c1f82
fix API key creation issue
Pwuts Sep 23, 2024
125b1f3
fix credentials picker API key display issue
Pwuts Sep 24, 2024
2e3c140
fix API key creation reponse type
Pwuts Sep 24, 2024
b58f9a8
fix credentials picker updating after creating API key
Pwuts Sep 24, 2024
92cbb37
Merge branch 'master' into reinier/open-1806-implement-front-to-back-…
Pwuts Sep 24, 2024
8388507
lint
Pwuts Sep 24, 2024
740868e
fix React error on `expiresAt` field in API key dialog
Pwuts Sep 24, 2024
cecfa0a
Merge branch 'master' into reinier/open-1806-implement-front-to-back-…
Pwuts Sep 25, 2024
eb2d2bc
add FRONTEND_BASE_URL to .env.example
Pwuts Sep 25, 2024
4244a90
add FRONTEND_BASE_URL to docker compose config
Pwuts Sep 25, 2024
08dfaa6
rename `SUPABASE_SERVICE_KEY` to `SUPABASE_SERVICE_ROLE_KEY`
Pwuts Sep 25, 2024
d555707
add note about OAuth callback URL to .env.example
Pwuts Sep 25, 2024
cf93167
tweak
Pwuts Sep 25, 2024
4f11b74
fix output types of `GithubListX` blocks
Pwuts Sep 25, 2024
8208236
further GitHub block tweaks
Pwuts Sep 25, 2024
ead01ff
add `GithubListPullRequestsBlock` + make GitHub block names consistent
Pwuts Sep 25, 2024
3d0d6b2
replace `GithubReadFileFromMasterBlock` and `GithubReadFileFolderRepo…
Pwuts Sep 25, 2024
e5cf691
untangle error handling in GitHub blocks + remove `GithubReadCodeowne…
Pwuts Sep 25, 2024
31bdd34
improve output of `GithubCreateCommentBlock`, `GithubMakeIssueBlock`,…
Pwuts Sep 25, 2024
1e3ddec
Disable GitHub OAuth on blocks if not configured
Pwuts Sep 25, 2024
60b7c00
fix output titles
Pwuts Sep 25, 2024
5abad45
Merge branch 'master' into reinier/open-1806-implement-front-to-back-…
Pwuts Sep 25, 2024
172fd8f
add to helm
aarushik93 Sep 25, 2024
14673a8
Merge branch 'master' into reinier/open-1806-implement-front-to-back-…
Pwuts Sep 25, 2024
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: 1 addition & 1 deletion .github/workflows/platform-backend-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ jobs:
LOG_LEVEL: ${{ runner.debug && 'DEBUG' || 'INFO' }}
DATABASE_URL: ${{ steps.supabase.outputs.DB_URL }}
SUPABASE_URL: ${{ steps.supabase.outputs.API_URL }}
SUPABASE_SERVICE_KEY: ${{ steps.supabase.outputs.SERVICE_ROLE_KEY }}
SUPABASE_SERVICE_ROLE_KEY: ${{ steps.supabase.outputs.SERVICE_ROLE_KEY }}
SUPABASE_JWT_SECRET: ${{ steps.supabase.outputs.JWT_SECRET }}
env:
CI: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@
from .jwt_utils import parse_jwt_token

security = HTTPBearer()
logger = logging.getLogger(__name__)


async def auth_middleware(request: Request):
if not settings.ENABLE_AUTH:
# If authentication is disabled, allow the request to proceed
logging.warn("Auth disabled")
logger.warn("Auth disabled")
return {}

security = HTTPBearer()
Expand All @@ -24,7 +25,7 @@ async def auth_middleware(request: Request):
try:
payload = parse_jwt_token(credentials.credentials)
request.state.user = payload
logging.info("Token decoded successfully")
logger.debug("Token decoded successfully")
except ValueError as e:
raise HTTPException(status_code=401, detail=str(e))
return payload
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,29 @@ class OAuth2Credentials(_BaseCredentials):
scopes: list[str]
Pwuts marked this conversation as resolved.
Show resolved Hide resolved
metadata: dict[str, Any] = Field(default_factory=dict)

def bearer(self) -> str:
return f"Bearer {self.access_token.get_secret_value()}"


class APIKeyCredentials(_BaseCredentials):
type: Literal["api_key"] = "api_key"
api_key: SecretStr
expires_at: Optional[int]
"""Unix timestamp (seconds) indicating when the API key expires (if at all)"""

def bearer(self) -> str:
return f"Bearer {self.api_key.get_secret_value()}"


Credentials = Annotated[
OAuth2Credentials | APIKeyCredentials,
Field(discriminator="type"),
]


CredentialsType = Literal["api_key", "oauth2"]


class OAuthState(BaseModel):
token: str
provider: str
Expand Down
23 changes: 20 additions & 3 deletions autogpt_platform/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,30 @@ REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=password

ENABLE_AUTH=false
ENABLE_CREDIT=false
APP_ENV="local"
PYRO_HOST=localhost
SENTRY_DSN=
# This is needed when ENABLE_AUTH is true
SUPABASE_JWT_SECRET=our-super-secret-jwt-token-with-at-least-32-characters-long

## User auth with Supabase is required for any of the 3rd party integrations with auth to work.
Pwuts marked this conversation as resolved.
Show resolved Hide resolved
ENABLE_AUTH=false
SUPABASE_URL=
SUPABASE_SERVICE_ROLE_KEY=
SUPABASE_JWT_SECRET=

# For local development, you may need to set FRONTEND_BASE_URL for the OAuth flow for integrations to work.
# FRONTEND_BASE_URL=http://localhost:3000

## == INTEGRATION CREDENTIALS == ##
# Each set of server side credentials is required for the corresponding 3rd party
# integration to work.
Pwuts marked this conversation as resolved.
Show resolved Hide resolved

# For the OAuth callback URL, use <your_frontend_url>/auth/integrations/oauth_callback,
# e.g. http://localhost:3000/auth/integrations/oauth_callback

# GitHub OAuth App server credentials - https://github.com/settings/developers
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=

## ===== OPTIONAL API KEYS ===== ##

Expand Down
15 changes: 7 additions & 8 deletions autogpt_platform/backend/backend/blocks/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import glob
import importlib
import os
import re
Expand All @@ -8,17 +7,17 @@

# Dynamically load all modules under backend.blocks
AVAILABLE_MODULES = []
current_dir = os.path.dirname(__file__)
modules = glob.glob(os.path.join(current_dir, "*.py"))
current_dir = Path(__file__).parent
modules = [
Path(f).stem
for f in modules
if os.path.isfile(f) and f.endswith(".py") and not f.endswith("__init__.py")
str(f.relative_to(current_dir))[:-3].replace(os.path.sep, ".")
for f in current_dir.rglob("*.py")
if f.is_file() and f.name != "__init__.py"
]
for module in modules:
if not re.match("^[a-z_]+$", module):
if not re.match("^[a-z_.]+$", module):
raise ValueError(
f"Block module {module} error: module name must be lowercase, separated by underscores, and contain only alphabet characters"
f"Block module {module} error: module name must be lowercase, "
"separated by underscores, and contain only alphabet characters"
)

importlib.import_module(f".{module}", package=__name__)
Expand Down
16 changes: 8 additions & 8 deletions autogpt_platform/backend/backend/blocks/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ def __init__(self):
static_output=True,
)

def run(self, input_data: Input) -> BlockOutput:
def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "output", input_data.data or input_data.input


Expand All @@ -79,7 +79,7 @@ def __init__(self):
test_output=("status", "printed"),
)

def run(self, input_data: Input) -> BlockOutput:
def run(self, input_data: Input, **kwargs) -> BlockOutput:
print(">>>>> Print: ", input_data.text)
yield "status", "printed"

Expand Down Expand Up @@ -118,7 +118,7 @@ def __init__(self):
categories={BlockCategory.BASIC},
)

def run(self, input_data: Input) -> BlockOutput:
def run(self, input_data: Input, **kwargs) -> BlockOutput:
obj = input_data.input
key = input_data.key

Expand Down Expand Up @@ -200,7 +200,7 @@ def __init__(self):
ui_type=BlockUIType.INPUT,
)

def run(self, input_data: Input) -> BlockOutput:
def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "result", input_data.value


Expand Down Expand Up @@ -283,7 +283,7 @@ def __init__(self):
ui_type=BlockUIType.OUTPUT,
)

def run(self, input_data: Input) -> BlockOutput:
def run(self, input_data: Input, **kwargs) -> BlockOutput:
"""
Attempts to format the recorded_value using the fmt_string if provided.
If formatting fails or no fmt_string is given, returns the original recorded_value.
Expand Down Expand Up @@ -343,7 +343,7 @@ def __init__(self):
],
)

def run(self, input_data: Input) -> BlockOutput:
def run(self, input_data: Input, **kwargs) -> BlockOutput:
try:
# If no dictionary is provided, create a new one
if input_data.dictionary is None:
Expand Down Expand Up @@ -414,7 +414,7 @@ def __init__(self):
],
)

def run(self, input_data: Input) -> BlockOutput:
def run(self, input_data: Input, **kwargs) -> BlockOutput:
try:
# If no list is provided, create a new one
if input_data.list is None:
Expand Down Expand Up @@ -455,5 +455,5 @@ def __init__(self):
ui_type=BlockUIType.NOTE,
)

def run(self, input_data: Input) -> BlockOutput:
def run(self, input_data: Input, **kwargs) -> BlockOutput:
yield "output", input_data.text
2 changes: 1 addition & 1 deletion autogpt_platform/backend/backend/blocks/block.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def __init__(self):
disabled=True,
)

def run(self, input_data: Input) -> BlockOutput:
def run(self, input_data: Input, **kwargs) -> BlockOutput:
code = input_data.code

if search := re.search(r"class (\w+)\(Block\):", code):
Expand Down
2 changes: 1 addition & 1 deletion autogpt_platform/backend/backend/blocks/branching.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def __init__(self):
],
)

def run(self, input_data: Input) -> BlockOutput:
def run(self, input_data: Input, **kwargs) -> BlockOutput:
value1 = input_data.value1
operator = input_data.operator
value2 = input_data.value2
Expand Down
2 changes: 1 addition & 1 deletion autogpt_platform/backend/backend/blocks/csv.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def __init__(self):
],
)

def run(self, input_data: Input) -> BlockOutput:
def run(self, input_data: Input, **kwargs) -> BlockOutput:
import csv
from io import StringIO

Expand Down
6 changes: 3 additions & 3 deletions autogpt_platform/backend/backend/blocks/discord.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,14 @@ async def on_message(message):

await client.start(token)

def run(self, input_data: "ReadDiscordMessagesBlock.Input") -> BlockOutput:
def run(self, input_data: Input, **kwargs) -> BlockOutput:
while True:
for output_name, output_value in self.__run(input_data):
yield output_name, output_value
if not input_data.continuous_read:
break

def __run(self, input_data: "ReadDiscordMessagesBlock.Input") -> BlockOutput:
def __run(self, input_data: Input) -> BlockOutput:
try:
loop = asyncio.get_event_loop()
future = self.run_bot(input_data.discord_bot_token.get_secret_value())
Expand Down Expand Up @@ -187,7 +187,7 @@ def chunk_message(self, message: str, limit: int = 2000) -> list:
"""Splits a message into chunks not exceeding the Discord limit."""
return [message[i : i + limit] for i in range(0, len(message), limit)]

def run(self, input_data: "SendDiscordMessageBlock.Input") -> BlockOutput:
def run(self, input_data: Input, **kwargs) -> BlockOutput:
try:
loop = asyncio.get_event_loop()
future = self.send_message(
Expand Down
2 changes: 1 addition & 1 deletion autogpt_platform/backend/backend/blocks/email_block.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def send_email(
except Exception as e:
return f"Failed to send email: {str(e)}"

def run(self, input_data: Input) -> BlockOutput:
def run(self, input_data: Input, **kwargs) -> BlockOutput:
status = self.send_email(
input_data.creds,
input_data.to_email,
Expand Down
54 changes: 54 additions & 0 deletions autogpt_platform/backend/backend/blocks/github/_auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
from typing import Literal

from autogpt_libs.supabase_integration_credentials_store.types import (
APIKeyCredentials,
OAuth2Credentials,
)
from pydantic import SecretStr

from backend.data.model import CredentialsField, CredentialsMetaInput
from backend.util.settings import Secrets

secrets = Secrets()
GITHUB_OAUTH_IS_CONFIGURED = bool(
secrets.github_client_id and secrets.github_client_secret
)

GithubCredentials = APIKeyCredentials | OAuth2Credentials
GithubCredentialsInput = CredentialsMetaInput[
Literal["github"],
Literal["api_key", "oauth2"] if GITHUB_OAUTH_IS_CONFIGURED else Literal["api_key"],
]


def GithubCredentialsField(scope: str) -> GithubCredentialsInput:
"""
Creates a GitHub credentials input on a block.

Params:
scope: The authorization scope needed for the block to work. ([list of available scopes](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/scopes-for-oauth-apps#available-scopes))
""" # noqa
return CredentialsField(
provider="github",
supported_credential_types=(
{"api_key", "oauth2"} if GITHUB_OAUTH_IS_CONFIGURED else {"api_key"}
),
required_scopes={scope},
description="The GitHub integration can be used with OAuth, "
"or any API key with sufficient permissions for the blocks it is used on.",
)


TEST_CREDENTIALS = APIKeyCredentials(
id="01234567-89ab-cdef-0123-456789abcdef",
provider="github",
api_key=SecretStr("mock-github-api-key"),
title="Mock GitHub API key",
expires_at=None,
)
TEST_CREDENTIALS_INPUT = {
"provider": TEST_CREDENTIALS.provider,
"id": TEST_CREDENTIALS.id,
"type": TEST_CREDENTIALS.type,
"title": TEST_CREDENTIALS.type,
}
Loading
Loading