package server import ( "errors" "net/http" "strings" "time" "steward/internal/api" "steward/internal/auth" "steward/internal/config" "steward/internal/model" "steward/internal/store" ) func (s *Server) changeOwnPassword(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { if p.Token != nil { return forbidden("passwords can't be changed with an API token; log in to the web UI") } if p.User.ID == 0 { return badRequest("%s on the local socket isn't a Steward user; use 'steward user update USER --password'", p.Name()) } var in api.PasswordChange if err := decodeJSON(r, &in); err != nil { return err } u, err := s.store.GetUser(r.Context(), p.User.ID) if err != nil { return err } if u.PasswordHash != "" && !auth.CheckPassword(u.PasswordHash, in.Current) { s.eng.Audit(r.Context(), p, "user.password", "user "+u.Username, "failure", "wrong current password") return &httpError{http.StatusForbidden, "wrong_password", "the current password is wrong"} } hash, err := auth.HashPassword(in.Password) if err != nil { return badRequest("%v", err) } u.PasswordHash = hash if err := s.store.UpdateUser(r.Context(), u); err != nil { return err } keep := "" if se, ok := r.Context().Value(keySession).(*model.Session); ok { keep = se.IDHash } s.store.DeleteUserSessions(r.Context(), u.ID, keep) s.eng.Audit(r.Context(), p, "user.password", "user "+u.Username, "success", "") w.WriteHeader(http.StatusNoContent) return nil } func grantsFrom(strs []string, role string) ([]model.Grant, bool, error) { var out []model.Grant set := false if role != "" { gs, ok := auth.Roles[role] if !ok { return nil, false, badRequest("unknown role %q (use %s)", role, strings.Join(auth.RoleNames(), ", ")) } out = append(out, gs...) set = true } for _, g := range strs { gr, err := auth.ParseGrant(g) if err != nil { return nil, false, badRequest("%v", err) } out = append(out, gr) set = true } if set && out == nil { out = []model.Grant{} } return out, set, nil } func grantStrings(gs []model.Grant) []string { out := []string{} for _, g := range gs { out = append(out, auth.FormatGrant(g)) } return out } func presentToken(t *model.Token) api.Token { out := api.Token{Token: t, GrantStrings: grantStrings(t.Grants)} if t.Grants == nil { out.GrantStrings = []string{"(all of the user's)"} } return out } func (s *Server) createTokenFor(w http.ResponseWriter, r *http.Request, p *auth.Principal, u *model.User) error { var in api.TokenInput if err := decodeJSON(r, &in); err != nil { return err } if strings.TrimSpace(in.Name) == "" { return badRequest("a token needs a name") } grants, set, err := grantsFrom(in.Grants, in.Role) if err != nil { return err } var exp *time.Time switch { case in.ExpiresAt != nil: if in.ExpiresAt.Before(time.Now()) { return badRequest("expires_at is in the past") } exp = in.ExpiresAt case in.ExpiresIn != "": d, err := config.ParseDuration(in.ExpiresIn) if err != nil || d <= 0 { return badRequest("invalid expires_in %q (use e.g. 90d or 720h)", in.ExpiresIn) } t := time.Now().Add(d) exp = &t } // A restricted token can only make tokens no wider than itself. if p.Token != nil && p.Token.Grants != nil { if !set { grants, set = p.Token.Grants, true } else if !auth.Narrower(grants, p.Token.Grants) { return forbidden("a restricted token can only create tokens with a subset of its own grants") } } secret, pid, hash := auth.NewToken() t := &model.Token{UserID: u.ID, Name: strings.TrimSpace(in.Name), PublicID: pid, SecretHash: hash, ExpiresAt: exp} if set { t.Grants = grants } if err := s.store.CreateToken(r.Context(), t); err != nil { return err } t.Username = u.Username s.eng.Audit(r.Context(), p, "token.create", "token "+u.Username+"/"+t.Name, "success", strings.Join(grantStrings(t.Grants), " ")) out := presentToken(t) out.Secret = secret writeJSON(w, http.StatusCreated, out) return nil } func (s *Server) createOwnToken(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { if p.User.ID == 0 { return badRequest("%s on the local socket isn't a Steward user and has no tokens; make the token for a user (steward token create NAME --user admin)", p.Name()) } return s.createTokenFor(w, r, p, p.User) } func (s *Server) listTokens(w http.ResponseWriter, r *http.Request, userID int64) error { ts, err := s.store.ListTokens(r.Context(), userID) if err != nil { return err } out := []api.Token{} for _, t := range ts { if t.RevokedAt != nil && !queryBool(r, "all") { continue } out = append(out, presentToken(t)) } writeJSON(w, http.StatusOK, out) return nil } func (s *Server) listOwnTokens(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { if p.User.ID == 0 { writeJSON(w, http.StatusOK, []api.Token{}) return nil } return s.listTokens(w, r, p.User.ID) } func (s *Server) listAllTokens(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { if err := requireAdmin(p); err != nil { return err } return s.listTokens(w, r, 0) } func (s *Server) revokeToken(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { id, err := pathID(r, "id") if err != nil { return err } t, err := s.store.GetToken(r.Context(), id) if err != nil || t.UserID != p.User.ID && !p.IsAdmin() { return notFound("token %d not found", id) } if err := s.store.RevokeToken(r.Context(), id); err != nil { if errors.Is(err, store.ErrNotFound) { return conflict("token %d is already revoked", id) } return err } s.eng.Audit(r.Context(), p, "token.revoke", "token "+t.Username+"/"+t.Name, "success", "") w.WriteHeader(http.StatusNoContent) return nil } func presentUser(u *model.User) api.User { return api.User{User: u, GrantStrings: grantStrings(u.Grants)} } func (s *Server) listUsers(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { if err := requireAdmin(p); err != nil { return err } us, err := s.store.ListUsers(r.Context()) if err != nil { return err } out := []api.User{} for _, u := range us { out = append(out, presentUser(u)) } writeJSON(w, http.StatusOK, out) return nil } // ValidUsername checks a user name. func ValidUsername(n string) error { if len(n) < 1 || len(n) > 64 { return badRequest("user names have 1 to 64 characters") } for _, c := range n { if !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || strings.ContainsRune("._-@", c)) { return badRequest("user names may have letters, digits, '.', '_', '-' and '@'") } } return nil } func applyUserInput(u *model.User, in *api.UserInput) error { role := "" if in.Role != nil { role = *in.Role } var strs []string if in.Grants != nil { strs = *in.Grants } grants, set, err := grantsFrom(strs, role) if err != nil { return err } if set || in.Grants != nil { if grants == nil { grants = []model.Grant{} } u.Grants = grants } if in.Admin != nil { u.Admin = *in.Admin } if in.Disabled != nil { u.Disabled = *in.Disabled } if in.Comment != nil { u.Comment = *in.Comment } if in.Password != nil { if *in.Password == "" { u.PasswordHash = "" // tokens only } else { h, err := auth.HashPassword(*in.Password) if err != nil { return badRequest("%v", err) } u.PasswordHash = h } } return nil } func (s *Server) createUser(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { if err := requireAdmin(p); err != nil { return err } var in api.UserInput if err := decodeJSON(r, &in); err != nil { return err } if in.Username == nil { return badRequest("a user needs a username") } u := &model.User{Username: strings.TrimSpace(*in.Username)} if err := ValidUsername(u.Username); err != nil { return err } if err := applyUserInput(u, &in); err != nil { return err } if err := s.store.CreateUser(r.Context(), u); err != nil { if errors.Is(err, store.ErrConflict) { return conflict("user %s already exists", u.Username) } return err } s.eng.Audit(r.Context(), p, "user.create", "user "+u.Username, "success", strings.Join(grantStrings(u.Grants), " ")) writeJSON(w, http.StatusCreated, presentUser(u)) return nil } func (s *Server) userByPath(r *http.Request) (*model.User, error) { id, err := pathID(r, "id") if err != nil { return nil, err } u, err := s.store.GetUser(r.Context(), id) if errors.Is(err, store.ErrNotFound) { return nil, notFound("user %d not found", id) } return u, err } func (s *Server) updateUser(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { if err := requireAdmin(p); err != nil { return err } u, err := s.userByPath(r) if err != nil { return err } var in api.UserInput if err := decodeJSON(r, &in); err != nil { return err } wasAdmin := u.Admin && !u.Disabled if in.Username != nil { u.Username = strings.TrimSpace(*in.Username) if err := ValidUsername(u.Username); err != nil { return err } } if err := applyUserInput(u, &in); err != nil { return err } if wasAdmin && (!u.Admin || u.Disabled) { if n, err := s.store.CountAdmins(r.Context()); err == nil && n <= 1 { return conflict("%s is the last administrator; make someone else an administrator first", u.Username) } } if err := s.store.UpdateUser(r.Context(), u); err != nil { if errors.Is(err, store.ErrConflict) { return conflict("user %s already exists", u.Username) } return err } if u.Disabled || in.Password != nil { s.store.DeleteUserSessions(r.Context(), u.ID, "") } s.eng.Audit(r.Context(), p, "user.update", "user "+u.Username, "success", strings.Join(grantStrings(u.Grants), " ")) writeJSON(w, http.StatusOK, presentUser(u)) return nil } func (s *Server) deleteUser(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { if err := requireAdmin(p); err != nil { return err } u, err := s.userByPath(r) if err != nil { return err } if u.ID == p.User.ID { return conflict("you can't delete yourself") } if u.Admin && !u.Disabled { if n, err := s.store.CountAdmins(r.Context()); err == nil && n <= 1 { return conflict("%s is the last administrator", u.Username) } } if err := s.store.DeleteUser(r.Context(), u.ID); err != nil { return err } s.eng.Audit(r.Context(), p, "user.delete", "user "+u.Username, "success", "") w.WriteHeader(http.StatusNoContent) return nil } func (s *Server) listUserTokens(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { u, err := s.userByPath(r) if err != nil { return err } if u.ID != p.User.ID && !p.IsAdmin() { return forbidden("only administrators can see other users' tokens") } return s.listTokens(w, r, u.ID) } func (s *Server) createUserToken(w http.ResponseWriter, r *http.Request, p *auth.Principal) error { u, err := s.userByPath(r) if err != nil { return err } if u.ID != p.User.ID && !p.IsAdmin() { return forbidden("only administrators can make tokens for other users") } return s.createTokenFor(w, r, p, u) }