Skip to content

Commit

Permalink
Refactor to remove separate farm_public endpoints. Related to farmOS#68
Browse files Browse the repository at this point in the history
  • Loading branch information
paul121 committed Feb 13, 2020
1 parent 288df5b commit 93a8e45
Show file tree
Hide file tree
Showing 9 changed files with 180 additions and 248 deletions.
11 changes: 1 addition & 10 deletions backend/app/app/api/api_v1/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from app.core import config
from app.api.api_v1.endpoints import login, users, utils
from app.api.api_v1.endpoints.farms import farms_public, farms, info, logs, assets, terms, areas
from app.api.api_v1.endpoints.farms import farms, info, logs, assets, terms, areas
from app.api.utils.security import get_farm_access

logger = logging.getLogger(__name__)
Expand All @@ -14,15 +14,6 @@
api_router.include_router(users.router, prefix="/users", tags=["users"])
api_router.include_router(utils.router, prefix="/utils", tags=["utils"])

# Include public /farms endpoints if enabled in config.
if (config.AGGREGATOR_OPEN_FARM_REGISTRATION):
logger.info("Open Farm Registration is enabled. Adding API to router.")
api_router.include_router(
farms_public.router,
prefix="/public/farms",
tags=["farms public"],
)

# Include /farms endpoints.
api_router.include_router(
farms.router,
Expand Down
7 changes: 4 additions & 3 deletions backend/app/app/api/api_v1/endpoints/farms/farms.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,16 @@

from app import crud
from app.api.utils.db import get_db
from app.api.utils.farms import get_farm_client, ClientError, get_farms_url_or_list, get_farm_by_id
from app.api.utils.security import get_farm_access
from app.api.utils.farms import get_farms_url_or_list, get_farm_by_id
from app.api.utils.security import get_farm_access, get_farm_access_allow_public
from app.schemas.farm import Farm, FarmCreate, FarmUpdate
from app.core.celery_app import celery_app

router = APIRouter()

# /farms/ endpoints for farmOS instances


@router.get(
"/",
response_model=List[Farm],
Expand Down Expand Up @@ -46,7 +47,7 @@ def read_farm_by_id(
@router.post(
"/",
response_model=Farm,
dependencies=[Security(get_farm_access, scopes=['farm:create'])]
dependencies=[Security(get_farm_access_allow_public, scopes=['farm:create'])]
)
async def create_farm(
*,
Expand Down
127 changes: 0 additions & 127 deletions backend/app/app/api/api_v1/endpoints/farms/farms_public.py

This file was deleted.

95 changes: 91 additions & 4 deletions backend/app/app/api/api_v1/endpoints/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from fastapi import APIRouter, Depends, Security, HTTPException
from fastapi import APIRouter, Depends, Security, HTTPException, Body
from pydantic.networks import EmailStr
from sqlalchemy.orm import Session
from farmOS import farmOS
from farmOS.config import ClientConfig

from app import crud
from app.api.utils.db import get_db
Expand All @@ -11,7 +13,7 @@
from app.schemas.farm import Farm
from app.schemas.farm_token import FarmTokenCreate, FarmAuthorizationParams
from app.api.utils.farms import get_farm_by_id, get_oauth_token
from app.api.utils.security import get_farm_access
from app.api.utils.security import get_farm_access, get_farm_access_allow_public
from app.utils import send_test_email, generate_farm_authorization_link, generate_farm_registration_link

router = APIRouter()
Expand Down Expand Up @@ -54,9 +56,59 @@ def farm_auth_link(
return link


@router.post(
"/authorize-farm/",
dependencies=[Security(get_farm_access_allow_public, scopes=['farm:create'])]
)
def authorize_farm(
*,
db: Session = Depends(get_db),
farm_url: str = Body(...),
auth_params: FarmAuthorizationParams,
):
"""
Authorize a new farm. Complete the OAuth Authorization Flow.
This endpoint is only used when authorizing a new farm, before creation.
See /authorize-farm/{farm_id} for authorizing existing farms.
"""
token = get_oauth_token(farm_url, auth_params)

client_id = 'farmos_api_client'
client_secret = 'client_secret'

config = ClientConfig()

config_values = {
'Profile': {
'development': 'True',
'hostname': farm_url,
'client_id': client_id,
'client_secret': client_secret,
}
}

if token is not None:
config_values['Profile']['access_token'] = token.access_token
config_values['Profile']['refresh_token'] = token.refresh_token
config_values['Profile']['expires_at'] = token.expires_at
config.read_dict(config_values)

try:
client = farmOS(config=config, profile_name="Profile")
info = client.info()

return {
'token': token,
'info': info
}
except Exception as e:
raise HTTPException(status_code=400, detail="Could not authenticate with farmOS server.")


@router.post(
"/authorize-farm/{farm_id}",
dependencies=[Security(get_farm_access, scopes=['farm:authorize'])]
dependencies=[Security(get_farm_access_allow_public, scopes=['farm:authorize'])]
)
def authorize_farm(
farm: Farm = Depends(get_farm_by_id),
Expand All @@ -65,7 +117,7 @@ def authorize_farm(
auth_params: FarmAuthorizationParams,
):
"""
Authorize a farm. Complete the OAuth Authorization Flow.
Authorize an existing farm. Complete the OAuth Authorization Flow.
"""
token = get_oauth_token(farm.url, auth_params)

Expand All @@ -78,3 +130,38 @@ def authorize_farm(
token = crud.farm_token.update_farm_token(db, token=old_token, token_in=new_token)

return token


@router.post(
"/validate-farm-url",
dependencies=[Security(get_farm_access_allow_public)]
)
def validate_farm_url(
*,
db: Session = Depends(get_db),
farm_url: str = Body(..., embed=True),
):
"""
Validate the farm_url when registering a new farm.
Check to make sure the url is not already in use, and check that
the url points to a valid farmOS server.
"""
existing_farm = crud.farm.get_by_url(db, farm_url=farm_url)
if existing_farm:
raise HTTPException(
status_code=409,
detail="A farm with this URL already exists.",
)

# Check that the `farm.json` endpoint returns 200
# TODO: Use farmOS.py helper function to validate server hostname.
response = {}
success = True
if not success:
raise HTTPException(
status_code=406,
detail="Invalid farmOS hostname. Make sure this is a valid hostname for your farmOS Server.",
)

return response

31 changes: 31 additions & 0 deletions backend/app/app/api/utils/security.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,37 @@ def get_farm_access(

return farm_access


def get_farm_access_allow_public(
user_access: dict = Depends(get_current_user_farm_access),
api_token_access: dict = Depends(get_api_token_farm_access),
):
farm_access = None

# If open registration is enabled, allow minimal access.
if config.AGGREGATOR_OPEN_FARM_REGISTRATION is True:
farm_access = FarmAccess(scopes=[], farm_id_list=[], all_farms=False)

# Still check for a request with higher permissions.
# This is the same as the get_farm_access dependency above.
if user_access is not None:
logger.debug(f"Request has user_access: {user_access}")
farm_access = user_access

if api_token_access is not None:
logger.debug(f"Request has api_token access: {api_token_access}")
farm_access = api_token_access

if farm_access is None:
logger.debug(f"Request has no farm access.")
raise HTTPException(
status_code=HTTP_401_UNAUTHORIZED,
detail="Could not validate credentials"
)

return farm_access


def _validate_token(token):
payload = jwt.decode(token, config.SECRET_KEY, algorithms=[ALGORITHM])
user_id: int = payload.get("sub", None)
Expand Down
23 changes: 9 additions & 14 deletions frontend/src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,13 @@ export const api = {
const headers = authHeaders(token, apiToken);
return axios.post(`${apiUrl}/api/v1/utils/authorize-farm/${farmID}`, data, headers);
},
async authorizeNewFarm(token: string, farmUrl: string, data: FarmProfileAuthorize, apiToken?: string) {
const headers = authHeaders(token, apiToken);
return axios.post(
`${apiUrl}/api/v1/utils/authorize-farm/`,
{farm_url: farmUrl, auth_params: data},
headers);
},
async createFarmAuthLink(token: string, farmID: number) {
return axios.post(`${apiUrl}/api/v1/utils/farm-auth-link/${farmID}`, null, authHeaders(token));
},
Expand All @@ -94,22 +101,10 @@ export const api = {
{params, headers: authHeaders(token).headers},
);
},

// Public APIs
async publicCreateFarm(token: string, data: FarmProfileCreate, apiToken?: string) {
return axios.post(`${apiUrl}/api/v1/public/farms/`, data, authHeaders(token, apiToken));
},
async publicAuthorizeFarm(token: string, farmUrl: string, data: FarmProfileAuthorize, apiToken?: string) {
const headers = authHeaders(token, apiToken);
return axios.post(
`${apiUrl}/api/v1/public/farms/authorize-farm/`,
{farm_url: farmUrl, auth_params: data},
headers);
},
async publicValidateFarmUrl(token: string, farmUrl: string, apiToken?: string) {
async validateFarmUrl(token: string, farmUrl: string, apiToken?: string) {
const headers = authHeaders(token, apiToken);
return axios.post(
`${apiUrl}/api/v1/public/farms/validate-farm-url`,
`${apiUrl}/api/v1/utils/validate-farm-url`,
{farm_url: farmUrl},
headers,
);
Expand Down
Loading

0 comments on commit 93a8e45

Please sign in to comment.