Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
1c8b62a
feat: adding database tables + scaffold for groups
Jun 12, 2026
0f72a0b
feat: add policy to group controller
Jun 12, 2026
6d7071c
fix: fix incorrect names
Jun 12, 2026
11d3d95
fix: remove old policy file
Jun 12, 2026
d9f6734
feat: can add user to groups on group edit page
Jun 12, 2026
7b4899c
feat: can add groups to a space in space form
Jun 12, 2026
ea5e26d
feat: add checkig perms for private spaces (TO TEST)
Jun 15, 2026
0fb001d
feat: add groups checking before setting current_space
Jun 16, 2026
a66bcdc
feat: remove unrelevant condition + add explaination
Jun 16, 2026
1cb4f05
feat: GroupPolicy extends RessourcePolicy
Jun 17, 2026
d770ffb
feat: only members of a space can access to a material and only from …
Jun 17, 2026
1f33205
feat: same thing for events
Jun 17, 2026
710c8f7
Merge branch 'ElixirTeSS:master' into private-spaces
valentinRyckaert Jun 17, 2026
21385b8
fix: correct code to access material only in the space
Jun 17, 2026
d2b29dd
Merge branch 'private-spaces' of github.com:valentinRyckaert/TeSS-pri…
Jun 17, 2026
ebbac18
feat: dont show private material/events on main space even with the t…
Jun 18, 2026
cd77d5c
feat: add is_private column to space
Jun 18, 2026
7ed0371
feat: if the space is not private, show its ressources in default spa…
Jun 18, 2026
383a26d
feat: centralize shown? function and link space show policy to it
Jun 18, 2026
3ba703f
fix: add specific shown check for spaces list to see private spaces t…
Jun 18, 2026
6618e5c
feat: add is_private and group selection input to space form
Jun 18, 2026
28d29e9
fix: add check in shown? to make private spaces accessible to authori…
Jun 18, 2026
95f439b
feat: add multiple owners to the database for groups
Jun 19, 2026
c80f4ad
update: improve policy for group creation
Jun 19, 2026
30e1f3a
update: updqte policy for groups
Jun 19, 2026
e944ddf
fix: cleanly add js script for private groups in space form
Jun 19, 2026
8f501ec
fix: minor improvement for private groups in space form
Jun 19, 2026
a60c709
fix: refuse access to space list if user not logged
Jun 19, 2026
321b414
feat: user needs to be in at least one of the required groups to acce…
Jun 19, 2026
8003739
fix: update migration to avoid errors
Jun 19, 2026
0f9bd1e
feat: add facilities to access groups URL by admin/owners
Jun 19, 2026
b3fb3b5
feat: add style for group views and improve UX
Jun 22, 2026
aeccb1b
feat: add groups membership infos in user profile page
Jun 22, 2026
90e3870
Merge branch 'group-management' into private-spaces-with-groups
Jun 22, 2026
c4d1152
feat: add tests
Jun 22, 2026
27a6d95
update: user can access to public spaces without being logged
Jun 23, 2026
75c094a
fix: only admin can create and destroy space
Jun 23, 2026
7f79e56
feat: adding database tables + scaffold for groups
Jun 12, 2026
8b5485a
feat: add policy to group controller
Jun 12, 2026
4294eb2
fix: fix incorrect names
Jun 12, 2026
0da2ea1
fix: remove old policy file
Jun 12, 2026
d048664
feat: can add user to groups on group edit page
Jun 12, 2026
c501872
feat: can add groups to a space in space form
Jun 12, 2026
8ddf2ce
feat: add checkig perms for private spaces (TO TEST)
Jun 15, 2026
6a8a000
feat: add groups checking before setting current_space
Jun 16, 2026
3995611
feat: remove unrelevant condition + add explaination
Jun 16, 2026
1cb3b0c
feat: GroupPolicy extends RessourcePolicy
Jun 17, 2026
a042a4b
feat: only members of a space can access to a material and only from …
Jun 17, 2026
d9a576c
feat: same thing for events
Jun 17, 2026
648567e
fix: correct code to access material only in the space
Jun 17, 2026
1cca46a
feat: dont show private material/events on main space even with the t…
Jun 18, 2026
24eb6ff
feat: add is_private column to space
Jun 18, 2026
6a81498
feat: if the space is not private, show its ressources in default spa…
Jun 18, 2026
99857cd
feat: centralize shown? function and link space show policy to it
Jun 18, 2026
63e875a
fix: add specific shown check for spaces list to see private spaces t…
Jun 18, 2026
51ad377
feat: add is_private and group selection input to space form
Jun 18, 2026
cbdd483
fix: add check in shown? to make private spaces accessible to authori…
Jun 18, 2026
6f8490f
fix: refuse access to space list if user not logged
Jun 19, 2026
3ba424b
feat: user needs to be in at least one of the required groups to acce…
Jun 19, 2026
76407fa
feat: add multiple owners to the database for groups
Jun 19, 2026
001b758
update: improve policy for group creation
Jun 19, 2026
0ab797d
update: updqte policy for groups
Jun 19, 2026
e357a35
fix: cleanly add js script for private groups in space form
Jun 19, 2026
2899f66
fix: minor improvement for private groups in space form
Jun 19, 2026
b529a92
fix: update migration to avoid errors
Jun 19, 2026
58026f4
feat: add facilities to access groups URL by admin/owners
Jun 19, 2026
b7856fc
feat: add style for group views and improve UX
Jun 22, 2026
6b44785
feat: add groups membership infos in user profile page
Jun 22, 2026
2586fa0
feat: add tests
Jun 22, 2026
897c4ce
update: user can access to public spaces without being logged
Jun 23, 2026
35f6c84
fix: only admin can create and destroy space
Jun 23, 2026
17b96ee
update: update tests
Jun 23, 2026
744aa63
merge
Jun 23, 2026
4a7ed8b
merge forgotten changes
Jun 23, 2026
215e749
fix: correct test
Jun 24, 2026
3aa7196
fix: fix a remaining problems
Jun 24, 2026
588cfd1
update: update group policy
Jun 24, 2026
81e0afb
feat: add redirection to main space (to test)
Jun 24, 2026
69543a2
fix: make redirection working fine
Jun 25, 2026
d544078
feat: admins can have access to all private spaces and cannot be adde…
Jul 1, 2026
b99a571
feat: add explanation for private spaces
Jul 1, 2026
9479b78
feat: when removing a space, if the space is public, keep ressources
Jul 1, 2026
dd01e60
feat: add visual list of available spaces for a group
Jul 1, 2026
09f68f7
feat: add logo for the views groups button
Jul 1, 2026
f5ffa03
feat: check that groups ae added if the space is private
Jul 2, 2026
50b1598
docs: add dev docs for private spaces
Jul 2, 2026
11a14da
feat: add rdoc and documentation
Jul 2, 2026
85ecd0f
Merge branch 'master' into private-spaces-with-groups
valentinRyckaert Jul 22, 2026
f3d670a
remove duplicate filed in spaces table in schema
Jul 22, 2026
58d8ee6
fix: run migrations
Jul 22, 2026
b89f5b0
fix: fix tests
Jul 22, 2026
068a785
feat: add copilot suggestions
Jul 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ group :development do
gem 'listen'
gem 'puma'
gem 'web-console'
gem 'rdoc', '>= 8.0'
end

