Skip to content

Commit

Permalink
start cleanning
Browse files Browse the repository at this point in the history
  • Loading branch information
guillaume-chervet committed Nov 1, 2024
1 parent 8219921 commit f251970
Show file tree
Hide file tree
Showing 18 changed files with 433 additions and 31 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ docker-compose up
- Add alerting
- Add logging
- Add Tests
- [api] Manage cors
- [api] Manage cors policy
- [webapp] Manage CSP policy
- [api] Manage rate limiting
- [api/webaap] Manage authentication and authorization
- [api/webapp] Manage authentication and authorization
- [webapp] Send small chunks to the api while recording
- [all] Add retry pattern
- [train] Train / deploy / tests the model like explain in https://www.youtube.com/watch?v=QFOdB9GPf_Y&list=PL8EMdIH6Mzxw5mVb0hz4n7xeIa5aloVmC
Expand Down
2 changes: 1 addition & 1 deletion deploy/kubernetes/api-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ spec:
livenessProbe:
httpGet:
path: /health
port: 5000
port: 8000
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 5
Expand Down
4 changes: 2 additions & 2 deletions deploy/kubernetes/ia-worker-deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ spec:
livenessProbe:
httpGet:
path: /health
port: 5000
initialDelaySeconds: 20
port: 8000
initialDelaySeconds: 30
periodSeconds: 5
timeoutSeconds: 5
ports:
Expand Down
171 changes: 170 additions & 1 deletion production/api/poetry.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions production/api/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ python-multipart = "^0.0.12"
redis = "^5.2.0"
httpx = "^0.27.2"
msgpack = "^1.1.0"
jwskate = "^0.11.1"


[build-system]
Expand Down
3 changes: 3 additions & 0 deletions production/ia-worker/app/app_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ class AppSettings:
url_slimfaas: str
server_host: str
server_port: int
oidc_issuer: Optional[str] = None
oidc_authority: Optional[str] = "true"



class Settings:
Expand Down
3 changes: 3 additions & 0 deletions production/ia-worker/app/http_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ def __init__(self):
self.http_async_client = httpx.AsyncClient(verify=False, timeout=60, follow_redirects=True)
atexit.register(BackgroundTask(self.http_async_client.aclose))

def get_http_async_client(self):
return self.http_async_client

def post(self, url, data, headers={}):
return self.http_async_client.post(url, data=data, headers=headers)

Expand Down
10 changes: 10 additions & 0 deletions production/ia-worker/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,16 @@ async def health():

if __name__ == "__main__":
import uvicorn
from oidc.authentication_middleware import authentication_middleware, XHttpServiceGet

app_settings = app_settings_factory_get()
if app_settings.oidc_enable == "true":
exclude_urls = ["/health", "/version", "/metrics", "/docs", "/openapi_json"]
app.add_middleware(authentication_middleware(
app_settings.oidc_authority,
["api"],
"api",
exclude_urls,
XHttpServiceGet(http_service_factory_get().get_http_async_client())
))
uvicorn.run(app, host=app_settings.server_host, port=app_settings.server_port)
109 changes: 109 additions & 0 deletions production/ia-worker/app/oidc/authentication.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@

import abc
import asyncio
from dataclasses import dataclass

from httpx import AsyncClient, Client
from jwskate import Jwt
from datetime import datetime


@dataclass
class AuthenticationResult:
success: bool
error: str = ""
payload: dict | None = None


def find_jwk(jwks, jwt):
jwk_key = None
jwks_keys = jwks["keys"]
for key in jwks_keys:
if key["kid"] == jwt.headers["kid"]:
jwk_key = {
"kty": key["kty"],
"kid": key["kid"],
"use": key["use"],
"alg": key["alg"],
"n": key["n"],
"e": key["e"],
}
break
return jwk_key


class IHttpServiceGet(abc.ABC):
@abc.abstractmethod
async def get_async(self, url: str) -> dict:
pass

class XHttpServiceGet(IHttpServiceGet):
def __init__(self,
http_client: Client,
http_async_client: AsyncClient):
self.http_async_client = http_async_client
self.http_client = http_client

async def get_async(self, url: str) -> dict:
reponse = await self.http_async_client.get(url)
return reponse.json()


class Authentication:
def __init__(self,
issuer: str | None,
scopes: list[str],
api_audience: str | None,
service: IHttpServiceGet):
self.service = service
self.issuer = issuer
self.api_audience = api_audience
self.algorithms = ["RS256"]
self.scopes = scopes
self.cache_timestamp = 0
self.cache_jwks = None
self.cache_token_endpoint = None

async def _get_jwks_async(self, service: IHttpServiceGet, issuer: str) -> dict:
timestamp = datetime.timestamp(datetime.now())
one_day = 86400
if self.cache_timestamp + one_day < timestamp:
wellknowurl = await service.get_async(issuer + "/.well-known/openid-configuration")
self.cache_jwks = await service.get_async(wellknowurl["jwks_uri"])
self.cache_token_endpoint = wellknowurl["token_endpoint"]
self.cache_timestamp = timestamp

return self.cache_jwks

async def get_token_endpoint_async(self) -> str:
await self._get_jwks_async(self.service, self.issuer)
return self.cache_token_endpoint

