...

Source file src/github.com/go-kivik/kivik/v4/x/server/auth.go

Documentation: github.com/go-kivik/kivik/v4/x/server

     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  }
   214  

View as plain text