group :test do
Expand Down
10 changes: 8 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,10 @@ GEM
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
rbs (4.0.3)
logger
prism (>= 1.6.0)
tsort
rdf (3.3.4)
bcp47_spec (~> 0.2)
bigdecimal (~> 3.1, >= 3.1.5)
Expand Down Expand Up @@ -615,9 +619,10 @@ GEM
rdf-xsd (3.3.0)
rdf (~> 3.3)
rexml (~> 3.2)
rdoc (7.2.0)
rdoc (8.0.0)
erb
psych (>= 4.0.0)
prism (>= 1.6.0)
rbs (>= 4.0.0)
tsort
readline (0.0.4)
reline
Expand Down Expand Up @@ -906,6 +911,7 @@ DEPENDENCIES
rails-controller-testing
rails-i18n
rails_admin
rdoc (>= 8.0)
recaptcha
redcarpet
redis
Expand Down
3 changes: 2 additions & 1 deletion app/assets/config/manifest.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//= link_tree ../images
//= link_directory ../stylesheets/themes .css
//= link application.js
//= link application.js
//= link show_private_groups.js
9 changes: 8 additions & 1 deletion app/assets/javascripts/autocompleters.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,14 @@ var Autocompleters = {
return { value: name, data: { id: item[config.idField], item: item } };
})
};
}
},
groups: function (response, config) {
return {
suggestions: $.map(response, function (item) {
return { value: item.title, data: { id: item[config.idField], item: item } };
})
};
},
},