async def validate_async(self, token: str) -> AuthenticationResult:
try:
jwt = Jwt(token)
if jwt.headers["alg"].upper() not in self.algorithms:
return AuthenticationResult(success=False, error="wrong algorithm used")
jwks = await self._get_jwks_async(self.service, self.issuer)
jwk_key = find_jwk(jwks, jwt)
if jwk_key is None:
return AuthenticationResult(success=False, error="JWK key not found")

payload = jwt.claims
scopes = payload["scope"].split(" ")
for scope in self.scopes:
if scope not in scopes:
return AuthenticationResult(success=False, error="scope not found")

if not self.api_audience:
if jwt.validate(jwk_key, issuer=self.issuer):
return AuthenticationResult(success=False, error="signature verification failed")

if jwt.validate(jwk_key, issuer=self.issuer, audience=self.api_audience):
return AuthenticationResult(success=False, error="signature verification failed")

return AuthenticationResult(success=True, payload=payload)

except Exception as e:
exception_message = str(e)
return AuthenticationResult(success=False, error=exception_message)
37 changes: 37 additions & 0 deletions production/ia-worker/app/oidc/authentication_middleware.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from http import HTTPStatus

from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import Request
from starlette.responses import Response

from authentication import Authentication, IHttpServiceGet


def authentication_middleware(issuer: str,
scopes: list[str],
api_audience: str,
exclude_urls: list[str],
service: IHttpServiceGet):
authentication = Authentication(issuer, scopes, api_audience, service)

class AuthenticateMiddleware(BaseHTTPMiddleware):
async def dispatch(self, request: Request, call_next: RequestResponseEndpoint):
try:
if request.url.path in exclude_urls or request.method == "OPTIONS":
response = await call_next(request)
return response

authorization = request.headers.get('Authorization')
if not authorization:
return Response(status_code=HTTPStatus.UNAUTHORIZED)
validation_result = await authentication.validate_async(authorization.replace("Bearer ", ""))
if not validation_result.success:
return Response(status_code=HTTPStatus.UNAUTHORIZED)
response = await call_next(request)
return response
except Exception as e:
return Response(status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
content="Internal Server Error: {0}".format(
str(e)))

return AuthenticateMiddleware
4 changes: 3 additions & 1 deletion production/ia-worker/app/settings.development.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
"redis_port": 6379,
"url_slimfaas": "http://slimfaas:5000",
"server_host": "localhost",
"server_port": 8000
"server_port": 8000,
"oidc_authority": "https://demo.duendesoftware.com",
"oidc_enable" : true
}
4 changes: 3 additions & 1 deletion production/ia-worker/app/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
"redis_port": 6379,
"url_slimfaas": "http://slimfaas:5000",
"server_host": "0.0.0.0",
"server_port": 8000
"server_port": 8000,
"oidc_authority": "https://demo.duendesoftware.com",
"oidc_enable" : true
}
4 changes: 3 additions & 1 deletion production/ia-worker/app/settings.production.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
"redis_port": 6379,
"url_slimfaas": "http://slimfaas:5000",
"server_host": "0.0.0.0",
"server_port": 8000
"server_port": 8000,
"oidc_authority": "https://demo.duendesoftware.com",
"oidc_enable" : true
}
10 changes: 10 additions & 0 deletions production/webapp/nginx.conf
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,15 @@ http {
location / {
try_files $uri /index.html;
}

add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self'; connect-src 'self' https://demo.duendesoftware.com; media-src 'self'; object-src 'none'; frame-src 'self' https://demo.duendesoftware.com; base-uri 'self'; form-action 'self'; frame-ancestors 'self' https://demo.duendesoftware.com; block-all-mixed-content; upgrade-insecure-requests;";
add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header X-XSS-Protection "1; mode=block";
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload";
add_header Referrer-Policy "no-referrer";
add_header X-Permitted-Cross-Domain-Policies "none";


}
}
28 changes: 28 additions & 0 deletions production/webapp/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions production/webapp/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"preview": "vite preview"
},
"dependencies": {
"@axa-fr/react-oidc": "7.22.32",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
Expand Down
47 changes: 31 additions & 16 deletions production/webapp/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,42 @@
import './App.css'
import AudioRecorderComponent from "./AudioRecorderComponent.jsx";
import EnvironmentStarter from "./EnvironmentStarter.jsx";
import {OidcProvider, OidcSecure} from "@axa-fr/react-oidc";

const configuration = {
client_id: 'interactive.public',
redirect_uri: window.location.origin + '/authentication/callback',
silent_redirect_uri: window.location.origin + '/authentication/silent-callback',
scope: 'openid profile email api offline_access',
authority: 'https://demo.duendesoftware.com',
};

function App() {

if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
return (
<EnvironmentStarter>
<div className="App">
<AudioRecorderComponent/>
</div>
</EnvironmentStarter>
);
} else {
console.error("L'API mediaDevices n'est pas supportée par ce navigateur.");
return (
<div className="App">
<p>L'API mediaDevices n'est pas supportée par ce navigateur.</p>
</div>
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
return (
<OidcProvider configuration={configuration}>
<AppSecure/>
</OidcProvider>
);
}
} else {
console.error("L'API mediaDevices n'est pas supportée par ce navigateur.");
return (
<div className="App">
<p>L'API mediaDevices n'est pas supportée par ce navigateur.</p>
</div>
);
}

}

const AppSecure = () => {
return <OidcSecure>
<EnvironmentStarter>
<div className="App">
<AudioRecorderComponent/>
</div>
</EnvironmentStarter>
</OidcSecure>;
}

export default App
Loading

0 comments on commit f251970

Please sign in to comment.