diff --git a/.env b/.env new file mode 100644 index 0000000..1fdce6f --- /dev/null +++ b/.env @@ -0,0 +1,3 @@ +LISTEN=:80 +POSTGRES=postgresql://root@pg:5432/core?sslmode=disable +REDIS=redis:6379 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9efb890 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.12-rc-alpine as build + +RUN apk add --no-cache git=2.20.1-r0 + +WORKDIR /src +COPY go.mod go.sum .env *.go ./ +RUN go get -d -v ./... +RUN CGO_ENABLED=0 go build -ldflags "-s -w" + +FROM scratch + +COPY --from=build /src/permissions /permissions +COPY --from=build /src/.env /.env + +ENTRYPOINT ["/permissions"] diff --git a/README.md b/README.md index b40de1a..2e3cf21 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,39 @@ # beep-permissions Beep backend handling user permissions. Currently, permissions are defined as user-scope (i.e. userid in conversationid). If no such pairing exists, permission is denied. Might consider moving to searchms style user-scope-action system later. + +Relations are cached in redis to avoid excessive querying time. A listener updates the cache on database changes. + +## Environment variables + +Supply environment variables by either exporting them or editing `.env`. + +| ENV | Description | Default | +| --- | ----------- | ------- | +| LISTEN | Host and port for service to listen on | :80 | +| POSTGRES | URL of postgres | postgresql://root@pg:5432/core?sslmode=disable | +| REDIS | URL of redis | redis:6379 | + +## API + +| Contents | +| -------- | +| Get Permission | + +--- + +### Get Permission + +``` +GET /user/:userid/conversation/:conversationid +``` + +Query to see if userid-conversationid is permissable. + +#### Params + +#### Success (200 OK) + +Empty body. + +#### Errors diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4f86bf2 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module permissions + +go 1.12 + +require ( + github.com/go-redis/redis v6.15.2+incompatible + github.com/joho/godotenv v1.3.0 + github.com/julienschmidt/httprouter v1.2.0 + github.com/lib/pq v1.1.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..5de4842 --- /dev/null +++ b/go.sum @@ -0,0 +1,8 @@ +github.com/go-redis/redis v6.15.2+incompatible h1:9SpNVG76gr6InJGxoZ6IuuxaCOQwDAhzyXg+Bs+0Sb4= +github.com/go-redis/redis v6.15.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA= +github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc= +github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg= +github.com/julienschmidt/httprouter v1.2.0 h1:TDTW5Yz1mjftljbcKqRcrYhd4XeOoI98t+9HbQbYf7g= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/lib/pq v1.1.1 h1:sJZmqHoEaY7f+NPP8pgLB/WxulyR3fewgCM2qaSlBb4= +github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= diff --git a/main.go b/main.go index 1ea73aa..42e3f71 100644 --- a/main.go +++ b/main.go @@ -5,14 +5,19 @@ import ( "log" "net/http" "os" + "time" "github.com/joho/godotenv" "github.com/julienschmidt/httprouter" - _ "github.com/lib/pq" + "github.com/lib/pq" + "github.com/go-redis/redis" ) var listen string var postgres string +var redisHost string + +var redisClient *redis.Client func main() { // Load .env @@ -22,6 +27,14 @@ func main() { } listen = os.Getenv("LISTEN") postgres = os.Getenv("POSTGRES") + redisHost = os.Getenv("REDIS") + + // Redis + redisClient = redis.NewClient(&redis.Options{ + Addr: redisHost, + Password: "", + DB: 2, + }) // Postgres log.Printf("connecting to postgres %s", postgres) @@ -29,12 +42,91 @@ func main() { if err != nil { log.Fatal(err) } - defer db.Close() + + // Populate cache + rows, err := db.Query(` + SELECT user, conversation FROM "member" + `) + if err != nil { + log.Fatal("Error retrieving records from database") + } + for rows.Next() { + var userID, conversationID string + if err := rows.Scan(&userID, &conversationID); err != nil { + log.Fatal("Error retrieving records from database") + } + id := userID + "+" + conversationID + redisClient.Set(id, true, 0) + } + rows.Close() + db.Close() + + // Start cache update listener + minReconn := 10 * time.Second + maxReconn := 1 * time.Minute + listener := pq.NewListener(postgres, minReconn, maxReconn, func(ev pq.ListenerEventType, err error) { + if err != nil { + log.Fatal(err) + } else if ev == pq.ListenerEventConnected { + log.Println("listener connected") + } + }) + + // INSERT/UPDATE Listener + err = listener.Listen("member_new") + if err != nil { + log.Fatal(err) + } + + // DELETE Listener + err = listener.Listen("member_delete") + if err != nil { + log.Fatal(err) + } + + // Process events + go ListenForEvents(listener) // Routes router := httprouter.New() + router.GET("/user/:userid/conversation/:conversationid", GetPermission) // Serve log.Printf("starting server on %s", listen) log.Fatal(http.ListenAndServe(listen, router)) } + +func ListenForEvents(listener *pq.Listener) { + for { + select { + case n := <-listener.Notify: + if n.Channel == "member_new" { + redisClient.Set(n.Extra, true, 0) + } else if n.Channel == "member_delete" { + redisClient.Del(n.Extra) + } + case <- time.After(90 * time.Second): + go func() { + listener.Ping() + }() + } + } +} + +func GetPermission(w http.ResponseWriter, r *http.Request, p httprouter.Params) { + userID := p.ByName("userid") + conversationID := p.ByName("conversationid") + + id := userID + "+" + conversationID + + exists, err := redisClient.Exists(id).Result() + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } else if exists == 0 { + http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) + return + } + + w.WriteHeader(200) +}