github.com/go-kivik/kivik/v4@v4.3.2/x/server/auth.go (about) 1 // Licensed under the Apache License, Version 2.0 (the "License"); you may not 2 // use this file except in compliance with the License. You may obtain a copy of 3 // the License at 4 // 5 // http://www.apache.org/licenses/LICENSE-2.0 6 // 7 // Unless required by applicable law or agreed to in writing, software 8 // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 9 // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 10 // License for the specific language governing permissions and limitations under 11 // the License. 12 13 //go:build !js 14 15 package server 16 17 import ( 18 "context" 19 "net/http" 20 21 "github.com/go-chi/chi/v5" 22 "gitlab.com/flimzy/httpe" 23 24 "github.com/go-kivik/kivik/v4" 25 internal "github.com/go-kivik/kivik/v4/int/errors" 26 "github.com/go-kivik/kivik/v4/x/server/auth" 27 ) 28 29 type contextKey struct{ name string } 30 31 var userContextKey = &contextKey{"userCtx"} 32 33 func userFromContext(ctx context.Context) *auth.UserContext { 34 user, _ := ctx.Value(userContextKey).(*auth.UserContext) 35 return user 36 } 37 38 type authService struct { 39 s *Server 40 } 41 42 var _ auth.Server = (*authService)(nil) 43 44 // UserStore returns the aggregate UserStore for the server. 45 func (s *authService) UserStore() auth.UserStore { 46 return s.s.userStores 47 } 48 49 func (s *authService) Bind(r *http.Request, v interface{}) error { 50 return s.s.bind(r, v) 51 } 52 53 type doneWriter struct { 54 http.ResponseWriter 55 done bool 56 } 57 58 func (w *doneWriter) WriteHeader(status int) { 59 w.done = true 60 w.ResponseWriter.WriteHeader(status) 61 } 62 63 func (w *doneWriter) Write(b []byte) (int, error) { 64 w.done = true 65 return w.ResponseWriter.Write(b) 66 } 67 68 // authMiddleware sets the user context based on the authenticated user, if any. 69 func (s *Server) authMiddleware(next httpe.HandlerWithError) httpe.HandlerWithError { 70 return httpe.HandlerWithErrorFunc(func(w http.ResponseWriter, r *http.Request) error { 71 ctx := r.Context() 72 if len(s.authFuncs) == 0 { 73 // Admin party! 74 r = r.WithContext(context.WithValue(ctx, userContextKey, &auth.UserContext{ 75 Name: "admin", 76 Roles: []string{auth.RoleAdmin}, 77 })) 78 return next.ServeHTTPWithError(w, r) 79 } 80 81 dw := &doneWriter{ResponseWriter: w} 82 83 var userCtx *auth.UserContext 84 var err error 85 for _, authFunc := range s.authFuncs { 86 userCtx, err = authFunc(dw, r) 87 if err != nil { 88 return err 89 } 90 if dw.done { 91 return nil 92 } 93 if userCtx != nil { 94 break 95 } 96 } 97 r = r.WithContext(context.WithValue(ctx, userContextKey, userCtx)) 98 return next.ServeHTTPWithError(w, r) 99 }) 100 } 101 102 // adminRequired returns Status Forbidden if the session is not authenticated as 103 // an admin. 104 func adminRequired(next httpe.HandlerWithError) httpe.HandlerWithError { 105 return httpe.HandlerWithErrorFunc(func(w http.ResponseWriter, r *http.Request) error { 106 userCtx, _ := r.Context().Value(userContextKey).(*auth.UserContext) 107 if userCtx == nil { 108 return &internal.Error{Status: http.StatusUnauthorized, Message: "User not authenticated"} 109 } 110 if !userCtx.HasRole(auth.RoleAdmin) { 111 return &internal.Error{Status: http.StatusForbidden, Message: "Admin privileges required"} 112 } 113 return next.ServeHTTPWithError(w, r) 114 }) 115 } 116 117 func (s *Server) dbMembershipRequired(next httpe.HandlerWithError) httpe.HandlerWithError { 118 return httpe.HandlerWithErrorFunc(func(w http.ResponseWriter, r *http.Request) error { 119 db := chi.URLParam(r, "db") 120 security, err := s.client.DB(db).Security(r.Context()) 121 if err != nil { 122 return &internal.Error{Status: http.StatusBadGateway, Err: err} 123 } 124 125 if err := validateDBMembership(userFromContext(r.Context()), security); err != nil { 126 return err 127 } 128 129 return next.ServeHTTPWithError(w, r) 130 }) 131 } 132 133 // validateDBMembership returns an error if the user lacks sufficient membership. 134 // 135 // See the [CouchDB documentation] for the rules for granting access. 136 // 137 // [CouchDB documentation]: https://docs.couchdb.org/en/stable/api/database/security.html#get--db-_security 138 func validateDBMembership(user *auth.UserContext, security *kivik.Security) error { 139 // No membership names/roles means open read access. 140 if len(security.Members.Names) == 0 && len(security.Members.Roles) == 0 { 141 return nil 142 } 143 144 if user == nil { 145 return &internal.Error{Status: http.StatusUnauthorized, Message: "User not authenticated"} 146 } 147 148 for _, name := range security.Members.Names { 149 if name == user.Name { 150 return nil 151 } 152 } 153 for _, role := range security.Members.Roles { 154 if user.HasRole(role) { 155 return nil 156 } 157 } 158 for _, name := range security.Admins.Names { 159 if name == user.Name { 160 return nil 161 } 162 } 163 for _, role := range security.Admins.Roles { 164 if user.HasRole(role) { 165 return nil 166 } 167 } 168 if user.HasRole(auth.RoleAdmin) { 169 return nil 170 } 171 return &internal.Error{Status: http.StatusForbidden, Message: "User lacks sufficient privileges"} 172 } 173 174 func (s *Server) dbAdminRequired(next httpe.HandlerWithError) httpe.HandlerWithError { 175 return httpe.HandlerWithErrorFunc(func(w http.ResponseWriter, r *http.Request) error { 176 db := chi.URLParam(r, "db") 177 security, err := s.client.DB(db).Security(r.Context()) 178 if err != nil { 179 return &internal.Error{Status: http.StatusBadGateway, Err: err} 180 } 181 182 if err := validateDBAdmin(userFromContext(r.Context()), security); err != nil { 183 return err 184 } 185 186 return next.ServeHTTPWithError(w, r) 187 }) 188 } 189 190 // validateDBAdmin returns an error if the user lacks sufficient membership. 191 // 192 // See the [CouchDB documentation] for the rules for granting access. 193 // 194 // [CouchDB documentation]: https://docs.couchdb.org/en/stable/api/database/security.html#get--db-_security 195 func validateDBAdmin(user *auth.UserContext, security *kivik.Security) error { 196 if user == nil { 197 return &internal.Error{Status: http.StatusUnauthorized, Message: "User not authenticated"} 198 } 199 for _, name := range security.Admins.Names { 200 if name == user.Name { 201 return nil 202 } 203 } 204 if user.HasRole(auth.RoleAdmin) { 205 return nil 206 } 207 for _, role := range security.Admins.Roles { 208 if user.HasRole(role) { 209 return nil 210 } 211 } 212 return &internal.Error{Status: http.StatusForbidden, Message: "User lacks sufficient privileges"} 213 }