Merge branch 'master' into refactor_proforma_import_export

This commit is contained in:
Karol
2022-09-13 22:47:50 +02:00
67 changed files with 1199 additions and 601 deletions

View File

@ -9,7 +9,7 @@ class ApplicationController < ActionController::Base
after_action :verify_authorized, except: %i[welcome]
around_action :mnemosyne_trace
around_action :switch_locale
before_action :set_sentry_context, :allow_iframe_requests, :load_embed_options
before_action :set_sentry_context, :load_embed_options
protect_from_forgery(with: :exception, prepend: true)
rescue_from Pundit::NotAuthorizedError, with: :render_not_authorized
rescue_from ActionController::InvalidAuthenticityToken, with: :render_csrf_error
@ -40,7 +40,10 @@ class ApplicationController < ActionController::Base
token = AuthenticationToken.find_by(shared_secret: params[:token])
return unless token
auto_login(token.user) if token.expire_at.future?
if token.expire_at.future?
token.update(expire_at: Time.zone.now)
auto_login(token.user)
end
end
def set_sentry_context
@ -93,10 +96,6 @@ class ApplicationController < ActionController::Base
# Show root page
end
def allow_iframe_requests
response.headers.delete('X-Frame-Options')
end
def load_embed_options
@embed_options = if session[:embed_options].present? && session[:embed_options].is_a?(Hash)
session[:embed_options].symbolize_keys

View File

@ -10,6 +10,15 @@ module CodeOcean
end
private :authorize!
def show_protected_upload
@file = CodeOcean::File.find(params[:id])
authorize!
raise Pundit::NotAuthorizedError if @embed_options[:disable_download] || @file.name_with_extension != params[:filename]
real_location = Pathname(@file.native_file.current_path).realpath
send_file(real_location, type: @file.native_file.content_type, filename: @file.name_with_extension, disposition: 'attachment')
end
def create
@file = CodeOcean::File.new(file_params)
if @file.file_template_id

View File

@ -5,8 +5,14 @@ module FileParameters
if exercise && params
params.reject do |_, file_attributes|
file = CodeOcean::File.find_by(id: file_attributes[:file_id])
next true if file.nil? || file.hidden || file.read_only
# avoid that public files from other contexts can be created
file.nil? || file.hidden || file.read_only || (file.context_type == 'Exercise' && file.context_id != exercise.id) || (file.context_type == 'CommunitySolution' && controller_name != 'community_solutions')
# `next` is similar to an early return and will proceed with the next iteration of the loop
next true if file.context_type == 'Exercise' && file.context_id != exercise.id
next true if file.context_type == 'Submission' && file.context.user != current_user
next true if file.context_type == 'CommunitySolution' && controller_name != 'community_solutions'
false
end
else
[]

View File

@ -22,6 +22,8 @@ class ExercisesController < ApplicationController
skip_after_action :verify_authorized, only: %i[import_exercise import_uuid_check]
skip_after_action :verify_policy_scoped, only: %i[import_exercise import_uuid_check], raise: false
rescue_from Pundit::NotAuthorizedError, with: :not_authorized_for_exercise
def authorize!
authorize(@exercise || @exercises)
end
@ -294,8 +296,6 @@ class ExercisesController < ApplicationController
private :update_exercise_tips
def implement
redirect_to(@exercise, alert: t('exercises.implement.unpublished')) if @exercise.unpublished? && current_user.role != 'admin' && current_user.role != 'teacher' # TODO: TESTESTEST
redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists?
user_solved_exercise = @exercise.solved_by?(current_user)
count_interventions_today = UserExerciseIntervention.where(user: current_user).where('created_at >= ?',
Time.zone.now.beginning_of_day).count
@ -324,6 +324,8 @@ class ExercisesController < ApplicationController
end
end
@embed_options[:disable_score] = true unless @exercise.teacher_defined_assessment?
@hide_rfc_button = @embed_options[:disable_rfc]
@search = Search.new
@ -432,6 +434,19 @@ class ExercisesController < ApplicationController
authorize!
end
def not_authorized_for_exercise(_exception)
return render_not_authorized unless current_user
return render_not_authorized unless %w[implement working_times intervention search reload].include?(action_name)
if current_user.admin? || current_user.teacher?
redirect_to(@exercise, alert: t('exercises.implement.unpublished')) if @exercise.unpublished?
redirect_to(@exercise, alert: t('exercises.implement.no_files')) unless @exercise.files.visible.exists?
else
render_not_authorized
end
end
private :not_authorized_for_exercise
def set_execution_environments
@execution_environments = ExecutionEnvironment.all.order(:name)
end