init: function () {
Expand Down
23 changes: 23 additions & 0 deletions app/assets/javascripts/show_private_groups.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// executed for space form
function show_private_groups() {
let checkbox = document.getElementById("space_is_private")
let container = document.getElementById("groups_container")

const toggleGroups = () => {
if (checkbox && checkbox.checked) {
container.style.display = "block"
} else {
container.style.display = "none"
}
}

if (checkbox) {
checkbox.addEventListener("change", toggleGroups)
}

toggleGroups()
}

window.addEventListener('turbolinks:load', function() {
show_private_groups();
});
7 changes: 7 additions & 0 deletions app/assets/javascripts/templates/autocompleter/group_id.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<li data-id="{{ item.id }}">
<input type="hidden" name="{{ prefix }}[]" value="{{ item.id }}" />
{{ item.title }}
<a href="#" class="delete-list-item btn btn-icon">
<i class="icon cross-icon icon-sm icon-greyscale"></i>
</a>
</li>
18 changes: 18 additions & 0 deletions app/assets/javascripts/templates/autocompleter/group_member.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<li data-id="{{ item.id }}">
<input type="hidden" name="{{ prefix }}[]" value="{{ item.id }}" />
<span class="group-member-name">{{ item.name }}</span>
{{#if item.email}}
<span class="group-member-email">{{ item.email }}</span>
{{/if}}
<label class="group-member-owner-label" title="Owner">
<input type="checkbox"
name="group[owner_ids][]"
value="{{ item.id }}"
class="group-owner-checkbox"
{{#if item.owner}}checked="checked"{{/if}} />
<i class="glyphicon glyphicon-star"></i> Owner
</label>
<a href="#" class="delete-list-item btn btn-icon" title="Remove member">
<i class="icon cross-icon icon-sm icon-greyscale"></i>
</a>
</li>
92 changes: 91 additions & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
require 'private_address_check'
require 'private_address_check/tcpsocket_ext'

# The controller for actions related to the core application
# Base controller for the whole application.
#
# ApplicationController centralizes cross-cutting concerns shared by every
# controller in TeSS: authentication (Devise + token auth), authorization
# (Pundit), multi-space resolution, error rendering, and a couple of small
# utility endpoints (+test_url+, +job_status+).
#
# All other controllers should inherit from this class rather than directly
# from <tt>ActionController::Base</tt>.
class ApplicationController < ActionController::Base
include BreadCrumbs
include PublicActivity::StoreController
Expand Down Expand Up @@ -30,10 +38,29 @@ class ApplicationController < ActionController::Base

rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized

# Builds the context object passed to every Pundit policy.
#
# Bundling +current_user+ together with the +request+ lets policies make
# decisions based on how the request was made (e.g. JSON API vs HTML),
# in addition to who is making it.
#
# Returns:: a Pundit::CurrentContext wrapping the current user and request.
def pundit_user
Pundit::CurrentContext.new(current_user, request)
end

# Renders a generic error page/response for a given HTTP status code.
#
# Accepts either a numeric or symbolic status code (e.g. :forbidden,
# :not_found) and renders the appropriate HTML error page, or a JSON/JSON:API
# error payload depending on the requested format. Falls back to a
# translated default message when none is supplied.
#
# status_code:: Integer or Symbol HTTP status code (default: 500). May be
# overridden by the <tt>params[:status_code]</tt> value set by
# the routes for 500, 503, 422 and 404 errors.
# message:: optional String error message to display; defaults to a
# localized message for the given status code.
def handle_error(status_code = 500, message = nil)
status_code = (params[:status_code] || status_code) # params[:status_code] comes from routes for 500, 503, 422 and 404 errors
if status_code.is_a?(Symbol) # Convert :forbidden, :not_found, etc. to 403, 404 etc.
Expand All @@ -56,6 +83,12 @@ def handle_error(status_code = 500, message = nil)
end
end

# Checks whether a given URL is reachable, guarding against SSRF by only
# allowing connections to public addresses (via PrivateAddressCheck).
#
# Expects <tt>params[:url]</tt> to contain the URL to test. Responds with a
# JSON body describing the outcome (HTTP code on success, or an explanatory
# message on failure/invalid URL).
def test_url
body = {}

Expand All @@ -79,6 +112,11 @@ def test_url
end
end

# Returns the status of a background job (Sidekiq) as JSON.
#
# Expects <tt>params[:id]</tt> to be the Sidekiq job id. Responds with
# <tt>404</tt> and <tt>{ status: 'not-found' }</tt> if no status is found
# for that id.
def job_status
begin
status = Sidekiq::Status::status(params[:id])
Expand All @@ -97,45 +135,93 @@ def job_status

private

# Checks whether the given feature is enabled for the current space.
#
# feature:: String or Symbol feature key (see Space::FEATURES).
#
# Returns:: +true+ or +false+.
def feature_enabled?(feature)
Space.current_space.feature_enabled?(feature)
end

helper_method :feature_enabled?

# before_action-style guard that raises a routing error (resulting in a
# 404) when the given feature is disabled globally via TeSS::Config.
#
# feature:: String or Symbol feature key; defaults to the current
# controller's name.
#
# Raises:: ActionController::RoutingError if the feature is explicitly
# disabled in the application configuration.
def ensure_feature_enabled(feature = controller_name)
if TeSS::Config.feature.key?(feature) && !TeSS::Config.feature[feature]
raise ActionController::RoutingError.new('Feature not enabled')
end
end

# Rescue handler for Pundit::NotAuthorizedError.
#
# Renders a localized "forbidden" error message based on the policy and
# query that denied access.
#
# exception:: the raised Pundit::NotAuthorizedError.
def user_not_authorized(exception)
policy_name = exception.policy.class.to_s.underscore
handle_error(:forbidden, t("#{policy_name}.#{exception.query}", scope: 'pundit', default: :default))
end

# before_action that resolves and stores the Space matching the current
# request host (or the default space if the +spaces+ feature is disabled),
# and redirects unauthorized users away from private spaces they cannot
# access.
def set_current_space
Space.current_space = TeSS::Config.feature['spaces'] ? Space.find_by_host(request.host) : Space.default
# if the current_space is a specific space (not the default one), we check if the user can access it
if TeSS::Config.feature['spaces'] && Space.current_space != Space.default
unless policy(Space.current_space).shown?
if current_user
flash[:alert] = "You are not authorized to access this page."
redirect_to TeSS::Config.base_url, allow_other_host: true
else
flash[:alert] = "Sign in to access this page."
redirect_to TeSS::Config.base_url + '/users/sign_in', allow_other_host: true
end
end
end
end

# Returns:: the Space resolved for the current request by #set_current_space.
def current_space
Space.current_space
end

helper_method :current_space

# before_action that stores the current user on the User class (for
# convenience access outside the request cycle) and reports the user to
# Sentry, when Sentry is enabled.
def set_current_user
User.current_user = current_user
if TeSS::Config.sentry_enabled?
Sentry.set_user(current_user ? { id: current_user.id, username: current_user.username } : {})
end
end

# Looks up the country of the current request's IP address.
#
# Uses the <tt>MOCK_IP</tt> environment variable outside of production so
# geolocation can be tested locally.
#
# Returns:: a country code/name Hash entry from the Locator lookup, or
# +nil+ if it could not be determined.
def current_user_country
remote_ip = ENV.fetch('MOCK_IP') { Rails.env.production? ? request.remote_ip : '130.88.0.0' }
Locator.instance.lookup(remote_ip)&.dig('country')
end

# Returns:: +true+ if the current request's country is in the configured
# list of blocked countries, +false+ otherwise.
def from_blocked_country?
return unless TeSS::Config.blocked_countries.present?
user_country = current_user_country
Expand All @@ -147,6 +233,8 @@ def from_blocked_country?

protected

# Configures the extra parameters Devise should permit for sign up,
# sign in and account update, beyond its defaults.
def configure_permitted_parameters
devise_parameter_sanitizer.permit(:sign_up) do |u| u.permit(:username, :email, :password, :password_confirmation,
:remember_me, :publicize_email, :processing_consent)
Expand All @@ -157,6 +245,8 @@ def configure_permitted_parameters
end
end

# Removes the <tt>X-Frame-Options</tt> header so the response can be
# embedded in an iframe on another site.
def allow_embedding
response.headers.delete 'X-Frame-Options'
end
Expand Down
Loading
Loading