From b1eae6578dac6bd9055e8569a47eea24215084ef Mon Sep 17 00:00:00 2001 From: tillkuhn Date: Thu, 8 Feb 2024 15:42:05 +0100 Subject: [PATCH] add support for metrics token management and validation --- Makefile | 25 +++++++----- go/imagine/Makefile | 6 ++- go/imagine/audio/audio_test.go | 5 ++- go/imagine/auth/jwtauth.go | 11 ++--- go/imagine/auth/middleware.go | 40 ++++++++++++++++--- go/imagine/auth/middleware_test.go | 20 +++++++++- .../auth/sample-cognito-token-resource.json | 12 ++++++ go/imagine/go.mod | 1 + go/imagine/go.sum | 2 + go/imagine/main.go | 33 ++++++++++----- go/imagine/server/http_test.go | 2 +- go/imagine/types/config.go | 1 + terraform/files/docker-compose.yml | 10 +++-- terraform/main.tf | 11 ++++- terraform/main_objects.tf | 3 +- terraform/modules/ec2/outputs.tf | 5 +++ terraform/modules/grafana/main.tf | 6 +-- terraform/modules/grafana/output.tf | 6 +-- terraform/modules/secrets/variables.tf | 1 - terraform/modules/tokens/README.adoc | 5 +++ terraform/modules/tokens/main.tf | 19 +++++++++ terraform/modules/tokens/output.tf | 9 +++++ terraform/modules/tokens/variables.tf | 19 +++++++++ terraform/modules/tokens/versions.tf | 12 ++++++ terraform/outputs.tf | 4 ++ terraform/templates/.env_config | 1 + 26 files changed, 217 insertions(+), 52 deletions(-) create mode 100644 go/imagine/auth/sample-cognito-token-resource.json create mode 100644 terraform/modules/tokens/README.adoc create mode 100644 terraform/modules/tokens/main.tf create mode 100644 terraform/modules/tokens/output.tf create mode 100644 terraform/modules/tokens/variables.tf create mode 100644 terraform/modules/tokens/versions.tf diff --git a/Makefile b/Makefile index f408d7c74..bb4e715f8 100644 --- a/Makefile +++ b/Makefile @@ -2,6 +2,16 @@ # Inspired by https://github.com/pgporada/terraform-makefile # Quick Reference: https://www.gnu.org/software/make/manual/html_node/Quick-Reference.html +# https://unix.stackexchange.com/questions/269077/tput-setaf-color-table-how-to-determine-color-codes +# DISABLE; will generate Makefile:11: not recursively expanding RESET to export to shell function +#BOLD=$(shell tput bold) +#RED=$(shell tput setaf 1) +#GREEN=$(shell tput setaf 2) +#YELLOW=$(shell tput setaf 3) +#CYAN=$(shell tput setaf 6) +#RESET=$(shell tput sgr0) +#STARTED=$(shell date +%s) + .DEFAULT_GOAL := help # default target when launched without arguments .EXPORT_ALL_VARIABLES: # especially important for sub-make calls .ONESHELL: @@ -14,14 +24,7 @@ AWS_CMD ?= aws # Our fingerprint changes rather often SSH_OPTIONS ?= -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -# https://unix.stackexchange.com/questions/269077/tput-setaf-color-table-how-to-determine-color-codes -BOLD=$(shell tput bold) -RED=$(shell tput setaf 1) -GREEN=$(shell tput setaf 2) -YELLOW=$(shell tput setaf 3) -CYAN=$(shell tput setaf 6) -RESET=$(shell tput sgr0) -STARTED=$(shell date +%s) + ############################ # self documenting makefile, recipe: @@ -47,15 +50,15 @@ help: ############################ tf-init: ## Runs terraform init on working directory ./terraform, switch to @$(MAKE) -C terraform init; - @echo "🏗️ $(GREEN)Terraform Infrastructure successfully initialized $(RESET)[$$(($$(date +%s)-$(STARTED)))s] " + @echo "🏗️ Terraform Infrastructure successfully initialized[$$(($$(date +%s)-$(STARTED)))s] " tf-plan: ## Runs terraform plan with implicit init and fmt (alias: plan) @$(MAKE) -C terraform plan; - @echo "🏗️ $(GREEN)Terraform Infrastructure successfully planned $(RESET)[$$(($$(date +%s)-$(STARTED)))s]" + @echo "🏗️ Terraform Infrastructure successfully planned[$$(($$(date +%s)-$(STARTED)))s]" tf-apply: ## Runs terraform apply with auto-approval (alias: apply) @$(MAKE) -C terraform deploy; - @echo "🏗️ $(GREEN)Terraform Infrastructure succcessfully deployed $(RESET)[$$(($$(date +%s)-$(STARTED)))s]" + @echo "🏗️ Terraform Infrastructure succcessfully deployed[$$(($$(date +%s)-$(STARTED)))s]" ssh: ## Runs terraform init on working directory ./terraform, switch to @$(MAKE) -C terraform ec2-login; diff --git a/go/imagine/Makefile b/go/imagine/Makefile index ea188509d..47e4838bf 100644 --- a/go/imagine/Makefile +++ b/go/imagine/Makefile @@ -7,7 +7,8 @@ BASE_URL ?= http://localhost:8090 # this is super ugly (share kafka dev credentials with api project), we need this in ~/.angkor/.env! KAFKA_SASL_USERNAME_DEV ?= $(shell grep sasl.jaas.config ../../kotlin/config/application.properties | cut -d\" -f2) KAFKA_SASL_PASSWORD_DEV ?= $(shell grep sasl.jaas.config ../../kotlin/config/application.properties | cut -d\" -f4) -IMAGINE_JWKS_ENDPOINT=$(shell grep "^IMAGINE_JWKS_ENDPOINT" ~/.angkor/.env |cut -d= -f2-) +IMAGINE_JWKS_ENDPOINT = $(shell grep "^IMAGINE_JWKS_ENDPOINT" ~/.angkor/.env |cut -d= -f2-) +IMAGINE_API_TOKEN_METRICS = $(shell grep "^APP_API_TOKEN_METRICS" ~/.angkor/.env |cut -d= -f2-) .ONESHELL: .PHONY: format fmt lint run build build-linux clean test @@ -48,6 +49,7 @@ run: ## run app IMAGINE_FORCE_GC=true \ IMAGINE_ENABLE_AUTH=true \ IMAGINE_JWKS_ENDPOINT=$(IMAGINE_JWKS_ENDPOINT) \ + IMAGINE_API_TOKEN_METRICS=$(IMAGINE_API_TOKEN_METRICS) \ IMAGINE_RESIZE_MODES=small:150,medium:300,large:600 IMAGINE_CONTEXTPATH=/imagine \ go run $(GO_FILES); \ else echo "AWS_SESSION_TOKEN is present, pls open a fresh terminal"; exit 1; fi @@ -86,7 +88,7 @@ get: ## test get files for id # X-Authorization and Authorization should work both get-metrics: ## get prometheus metrics - curl -isSH "Authorization: Bearer $(JWT_TOKEN)" $(BASE_URL)/imagine/metrics + curl -isSH "Authorization: Bearer $(IMAGINE_API_TOKEN_METRICS)" $(BASE_URL)/imagine/metrics get-presign-url: ## test get files for id curl -i -Ss $(BASE_URL)/imagine/places/$(TEST_ID)/hase2.jpeg diff --git a/go/imagine/audio/audio_test.go b/go/imagine/audio/audio_test.go index 37f7d9391..18742685c 100644 --- a/go/imagine/audio/audio_test.go +++ b/go/imagine/audio/audio_test.go @@ -1,11 +1,12 @@ package audio import ( + "os" + "testing" + "github.com/dhowden/tag" "github.com/rs/zerolog" "github.com/rs/zerolog/log" - "os" - "testing" "github.com/stretchr/testify/assert" ) diff --git a/go/imagine/auth/jwtauth.go b/go/imagine/auth/jwtauth.go index e3b43fdb3..cc28ef1b5 100644 --- a/go/imagine/auth/jwtauth.go +++ b/go/imagine/auth/jwtauth.go @@ -63,10 +63,11 @@ func (t *TokenInfo) Scope() interface{} { } // Roles returns the Cognito specific roles claim "cognito:roles" -// "cognito:roles": [ -// "arn:aws:iam::06**********:role/*******-cognito-role-user", -// "arn:aws:iam::06**********:role/*******-cognito-role-admin" -// ] +// +// "cognito:roles": [ +// "arn:aws:iam::06**********:role/*******-cognito-role-user", +// "arn:aws:iam::06**********:role/*******-cognito-role-admin" +// ] func (t *TokenInfo) Roles() []interface{} { if roles, ok := t.claims["cognito:roles"].([]interface{}); ok { return roles @@ -87,5 +88,5 @@ func (t *TokenInfo) Subject() interface{} { // extractToken returns the part after "Bearer " from the auth header func extractToken(authHeader string) string { - return strings.Split(authHeader, "Bearer ")[1] + return strings.Fields(authHeader)[1] } diff --git a/go/imagine/auth/middleware.go b/go/imagine/auth/middleware.go index aac16eebe..315e94ec6 100644 --- a/go/imagine/auth/middleware.go +++ b/go/imagine/auth/middleware.go @@ -7,6 +7,8 @@ import ( "reflect" "strings" + "github.com/cdfmlr/ellipsis" + "github.com/gorilla/context" "github.com/rs/zerolog" "github.com/rs/zerolog/log" @@ -24,6 +26,8 @@ type ContextKey string // ContextAuthKey used to store auth info such as claims in request context const ContextAuthKey ContextKey = "auth" +var authError = errors.New("auth error") + // New constructs a new Handler Context func New(enabled bool, jwkUrl string, account string) *Handler { ctxLogger := log.With().Str("logger", "auth").Logger() @@ -40,6 +44,25 @@ func New(enabled bool, jwkUrl string, account string) *Handler { } } +// CheckTokenMiddleware simple validation of static tokens +func (ah *Handler) CheckTokenMiddleware(token string, next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, req *http.Request) { + authHeader := getAuthHeader(req) + if strings.Contains(getAuthHeader(req), "Bearer") { + hToken := strings.Fields(authHeader)[1] + if hToken != token { + handleError(w, fmt.Sprintf("token %s does not match expected token %s", + ellipsis.Ending(hToken, 7), ellipsis.Ending(token, 7)), authError, http.StatusForbidden) + return + } + } else { + handleError(w, fmt.Sprintf("Invalid Auth Header %v", req.Header), authError, http.StatusForbidden) + return + } + next(w, req) + } +} + // ValidationMiddleware a wrapper around the actual request // to validate the Authorization header and either stop processing (invalid / no token) // or continue with the next HandlerFunc @@ -54,12 +77,9 @@ func (ah *Handler) ValidationMiddleware(next http.HandlerFunc) http.HandlerFunc // TODO Delegate to middleware, e.g. like this // https://hackernoon.com/creating-a-middleware-in-golang-for-jwt-based-authentication-cx3f32z8 if ah.enabled { - authHeader := req.Header.Get("X-Authorization") // case-insensitive - if authHeader == "" { - authHeader = req.Header.Get("Authorization") // fallback (e.g. for Grafana metrics scraping) - } + authHeader := getAuthHeader(req) if strings.Contains(authHeader, "Bearer") { - jwtB64 := strings.Split(authHeader, "Bearer ")[1] + jwtB64 := strings.Fields(authHeader)[1] // bearer part is first field claims, err := ah.jwtAuth.ParseClaims(authHeader) if err != nil { handleError(w, fmt.Sprintf("Failed to parse jwtb64 %v: %v", jwtB64, err), err, http.StatusForbidden) @@ -77,7 +97,7 @@ func (ah *Handler) ValidationMiddleware(next http.HandlerFunc) http.HandlerFunc claims.Subject(), claims.Scope(), claims.Roles(), claims.Name(), reflect.TypeOf(claims.Roles())) context.Set(req, ContextAuthKey, claims) } else { - handleError(w, fmt.Sprintf("Cannot find/validate (X-)Authorization header in %v", req.Header), errors.New("oops"), http.StatusForbidden) + handleError(w, fmt.Sprintf("Cannot find (X-)Authorization with Bearer Token in %v", req.Header), authError, http.StatusForbidden) return } } else { @@ -92,3 +112,11 @@ func handleError(writer http.ResponseWriter, msg string, err error, code int) { log.Error().Msgf("Error %s - %v", msg, err) http.Error(writer, fmt.Sprintf("%s - %v", msg, err), code) } + +func getAuthHeader(req *http.Request) string { + authHeader := req.Header.Get("X-Authorization") // case-insensitive + if authHeader == "" { + authHeader = req.Header.Get("Authorization") // fallback (e.g. for Grafana metrics scraping) + } + return authHeader +} diff --git a/go/imagine/auth/middleware_test.go b/go/imagine/auth/middleware_test.go index 5fc47d603..3c5915932 100644 --- a/go/imagine/auth/middleware_test.go +++ b/go/imagine/auth/middleware_test.go @@ -43,7 +43,25 @@ func TestValidTokenInvalidMiddleware(t *testing.T) { for _, ah := range []string{"X-Authorization", "Authorization"} { req.Header.Set(ah, "Bearer invalid-string") authContextEnabled.ValidationMiddleware(testHandler).ServeHTTP(rr, req) - assert.Equal(t, rr.Code, http.StatusForbidden, rr.Body.String()) + assert.Equal(t, http.StatusForbidden, rr.Code, rr.Body.String()) assert.Contains(t, strings.ToLower(rr.Body.String()), "invalid number of segments") } } + +func TestSimpleTokenInvalidMiddleware(t *testing.T) { + rr := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/metrics", nil) + testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + // encToken, _ := issueToken("extra-protected") + // both headers should be checked + validToken := "hello-bear" + for _, ah := range []string{"X-Authorization"} { + req.Header.Set(ah, "Bearer "+validToken) + authContextEnabled.CheckTokenMiddleware(validToken, testHandler).ServeHTTP(rr, req) + assert.Equal(t, http.StatusOK, rr.Code, rr.Body.String()) + req.Header.Set(ah, "Bearer invalidToken") + authContextEnabled.CheckTokenMiddleware(validToken, testHandler).ServeHTTP(rr, req) + assert.Equal(t, http.StatusForbidden, rr.Code, rr.Body.String()) + assert.Contains(t, strings.ToLower(rr.Body.String()), "does not match expected t") + } +} diff --git a/go/imagine/auth/sample-cognito-token-resource.json b/go/imagine/auth/sample-cognito-token-resource.json new file mode 100644 index 000000000..6982bc621 --- /dev/null +++ b/go/imagine/auth/sample-cognito-token-resource.json @@ -0,0 +1,12 @@ +{ + "sub": "4unkt2ome*****", + "token_use": "access", + "scope": "a*****-resources/read a*****r-resources/write", + "auth_time": 1607340912, + "iss": "https://cognito-idp.eu-central-1.amazonaws.com/eu-central-1_*****", + "exp": 1607344512, + "iat": 1607340912, + "version": 2, + "jti": "e3570b50-3f93-*****-*****-**********", + "client_id": "4unkt2ome*****" +} diff --git a/go/imagine/go.mod b/go/imagine/go.mod index 47a843677..d09f19683 100644 --- a/go/imagine/go.mod +++ b/go/imagine/go.mod @@ -6,6 +6,7 @@ require ( github.com/IBM/sarama v1.42.1 // indirect github.com/MicahParks/keyfunc v1.9.0 github.com/aws/aws-sdk-go v1.50.12 + github.com/cdfmlr/ellipsis v0.0.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dhowden/tag v0.0.0-20240122214204-713ab0e94639 github.com/disintegration/imaging v1.6.2 diff --git a/go/imagine/go.sum b/go/imagine/go.sum index 8dc264521..eac3a3ef8 100644 --- a/go/imagine/go.sum +++ b/go/imagine/go.sum @@ -668,6 +668,8 @@ github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/cdfmlr/ellipsis v0.0.1 h1:4pwrPbKPMd4mXSdJA4CSRjgEzCbXyRiFBkmgg2KclBI= +github.com/cdfmlr/ellipsis v0.0.1/go.mod h1:hulYx9m/7Edoo2AkRzkJ/YPDlLB45BgjitI3z0sMVFI= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= diff --git a/go/imagine/main.go b/go/imagine/main.go index b294f9bfe..faf08981a 100644 --- a/go/imagine/main.go +++ b/go/imagine/main.go @@ -3,16 +3,17 @@ package main import ( "flag" "fmt" - "github.com/aws/aws-sdk-go/service/sts" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/collectors" - "github.com/prometheus/client_golang/prometheus/promhttp" "net/http" "os" "os/signal" "runtime" "syscall" + "github.com/aws/aws-sdk-go/service/sts" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/rs/zerolog" "github.com/tillkuhn/angkor/tools/imagine/s3" "github.com/tillkuhn/angkor/tools/imagine/server" @@ -105,11 +106,6 @@ func main() { cp := config.ContextPath router := mux.NewRouter() - // Prometheus Preparation - // reduce noise (default init https://github.com/prometheus/client_golang/blob/main/prometheus/registry.go#L60) - prometheus.Unregister(collectors.NewGoCollector()) - prometheus.Unregister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) - // Setup AWS and init S3 Upload Worker mainLogger.Info().Msgf("[AWS] Establish session target bucket=%s prefix=%s", config.S3Bucket, config.S3Prefix) awsSession, err := session.NewSession(&aws.Config{Region: aws.String(config.AWSRegion)}) @@ -139,9 +135,20 @@ func main() { // return metrics such as // # TYPE promhttp_metric_handler_requests_total counter // promhttp_metric_handler_requests_total{code="200"} 3 + // Prometheus Preparation + // reduce noise (default init https://github.com/prometheus/client_golang/blob/main/prometheus/registry.go#L60) + // reg := prometheus.NewRegistry() + prometheus.Unregister(collectors.NewGoCollector()) + prometheus.Unregister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) ph := http.HandlerFunc(promhttp.Handler().ServeHTTP) // need to wrap from http.Handler to HandlerFunc - router.Handle(cp+"/metrics", authHandler.ValidationMiddleware(ph)).Methods(http.MethodGet) + // ph := http.HandlerFunc(promhttp.HandlerFor(reg, promhttp.HandlerOpts{Registry: reg}).ServeHTTP) // need to wrap from http.Handler to HandlerFunc + metricsToken := os.Getenv("IMAGINE_API_TOKEN_METRICS") + if metricsToken != "" { + router.Handle(cp+"/metrics", authHandler.CheckTokenMiddleware(metricsToken, ph)).Methods(http.MethodGet) + } else { + log.Warn().Msgf("IMAGINE_API_TOKEN_METRICS not set, cannot expose prometheus metrics handler") + } // Redirect to presigned url for a particular song (protected) // router.HandleFunc(cp+"/songs/{item}", authHandler.ValidationMiddleware(GetSongPresignUrl)).Methods(http.MethodGet) router.HandleFunc(cp+"/songs/{folder}/{item}", authHandler.ValidationMiddleware(httpHandler.GetSongPresignUrl)).Methods(http.MethodGet) @@ -177,7 +184,11 @@ func main() { dumpRoutes(router) // Launch re-tagger in separate go routine - go s3Handler.Retag() + if config.MP3Retag { + go s3Handler.Retag() + } else { + log.Warn().Msg("MP3 Retag is separate goroutine is disabled") + } // Launch the HTTP Server and block mainLogger.Info().Msgf("[HTTP] Start HTTPServer http://localhost:%d%s", config.Port, config.ContextPath) diff --git a/go/imagine/server/http_test.go b/go/imagine/server/http_test.go index 125abe7ce..975969198 100644 --- a/go/imagine/server/http_test.go +++ b/go/imagine/server/http_test.go @@ -58,7 +58,7 @@ func TestShouldRejectPostIfUnauthenticated(t *testing.T) { fmt.Println(targetUrl) filename := "../README.md" err = postFile(filename, targetUrl) - assert.Contains(t, err.Error(), "Authorization header") + assert.Contains(t, err.Error(), "auth error") assert.Contains(t, err.Error(), "403") } diff --git a/go/imagine/types/config.go b/go/imagine/types/config.go index b9ca29da0..ad96b8500 100644 --- a/go/imagine/types/config.go +++ b/go/imagine/types/config.go @@ -17,6 +17,7 @@ type Config struct { JwksEndpoint string `split_words:"true" desc:"Endpoint to download JWKS"` KafkaSupport bool `default:"true" desc:"Send important events to Kafka Topic(s)" split_words:"true"` KafkaTopic string `default:"app" desc:"Default Kafka Topic for published Events" split_words:"true"` + MP3Retag bool `default:"false" desc:"MP3 retag on start enabled" split_words:"true"` Port int `default:"8090" desc:"http server port"` PresignExpiry time.Duration `default:"30m" desc:"how long presigned urls are valid"` QueueSize int `default:"10" split_words:"true" desc:"max capacity of s3 upload worker queue"` diff --git a/terraform/files/docker-compose.yml b/terraform/files/docker-compose.yml index 6e84356c7..3f54eec1d 100644 --- a/terraform/files/docker-compose.yml +++ b/terraform/files/docker-compose.yml @@ -9,10 +9,13 @@ services: angkor-api: image: ${DOCKER_USER}/${APPID}-api:${API_VERSION} container_name: angkor-api + # https://stackoverflow.com/a/58048994/4292075 + # existing .env file will be loaded implicitly by docker-compose to adjust the environment of the docker-compose + # command itself, whereas defining an "env_file: ..." inside the yaml will take environment variables from the file + # and inject them into the container (i.e. they cannot be used for variables inside your yaml) # env_file: "./.env" # KEEP DISABLED - # make sure all vars in environment: block below (right side) are defined in .env - # which is loaded implicitly by docker.compose - # 'docker-compose config' will show the resolved application config + # make sure all vars in environment: block below (right side) are defined in .env so they can be expanded + # TIP: 'docker-compose config' will show the resolved application config environment: ## JDBC config @@ -95,6 +98,7 @@ services: IMAGINE_S3PREFIX: "imagine/" # todo this is already new default in recent version IMAGINE_DUMPDIR: "/tmp" # use /tmp root in container, no volume mount necessary IMAGINE_FORCE_GC: "true" # call freeMemory / force gc after expensive operations + IMAGINE_API_TOKEN_METRICS: "${APP_API_TOKEN_METRICS}" ports: - "8090:8090" restart: always diff --git a/terraform/main.tf b/terraform/main.tf index 031d52f18..c3d9e3462 100644 --- a/terraform/main.tf +++ b/terraform/main.tf @@ -275,6 +275,13 @@ module "grafana" { cloud_api_key = data.hcp_vault_secrets_app.rt_secrets_manual.secrets["GRAFANA_CLOUD_API_KEY"] } -output "grafana_stack" { - value = module.grafana.cloud_stack + +#output "grafana_stack" { +# value = module.grafana.cloud_stack +#} + +module "tokens" { + source = "./modules/tokens" + app_id = var.appid + keeper = module.ec2.ami_id } diff --git a/terraform/main_objects.tf b/terraform/main_objects.tf index 796ec8cb7..0757f28f5 100644 --- a/terraform/main_objects.tf +++ b/terraform/main_objects.tf @@ -6,9 +6,10 @@ locals { dotenv_content = templatefile("${path.module}/templates/.env_config", { account_id = module.vpcinfo.account_id - aws_region = module.vpcinfo.aws_region api_version = var.api_version + api_token_metrics = module.tokens.api_token_metrics appid = var.appid + aws_region = module.vpcinfo.aws_region bucket_name = module.s3.bucket_name certbot_domain_name = var.certbot_domain_name certbot_domain_str = format("-d %s", join(" -d ", concat([ diff --git a/terraform/modules/ec2/outputs.tf b/terraform/modules/ec2/outputs.tf index 135512150..3f9ffb55c 100644 --- a/terraform/modules/ec2/outputs.tf +++ b/terraform/modules/ec2/outputs.tf @@ -37,3 +37,8 @@ output "ami_info" { "Most recent available: ${data.aws_ami.amazon-linux-2.id} dd ${data.aws_ami.amazon-linux-2.creation_date}", aws_instance.instance.ami != data.aws_ami.amazon-linux-2.id) } + +# AMI ID, useful as stable "keeper" for random resources, e.g. random_uuid +output "ami_id" { + value = data.aws_ami.amazon-linux-2.id +} diff --git a/terraform/modules/grafana/main.tf b/terraform/modules/grafana/main.tf index d23f24df9..09ba0ad2d 100644 --- a/terraform/modules/grafana/main.tf +++ b/terraform/modules/grafana/main.tf @@ -32,9 +32,9 @@ resource "grafana_service_account_token" "viewer" { # this required cloud_api_key on provider with at least stack:read privileges -data "grafana_cloud_stack" "current" { - slug = var.slug -} +#data "grafana_cloud_stack" "current" { +# slug = var.slug +#} # │ Error: the Cloud API client is required for `grafana_cloud_access_policy`. Set the cloud_api_key provider attribute # https://grafana.com/docs/grafana-cloud/account-management/cloud-portal/: diff --git a/terraform/modules/grafana/output.tf b/terraform/modules/grafana/output.tf index f553926f4..dce6f5ab3 100644 --- a/terraform/modules/grafana/output.tf +++ b/terraform/modules/grafana/output.tf @@ -9,6 +9,6 @@ output "service_account_token_viewer_key" { } # expose to caller -output "cloud_stack" { - value = data.grafana_cloud_stack.current.url -} +#output "cloud_stack" { +#value = data.grafana_cloud_stack.current.url +#} diff --git a/terraform/modules/secrets/variables.tf b/terraform/modules/secrets/variables.tf index 67ab3c9ed..64fc23240 100644 --- a/terraform/modules/secrets/variables.tf +++ b/terraform/modules/secrets/variables.tf @@ -24,7 +24,6 @@ variable "secrets" { } - ## more complex #variable "topics" { # description = "List of Kafka Topics" diff --git a/terraform/modules/tokens/README.adoc b/terraform/modules/tokens/README.adoc new file mode 100644 index 000000000..f769b6e4c --- /dev/null +++ b/terraform/modules/tokens/README.adoc @@ -0,0 +1,5 @@ += Manage generated tokens + +== References + +* https://registry.terraform.io/providers/hashicorp/random/latest[] diff --git a/terraform/modules/tokens/main.tf b/terraform/modules/tokens/main.tf new file mode 100644 index 000000000..cc243edf6 --- /dev/null +++ b/terraform/modules/tokens/main.tf @@ -0,0 +1,19 @@ +locals { + +} + +# The resource random_pet generates random pet names that are intended +# to be used as unique identifiers for other resources. +resource "random_pet" "api_token_metrics" { + keepers = { + default = var.keeper + } +} + + +resource "random_string" "api_token_metrics" { + keepers = { + default = var.keeper + } + length = 8 +} diff --git a/terraform/modules/tokens/output.tf b/terraform/modules/tokens/output.tf new file mode 100644 index 000000000..068c2b3e2 --- /dev/null +++ b/terraform/modules/tokens/output.tf @@ -0,0 +1,9 @@ +output "api_token_metrics" { + value = base64encode(jsonencode({ + # jwt style fields https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-token-claims#registered-claims + "iss" = var.app_id # Identifies principal that issued the JWT. + "aud" = "prometheus" # (audience): Recipient for which the JWT is intended + "token" = "${random_pet.api_token_metrics.id}-${random_string.api_token_metrics.id}" + })) +} + diff --git a/terraform/modules/tokens/variables.tf b/terraform/modules/tokens/variables.tf new file mode 100644 index 000000000..d3d2b4ef3 --- /dev/null +++ b/terraform/modules/tokens/variables.tf @@ -0,0 +1,19 @@ +variable "app_id" { + description = "Application ID" +} + +variable "keeper" { + description = "Values that, when changed, will trigger recreation of resource." +} + +## more complex +#variable "topics" { +# description = "List of Kafka Topics" +# type = list(object({ +# name = string +# partitions_count = number +# retention_hours = number +# })) +# default = [] +#} + diff --git a/terraform/modules/tokens/versions.tf b/terraform/modules/tokens/versions.tf new file mode 100644 index 000000000..99ab3c828 --- /dev/null +++ b/terraform/modules/tokens/versions.tf @@ -0,0 +1,12 @@ +# don't configure provider in modules (e.g. credentials for hcp), +# only declare what's required here and use relaxed version ranges +# since callers of the module may run different versions +terraform { + required_version = "~>1.5" + required_providers { + # hcp = { + # source = "hashicorp/hcp" + # version = "~> 0.71" + # } + } +} diff --git a/terraform/outputs.tf b/terraform/outputs.tf index 58282567d..53567ff89 100644 --- a/terraform/outputs.tf +++ b/terraform/outputs.tf @@ -26,3 +26,7 @@ output "confluent_cluster_rest_endpoint" { output "confluent_cluster_id" { value = module.confluent.cluster_id } + +output "api_token_metrics" { + value = module.tokens.api_token_metrics +} diff --git a/terraform/templates/.env_config b/terraform/templates/.env_config index a56761741..86c7a48b8 100644 --- a/terraform/templates/.env_config +++ b/terraform/templates/.env_config @@ -32,6 +32,7 @@ OAUTH2_CLIENT_CLI_SECRET=${oauth2_client_cli_secret} # Configure Backend (API), values mapped to custom AppProperties class in our SpringBoot App # APP_API_TOKEN now managed via HCP Vault Secrets +APP_API_TOKEN_METRICS=${api_token_metrics} APP_EXTERNAL_BASE_URL=https://${certbot_domain_name} APP_TOURS_API_BASE_URL=${tours_api_base_url} APP_TOURS_API_USER_ID=${tours_api_user_id}