Last active
August 13, 2020 12:19
-
-
Save philippta/a9fac29dccecd88fbfca643b5e5b7b68 to your computer and use it in GitHub Desktop.
Clean Go API example with access policies
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Update:
Cleaner Go API example with access policies