Skip to content

Commit

Permalink
switch from hostname-based auth to zone-id-based
Browse files Browse the repository at this point in the history
  • Loading branch information
pickledish committed Jul 20, 2024
1 parent 2787fc1 commit 670d9dc
Show file tree
Hide file tree
Showing 11 changed files with 153 additions and 180 deletions.
40 changes: 15 additions & 25 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,36 +1,26 @@
############################
# STAGE 0 - Build
############################
FROM ubuntu:24.04

# https://hub.docker.com/r/jetpackio/devbox/tags
FROM jetpackio/devbox:0.12.0
WORKDIR /src

WORKDIR /code
# Install some system dependencies
RUN apt-get update && apt-get install -y xz-utils curl git ca-certificates lftp
RUN update-ca-certificates

USER root:root
RUN mkdir -p /code && chown ${DEVBOX_USER}:${DEVBOX_USER} /code
USER ${DEVBOX_USER}:${DEVBOX_USER}
COPY --chown=${DEVBOX_USER}:${DEVBOX_USER} devbox.json devbox.json
COPY --chown=${DEVBOX_USER}:${DEVBOX_USER} devbox.lock devbox.lock
# This is the official way to install it :(
RUN curl -fsSL https://get.jetify.com/devbox -o install-devbox.sh
RUN chmod +x install-devbox.sh
RUN ./install-devbox.sh -f

# Do just dependencies first, to benefit from layer caching
COPY devbox.json devbox.json
COPY devbox.lock devbox.lock

# This also installs nix, takes a while, todo, do this earlier in Dockerfile
RUN devbox install

# Now the rest!

COPY --chown=${DEVBOX_USER}:${DEVBOX_USER} . .

COPY . .
RUN devbox run build

############################
# STAGE 1 - Run
############################

# https://hub.docker.com/_/ubuntu/tags
FROM ubuntu:22.04

RUN apt-get update && apt-get install -y git ca-certificates lftp
RUN update-ca-certificates

COPY --from=0 /code/out/cbnr /usr/bin/cbnr
# And copy the command to somewhere we can find it
RUN mv /src/out/cbnr /usr/bin/cbnr
57 changes: 10 additions & 47 deletions client/supabase_admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,8 @@ type CreateSiteRowBody struct {
Hostname string `json:"hostname"`
}

type AddHostnameToSiteRowBody struct {
CustomHostname string `json:"custom_hostname"`
}

type AuthorizeHostnameBody struct {
AppMetadata map[string][]string `json:"app_metadata"`
type AuthorizeZoneIdBody struct {
AppMetadata map[string][]int `json:"app_metadata"`
}

func (s SupabaseAdminClient) CreateSiteRow(ctx context.Context, userId, siteId, nickname string, storage *CreateStorageZoneResponse, pull *CreatePullZoneResponse) bool {
Expand Down Expand Up @@ -67,18 +63,18 @@ func (s SupabaseAdminClient) CreateSiteRow(ctx context.Context, userId, siteId,
return true
}

func (s SupabaseAdminClient) AuthorizeHostname(ctx context.Context, userId, newHostname string, existingHostnames []string) bool {
func (s SupabaseAdminClient) AuthorizeZoneId(ctx context.Context, userId string, newZoneId int, existingZoneIds []int) bool {

// mutating in place because Go makes anything else annoyingly difficult
existingHostnames = append(existingHostnames, newHostname)
existingZoneIds = append(existingZoneIds, newZoneId)

body := AuthorizeHostnameBody{
AppMetadata: map[string][]string{
"hostnames": existingHostnames,
body := AuthorizeZoneIdBody{
AppMetadata: map[string][]int{
"zones": existingZoneIds,
},
}

log.Printf("[INFO] Authorizing user %v for hostname with request body: %+v", userId, body)
log.Printf("[INFO] Authorizing user %v for zone with request body: %+v", userId, body)

var errorJson map[string]interface{}

Expand All @@ -94,44 +90,11 @@ func (s SupabaseAdminClient) AuthorizeHostname(ctx context.Context, userId, newH
Fetch(ctx)

if err != nil {
log.Printf("[ERROR] Unable to create pull zone in BunnyCDN: %v, response: %+v", err, errorJson)
return false
}

log.Printf("[INFO] Successfully authorized user %v for hostname %v", userId, newHostname)

return true
}

// todo - unclear why this needs the admin client, can service role bypass no-update trigger?
func (s SupabaseAdminClient) AddHostnameToSiteRow(ctx context.Context, siteId, hostname string) bool {

body := AddHostnameToSiteRowBody{
CustomHostname: hostname,
}

log.Printf("[INFO] Adding hostname to SITE row %v with request body: %+v", siteId, body)

var errorJson map[string]interface{}

err := requests.
URL(s.SupabaseUrl).
Path("/rest/v1/site").
Param("id", fmt.Sprintf("eq.%v", siteId)).
Header("apikey", s.SupabaseAnonKey).
Header("Authorization", fmt.Sprintf("Bearer %v", s.SupabaseServiceKey)).
ContentType("application/json").
BodyJSON(&body).
ErrorJSON(&errorJson).
Patch().
Fetch(ctx)

if err != nil {
log.Printf("[ERROR] Unable to add hostname to SITE row %v: %v, response: %+v", siteId, err, errorJson)
log.Printf("[ERROR] Unable to authorize user %v for zone: %v, response: %+v", userId, err, errorJson)
return false
}

log.Printf("[INFO] Successfully added new hostname for site %v, hostname %v", siteId, hostname)
log.Printf("[INFO] Successfully authorized user %v for zone ID %v", userId, newZoneId)

return true
}
42 changes: 39 additions & 3 deletions client/supabase_normie.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,15 @@ type SiteRow struct {
Hostname string `json:"hostname"`
}

type UpdateFieldsBody struct {
type UpdateDeployedShaBody struct {
DeployedSha string `json:"deployed_sha"`
LastUpdatedAt string `json:"last_updated_at"`
}

type AddHostnameToSiteRowBody struct {
CustomHostname string `json:"custom_hostname"`
}

func (s SupabaseNormieClient) GetSiteRow(ctx context.Context, jwt, siteId string) *SiteRow {

log.Printf("[INFO] Attempting to fetch row for site ID %v from supabase", siteId)
Expand Down Expand Up @@ -71,9 +75,9 @@ func (s SupabaseNormieClient) GetSiteRow(ctx context.Context, jwt, siteId string
return &rows[0]
}

func (s SupabaseNormieClient) UpdateFields(ctx context.Context, jwt, siteId, sha string) bool {
func (s SupabaseNormieClient) UpdateDeployedSha(ctx context.Context, jwt, siteId, sha string) bool {

body := UpdateFieldsBody{
body := UpdateDeployedShaBody{
DeployedSha: sha,
LastUpdatedAt: "now", // special value understood by postgrest, me being lazy
}
Expand Down Expand Up @@ -103,3 +107,35 @@ func (s SupabaseNormieClient) UpdateFields(ctx context.Context, jwt, siteId, sha

return true
}

func (s SupabaseNormieClient) AddHostnameToSiteRow(ctx context.Context, jwt, siteId, hostname string) bool {

body := AddHostnameToSiteRowBody{
CustomHostname: hostname,
}

log.Printf("[INFO] Adding hostname to SITE row %v with request body: %+v", siteId, body)

var errorJson map[string]interface{}

err := requests.
URL(s.SupabaseUrl).
Path("/rest/v1/site").
Param("id", fmt.Sprintf("eq.%v", siteId)).
Header("apikey", s.SupabaseAnonKey).
Header("Authorization", jwt).
ContentType("application/json").
BodyJSON(&body).
ErrorJSON(&errorJson).
Patch().
Fetch(ctx)

if err != nil {
log.Printf("[ERROR] Unable to add hostname to SITE row %v: %v, response: %+v", siteId, err, errorJson)
return false
}

log.Printf("[INFO] Successfully added new hostname for site %v, hostname %v", siteId, hostname)

return true
}
53 changes: 8 additions & 45 deletions cmd/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package api

import (
"encoding/json"
"fmt"
"log"
"net/http"

Expand Down Expand Up @@ -62,10 +61,10 @@ func (s Server) CreateSite(out http.ResponseWriter, req *http.Request) {
return
}

// get the currently-authorized hostnames from the claims for appending later
existingHostnames, err := util.GetHostnamesFromClaims(claims)
// get the currently-authorized sites from the claims for appending later
existingZoneIds, err := util.GetZoneIdsFromClaims(claims)
if err != nil {
log.Printf("[ERROR] Unable to get hostnames from JWT claims: %v", err)
log.Printf("[ERROR] Unable to get site IDs from JWT claims: %v", err)
http.Error(out, "Invalid JWT app metadata", http.StatusUnauthorized)
return
}
Expand All @@ -81,7 +80,7 @@ func (s Server) CreateSite(out http.ResponseWriter, req *http.Request) {
}

// needed by pretty much all the below functions, so let's gen it here
siteId := fmt.Sprintf("%v-%v-%v", util.RandomString(3), util.RandomString(3), util.RandomString(3))
siteId := util.RandomIamTriple()
log.Printf("[INFO] Creating a new site with generated ID %v...", siteId)

storage := s.BunnyAdmin.CreateStorageZone(req.Context(), siteId)
Expand All @@ -102,9 +101,9 @@ func (s Server) CreateSite(out http.ResponseWriter, req *http.Request) {
return
}

worked = s.SupaAdmin.AuthorizeHostname(req.Context(), userId, pull.Hostnames[0].Value, existingHostnames)
worked = s.SupaAdmin.AuthorizeZoneId(req.Context(), userId, int(pull.Id), existingZoneIds)
if !worked {
http.Error(out, "Unable to authorize user for new hostname", http.StatusInternalServerError)
http.Error(out, "Unable to authorize user for new zone ID", http.StatusInternalServerError)
return
}

Expand All @@ -130,36 +129,6 @@ func (s Server) AddHostname(out http.ResponseWriter, req *http.Request) {

var err error

_, claims, err := jwtauth.FromContext(req.Context())
if err != nil {
log.Printf("[ERROR] Unable to parse claims from JWT: %v", err)
http.Error(out, "Unable to parse claims from JWT", http.StatusUnauthorized)
return
}

// what is "sub"? If nothing else, seems to be the JWT's slang for user ID
userIdUntyped, found := claims["sub"]
if !found {
log.Printf("[ERROR] No 'user_id' field found in JWT claims: %v", claims)
http.Error(out, "Unable to parse claims from JWT", http.StatusUnauthorized)
return
}

userId, ok := userIdUntyped.(string)
if !ok {
log.Printf("[ERROR] Claims 'user_id' could not be parsed as string")
http.Error(out, "Unable to parse claims from JWT", http.StatusUnauthorized)
return
}

// get the currently-authorized hostnames from the claims for appending later
existingHostnames, err := util.GetHostnamesFromClaims(claims)
if err != nil {
log.Printf("[ERROR] Unable to get hostnames from JWT claims: %v", err)
http.Error(out, "Invalid JWT app metadata", http.StatusUnauthorized)
return
}

// no need to validate here, impossible to get this far if JWT is invalid
jwt := req.Header.Get("Authorization")

Expand Down Expand Up @@ -212,13 +181,7 @@ func (s Server) AddHostname(out http.ResponseWriter, req *http.Request) {

// todo -- rollback the new hostname if the below don't work, too? Agh

worked = s.SupaAdmin.AuthorizeHostname(req.Context(), userId, body.Hostname, existingHostnames)
if !worked {
http.Error(out, "Unable to authorize user for new hostname", http.StatusInternalServerError)
return
}

worked = s.SupaAdmin.AddHostnameToSiteRow(req.Context(), body.SiteId, body.Hostname)
worked = s.SupaNormie.AddHostnameToSiteRow(req.Context(), jwt, body.SiteId, body.Hostname)
if !worked {
http.Error(out, "Unable to add new hostname to Supabase row", http.StatusInternalServerError)
return
Expand All @@ -245,7 +208,7 @@ func (s Server) PostPush(out http.ResponseWriter, req *http.Request) {

// todo -- validate that sha is not empty and looks like a real SHA

worked := s.SupaNormie.UpdateFields(req.Context(), jwt, body.SiteId, body.Sha)
worked := s.SupaNormie.UpdateDeployedSha(req.Context(), jwt, body.SiteId, body.Sha)
if !worked {
http.Error(out, "Unable to update site row in Supabase", http.StatusInternalServerError)
return
Expand Down
32 changes: 16 additions & 16 deletions cmd/intake/enirched.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ import (
// EnrichedLog is responsible for turning a BunnyLog into a point for influx
// with all the necessary tags, timestamps, etc
type EnrichedLog struct {
StatusCode int
StatusCategory string
PullZoneId int
Timestamp int64
BytesSent int
RemoteIp string
StatusCode int
StatusCategory string
Host string
Path string
Referrer string
Expand All @@ -36,21 +36,21 @@ type EnrichedLog struct {
func Enrich(bunny BunnyLog) EnrichedLog {
ua := useragent.Parse(bunny.UserAgent)
return EnrichedLog{
PullZoneId: bunny.PullZoneId,
// bunny comes in epoch ms, CH wants epoch sec
Timestamp: bunny.Timestamp / 1000,
BytesSent: bunny.BytesSent,
StatusCode: bunny.Status,
StatusCategory: StatusCategory(bunny),
// bunny comes in epoch ms, CH wants epoch sec
Timestamp: bunny.Timestamp / 1000,
BytesSent: bunny.BytesSent,
RemoteIp: bunny.RemoteIp,
Host: bunny.Host,
Path: bunny.PathAndQuery,
Referrer: Referrer(bunny),
Device: Device(ua),
Browser: Browser(ua),
Os: Os(ua),
Country: bunny.Country,
FileType: FileType(bunny),
IsProbablyBot: IsProbablyBot(bunny),
Host: bunny.Host,
Path: bunny.PathAndQuery,
Referrer: Referrer(bunny),
Device: Device(ua),
Browser: Browser(ua),
Os: Os(ua),
Country: bunny.Country,
FileType: FileType(bunny),
IsProbablyBot: IsProbablyBot(bunny),
}
}

Expand Down
6 changes: 3 additions & 3 deletions cmd/intake/intaker.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,11 @@ func (i Intaker) Consume(ctx context.Context) {
func addToBatch(batch ch.Batch, enriched EnrichedLog) error {
// must match the order in the schema exactly
return batch.Append(
enriched.StatusCode,
enriched.StatusCategory,
enriched.PullZoneId,
enriched.Timestamp,
enriched.BytesSent,
enriched.RemoteIp,
enriched.StatusCode,
enriched.StatusCategory,
enriched.Host,
enriched.Path,
enriched.Referrer,
Expand Down
2 changes: 1 addition & 1 deletion cmd/query/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ var QueryCmd = &cobra.Command{
r.Use(cors.Handler(corsOptions))
r.Use(jwtauth.Verifier(jwtSecret))
r.Use(util.CheckJwtMiddleware((config["PERMISSIVE_MODE"] == "true"), false))
r.Use(util.CheckHostnameMiddleware(config["PERMISSIVE_MODE"] == "true"))
r.Use(util.CheckZoneIdMiddleware(config["PERMISSIVE_MODE"] == "true"))
r.Use(promHttpStd.HandlerProvider("", promMiddleware))

r.Get("/query", q.HandleQuery)
Expand Down
Loading

0 comments on commit 670d9dc

Please sign in to comment.