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
This commit is contained in:
parent
b6529bedc6
commit
980aa2a8ba
39 changed files with 552 additions and 355 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,5 +1,6 @@
|
||||||
spadinaistan.venv
|
spadinaistan.venv
|
||||||
|
|
||||||
|
**/.env
|
||||||
pyinfra-debug.log
|
pyinfra-debug.log
|
||||||
deluge/config
|
deluge/config
|
||||||
deluge/downloads
|
deluge/downloads
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
|[Plex](./services/plex)|Plex media server|
|
|[Plex](./services/plex)|Plex media server|
|
||||||
|[Deluge](./services/deluge)|Deluge Web service|
|
|[Deluge](./services/deluge)|Deluge Web service|
|
||||||
|[Traefik](./services/traefik)|Traefik API Gateway|
|
|[Traefik](./services/traefik)|Traefik API Gateway|
|
||||||
|[Monolith](./services/monolith)|Usercode monolith|
|
|
||||||
|[Bitwarden](./services/bitwarden)|Bitwarden secrets management|
|
|[Bitwarden](./services/bitwarden)|Bitwarden secrets management|
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
15
services/auth-service/Dockerfile
Normal file
15
services/auth-service/Dockerfile
Normal file
|
@ -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"]
|
106
services/auth-service/cmd/authservice.go
Normal file
106
services/auth-service/cmd/authservice.go
Normal file
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,17 +1,18 @@
|
||||||
version: "3.7"
|
version: '3.7'
|
||||||
|
|
||||||
services:
|
services:
|
||||||
monolith:
|
auth-service:
|
||||||
restart: always
|
restart: always
|
||||||
build: .
|
build: .
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- 8080:8080
|
||||||
environment:
|
|
||||||
- SPADINAISTAN_ENV=prod
|
|
||||||
volumes:
|
volumes:
|
||||||
- ./src:/app/src
|
- ./config.json:/app/config.json
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
default:
|
default:
|
||||||
name: web
|
name: web
|
||||||
external: true
|
external: true
|
||||||
|
|
8
services/auth-service/go.mod
Normal file
8
services/auth-service/go.mod
Normal file
|
@ -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
|
||||||
|
)
|
4
services/auth-service/go.sum
Normal file
4
services/auth-service/go.sum
Normal file
|
@ -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=
|
55
services/auth-service/internal/helpers/cache.go
Normal file
55
services/auth-service/internal/helpers/cache.go
Normal file
|
@ -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}
|
||||||
|
}
|
49
services/auth-service/internal/helpers/configuration.go
Normal file
49
services/auth-service/internal/helpers/configuration.go
Normal file
|
@ -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
|
||||||
|
}
|
57
services/auth-service/internal/httpUtils/httpHandler.go
Normal file
57
services/auth-service/internal/httpUtils/httpHandler.go
Normal file
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
51
services/auth-service/internal/httpUtils/middlewares.go
Normal file
51
services/auth-service/internal/httpUtils/middlewares.go
Normal file
|
@ -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
|
||||||
|
}
|
130
services/auth-service/internal/oauth/oauth.go
Normal file
130
services/auth-service/internal/oauth/oauth.go
Normal file
|
@ -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
|
||||||
|
}
|
3
services/monolith/.gitignore
vendored
3
services/monolith/.gitignore
vendored
|
@ -1,3 +0,0 @@
|
||||||
*.sqlite3
|
|
||||||
spadinaistan-monolith.venv
|
|
||||||
__pycache__
|
|
|
@ -1 +0,0 @@
|
||||||
3.11.3
|
|
|
@ -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"]
|
|
|
@ -1,3 +0,0 @@
|
||||||
django
|
|
||||||
django-extensions
|
|
||||||
invoke
|
|
|
@ -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
|
|
|
@ -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
|
|
|
@ -1,4 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
pip-compile ./requirements.in
|
|
||||||
|
|
|
@ -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()
|
|
|
@ -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"
|
|
|
@ -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)),
|
|
||||||
]
|
|
||||||
),
|
|
||||||
)
|
|
||||||
]
|
|
|
@ -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()
|
|
|
@ -1,3 +0,0 @@
|
||||||
from django.contrib import admin
|
|
||||||
|
|
||||||
# Register your models here.
|
|
|
@ -1,6 +0,0 @@
|
||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class IdentityConfig(AppConfig):
|
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
|
||||||
name = "identity"
|
|
|
@ -1,3 +0,0 @@
|
||||||
from django.db import models
|
|
||||||
|
|
||||||
# Create your models here.
|
|
|
@ -1,3 +0,0 @@
|
||||||
from django.test import TestCase
|
|
||||||
|
|
||||||
# Create your tests here.
|
|
|
@ -1,5 +0,0 @@
|
||||||
import django.urls
|
|
||||||
|
|
||||||
import identity.views
|
|
||||||
|
|
||||||
url_patterns = [django.urls.path("me/", identity.views.identity_check)]
|
|
|
@ -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/")
|
|
|
@ -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()
|
|
|
@ -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)
|
|
|
@ -7,7 +7,6 @@ services:
|
||||||
ports:
|
ports:
|
||||||
- "80:80"
|
- "80:80"
|
||||||
- "443:443"
|
- "443:443"
|
||||||
- "8080:8080"
|
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- ./traefik.toml:/traefik.toml
|
- ./traefik.toml:/traefik.toml
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
[http.routers.api]
|
[http.routers.api]
|
||||||
rule = "Host(`spadinaistan.karnov.club`)"
|
rule = "Host(`spadinaistan.karnov.club`)"
|
||||||
entrypoints = ["websecure"]
|
entrypoints = ["websecure"]
|
||||||
middlewares = ["monolith-auth"]
|
middlewares = ["auth-service"]
|
||||||
service = "api@internal"
|
service = "api@internal"
|
||||||
[http.routers.api.tls]
|
[http.routers.api.tls]
|
||||||
certResolver = "lets-encrypt"
|
certResolver = "lets-encrypt"
|
||||||
|
@ -10,16 +10,10 @@
|
||||||
[http.routers.deluge]
|
[http.routers.deluge]
|
||||||
rule = "Host(`spadinaistan.karnov.club`) && PathPrefix(`/deluge/`)"
|
rule = "Host(`spadinaistan.karnov.club`) && PathPrefix(`/deluge/`)"
|
||||||
service = "deluge"
|
service = "deluge"
|
||||||
middlewares = ["deluge-base-headers", "monolith-auth", "deluge-stripprefix"]
|
middlewares = ["deluge-base-headers", "auth-service", "deluge-stripprefix"]
|
||||||
[http.routers.deluge.tls]
|
[http.routers.deluge.tls]
|
||||||
certResolver = "lets-encrypt"
|
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]
|
[http.routers.bitwarden]
|
||||||
rule = "Host(`spadinaistan.karnov.club`) && (PathPrefix(`/bitwarden/`) || HeadersRegexp(`Bitwarden-Client-Name`, `.*`))"
|
rule = "Host(`spadinaistan.karnov.club`) && (PathPrefix(`/bitwarden/`) || HeadersRegexp(`Bitwarden-Client-Name`, `.*`))"
|
||||||
service = "bitwarden"
|
service = "bitwarden"
|
||||||
|
@ -27,10 +21,13 @@
|
||||||
[http.routers.bitwarden.tls]
|
[http.routers.bitwarden.tls]
|
||||||
certResolver = "lets-encrypt"
|
certResolver = "lets-encrypt"
|
||||||
|
|
||||||
[http.middlewares]
|
[http.routers.auth-service]
|
||||||
[http.middlewares.monolith-auth.forwardauth]
|
rule = "Host(`spadinaistan.karnov.club`) && PathPrefix(`/auth/`)"
|
||||||
address = "http://monolith:8000/app/identity/me/"
|
service = "auth-service"
|
||||||
|
[http.routers.auth-service.tls]
|
||||||
|
certResolver = "lets-encrypt"
|
||||||
|
|
||||||
|
[http.middlewares]
|
||||||
[http.middlewares.deluge-base-headers.headers.customRequestHeaders]
|
[http.middlewares.deluge-base-headers.headers.customRequestHeaders]
|
||||||
X-Deluge-Base = "/deluge/"
|
X-Deluge-Base = "/deluge/"
|
||||||
|
|
||||||
|
@ -40,15 +37,18 @@
|
||||||
[http.middlewares.bitwarden-stripprefix.stripprefix]
|
[http.middlewares.bitwarden-stripprefix.stripprefix]
|
||||||
prefixes = ["/bitwarden"]
|
prefixes = ["/bitwarden"]
|
||||||
|
|
||||||
|
[http.middlewares.auth-service.forwardauth]
|
||||||
|
address = "http://auth-service:8080/auth/login"
|
||||||
|
|
||||||
[http.services]
|
[http.services]
|
||||||
[http.services.deluge.loadBalancer]
|
[http.services.deluge.loadBalancer]
|
||||||
[[http.services.deluge.loadBalancer.servers]]
|
[[http.services.deluge.loadBalancer.servers]]
|
||||||
url = "http://deluge:8112/"
|
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]
|
||||||
[[http.services.bitwarden.loadBalancer.servers]]
|
[[http.services.bitwarden.loadBalancer.servers]]
|
||||||
url = "http://bitwarden:8080/"
|
url = "http://bitwarden:8080/"
|
||||||
|
|
||||||
|
[http.services.auth-service.loadBalancer]
|
||||||
|
[[http.services.auth-service.loadBalancer.servers]]
|
||||||
|
url = "http://auth-service:8080/"
|
||||||
|
|
53
services/traefik/traefik_dynamic_local.toml
Normal file
53
services/traefik/traefik_dynamic_local.toml
Normal file
|
@ -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/"
|
3
tasks.py
3
tasks.py
|
@ -3,13 +3,11 @@ import invoke
|
||||||
import services.plex.tasks
|
import services.plex.tasks
|
||||||
import services.deluge.tasks
|
import services.deluge.tasks
|
||||||
import services.traefik.tasks
|
import services.traefik.tasks
|
||||||
import services.monolith.tasks
|
|
||||||
|
|
||||||
ns = invoke.Collection()
|
ns = invoke.Collection()
|
||||||
|
|
||||||
PYINFRA_COMMON_PREFIX = "pyinfra -vvv pyinfra/inventory.py"
|
PYINFRA_COMMON_PREFIX = "pyinfra -vvv pyinfra/inventory.py"
|
||||||
|
|
||||||
|
|
||||||
@invoke.task()
|
@invoke.task()
|
||||||
def system_updates(ctx):
|
def system_updates(ctx):
|
||||||
ctx.run(f"{PYINFRA_COMMON_PREFIX} pyinfra/system_updates.py")
|
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(server)
|
||||||
|
|
||||||
ns.add_collection(services.monolith.tasks.ns)
|
|
||||||
ns.add_collection(services.plex.tasks.ns)
|
ns.add_collection(services.plex.tasks.ns)
|
||||||
ns.add_collection(services.traefik.tasks.ns)
|
ns.add_collection(services.traefik.tasks.ns)
|
||||||
ns.add_collection(services.deluge.tasks.ns)
|
ns.add_collection(services.deluge.tasks.ns)
|
||||||
|
|
Reference in a new issue