From 980aa2a8ba1654a456ae4bbffd30abd290d455dd Mon Sep 17 00:00:00 2001 From: Marc Cataford Date: Mon, 22 May 2023 14:10:38 -0400 Subject: [PATCH] feat: better auth service (#20) * feat: forwardauth + oauth implementation * build: ignore env dotfiles * infra: local dynamic config, add service and forwardauth, remove unneeded port expose * fix: routing on callback, extraneous COPY calls * infra: auth service config * chore: remove legacy monolith auth * infra: ensure that auth container restarts always --- .gitignore | 1 + README.md | 1 - services/auth-service/Dockerfile | 15 ++ services/auth-service/cmd/authservice.go | 106 ++++++++++++++ .../docker-compose.yml | 13 +- services/auth-service/go.mod | 8 ++ services/auth-service/go.sum | 4 + .../auth-service/internal/helpers/cache.go | 55 ++++++++ .../internal/helpers/configuration.go | 49 +++++++ .../internal/httpUtils/httpHandler.go | 57 ++++++++ .../internal/httpUtils/middlewares.go | 51 +++++++ services/auth-service/internal/oauth/oauth.go | 130 ++++++++++++++++++ services/monolith/.gitignore | 3 - services/monolith/.python-version | 1 - services/monolith/Dockerfile | 11 -- services/monolith/requirements.in | 3 - services/monolith/requirements.txt | 18 --- services/monolith/script/bootstrap | 14 -- services/monolith/script/lock | 4 - services/monolith/src/__init__.py | 0 services/monolith/src/base/__init__.py | 0 services/monolith/src/base/asgi.py | 16 --- services/monolith/src/base/settings.py | 126 ----------------- services/monolith/src/base/urls.py | 31 ----- services/monolith/src/base/wsgi.py | 16 --- services/monolith/src/identity/__init__.py | 0 services/monolith/src/identity/admin.py | 3 - services/monolith/src/identity/apps.py | 6 - .../src/identity/migrations/__init__.py | 0 services/monolith/src/identity/models.py | 3 - services/monolith/src/identity/tests.py | 3 - services/monolith/src/identity/urls.py | 5 - services/monolith/src/identity/views.py | 14 -- services/monolith/src/manage.py | 22 --- services/monolith/tasks.py | 29 ---- services/traefik/docker-compose.yml | 1 - services/traefik/traefik_dynamic.toml | 32 ++--- services/traefik/traefik_dynamic_local.toml | 53 +++++++ tasks.py | 3 - 39 files changed, 552 insertions(+), 355 deletions(-) create mode 100644 services/auth-service/Dockerfile create mode 100644 services/auth-service/cmd/authservice.go rename services/{monolith => auth-service}/docker-compose.yml (51%) create mode 100644 services/auth-service/go.mod create mode 100644 services/auth-service/go.sum create mode 100644 services/auth-service/internal/helpers/cache.go create mode 100644 services/auth-service/internal/helpers/configuration.go create mode 100644 services/auth-service/internal/httpUtils/httpHandler.go create mode 100644 services/auth-service/internal/httpUtils/middlewares.go create mode 100644 services/auth-service/internal/oauth/oauth.go delete mode 100644 services/monolith/.gitignore delete mode 100644 services/monolith/.python-version delete mode 100644 services/monolith/Dockerfile delete mode 100644 services/monolith/requirements.in delete mode 100644 services/monolith/requirements.txt delete mode 100644 services/monolith/script/bootstrap delete mode 100644 services/monolith/script/lock delete mode 100644 services/monolith/src/__init__.py delete mode 100644 services/monolith/src/base/__init__.py delete mode 100644 services/monolith/src/base/asgi.py delete mode 100644 services/monolith/src/base/settings.py delete mode 100644 services/monolith/src/base/urls.py delete mode 100644 services/monolith/src/base/wsgi.py delete mode 100644 services/monolith/src/identity/__init__.py delete mode 100644 services/monolith/src/identity/admin.py delete mode 100644 services/monolith/src/identity/apps.py delete mode 100644 services/monolith/src/identity/migrations/__init__.py delete mode 100644 services/monolith/src/identity/models.py delete mode 100644 services/monolith/src/identity/tests.py delete mode 100644 services/monolith/src/identity/urls.py delete mode 100644 services/monolith/src/identity/views.py delete mode 100755 services/monolith/src/manage.py delete mode 100644 services/monolith/tasks.py create mode 100644 services/traefik/traefik_dynamic_local.toml diff --git a/.gitignore b/.gitignore index b7c8d14..0af62c6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ spadinaistan.venv +**/.env pyinfra-debug.log deluge/config deluge/downloads diff --git a/README.md b/README.md index 000612e..6572d95 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ |[Plex](./services/plex)|Plex media server| |[Deluge](./services/deluge)|Deluge Web service| |[Traefik](./services/traefik)|Traefik API Gateway| -|[Monolith](./services/monolith)|Usercode monolith| |[Bitwarden](./services/bitwarden)|Bitwarden secrets management| ## Getting started diff --git a/services/auth-service/Dockerfile b/services/auth-service/Dockerfile new file mode 100644 index 0000000..9660476 --- /dev/null +++ b/services/auth-service/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.20 + +WORKDIR /app + +COPY go.mod go.sum ./ +RUN go mod download + +COPY ./cmd ./cmd +COPY ./internal ./internal + +RUN go build ./cmd/authservice.go + +EXPOSE 8080 + +CMD ["./authservice", "./config.json"] diff --git a/services/auth-service/cmd/authservice.go b/services/auth-service/cmd/authservice.go new file mode 100644 index 0000000..ff17ea4 --- /dev/null +++ b/services/auth-service/cmd/authservice.go @@ -0,0 +1,106 @@ +package main + +import ( + "authservice/internal/helpers" + "authservice/internal/httpUtils" + "authservice/internal/oauth" + "fmt" + "log" + "net/http" + "net/url" + "os" +) + +// Healthcheck endpoint +// +// Responds to pings with a 200, quick way to verify +// if the service is up and can respond to requests. +func healthCheck(request *http.Request) httpUtils.HttpResponse { + return httpUtils.HttpResponse{Body: "Up!", StatusCode: 200} +} + +func login(request *http.Request) httpUtils.HttpResponse { + if request.Context().Value("user") != nil { + return httpUtils.HttpResponse{StatusCode: 200} + } + + configuration := helpers.GetConfiguration() + requestId := request.Context().Value("requestId").(string) + queryParameters := url.Values{ + "client_id": {os.Getenv("CLIENT_ID")}, + "response_type": {"code"}, + "scope": {"https://www.googleapis.com/auth/userinfo.email"}, + "redirect_uri": {configuration.OAuth.CallbackUrl}, + "state": {requestId}, + } + + redirectUrl, _ := url.Parse(configuration.OAuth.Url) + redirectUrl.RawQuery = queryParameters.Encode() + + intendedHost := request.Header["X-Forwarded-Host"] + intendedDestination := request.Header["X-Forwarded-Uri"] + + if len(intendedDestination) == 1 { + helpers.SetCachedValue(fmt.Sprintf("postlogin_redir_%s", requestId), fmt.Sprintf("https://%s%s", intendedHost[0], intendedDestination[0]), 60) + } + + return httpUtils.HttpResponse{StatusCode: 302, RedirectTo: redirectUrl.String()} +} + +func oauthRedirect(request *http.Request) httpUtils.HttpResponse { + configuration := helpers.GetConfiguration() + requestId := request.URL.Query()["state"][0] + intendedDestination, hasIntendedDestination := helpers.GetCachedValue(fmt.Sprintf("postlogin_redir_%s", requestId)) + queryParameters, _ := url.ParseQuery(request.URL.RawQuery) + + code, _ := url.QueryUnescape(queryParameters["code"][0]) + + token, tokenError := oauth.GetTokenFromProvider(code) + + if tokenError != nil { + return httpUtils.HttpResponse{StatusCode: 400, Body: "Failed to get token from oauth"} + } + + log.Println(fmt.Sprintf("Received token: %s", token.IdToken)) + cookie := http.Cookie{Name: "jwt", Value: token.IdToken, Domain: configuration.Hostname, Path: "/"} + if hasIntendedDestination { + return httpUtils.HttpResponse{StatusCode: 302, RedirectTo: intendedDestination, Cookies: []http.Cookie{cookie}} + } + + return httpUtils.HttpResponse{StatusCode: 200, Cookies: []http.Cookie{cookie}} +} + +func main() { + configurationPath := os.Args[1] + + configuration, err := helpers.LoadConfiguration(configurationPath) + + helpers.LoadedConfiguration = &configuration + + if err != nil { + log.Fatal(err) + } + + mux := http.NewServeMux() + host := ":8080" + + routes := map[string](func(*http.Request) httpUtils.HttpResponse){ + "/": healthCheck, + "/auth/callback": oauthRedirect, + "/auth/login": login, + } + + for path, handler := range routes { + mux.HandleFunc(path, httpUtils.MakeHandler(handler)) + } + + log.Println(fmt.Sprintf("Listening on %s", host)) + + err = http.ListenAndServe(host, mux) + + if err != nil { + log.Fatal(err) + } else { + log.Println("Exiting") + } +} diff --git a/services/monolith/docker-compose.yml b/services/auth-service/docker-compose.yml similarity index 51% rename from services/monolith/docker-compose.yml rename to services/auth-service/docker-compose.yml index ffe7b5c..7f807e9 100644 --- a/services/monolith/docker-compose.yml +++ b/services/auth-service/docker-compose.yml @@ -1,17 +1,18 @@ -version: "3.7" +version: '3.7' services: - monolith: + auth-service: restart: always build: . + env_file: + - .env ports: - - "8000:8000" - environment: - - SPADINAISTAN_ENV=prod + - 8080:8080 volumes: - - ./src:/app/src + - ./config.json:/app/config.json networks: default: name: web external: true + diff --git a/services/auth-service/go.mod b/services/auth-service/go.mod new file mode 100644 index 0000000..f3ebba6 --- /dev/null +++ b/services/auth-service/go.mod @@ -0,0 +1,8 @@ +module authservice + +go 1.20 + +require ( + github.com/golang-jwt/jwt/v5 v5.0.0 + github.com/google/uuid v1.3.0 +) diff --git a/services/auth-service/go.sum b/services/auth-service/go.sum new file mode 100644 index 0000000..80c8498 --- /dev/null +++ b/services/auth-service/go.sum @@ -0,0 +1,4 @@ +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/services/auth-service/internal/helpers/cache.go b/services/auth-service/internal/helpers/cache.go new file mode 100644 index 0000000..7789a03 --- /dev/null +++ b/services/auth-service/internal/helpers/cache.go @@ -0,0 +1,55 @@ +package helpers + +import ( + "fmt" + "log" + "time" +) + +type CacheEntry struct { + Value string + Expiration int64 +} + +var InMemoryCache *map[string]CacheEntry + +func getCache() map[string]CacheEntry { + if InMemoryCache == nil { + InMemoryCache = &map[string]CacheEntry{} + } + + return *InMemoryCache +} + +func pruneExpiredEntries() int { + now := time.Now().Unix() + + pruned := 0 + for key, value := range getCache() { + if value.Expiration < now { + delete(*InMemoryCache, key) + pruned += 1 + } + } + + return pruned +} + +func GetCachedValue(key string) (string, bool) { + pruneExpiredEntries() + + entry, ok := getCache()[key] + + log.Println(fmt.Sprintf("[Cache] Retrieved %s=%s from cache", key, entry.Value)) + + return entry.Value, ok +} + +func SetCachedValue(key string, value string, timeToLive int) { + pruneExpiredEntries() + + expiration := time.Now().Unix() + int64(timeToLive) + log.Println(fmt.Sprintf("[Cache] Created or updated %s=%s (exp=%d) in cache", key, value, expiration)) + + getCache()[key] = CacheEntry{Value: value, Expiration: expiration} +} diff --git a/services/auth-service/internal/helpers/configuration.go b/services/auth-service/internal/helpers/configuration.go new file mode 100644 index 0000000..bbcad72 --- /dev/null +++ b/services/auth-service/internal/helpers/configuration.go @@ -0,0 +1,49 @@ +package helpers + +import ( + "encoding/json" + "io/ioutil" + "log" +) + +var LoadedConfiguration *Configuration + +type OAuthConfiguration struct { + Url string `json:"url"` + TokenUrl string `json:"token_url"` + PublicKeyUrl string `json:"public_key_url"` + CallbackUrl string `json:"callback_url"` + Issuer string `json:"issuer"` +} + +type Configuration struct { + VerifiedUsers []string `json:"verified_users"` + Hostname string `json:"hostname"` + OAuth OAuthConfiguration `json:"oauth_configuration"` +} + +func GetConfiguration() *Configuration { + if LoadedConfiguration == nil { + log.Fatal("Configuration file has not been loaded yet.") + } + + return LoadedConfiguration +} + +func LoadConfiguration(configPath string) (Configuration, error) { + file, fileError := ioutil.ReadFile(configPath) + + if fileError != nil { + return Configuration{}, fileError + } + + var parsedConfiguration Configuration + + jsonError := json.Unmarshal(file, &parsedConfiguration) + + if jsonError != nil { + return Configuration{}, jsonError + } + + return parsedConfiguration, nil +} diff --git a/services/auth-service/internal/httpUtils/httpHandler.go b/services/auth-service/internal/httpUtils/httpHandler.go new file mode 100644 index 0000000..8852ae2 --- /dev/null +++ b/services/auth-service/internal/httpUtils/httpHandler.go @@ -0,0 +1,57 @@ +package httpUtils + +import ( + "fmt" + "log" + "net/http" +) + +type HttpResponse struct { + Body string + StatusCode int + Headers map[string]string + Cookies []http.Cookie + RedirectTo string + Authorization string +} + +// Wraps handlers and abstracts response writing away from HTTP handlers themselves. +// +// This expects handlers to return a HttpResponse struct and handles basic logging +// before finishing the request handling. This should wrap all handlers passed to HandlerFunc. +func MakeHandler(handler func(*http.Request) HttpResponse) func(http.ResponseWriter, *http.Request) { + return func(responseWriter http.ResponseWriter, request *http.Request) { + // The authorization middleware will handle validating tokens included + // in cookies or Authorization headers. If a token is present and valid, + // a `user` context value is added to the request context, otherwise + // it is nil. + enhancedRequest, err := AuthorizationMiddleware(request) + enhancedRequest, _ = RequestIdMiddleware(enhancedRequest) + + var response HttpResponse + + if err != nil { + response = HttpResponse{StatusCode: 401} + } else { + response = handler(enhancedRequest) + } + + defer log.Println(fmt.Sprintf("%s %s - %d", request.Method, request.URL, response.StatusCode)) + + for _, cookie := range response.Cookies { + http.SetCookie(responseWriter, &cookie) + } + + for key, value := range response.Headers { + responseWriter.Header().Set(key, value) + } + + if response.StatusCode == 302 { + http.Redirect(responseWriter, request, response.RedirectTo, 302) + } else if response.StatusCode > 100 && response.StatusCode != 200 { + responseWriter.WriteHeader(response.StatusCode) + } + + responseWriter.Write([]byte(response.Body)) + } +} diff --git a/services/auth-service/internal/httpUtils/middlewares.go b/services/auth-service/internal/httpUtils/middlewares.go new file mode 100644 index 0000000..57004a3 --- /dev/null +++ b/services/auth-service/internal/httpUtils/middlewares.go @@ -0,0 +1,51 @@ +package httpUtils + +import ( + "authservice/internal/oauth" + "context" + "github.com/golang-jwt/jwt/v5" + "github.com/google/uuid" + "net/http" +) + +type UserContext struct { + Token string + Email string + Verified bool +} + +func RequestIdMiddleware(request *http.Request) (*http.Request, error) { + requestId := uuid.New().String() + + requestCtx := context.WithValue(request.Context(), "requestId", requestId) + + return request.WithContext(requestCtx), nil +} + +func AuthorizationMiddleware(request *http.Request) (*http.Request, error) { + authorizationHeader := request.Header["Authorization"] + jwtCookie, _ := request.Cookie("jwt") + + var token string + + if len(authorizationHeader) != 0 { + token = authorizationHeader[0] + } else if jwtCookie != nil { + token = jwtCookie.Value + } + + if len(token) == 0 { + return request, nil + } + + decodedToken, err := oauth.AuthorizeToken(token) + + if err != nil { + return request, err + } + + userContext := UserContext{Token: token, Email: decodedToken.Claims.(jwt.MapClaims)["email"].(string), Verified: decodedToken.Claims.(jwt.MapClaims)["email_verified"].(bool)} + requestCtx := context.WithValue(request.Context(), "user", userContext) + + return request.WithContext(requestCtx), nil +} diff --git a/services/auth-service/internal/oauth/oauth.go b/services/auth-service/internal/oauth/oauth.go new file mode 100644 index 0000000..a2ab691 --- /dev/null +++ b/services/auth-service/internal/oauth/oauth.go @@ -0,0 +1,130 @@ +package oauth + +import ( + "authservice/internal/helpers" + "encoding/json" + "fmt" + "github.com/golang-jwt/jwt/v5" + "io/ioutil" + "log" + "net/http" + "net/url" + "os" + "strings" +) + +type AccessToken struct { + AccessToken string `json:"access_token"` + ExpiresIn int `json:"expires_in"` + TokenType string `json:"token_type"` + Scope string `json:"scope"` + RefreshToken string `json:"refresh_token"` + IdToken string `json:"id_token"` +} + +func getPublicKeysFromIssuer(publicKeyUrl string) (map[string]string, error) { + response, requestError := http.Get(publicKeyUrl) + + if requestError != nil { + return map[string]string{}, requestError + } + + responseBody, responseBodyError := ioutil.ReadAll(response.Body) + + if responseBodyError != nil { + return map[string]string{}, responseBodyError + } + + publicKeys := map[string]string{} + + jsonError := json.Unmarshal(responseBody, &publicKeys) + + if jsonError != nil { + return map[string]string{}, jsonError + } + + return publicKeys, nil +} + +func GetTokenFromProvider(code string) (AccessToken, error) { + configuration := helpers.GetConfiguration() + + requestData := url.Values{ + "code": {code}, + "client_id": {os.Getenv("CLIENT_ID")}, + "client_secret": {os.Getenv("CLIENT_SECRET")}, + "redirect_uri": {configuration.OAuth.CallbackUrl}, + "grant_type": {"authorization_code"}, + } + + response, requestError := http.Post(configuration.OAuth.TokenUrl, "application/x-www-form-urlencoded", strings.NewReader(requestData.Encode())) + + if requestError != nil { + return AccessToken{}, requestError + } + + responseBody, responseBodyError := ioutil.ReadAll(response.Body) + + if responseBodyError != nil { + return AccessToken{}, responseBodyError + } + + retrievedToken := AccessToken{} + + jsonError := json.Unmarshal(responseBody, &retrievedToken) + + if jsonError != nil { + return AccessToken{}, jsonError + } + + return retrievedToken, nil +} + +func AuthorizeToken(token string) (*jwt.Token, error) { + configuration := helpers.GetConfiguration() + + publicKeys, publicKeyError := getPublicKeysFromIssuer(configuration.OAuth.PublicKeyUrl) + + if publicKeyError != nil { + log.Println(fmt.Sprintf("ERROR! %s", publicKeyError)) + return nil, publicKeyError + } + + decodedToken, decodeError := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) { + claims := token.Claims.(jwt.MapClaims) + + issuer, _ := claims.GetIssuer() + audience, _ := claims.GetAudience() + + if audience[0] != os.Getenv("CLIENT_ID") { + return nil, fmt.Errorf("Invalid claim did not match client ID: %v", claims["aud"]) + } + + if issuer != configuration.OAuth.Issuer { + return nil, fmt.Errorf("Invalid claim did not match issuer: %v", claims["iss"]) + } + + tokenKeyId := token.Header["kid"].(string) + + return jwt.ParseRSAPublicKeyFromPEM([]byte(publicKeys[tokenKeyId])) + }) + + if decodeError != nil { + log.Println(fmt.Sprintf("ERROR! %s", decodeError)) + return decodedToken, decodeError + } + + verified := false + for _, verifiedUser := range configuration.VerifiedUsers { + claims := decodedToken.Claims.(jwt.MapClaims) + if verifiedUser == claims["email"].(string) && claims["email_verified"].(bool) { + verified = true + } + } + + if !verified { + return decodedToken, fmt.Errorf("Forbidden") + } + + return decodedToken, nil +} diff --git a/services/monolith/.gitignore b/services/monolith/.gitignore deleted file mode 100644 index 2245169..0000000 --- a/services/monolith/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*.sqlite3 -spadinaistan-monolith.venv -__pycache__ diff --git a/services/monolith/.python-version b/services/monolith/.python-version deleted file mode 100644 index d2c96c0..0000000 --- a/services/monolith/.python-version +++ /dev/null @@ -1 +0,0 @@ -3.11.3 diff --git a/services/monolith/Dockerfile b/services/monolith/Dockerfile deleted file mode 100644 index 395bc56..0000000 --- a/services/monolith/Dockerfile +++ /dev/null @@ -1,11 +0,0 @@ -FROM python:3.9.15 - -ENV PYTHONUNBUFFERED=1 - -WORKDIR /app - -COPY ./requirements.txt ./requirements.txt - -RUN pip install -r ./requirements.txt - -CMD ["python", "src/manage.py", "runserver", "0.0.0.0:8000"] diff --git a/services/monolith/requirements.in b/services/monolith/requirements.in deleted file mode 100644 index 9fd9cd2..0000000 --- a/services/monolith/requirements.in +++ /dev/null @@ -1,3 +0,0 @@ -django -django-extensions -invoke diff --git a/services/monolith/requirements.txt b/services/monolith/requirements.txt deleted file mode 100644 index 15a071b..0000000 --- a/services/monolith/requirements.txt +++ /dev/null @@ -1,18 +0,0 @@ -# -# This file is autogenerated by pip-compile with Python 3.11 -# by the following command: -# -# pip-compile ./requirements.in -# -asgiref==3.5.2 - # via django -django==4.1.3 - # via - # -r ./requirements.in - # django-extensions -django-extensions==3.2.1 - # via -r ./requirements.in -invoke==2.1.0 - # via -r ./requirements.in -sqlparse==0.4.3 - # via django diff --git a/services/monolith/script/bootstrap b/services/monolith/script/bootstrap deleted file mode 100644 index d61d1cb..0000000 --- a/services/monolith/script/bootstrap +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -VENV="spadinaistan-monolith.venv" - -python -m pip install pip~=23.1 pip-tools==6.13.0 --no-cache - -if [ ! -d "./$VENV" ]; then - python -m venv ./$VENV -fi - -source ./$VENV/bin/activate - - -pip-sync ./requirements.txt diff --git a/services/monolith/script/lock b/services/monolith/script/lock deleted file mode 100644 index 9e6b834..0000000 --- a/services/monolith/script/lock +++ /dev/null @@ -1,4 +0,0 @@ -#!/bin/bash - -pip-compile ./requirements.in - diff --git a/services/monolith/src/__init__.py b/services/monolith/src/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/services/monolith/src/base/__init__.py b/services/monolith/src/base/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/services/monolith/src/base/asgi.py b/services/monolith/src/base/asgi.py deleted file mode 100644 index 0d9a352..0000000 --- a/services/monolith/src/base/asgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -ASGI config for monolith project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/4.1/howto/deployment/asgi/ -""" - -import os - -from django.core.asgi import get_asgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "base.settings") - -application = get_asgi_application() diff --git a/services/monolith/src/base/settings.py b/services/monolith/src/base/settings.py deleted file mode 100644 index fe3078a..0000000 --- a/services/monolith/src/base/settings.py +++ /dev/null @@ -1,126 +0,0 @@ -import os - -from pathlib import Path - -ENVIRONMENT = os.getenv("SPADINAISTAN_ENV", "dev") - -# Build paths inside the project like this: BASE_DIR / 'subdir'. -BASE_DIR = Path(__file__).resolve().parent.parent - - -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/4.1/howto/deployment/checklist/ - -# SECURITY WARNING: keep the secret key used in production secret! -SECRET_KEY = "django-insecure-_=k!twt-5o^rw$&fishn18n8pcw2(!z3#k(+$e)=ehorx7!q^(" - -DEBUG = ENVIRONMENT == "dev" - -ALLOWED_HOSTS_PROD = ["spadinaistan.karnov.club"] - -ALLOWED_HOSTS_DEV = ["localhost.karnov.club", "monolith", "localhost"] - -ALLOWED_HOSTS = ALLOWED_HOSTS_PROD if ENVIRONMENT == "prod" else ALLOWED_HOSTS_DEV - -BASE_HOST = ALLOWED_HOSTS[0] - -CSRF_TRUSTED_ORIGINS = ["https://spadinaistan.karnov.club"] - -USE_X_FORWARDED_HOST = True - -# Application definition - -INSTALLED_APPS = [ - "django.contrib.admin", - "django.contrib.auth", - "django.contrib.contenttypes", - "django.contrib.sessions", - "django.contrib.messages", - "django.contrib.staticfiles", - "django_extensions", - "base", - "identity", -] - -MIDDLEWARE = [ - "django.middleware.security.SecurityMiddleware", - "django.contrib.sessions.middleware.SessionMiddleware", - "django.middleware.common.CommonMiddleware", - "django.middleware.csrf.CsrfViewMiddleware", - "django.contrib.auth.middleware.AuthenticationMiddleware", - "django.contrib.messages.middleware.MessageMiddleware", - "django.middleware.clickjacking.XFrameOptionsMiddleware", -] - -ROOT_URLCONF = "base.urls" - -TEMPLATES = [ - { - "BACKEND": "django.template.backends.django.DjangoTemplates", - "DIRS": [], - "APP_DIRS": True, - "OPTIONS": { - "context_processors": [ - "django.template.context_processors.debug", - "django.template.context_processors.request", - "django.contrib.auth.context_processors.auth", - "django.contrib.messages.context_processors.messages", - ], - }, - }, -] - -WSGI_APPLICATION = "base.wsgi.application" - - -# Database -# https://docs.djangoproject.com/en/4.1/ref/settings/#databases - -DATABASES = { - "default": { - "ENGINE": "django.db.backends.sqlite3", - "NAME": BASE_DIR / "db.sqlite3", - } -} - - -# Password validation -# https://docs.djangoproject.com/en/4.1/ref/settings/#auth-password-validators - -AUTH_PASSWORD_VALIDATORS = [ - { - "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", - }, - { - "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", - }, -] - - -# Internationalization -# https://docs.djangoproject.com/en/4.1/topics/i18n/ - -LANGUAGE_CODE = "en-us" - -TIME_ZONE = "UTC" - -USE_I18N = True - -USE_TZ = True - - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/4.1/howto/static-files/ - -STATIC_URL = "app/static/" - -# Default primary key field type -# https://docs.djangoproject.com/en/4.1/ref/settings/#default-auto-field - -DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/services/monolith/src/base/urls.py b/services/monolith/src/base/urls.py deleted file mode 100644 index e5f1ed2..0000000 --- a/services/monolith/src/base/urls.py +++ /dev/null @@ -1,31 +0,0 @@ -"""monolith URL Configuration - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/4.1/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" -from django.contrib import admin -from django.urls import path, include - -import identity.urls - -urlpatterns = [ - path( - "app/", - include( - [ - path("admin/", admin.site.urls), - path("identity/", include(identity.urls.url_patterns)), - ] - ), - ) -] diff --git a/services/monolith/src/base/wsgi.py b/services/monolith/src/base/wsgi.py deleted file mode 100644 index 8dc93e5..0000000 --- a/services/monolith/src/base/wsgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -WSGI config for monolith project. - -It exposes the WSGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/4.1/howto/deployment/wsgi/ -""" - -import os - -from django.core.wsgi import get_wsgi_application - -os.environ.setdefault("DJANGO_SETTINGS_MODULE", "base.settings") - -application = get_wsgi_application() diff --git a/services/monolith/src/identity/__init__.py b/services/monolith/src/identity/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/services/monolith/src/identity/admin.py b/services/monolith/src/identity/admin.py deleted file mode 100644 index 8c38f3f..0000000 --- a/services/monolith/src/identity/admin.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.contrib import admin - -# Register your models here. diff --git a/services/monolith/src/identity/apps.py b/services/monolith/src/identity/apps.py deleted file mode 100644 index d4b74e0..0000000 --- a/services/monolith/src/identity/apps.py +++ /dev/null @@ -1,6 +0,0 @@ -from django.apps import AppConfig - - -class IdentityConfig(AppConfig): - default_auto_field = "django.db.models.BigAutoField" - name = "identity" diff --git a/services/monolith/src/identity/migrations/__init__.py b/services/monolith/src/identity/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/services/monolith/src/identity/models.py b/services/monolith/src/identity/models.py deleted file mode 100644 index 71a8362..0000000 --- a/services/monolith/src/identity/models.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.db import models - -# Create your models here. diff --git a/services/monolith/src/identity/tests.py b/services/monolith/src/identity/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/services/monolith/src/identity/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/services/monolith/src/identity/urls.py b/services/monolith/src/identity/urls.py deleted file mode 100644 index fba701d..0000000 --- a/services/monolith/src/identity/urls.py +++ /dev/null @@ -1,5 +0,0 @@ -import django.urls - -import identity.views - -url_patterns = [django.urls.path("me/", identity.views.identity_check)] diff --git a/services/monolith/src/identity/views.py b/services/monolith/src/identity/views.py deleted file mode 100644 index e0ca72b..0000000 --- a/services/monolith/src/identity/views.py +++ /dev/null @@ -1,14 +0,0 @@ -import django.http -import django.shortcuts -import django.conf - - -def identity_check(request: django.http.HttpRequest) -> django.http.HttpResponse: - """ - Verifies if the requesting user is logged in. - """ - - if request.user.is_authenticated: - return django.http.HttpResponse(status=200) - - return django.shortcuts.redirect("https://spadinaistan.karnov.club/app/admin/") diff --git a/services/monolith/src/manage.py b/services/monolith/src/manage.py deleted file mode 100755 index 214087c..0000000 --- a/services/monolith/src/manage.py +++ /dev/null @@ -1,22 +0,0 @@ -#!/usr/bin/env python -"""Django's command-line utility for administrative tasks.""" -import os -import sys - - -def main(): - """Run administrative tasks.""" - os.environ.setdefault("DJANGO_SETTINGS_MODULE", "base.settings") - try: - from django.core.management import execute_from_command_line - except ImportError as exc: - raise ImportError( - "Couldn't import Django. Are you sure it's installed and " - "available on your PYTHONPATH environment variable? Did you " - "forget to activate a virtual environment?" - ) from exc - execute_from_command_line(sys.argv) - - -if __name__ == "__main__": - main() diff --git a/services/monolith/tasks.py b/services/monolith/tasks.py deleted file mode 100644 index 06aed83..0000000 --- a/services/monolith/tasks.py +++ /dev/null @@ -1,29 +0,0 @@ -import invoke -import pathlib - -PATH = pathlib.Path(__file__).parent - - -@invoke.task() -def start(ctx): - with ctx.cd(PATH): - ctx.run("docker-compose up -d") - - -@invoke.task() -def stop(ctx): - with ctx.cd(PATH): - ctx.run("docker-compose down") - - -@invoke.task() -def restart(ctx): - with ctx.cd(PATH): - ctx.run("docker-compose restart") - - -ns = invoke.Collection("monolith") - -ns.add_task(start) -ns.add_task(restart) -ns.add_task(stop) diff --git a/services/traefik/docker-compose.yml b/services/traefik/docker-compose.yml index e8fcc2b..c7ae1d2 100644 --- a/services/traefik/docker-compose.yml +++ b/services/traefik/docker-compose.yml @@ -7,7 +7,6 @@ services: ports: - "80:80" - "443:443" - - "8080:8080" volumes: - /var/run/docker.sock:/var/run/docker.sock - ./traefik.toml:/traefik.toml diff --git a/services/traefik/traefik_dynamic.toml b/services/traefik/traefik_dynamic.toml index 2caf48c..633b88e 100644 --- a/services/traefik/traefik_dynamic.toml +++ b/services/traefik/traefik_dynamic.toml @@ -2,7 +2,7 @@ [http.routers.api] rule = "Host(`spadinaistan.karnov.club`)" entrypoints = ["websecure"] - middlewares = ["monolith-auth"] + middlewares = ["auth-service"] service = "api@internal" [http.routers.api.tls] certResolver = "lets-encrypt" @@ -10,16 +10,10 @@ [http.routers.deluge] rule = "Host(`spadinaistan.karnov.club`) && PathPrefix(`/deluge/`)" service = "deluge" - middlewares = ["deluge-base-headers", "monolith-auth", "deluge-stripprefix"] + middlewares = ["deluge-base-headers", "auth-service", "deluge-stripprefix"] [http.routers.deluge.tls] certResolver = "lets-encrypt" - - [http.routers.monolith] - rule = "Host(`spadinaistan.karnov.club`) && PathPrefix(`/app/`)" - service = "monolith" - [http.routers.monolith.tls] - certResolver = "lets-encrypt" - + [http.routers.bitwarden] rule = "Host(`spadinaistan.karnov.club`) && (PathPrefix(`/bitwarden/`) || HeadersRegexp(`Bitwarden-Client-Name`, `.*`))" service = "bitwarden" @@ -27,10 +21,13 @@ [http.routers.bitwarden.tls] certResolver = "lets-encrypt" -[http.middlewares] - [http.middlewares.monolith-auth.forwardauth] - address = "http://monolith:8000/app/identity/me/" + [http.routers.auth-service] + rule = "Host(`spadinaistan.karnov.club`) && PathPrefix(`/auth/`)" + service = "auth-service" + [http.routers.auth-service.tls] + certResolver = "lets-encrypt" +[http.middlewares] [http.middlewares.deluge-base-headers.headers.customRequestHeaders] X-Deluge-Base = "/deluge/" @@ -40,15 +37,18 @@ [http.middlewares.bitwarden-stripprefix.stripprefix] prefixes = ["/bitwarden"] + [http.middlewares.auth-service.forwardauth] + address = "http://auth-service:8080/auth/login" + [http.services] [http.services.deluge.loadBalancer] [[http.services.deluge.loadBalancer.servers]] url = "http://deluge:8112/" - [http.services.monolith.loadBalancer] - [[http.services.monolith.loadBalancer.servers]] - url = "http://monolith:8000/" - [http.services.bitwarden.loadBalancer] [[http.services.bitwarden.loadBalancer.servers]] url = "http://bitwarden:8080/" + + [http.services.auth-service.loadBalancer] + [[http.services.auth-service.loadBalancer.servers]] + url = "http://auth-service:8080/" diff --git a/services/traefik/traefik_dynamic_local.toml b/services/traefik/traefik_dynamic_local.toml new file mode 100644 index 0000000..8f48d50 --- /dev/null +++ b/services/traefik/traefik_dynamic_local.toml @@ -0,0 +1,53 @@ +[http.routers] + [http.routers.api] + rule = "Host(`localhost`)" + entrypoints = ["web"] + middlewares = ["auth-service"] + service = "api@internal" + + [http.routers.deluge] + rule = "Host(`localhost`) && PathPrefix(`/deluge/`)" + service = "deluge" + middlewares = ["deluge-base-headers", "monolith-auth", "deluge-stripprefix"] + + [http.routers.monolith] + rule = "Host(`localhost`) && PathPrefix(`/app/`)" + service = "monolith" + + [http.routers.bitwarden] + rule = "Host(`localhost`) && (PathPrefix(`/bitwarden/`) || HeadersRegexp(`Bitwarden-Client-Name`, `.*`))" + service = "bitwarden" + middlewares = ["bitwarden-stripprefix"] + +[http.middlewares] + [http.middlewares.monolith-auth.forwardauth] + address = "http://monolith:8000/app/identity/me/" + + [http.middlewares.auth-service.forwardauth] + address = "http://auth-service:8080/auth/login" + + [http.middlewares.deluge-base-headers.headers.customRequestHeaders] + X-Deluge-Base = "/deluge/" + + [http.middlewares.deluge-stripprefix.stripprefix] + prefixes = ["/deluge"] + + [http.middlewares.bitwarden-stripprefix.stripprefix] + prefixes = ["/bitwarden"] + +[http.services] + [http.services.authservice.loadBalancer] + [[http.services.authservice.loadBalancer.servers]] + url = "http://authservice:8080/" + + [http.services.deluge.loadBalancer] + [[http.services.deluge.loadBalancer.servers]] + url = "http://deluge:8112/" + + [http.services.monolith.loadBalancer] + [[http.services.monolith.loadBalancer.servers]] + url = "http://monolith:8000/" + + [http.services.bitwarden.loadBalancer] + [[http.services.bitwarden.loadBalancer.servers]] + url = "http://bitwarden:8080/" diff --git a/tasks.py b/tasks.py index 57ac341..dd31d50 100644 --- a/tasks.py +++ b/tasks.py @@ -3,13 +3,11 @@ import invoke import services.plex.tasks import services.deluge.tasks import services.traefik.tasks -import services.monolith.tasks ns = invoke.Collection() PYINFRA_COMMON_PREFIX = "pyinfra -vvv pyinfra/inventory.py" - @invoke.task() def system_updates(ctx): ctx.run(f"{PYINFRA_COMMON_PREFIX} pyinfra/system_updates.py") @@ -27,7 +25,6 @@ server.add_task(system_reboot, name="reboot") ns.add_collection(server) -ns.add_collection(services.monolith.tasks.ns) ns.add_collection(services.plex.tasks.ns) ns.add_collection(services.traefik.tasks.ns) ns.add_collection(services.deluge.tasks.ns)