diff --git a/Gemfile b/Gemfile
index 7342df7bc001dd45ae8116efe417f4f191626f6d..ff36280ae62dba7835de29234c1bf9e32ee9c02d 100644
--- a/Gemfile
+++ b/Gemfile
@@ -37,6 +37,7 @@ gem 'two_factor_authentication', git: 'https://github.com/noesya/two_factor_auth
 # gem 'two_factor_authentication', path: '../two_factor_authentication'
 gem 'curation'#, path: '../../arnaudlevy/curation'
 gem "cocoon", "~> 1.2"
+gem "has_scope", "~> 0.8.0"
 
 # Front
 gem 'jquery-rails'
diff --git a/Gemfile.lock b/Gemfile.lock
index 164634b6632742a8a7d721823e1824f707be1ab3..cb51e670c31951d6e0542d52b9680194633b2e4d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -204,6 +204,9 @@ GEM
       sassc-rails
     globalid (1.0.0)
       activesupport (>= 5.0)
+    has_scope (0.8.0)
+      actionpack (>= 5.2)
+      activesupport (>= 5.2)
     http-cookie (1.0.4)
       domain_name (~> 0.5)
     i18n (1.9.1)
@@ -437,6 +440,7 @@ DEPENDENCIES
   figaro
   front_matter_parser
   gdpr
+  has_scope (~> 0.8.0)
   image_processing
   jbuilder
   jquery-rails
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 1d00d143ee4a55d2065f34f21947215c3da09892..36cb5ed0b39a8b735c9071d43505f253d6b837b8 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -1,8 +1,12 @@
 class Admin::UsersController < Admin::ApplicationController
   load_and_authorize_resource through: :current_university
 
+  has_scope :for_role
+  has_scope :for_search_term
+
   def index
-    @users = @users.ordered.page(params[:page])
+    @filters = ::Filters::User.new(current_user).list
+    @users = apply_scopes(@users).ordered.page(params[:page])
     breadcrumb
   end
 
diff --git a/app/models/user.rb b/app/models/user.rb
index 602fa6c6fbeacd71974cace3381ced6872fd5fa9..e5d355ceaababe864aafae32d30ca31ec71c3a21 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -63,6 +63,16 @@ class User < ApplicationRecord
   belongs_to :language
 
   scope :ordered, -> { order(:last_name, :first_name) }