View File

@ -9,7 +9,7 @@ class ExternalUsersController < ApplicationController
private :authorize!
def index
@search = ExternalUser.ransack(params[:q])
@search = ExternalUser.ransack(params[:q], {auth_object: current_user})
@users = @search.result.in_study_group_of(current_user).includes(:consumer).paginate(page: params[:page], per_page: per_page_param)
authorize!
end
@ -43,15 +43,15 @@ class ExternalUsersController < ApplicationController
(created_at - lag(created_at) over (PARTITION BY user_id, exercise_id
ORDER BY created_at)) AS working_time
FROM submissions
WHERE user_id = #{@user.id}
WHERE #{ExternalUser.sanitize_sql(['user_id = ?', @user.id])}
AND user_type = 'ExternalUser'
#{current_user.admin? ? '' : "AND study_group_id IN (#{current_user.study_groups.pluck(:id).join(', ')}) AND cause = 'submit'"}
#{current_user.admin? ? '' : "AND #{ExternalUser.sanitize_sql(['study_group_id IN (?)', current_user.study_groups.pluck(:id)])} AND cause = 'submit'"}
GROUP BY exercise_id,
user_id,
id
) AS foo
) AS bar
#{tag.nil? ? '' : " JOIN exercise_tags et ON et.exercise_id = bar.exercise_id AND et.tag_id = #{tag} "}
#{tag.nil? ? '' : " JOIN exercise_tags et ON et.exercise_id = bar.exercise_id AND #{ExternalUser.sanitize_sql(['et.tag_id = ?', tag])}"}
GROUP BY user_id,
bar.exercise_id;
"
@ -60,10 +60,14 @@ class ExternalUsersController < ApplicationController
def statistics
@user = ExternalUser.find(params[:id])
authorize!
if params[:tag].present?
tag = Tag.find(params[:tag])
authorize(tag, :show?)
end
statistics = {}
ApplicationRecord.connection.execute(working_time_query(params[:tag])).each do |tuple|
ApplicationRecord.connection.execute(working_time_query(tag&.id)).each do |tuple|
statistics[tuple['exercise_id'].to_i] = tuple
end

View File

@ -67,7 +67,7 @@ class InternalUsersController < ApplicationController
end
def index
@search = InternalUser.ransack(params[:q])
@search = InternalUser.ransack(params[:q], {auth_object: current_user})
@users = @search.result.includes(:consumer).order(:name).paginate(page: params[:page], per_page: per_page_param)
authorize!
end

View File

@ -70,12 +70,9 @@ class ProxyExercisesController < ApplicationController
def show
@search = @proxy_exercise.exercises.ransack
@exercises = @proxy_exercise.exercises.ransack.result.order(:title) # @search.result.order(:title)
@exercises = @proxy_exercise.exercises.ransack.result.order(:title)
end
# we might want to think about auth here
def reload; end
def update
myparams = proxy_exercise_params
myparams[:exercises] = Exercise.find(myparams[:exercise_ids].compact_blank)

View File

