package services import ( "context" "errors" "git.aiterp.net/stufflog/server/database/repositories" "git.aiterp.net/stufflog/server/graph/loaders" "git.aiterp.net/stufflog/server/internal/generate" "git.aiterp.net/stufflog/server/internal/slerrors" "git.aiterp.net/stufflog/server/models" "github.com/gin-gonic/gin" "math/rand" "time" ) var ErrLoginFailed = errors.New("login failed") var ErrInternalLoginFailure = errors.New("login failed due to internal error") var ErrInternalPermissionFailure = errors.New("permission check failed due to internal error") var ErrWrongCurrentPassword = errors.New("current password is missing") var ErrMissingCurrentPassword = errors.New("current password is missing") var authCookieName = "stufflog_cookie" var authCtxKey = "stufflog.auth" var ginCtxKey = "stufflog.gin" type Auth struct { users repositories.UserRepository session repositories.SessionRepository projects repositories.ProjectRepository issues repositories.IssueRepository } func (auth *Auth) Login(ctx context.Context, username, password string) (*models.User, error) { user, err := auth.users.Find(ctx, username) if err != nil { select { case <-time.After(time.Millisecond * time.Duration(rand.Int63n(100)+100)): case <-ctx.Done(): } return nil, ErrLoginFailed } if !user.CheckPassword(password) { select { case <-time.After(time.Millisecond * time.Duration(rand.Int63n(50))): case <-ctx.Done(): } return nil, ErrLoginFailed } session := models.Session{ ID: generate.SessionID(), UserID: user.ID, ExpiryTime: time.Now().Add(time.Hour * 168), } err = auth.session.Save(ctx, session) if err != nil { return nil, ErrInternalLoginFailure } if c := ctx.Value(ginCtxKey).(*gin.Context); c != nil { c.SetCookie(authCookieName, session.ID, 3600*168, "/", "", false, true) } else { return nil, ErrInternalLoginFailure } return user, nil } func (auth *Auth) Logout(ctx context.Context) (*models.User, error) { user := auth.UserFromContext(ctx) if user == nil { return nil, slerrors.PermissionDenied } c, ok := ctx.Value(ginCtxKey).(*gin.Context) if !ok { return nil, ErrInternalLoginFailure } c.SetCookie(authCookieName, "", 0, "/", "", false, true) return user, nil } func (auth *Auth) CreateUser(ctx context.Context, username, password, name string, active, admin bool) (*models.User, error) { loggedInUser := auth.UserFromContext(ctx) if loggedInUser == nil || !loggedInUser.Admin { return nil, slerrors.PermissionDenied } user := &models.User{ ID: username, Name: name, Active: active, Admin: admin, } err := user.SetPassword(password) if err != nil { return nil, err } user, err = auth.users.Insert(ctx, *user) if err != nil { return nil, err } return user, nil } func (auth *Auth) UserFromContext(ctx context.Context) *models.User { user, _ := ctx.Value(authCtxKey).(*models.User) return user } func (auth *Auth) ProjectPermission(ctx context.Context, projectID string) (*models.ProjectPermission, error) { user := auth.UserFromContext(ctx) if user == nil || !user.Active { return nil, slerrors.PermissionDenied } permission, err := loaders.ProjectPermissionLoaderFromContext(ctx).Load(projectID) if err != nil { return nil, ErrInternalPermissionFailure } if permission.Level == models.ProjectPermissionLevelNoAccess { return nil, slerrors.PermissionDenied } return permission, nil } func (auth *Auth) IssuePermission(ctx context.Context, issue models.Issue) (*models.ProjectPermission, error) { user := auth.UserFromContext(ctx) if user == nil || !user.Active { return nil, slerrors.PermissionDenied } isOwnedOrAssigned := issue.AssigneeID == user.ID || issue.OwnerID == user.ID permission, err := loaders.ProjectPermissionLoaderFromContext(ctx).Load(issue.ProjectID) if err != nil { return nil, ErrInternalPermissionFailure } if permission.Level == models.ProjectPermissionLevelNoAccess { return nil, slerrors.PermissionDenied } if !(permission.CanViewAnyIssue() || (permission.CanViewOwnIssue() && isOwnedOrAssigned)) { return nil, slerrors.PermissionDenied } return permission, nil } func (auth *Auth) CheckGinSession(c *gin.Context) { ctx := context.WithValue(c.Request.Context(), ginCtxKey, c) cookie, err := c.Cookie(authCookieName) if err != nil { c.Request = c.Request.WithContext(ctx) return } session, err := auth.session.Find(c.Request.Context(), cookie) if err != nil { c.Request = c.Request.WithContext(ctx) return } if time.Until(session.ExpiryTime) < time.Hour*167 { session.ExpiryTime = time.Now().Add(time.Hour * 168) _ = auth.session.Save(c.Request.Context(), *session) c.SetCookie(authCookieName, session.ID, 3600*168, "/", "", false, true) } user, err := auth.users.Find(c.Request.Context(), session.UserID) if err != nil { c.Request = c.Request.WithContext(ctx) return } ctx = context.WithValue(ctx, authCtxKey, user) c.Request = c.Request.WithContext(ctx) } func (auth *Auth) EditUser(ctx context.Context, username string, setName *string, currentPassword *string, newPassword *string) (*models.User, error) { loggedInUser := auth.UserFromContext(ctx) if loggedInUser == nil { return nil, slerrors.PermissionDenied } user, err := auth.users.Find(ctx, username) if err != nil { return nil, err } if user.ID != loggedInUser.ID && !loggedInUser.Admin { return nil, slerrors.PermissionDenied } if newPassword != nil { // Only require current password if it's given, or if the user is NOT an admin changing // another user's password if currentPassword != nil || !(loggedInUser.Admin && loggedInUser.ID != user.ID) { if currentPassword == nil { return nil, ErrMissingCurrentPassword } if !user.CheckPassword(*currentPassword) { return nil, ErrWrongCurrentPassword } } err = user.SetPassword(*newPassword) if err != nil { return nil, err } } if setName != nil && *setName != "" { user.Name = *setName } err = auth.users.Save(ctx, *user) if err != nil { return nil, err } return user, nil } func (auth *Auth) FilterLogListCopy(ctx context.Context, logs []*models.Log) []*models.Log { logs2 := make([]*models.Log, 0, len(logs)) for _, log := range logs { logs2 = append(logs2, log.Copy()) } auth.FilterLogList(ctx, &logs2) return logs2 } func (auth *Auth) FilterLogList(ctx context.Context, logs *[]*models.Log) { user := auth.UserFromContext(ctx) if user == nil { panic("Auth.FilterLogList called without user") } auth.FilterLog(ctx, *logs...) deleteList := make([]int, 0, len(*logs)/2) for i, log := range *logs { if log.Empty() && log.UserID != user.ID { deleteList = append(deleteList, i-len(deleteList)) } } list := *logs for _, index := range deleteList { list = append(list[:index], list[index+1:]...) } *logs = list } func (auth *Auth) FilterLog(ctx context.Context, logs ...*models.Log) { userID := "" if user := auth.UserFromContext(ctx); user != nil { userID = user.ID } accessMap := make(map[string]bool) deleteList := make([]int, 0, 16) for _, log := range logs { if userID == log.UserID { continue } deleteList = deleteList[:0] for i, item := range log.Items { if access, ok := accessMap[item.IssueID]; ok && access { continue } else if ok && !access { deleteList = append(deleteList, i-len(deleteList)) continue } issue, err := loaders.IssueLoaderFromContext(ctx).Load(item.IssueID) if err != nil { deleteList = append(deleteList, i-len(deleteList)) accessMap[item.IssueID] = true continue } _, err = auth.IssuePermission(ctx, *issue) if err != nil { deleteList = append(deleteList, i-len(deleteList)) } accessMap[issue.ID] = err != nil } for _, index := range deleteList { log.Items = append(log.Items[:index], log.Items[index+1:]...) } deleteList = deleteList[:0] for i, task := range log.Tasks { if access, ok := accessMap[task.IssueID]; ok && access { continue } else if ok && !access { deleteList = append(deleteList, i-len(deleteList)) continue } issue, err := loaders.IssueLoaderFromContext(ctx).Load(task.IssueID) if err != nil { deleteList = append(deleteList, i-len(deleteList)) accessMap[task.IssueID] = true continue } _, err = auth.IssuePermission(ctx, *issue) if err != nil { deleteList = append(deleteList, i-len(deleteList)) } accessMap[issue.ID] = err != nil } for _, index := range deleteList { log.Tasks = append(log.Tasks[:index], log.Tasks[index+1:]...) } } }