+  scope :for_search_term, -> (term) {
+    where("
+      unaccent(concat(users.first_name, ' ', users.last_name)) ILIKE unaccent(:term) OR
+      unaccent(concat(users.last_name, ' ', users.first_name)) ILIKE unaccent(:term) OR
+      unaccent(users.first_name) ILIKE unaccent(:term) OR
+      unaccent(users.last_name) ILIKE unaccent(:term) OR
+      unaccent(users.email) ILIKE unaccent(:term) OR
+      unaccent(users.mobile_phone) ILIKE unaccent(:term)
+    ", term: "%#{sanitize_sql_like(term)}%")
+  }
 
   def to_s
     "#{first_name} #{last_name}"
diff --git a/app/services/filters/base.rb b/app/services/filters/base.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b3a1a56740a09939a8ee0d7e4b53c4dcf6d2b52d
--- /dev/null
+++ b/app/services/filters/base.rb
@@ -0,0 +1,39 @@
+module Filters
+  class Base
+    attr_accessor :list
+
+    def initialize(user)
+      @user = user
+      @list = []
+    end
+
+    protected
+
+    def add(scope_name, choices, label, multiple = false)
+      @list << {
+        scope_name: scope_name,
+        choices: choices,
+        label: label,
+        multiple: multiple
+      }
+    end
+
+    def add_if(condition, args)
+      add *args if condition
+    end
+
+    def add_search
+      add :for_search_term, nil, I18n.t('search')
+    end
+
+    def add_date_filter(objects, attribute)
+      dates = objects.map { |obj|
+        {
+          to_s: I18n.l(obj[attribute], format: "%B %Y"),
+          id: I18n.l(obj[attribute], format: "%Y-%m")
+        }
+      }.uniq
+      add :for_date, dates, t('filters.attributes.date')
+    end
+  end
+end
diff --git a/app/services/filters/user.rb b/app/services/filters/user.rb
new file mode 100644
index 0000000000000000000000000000000000000000..a71e11effad3718dc0f3e890188e92e6ec83c9e1
--- /dev/null
+++ b/app/services/filters/user.rb
@@ -0,0 +1,9 @@
+module Filters
+  class User < Base
+    def initialize(user)
+      super
+      add_search
+      add :for_role, ::User.roles.keys.map { |r| { to_s: r.humanize, id: r } }, I18n.t('filters.attributes.role')
+    end
+  end
+end
diff --git a/app/views/admin/application/_filters.html.erb b/app/views/admin/application/_filters.html.erb
new file mode 100644
index 0000000000000000000000000000000000000000..4c2238c74afe79c882e0eee50241274c4f0e37a9
--- /dev/null
+++ b/app/views/admin/application/_filters.html.erb
@@ -0,0 +1,43 @@
+<%
+collapsable = true if collapsable.nil? # not ||= because collapsable can be "false"
+should_be_open = false
+filters.each { |filter| should_be_open = true if params.has_key?(filter[:scope_name]) }
+%>
+<div class="row">
+  <% if collapsable %>
+    <div class="col-md-2">
+      <a  class="btn btn-primary"
+          data-bs-toggle="collapse"
+          href="#collapseFilters"
+          role="button"
+          aria-expanded="false"
+          aria-controls="collapseFilters">
+        <%= t('filters.buttons.expand') %>
+      </a>
+    </div>
+  <% end %>
+  <div class="col-md-<%= collapsable ? '10' : '12' %>">
+    <div class="collapse <%= (!collapsable || should_be_open) ? 'show' : '' %>" id="collapseFilters">
+        <%= form_tag current_path, method: :get, class: 'do-not-unlock' do |f| %>
+        <div class="row">
+          <div class="col-md-4">
+            <% filters.each do |filter| %>
+              <% if filter[:scope_name] == :for_search_term %>
+                <%= text_field_tag filter[:scope_name], params[filter[:scope_name]], placeholder: filter[:label], class: 'form-control mb-1 filter' %>
+              <% else %>
+                <% choices = filter[:choices].map { |elmt| elmt.is_a?(String) ?  [elmt, elmt] : [elmt.is_a?(Hash) ? elmt[:to_s] : elmt.to_s, elmt[:id]] } %>
+                <% field_name = filter[:multiple] ? "#{filter[:scope_name]}[]" : filter[:scope_name] %>
+                <%= select_tag field_name, options_for_select(choices, params[filter[:scope_name]]), include_blank: filter[:label], class: 'form-select mb-1 filter' %>
+              <% end %>
+            <% end %>
+          </div>
+          <div class="col-md-3">
+            <%= submit_tag t('filters.buttons.submit'), class: 'btn btn-primary btn-submit' %>
+            <%= link_to t('reset'), current_path, class: 'btn btn-warning' %>
+          </div>
+        </div>
+        <% end %>
+    </div>
+  </div>
+</div>
+<br>
diff --git a/app/views/admin/users/index.html.erb b/app/views/admin/users/index.html.erb
index b161dcbcd90a13939ca78fddb993908723e7f261..af80ff5b3d6d1520db414931e45d74790689756d 100644
--- a/app/views/admin/users/index.html.erb
+++ b/app/views/admin/users/index.html.erb
@@ -1,5 +1,8 @@
 <% content_for :title, "#{User.model_name.human(count: 2)} (#{@users.total_count})" %>
 
+<%= render 'admin/application/filters',
+    current_path: admin_users_path,
+    filters: @filters if @filters.any? %>
 
 <table class="table">
   <thead>
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 9153a8cc3e5c2403e1253442f81e5268959a0216..e535a3d133575b6ec0d4b52886dd78a7689a5088 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -88,6 +88,13 @@ en:
   edit: Edit
   empty_folder: Empty folder
   false: No
+  filters:
+    attributes:
+      date: Filter by Date
+      role: Filter by Role
+    buttons:
+      expand: Filter table
+      submit: Filter
   gdpr:
     privacy_policy: https://osuny.org/politique-de-confidentialite
   hello: "Hello %{name}!"
@@ -113,7 +120,9 @@ en:
   privacy_policy_url: https://osuny.org/politique-de-confidentialite
   quit: Quit
   remove: Remove
+  reset: Reset
   save: Save
+  search: Search
   select_language: Select language
   simple_form:
     error_notification:
diff --git a/config/locales/fr.yml b/config/locales/fr.yml
index ca2c06384ec47c404be424affa5445723faffe80..f16346f8df5c30959b017b8be3cd7321791864fd 100644
--- a/config/locales/fr.yml
+++ b/config/locales/fr.yml
@@ -88,6 +88,13 @@ fr:
   edit: Modifier
   empty_folder: Dossier vide
   false: Non
+  filters:
+    attributes:
+      date: Filtrer par Date
+      role: Filtrer par Rôle
+    buttons:
+      expand: Filtrer le tableau
+      submit: Filtrer
   gdpr:
     privacy_policy: https://osuny.org/politique-de-confidentialite
   hello: "Bonjour %{name} !"
@@ -113,7 +120,9 @@ fr:
   privacy_policy_url: https://osuny.org/politique-de-confidentialite
   quit: Quitter
   remove: Retirer
+  reset: Réinitialiser
   save: Enregistrer
+  search: Rechercher
   select_language: Sélectionnez une langue
   simple_form:
     error_notification:
diff --git a/db/migrate/20220207093702_add_unaccent_extension.rb b/db/migrate/20220207093702_add_unaccent_extension.rb
new file mode 100644
index 0000000000000000000000000000000000000000..f1d34afada495e269aaab53e2547af11082a09d7
--- /dev/null
+++ b/db/migrate/20220207093702_add_unaccent_extension.rb
@@ -0,0 +1,5 @@
+class AddUnaccentExtension < ActiveRecord::Migration[6.1]
+  def change
+    enable_extension "unaccent"
+  end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 69faf1dd08219eaefd1d8aa3edc4015d2403dba9..6fa4a6e619daf0b0e010e9a61b6ffdab27f367f8 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,11 +10,12 @@
 #
 # It's strongly recommended that you check this file into your version control system.
 
-ActiveRecord::Schema.define(version: 2022_02_03_160802) do
+ActiveRecord::Schema.define(version: 2022_02_07_093702) do
 
   # These are extensions that must be enabled in order to support this database
   enable_extension "pgcrypto"
   enable_extension "plpgsql"
+  enable_extension "unaccent"
 
   create_table "action_text_rich_texts", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
     t.string "name", null: false