@ -12,7 +12,17 @@ class SubmissionsController < ApplicationController
before_action :set_testrun, only: %i[run score test]
before_action :set_files, only: %i[download show]
before_action :set_files_and_specific_file, only: %i[download_file render_file run test]
before_action :set_mime_type, only: %i[download_file render_file]
before_action :set_content_type_nosniff, only: %i[download download_file render_file]
# Overwrite the CSP header for the :render_file action
content_security_policy only: :render_file do |policy|
policy.img_src :none
policy.script_src :none
policy.font_src :none
policy.style_src :none
policy.connect_src :none
policy.form_action :none
end
def create
@submission = Submission.new(submission_params)
@ -56,7 +66,11 @@ class SubmissionsController < ApplicationController
def download_file
raise Pundit::NotAuthorizedError if @embed_options[:disable_download]
send_data(@file.read, filename: @file.name_with_extension)
if @file.native_file?
redirect_to protected_upload_path(id: @file.id, filename: @file.name_with_extension)
else
send_data(@file.content, filename: @file.name_with_extension, disposition: 'attachment')
end
end
def index
@ -66,13 +80,13 @@ class SubmissionsController < ApplicationController
end
def render_file
if @file.native_file?
send_data(@file.read, filename: @file.name_with_extension, disposition: 'inline')
else
render(plain: @file.content)
end
# If a file should not be downloaded, it should not be rendered either
raise Pundit::NotAuthorizedError if @embed_options[:disable_download]
send_data(@file.read, filename: @file.name_with_extension, disposition: 'inline')
end
# rubocop:disable Metrics/CyclomaticComplexity
def run
# These method-local socket variables are required in order to use one socket
# in the callbacks of the other socket. As the callbacks for the client socket
@ -83,7 +97,7 @@ class SubmissionsController < ApplicationController
client_socket = tubesock
client_socket.onopen do |_event|
kill_client_socket(client_socket) if @embed_options[:disable_run]
return kill_client_socket(client_socket) if @embed_options[:disable_run]
end
client_socket.onclose do |_event|
@ -167,7 +181,8 @@ class SubmissionsController < ApplicationController
@testrun[:status] = :failed
"\n#{t('exercises.implement.exit_failure', timestamp: l(Time.zone.now, format: :short), exit_code: exit_code)}"
end
send_and_store client_socket, {cmd: :write, stream: :stdout, data: "#{exit_statement}\n"}
stream = @testrun[:status] == :ok ? :stdout : :stderr
send_and_store client_socket, {cmd: :write, stream: stream, data: "#{exit_statement}\n"}
if exit_code == 137
send_and_store client_socket, {cmd: :status, status: :out_of_memory}
@testrun[:status] = :out_of_memory
@ -194,12 +209,13 @@ class SubmissionsController < ApplicationController
ensure
save_testrun_output 'run'
end
# rubocop:enable Metrics/CyclomaticComplexity:
def score
hijack do |tubesock|
tubesock.onopen do |_event|
switch_locale do
kill_client_socket(tubesock) if @embed_options[:disable_score]
return kill_client_socket(tubesock) if @embed_options[:disable_score] || !@submission.exercise.teacher_defined_assessment?
# The score is stored separately, we can forward it to the client immediately
tubesock.send_data(JSON.dump(@submission.calculate_score))
@ -226,7 +242,7 @@ class SubmissionsController < ApplicationController
hijack do |tubesock|
tubesock.onopen do |_event|
switch_locale do
kill_client_socket(tubesock) if @embed_options[:disable_run]
return kill_client_socket(tubesock) if @embed_options[:disable_run]
# The score is stored separately, we can forward it to the client immediately
tubesock.send_data(JSON.dump(@submission.test(@file)))
@ -363,9 +379,9 @@ class SubmissionsController < ApplicationController
@files = @submission.collect_files.select(&:visible)
end
def set_mime_type
@mime_type = Mime::Type.lookup_by_extension(@file.file_type.file_extension.gsub(/^\./, ''))
response.headers['Content-Type'] = @mime_type.to_s
def set_content_type_nosniff
# When sending a file, we want to ensure that browsers follow our Content-Type header
response.headers['X-Content-Type-Options'] = 'nosniff'
end
def set_submission

View File

@ -74,7 +74,7 @@ class UserExerciseFeedbacksController < ApplicationController
def update
submission = begin
current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').first
current_user.submissions.where(exercise_id: @exercise.id).order('created_at DESC').final.first
rescue StandardError
nil
end
@ -127,14 +127,16 @@ class UserExerciseFeedbacksController < ApplicationController
user_type = current_user.class.name
latest_submission = Submission
.where(user_id: user_id, user_type: user_type, exercise_id: exercise_id)
.order(created_at: :desc).first
.order(created_at: :desc).final.first
authorize(latest_submission, :show?)
params[:user_exercise_feedback]
.permit(:feedback_text, :difficulty, :exercise_id, :user_estimated_worktime)
.merge(user_id: user_id,
user_type: user_type,
submission: latest_submission,
normalized_score: latest_submission.normalized_score)
normalized_score: latest_submission&.normalized_score)
end
def validate_inputs(uef_params)