Skip to content

Instantly share code, notes, and snippets.

@philippta
Last active August 13, 2020 12:19
Show Gist options
  • Save philippta/a9fac29dccecd88fbfca643b5e5b7b68 to your computer and use it in GitHub Desktop.
Save philippta/a9fac29dccecd88fbfca643b5e5b7b68 to your computer and use it in GitHub Desktop.
Clean Go API example with access policies
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"github.com/go-chi/chi"
)
type User struct {
ID string
}
type Todo struct {
UserID string
Task string
Done bool
}
var users = map[string]User{
"1": {
ID: "1",
},
"2": {
ID: "2",
},
}
var todos = map[string]Todo{
"1": {
UserID: "1",
Task: "hello",
Done: false,
},
"2": {
UserID: "2",
Task: "hello",
Done: false,
},
}
func withUser(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get user id from url query: /todos?user=1
id := r.URL.Query().Get("user")
// Find user in users
user, ok := users[id]
if !ok {
// If not found, don't add user to request context
h.ServeHTTP(w, r)
return
}
// If found, add user to request context
ctx := context.WithValue(r.Context(), "user", user)
h.ServeHTTP(w, r.WithContext(ctx))
})
}
func withTodo(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get todo id from url: /todos/{todoID}
id := chi.URLParam(r, "todoID")
// Find todo in todos
todo, ok := todos[id]
if !ok {
// If not found: 404
http.NotFound(w, r)
return
}
// If found, add todo to request context
ctx := context.WithValue(r.Context(), "todo", todo)
h.ServeHTTP(w, r.WithContext(ctx))
})
}
func withTodos(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Add all todos to request context
ctx := context.WithValue(r.Context(), "todos", todos)
h.ServeHTTP(w, r.WithContext(ctx))
})
}
func canViewTodos(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get user from request context
user, hasUser := r.Context().Value("user").(User)
fmt.Println(user, hasUser)
if !hasUser {
// If no user present, send 403
http.Error(w, "403 forbidden", http.StatusForbidden)
return
}
h.ServeHTTP(w, r)
})
}
func canUpdateTodo(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Get user from request context
user, hasUser := r.Context().Value("user").(User)
// Get todo from request context
todo, hasTodo := r.Context().Value("todo").(Todo)
fmt.Println(user, hasUser)
fmt.Println(todo, hasTodo)
if !hasUser || !hasTodo || todo.UserID != user.ID {
// If no user, no todo present or todo does not belong to user: 403
http.Error(w, "403 forbidden", http.StatusForbidden)
return
}
h.ServeHTTP(w, r)
})
}
func index(w http.ResponseWriter, r *http.Request) {
// Print all todos as json
todos, _ := r.Context().Value("todos").(map[string]Todo)
json.NewEncoder(w).Encode(todos)
}
func update(w http.ResponseWriter, r *http.Request) {
// Get todo from request context
todo, _ := r.Context().Value("todo").(Todo)
// Manipulate todo
todo.Done = true
// Save todo
// Send updated todo as json
json.NewEncoder(w).Encode(todo)
}
func main() {
r := chi.NewRouter()
r.Use(withUser)
// http://localhost:8081/todos -> Forbidden
// http://localhost:8081/todos?user=1 -> OK
r.With(withTodos, canViewTodos).Get("/todos", index)
// http://localhost:8081/todos/1?user=1 -> OK
// http://localhost:8081/todos/2?user=2 -> OK
// http://localhost:8081/todos/1?user=2 -> Forbidden
// http://localhost:8081/todos/2?user=1 -> Forbidden
// http://localhost:8081/todos/3?user=1 -> Not found
r.With(withTodo, canUpdateTodo).Get("/todos/{todoID}", update)
http.ListenAndServe(":8081", r)
}
@philippta
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment