server.go 4.36 KB
Newer Older
ale's avatar
ale committed
1 2 3
package server

import (
ale's avatar
ale committed
4
	"encoding/json"
5
	"fmt"
ale's avatar
ale committed
6 7
	"log"
	"net/http"
8
	"reflect"
ale's avatar
ale committed
9 10 11

	"git.autistici.org/ai3/go-common/serverutil"

ale's avatar
ale committed
12
	as "git.autistici.org/ai3/accountserver"
ale's avatar
ale committed
13 14
)

15 16
type actionRegistry struct {
	handlers map[string]reflect.Type
ale's avatar
ale committed
17 18
}

19 20 21
func newActionRegistry() *actionRegistry {
	return &actionRegistry{
		handlers: make(map[string]reflect.Type),
22
	}
ale's avatar
ale committed
23 24
}

25 26
func (r *actionRegistry) Register(path string, rtype as.Request) {
	r.handlers[path] = reflect.ValueOf(rtype).Elem().Type()
ale's avatar
ale committed
27 28
}

29 30 31 32 33 34
func (r *actionRegistry) newRequest(path string) (as.Request, bool) {
	h, ok := r.handlers[path]
	if !ok {
		return nil, false
	}
	return reflect.New(h).Interface().(as.Request), true
35 36
}

37 38
// APIServer is the HTTP API interface to AccountService.
type APIServer struct {
39 40
	*actionRegistry
	service *as.AccountService
ale's avatar
ale committed
41 42
}

43 44 45
// New creates a new APIServer.
func New(service *as.AccountService, backend as.Backend) *APIServer {
	s := &APIServer{
46 47 48
		actionRegistry: newActionRegistry(),
		service:        service,
	}
49

50
	s.Register("/api/user/get", &as.GetUserRequest{})
ale's avatar
ale committed
51
	s.Register("/api/user/search", &as.SearchUserRequest{})
52 53 54
	s.Register("/api/user/create", &as.CreateUserRequest{})
	s.Register("/api/user/update", &as.UpdateUserRequest{})
	s.Register("/api/user/change_password", &as.ChangeUserPasswordRequest{})
55
	s.Register("/api/user/set_account_recovery_hint", &as.SetAccountRecoveryHintRequest{})
56 57 58 59
	s.Register("/api/user/enable_otp", &as.EnableOTPRequest{})
	s.Register("/api/user/disable_otp", &as.DisableOTPRequest{})
	s.Register("/api/user/create_app_specific_password", &as.CreateApplicationSpecificPasswordRequest{})
	s.Register("/api/user/delete_app_specific_password", &as.DeleteApplicationSpecificPasswordRequest{})
ale's avatar
ale committed
60
	s.Register("/api/resource/get", &as.GetResourceRequest{})
ale's avatar
ale committed
61
	s.Register("/api/resource/search", &as.SearchResourceRequest{})
62 63 64 65 66 67 68
	s.Register("/api/resource/enable", &as.EnableResourceRequest{})
	s.Register("/api/resource/disable", &as.DisableResourceRequest{})
	s.Register("/api/resource/create", &as.CreateResourcesRequest{})
	s.Register("/api/resource/move", &as.MoveResourceRequest{})
	s.Register("/api/resource/reset_password", &as.ResetPasswordRequest{})
	s.Register("/api/resource/email/add_alias", &as.AddEmailAliasRequest{})
	s.Register("/api/resource/email/delete_alias", &as.DeleteEmailAliasRequest{})
69
	s.Register("/api/recover_account", &as.AccountRecoveryRequest{})
70 71

	return s
72 73
}

74
var emptyResponse struct{}
75

76 77 78 79
type jsonError interface {
	JSON() []byte
}

80
func (s *APIServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
81 82 83 84 85 86 87 88 89 90 91
	// Create a new empty request object based on the request
	// path, then decode the HTTP request JSON body onto it.
	r, ok := s.newRequest(req.URL.Path)
	if !ok {
		http.NotFound(w, req)
		return
	}
	if !serverutil.DecodeJSONRequest(w, req, r) {
		http.Error(w, "bad request", http.StatusBadRequest)
		return
	}
92

93 94
	resp, err := s.service.Handle(req.Context(), r)
	if err != nil {
95 96 97 98 99 100 101 102 103
		// Handle structured errors, serve a JSON response.
		status := errToStatus(err)
		if jerr, ok := err.(jsonError); ok {
			w.Header().Set("Content-Type", "application/json")
			w.WriteHeader(status)
			w.Write(jerr.JSON()) // nolint
		} else {
			http.Error(w, err.Error(), status)
		}
104 105 106 107
	} else {
		// Don't send nulls, send empty dicts instead.
		if resp == nil {
			resp = emptyResponse
108
		}
ale's avatar
ale committed
109
		serverutil.EncodeJSONResponse(w, resp)
110
	}
ale's avatar
ale committed
111

112 113 114 115 116 117 118 119 120
	// Now that all is done, we can log the request/response
	// (sanitization might modify the objects in place).
	reqData := marshalJSONSanitized(r)
	if err != nil {
		log.Printf("request: %s -> %v", reqData, err)
	} else {
		respData := marshalJSONSanitized(resp)
		log.Printf("request: %s -> %s", reqData, respData)
	}
ale's avatar
ale committed
121
}
ale's avatar
ale committed
122 123 124 125 126 127

func errToStatus(err error) int {
	switch {
	case err == as.ErrUserNotFound, err == as.ErrResourceNotFound:
		return http.StatusNotFound
	case as.IsAuthError(err):
ale's avatar
ale committed
128
		return http.StatusForbidden
ale's avatar
ale committed
129 130 131 132 133 134 135
	case as.IsRequestError(err):
		return http.StatusBadRequest
	default:
		return http.StatusInternalServerError
	}
}

136 137 138 139 140 141 142 143 144 145
// Some requests contain private information that should not be
// logged: these objects should implement a Sanitize() method that
// modifies the object in-place by editing out the private fields.
type hasSanitize interface {
	Sanitize()
}

func marshalJSONSanitized(obj interface{}) string {
	if s, ok := obj.(hasSanitize); ok {
		s.Sanitize()
ale's avatar
ale committed
146
	}
147
	data, err := json.Marshal(obj)
ale's avatar
ale committed
148
	if err != nil {
149
		return fmt.Sprintf("SERIALIZATION ERROR: %v", err)
ale's avatar
ale committed
150 151 152
	}
	return string(data)
}