package classes import ( "context" "fmt" "strings" "git.golem15.com/golem15/sm-user-plugin/models" "gorm.io/gorm" ) // MergedPermissions returns the frontend permissions of a user as PHP // User::getMergedPermissions builds them: // // 1. the permissions of every group the user belongs to are merged, and // where two groups name the same code the higher value wins ("most // positive wins": 1 over 0 over -1); // 2. the user's own permissions are laid over the result, and a user value // replaces the groups' value whatever it is (a user-level -1 overrides a // group's 1, and a user-level 1 overrides a group's -1). // // The result maps a code to 1 (allowed), -1 (denied) or 0. Use HasPermission // to ask it a question. A deactivated user's permissions are read as well. func MergedPermissions(ctx context.Context, db *gorm.DB, userID uint) (map[string]int, error) { if db == nil { return nil, fmt.Errorf("golem15.user: gorm handle is missing") } var groups []struct{ Permissions models.PermissionSet } err := fresh(ctx, db). Table("user_groups"). Select("user_groups.permissions"). Joins("JOIN users_groups ON users_groups.user_group_id = user_groups.id"). Where("users_groups.user_id = ?", userID). Order("user_groups.id"). Scan(&groups).Error if err != nil { return nil, err } merged := map[string]int{} for _, group := range groups { for code, value := range group.Permissions { if held, ok := merged[code]; !ok || value > held { merged[code] = value } } } var own []struct{ Permissions models.PermissionSet } err = fresh(ctx, db). Table("users"). Select("users.permissions"). Where("users.id = ?", userID). Scan(&own).Error if err != nil { return nil, err } for _, row := range own { for code, value := range row.Permissions { merged[code] = value } } return merged, nil } // HasPermission reports whether merged holds code, the single-permission // case of Winter's User::hasPermission. A permission is held only when the // matching value is exactly 1. Wildcards work as in Winter: // // - code ending in "*" is held when a different stored code starts with the // part before the asterisk and has the value 1; // - code starting with "*" is held when a different stored code ends with // the part after the asterisk and has the value 1; // - otherwise code is held when it is stored with the value 1, or when a // stored code ending in "*" with the value 1 is a proper prefix of it. // // A stored code that starts with "*" is not treated as a wildcard; Winter // does not treat it as one either. func HasPermission(merged map[string]int, code string) bool { switch { case len(code) > 1 && strings.HasSuffix(code, "*"): prefix := strings.TrimSuffix(code, "*") for stored, value := range merged { if prefix != stored && strings.HasPrefix(stored, prefix) && value == 1 { return true } } return false case len(code) > 1 && strings.HasPrefix(code, "*"): suffix := strings.TrimPrefix(code, "*") for stored, value := range merged { if suffix != stored && strings.HasSuffix(stored, suffix) && value == 1 { return true } } return false } for stored, value := range merged { if len(stored) > 1 && strings.HasSuffix(stored, "*") { prefix := strings.TrimSuffix(stored, "*") if prefix != code && strings.HasPrefix(code, prefix) && value == 1 { return true } continue } if stored == code && value == 1 { return true } } return false } // UserHasPermission reports whether the user holds the frontend permission // code: MergedPermissions followed by HasPermission. func UserHasPermission(ctx context.Context, db *gorm.DB, userID uint, code string) (bool, error) { merged, err := MergedPermissions(ctx, db, userID) if err != nil { return false, err } return HasPermission(merged, code), nil } // FrontendPermissionOptions returns the rows of // golem15_user_frontend_permissions ordered by tab, then code, as PHP // FrontendPermission::listTabbedPermissions reads them. func FrontendPermissionOptions(ctx context.Context, db *gorm.DB) ([]models.FrontendPermission, error) { if db == nil { return nil, fmt.Errorf("golem15.user: gorm handle is missing") } rows := []models.FrontendPermission{} if err := fresh(ctx, db).Order("tab ASC, code ASC").Find(&rows).Error; err != nil { return nil, err } return rows, nil }