canvas-lms/app/controllers/pseudonyms_controller.rb

420 lines
16 KiB
Ruby

#
# Copyright (C) 2011 - present Instructure, Inc.
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
# @API Logins
# API for creating and viewing user logins under an account
class PseudonymsController < ApplicationController
before_action :get_context, :only => [:index, :create]
before_action :require_user, :only => [:create, :show, :edit, :update]
before_action :reject_student_view_student, :only => [:create, :show, :edit, :update]
protect_from_forgery :except => [:registration_confirmation, :change_password, :forgot_password], with: :exception
include Api::V1::Pseudonym
# @API List user logins
# Given a user ID, return a paginated list of that user's logins for the given account.
#
# @response_field account_id The ID of the login's account.
# @response_field id The unique, numeric ID for the login.
# @response_field sis_user_id The login's unique SIS ID.
# @response_field integration_id The login's unique integration ID.
# @response_field unique_id The unique ID for the login.
# @response_field user_id The unique ID of the login's user.
# @response_field authentication_provider_id The ID of the authentication
# provider that this login is associated with
# @response_field authentication_provider_type The type of the authentication
# provider that this login is associated with
#
# @example_response
# [
# {
# "account_id": 1,
# "id" 2,
# "sis_user_id": null,
# "unique_id": "belieber@example.com",
# "user_id": 2,
# "authentication_provider_id": 1,
# "authentication_provider_type": "facebook"
# }
# ]
def index
return unless get_user && authorized_action(@user, @current_user, :read)
if @context.is_a?(Account)
return unless context_is_root_account?
scope = @context.pseudonyms.active.where(:user_id => @user)
@pseudonyms = Api.paginate(
scope,
self, api_v1_account_pseudonyms_url)
else
bookmark = BookmarkedCollection::SimpleBookmarker.new(Pseudonym, :id)
@pseudonyms = ShardedBookmarkedCollection.build(bookmark, @user.pseudonyms.shard(@user).active.order(:id))
@pseudonyms = Api.paginate(@pseudonyms, self, api_v1_user_pseudonyms_url)
end
render :json => @pseudonyms.map { |p| pseudonym_json(p, @current_user, session) }
end
def forgot_password
email = params[:pseudonym_session][:unique_id_forgot] if params[:pseudonym_session]
@ccs = []
if email.present?
@ccs = CommunicationChannel.email.by_path(email).active.to_a
if @ccs.empty?
@ccs += CommunicationChannel.email.by_path(email).to_a
end
if @domain_root_account
@domain_root_account.pseudonyms.active.by_unique_id(email).each do |p|
cc = p.communication_channel if p.communication_channel && p.user
cc ||= p.user.communication_channel rescue nil
@ccs << cc
end
end
end
@ccs = @ccs.flatten.compact.uniq.select do |cc|
if !cc.user
false
else
cc.pseudonym ||= cc.user.pseudonym rescue nil
cc.save if cc.changed?
@domain_root_account.pseudonyms.active.where(user_id: cc.user_id).exists?
end
end
respond_to do |format|
# Whether the email was actually found or not, we display the same
# message. Otherwise this form could be used to fish for valid
# email addresses.
flash[:notice] = t 'notices.email_sent', "Confirmation email sent to %{email}, make sure to check your spam box", :email => email
@ccs.each do |cc|
cc.forgot_password!
end
format.html { redirect_to(login_url) }
format.json { render :json => {:requested => true} }
format.js { render :json => {:requested => true} }
end
end
def confirm_change_password
@pseudonym = Pseudonym.find(params[:pseudonym_id])
@cc = @pseudonym.user.communication_channels.where(confirmation_code: params[:nonce]).first
@cc = nil if @pseudonym.managed_password?
@headers = false
# Allow unregistered users to change password. How else can they come back later
# and finish the registration process?
if !@cc || @cc.path_type != 'email'
flash[:error] = t 'errors.cant_change_password', "Cannot change the password for that login, or login does not exist"
redirect_to canvas_login_url
else
if @cc.confirmation_code_expires_at.present? && @cc.confirmation_code_expires_at <= Time.now.utc
flash[:error] = t 'The link you used has expired. Click "Forgot Password?" to get a new reset-password link.'
redirect_to canvas_login_url
end
@password_pseudonyms = @cc.user.pseudonyms.active.select{|p| p.account.canvas_authentication? }
js_env :PASSWORD_POLICY => @domain_root_account.password_policy,
:PASSWORD_POLICIES => Hash[@password_pseudonyms.map{ |p| [p.id, p.account.password_policy]}]
end
end
def change_password
@pseudonym = Pseudonym.find(params[:pseudonym][:id] || params[:pseudonym_id])
if @cc = @pseudonym.user.communication_channels.where(confirmation_code: params[:nonce]).
where('confirmation_code_expires_at IS NULL OR confirmation_code_expires_at > ?', Time.now.utc).first
@pseudonym.require_password = true
@pseudonym.password = params[:pseudonym][:password]
@pseudonym.password_confirmation = params[:pseudonym][:password_confirmation]
if @pseudonym.save_without_session_maintenance
# If they changed the password (and we subsequently log them in) then
# we're pretty confident this is the right user, and the communication
# channel is valid, so register the user and approve the channel.
@cc.set_confirmation_code(true)
@cc.confirm
@cc.save
@pseudonym.user.register
# reset the session id cookie to prevent session fixation.
reset_session
@pseudonym_session = PseudonymSession.new(@pseudonym, true)
render :json => @pseudonym, :status => :ok # -> dashboard
else
render :json => {:pseudonym => @pseudonym.errors.as_json[:errors]}, :status => :bad_request
end
else
render :json => {:errors => {:nonce => 'expired'}}, :status => :bad_request # -> login url
end
end
def show
@user = @current_user
@pseudonym = @current_pseudonym
end
def new
@pseudonym = @domain_root_account.pseudonyms.build(:user => @current_user)
end
# @API Create a user login
# Create a new login for an existing user in the given account.
#
# @argument user[id] [Required, String]
# The ID of the user to create the login for.
#
# @argument login[unique_id] [Required, String]
# The unique ID for the new login.
#
# @argument login[password] [String]
# The new login's password.
#
# @argument login[sis_user_id] [String]
# SIS ID for the login. To set this parameter, the caller must be able to
# manage SIS permissions on the account.
#
# @argument login[integration_id] [String]
# Integration ID for the login. To set this parameter, the caller must be able to
# manage SIS permissions on the account. The Integration ID is a secondary
# identifier useful for more complex SIS integrations.
#
# @argument login[authentication_provider_id] [String]
# The authentication provider this login is associated with. Logins
# associated with a specific provider can only be used with that provider.
# Legacy providers (LDAP, CAS, SAML) will search for logins associated with
# them, or unassociated logins. New providers will only search for logins
# explicitly associated with them. This can be the integer ID of the
# provider, or the type of the provider (in which case, it will find the
# first matching provider).
#
# @example_request
#
# #create a facebook login for user with ID 123
# curl 'https://<canvas>/api/v1/accounts/<account_id>/logins' \
# -F 'user[id]=123' \
# -F 'login[unique_id]=112233445566' \
# -F 'login[authentication_provider_id]=facebook' \
# -H 'Authorization: Bearer <token>'
def create
return unless get_user
if api_request?
return unless context_is_root_account?
@account = @context
params[:login] ||= {}
params[:login][:password_confirmation] = params[:login][:password]
params[:pseudonym] = params[:login]
else
account_id = params[:pseudonym].delete(:account_id)
@account = Account.root_accounts.find(account_id) if account_id
@account ||= @domain_root_account
end
@pseudonym = @account.pseudonyms.build(user: @user)
return unless authorized_action(@pseudonym, @current_user, :create)
return unless find_authentication_provider
return unless update_pseudonym_from_params
@pseudonym.generate_temporary_password if !params[:pseudonym][:password]
if @pseudonym.save_without_session_maintenance
respond_to do |format|
flash[:notice] = t 'notices.account_registered', "Account registered!"
format.html { redirect_to user_profile_url(@current_user) }
format.json { render :json => pseudonym_json(@pseudonym, @current_user, session) }
end
else
respond_to do |format|
format.html { render :new }
format.json { render :json => @pseudonym.errors, :status => :bad_request }
end
end
end
def edit
@user = @current_user
@pseudonym = @current_pseudonym
end
def get_user
user_id = params[:user_id] || params[:user].try(:[], :id)
@user = case
when user_id
api_find(User, user_id)
else
@current_user
end
true
end
protected :get_user
# @API Edit a user login
# Update an existing login for a user in the given account.
#
# @argument login[unique_id] [String]
# The new unique ID for the login.
#
# @argument login[password] [String]
# The new password for the login. Can only be set by an admin user if admins
# are allowed to change passwords for the account.
#
# @argument login[sis_user_id] [String]
# SIS ID for the login. To set this parameter, the caller must be able to
# manage SIS permissions on the account.
#
# @argument login[integration_id] [String]
# Integration ID for the login. To set this parameter, the caller must be able to
# manage SIS permissions on the account. The Integration ID is a secondary
# identifier useful for more complex SIS integrations.
def update
if api_request?
@pseudonym = Pseudonym.active.find(params[:id])
return unless @user = @pseudonym.user
params[:login][:password_confirmation] = params[:login][:password] if params[:login][:password]
params[:pseudonym] = params[:login]
else
return unless get_user
@pseudonym = Pseudonym.active.find(params[:id])
raise ActiveRecord::RecordNotFound unless @pseudonym.user_id == @user.id
end
return unless authorized_action(@pseudonym, @current_user, [:update, :change_password])
return unless update_pseudonym_from_params
if @pseudonym.save_without_session_maintenance
flash[:notice] = t 'notices.account_updated', "Account updated!"
respond_to do |format|
format.html { redirect_to user_profile_url(@current_user) }
format.json { render :json => pseudonym_json(@pseudonym, @current_user, session) }
end
else
respond_to do |format|
format.html { render :edit }
format.json { render :json => @pseudonym.errors, :status => :bad_request }
end
end
end
# @API Delete a user login
# Delete an existing login.
#
# @example_request
# curl https://<canvas>/api/v1/users/:user_id/logins/:login_id \
# -H "Authorization: Bearer <ACCESS-TOKEN>" \
# -X DELETE
#
# @example_response
# {
# "unique_id": "bieber@example.com",
# "sis_user_id": null,
# "account_id": 1,
# "id": 12345,
# "user_id": 2
# }
def destroy
return unless get_user
@pseudonym = Pseudonym.active.find(params[:id])
raise ActiveRecord::RecordNotFound unless @pseudonym.user_id == @user.id
return unless authorized_action(@pseudonym, @current_user, :delete)
if @user.all_active_pseudonyms.length < 2
@pseudonym.errors.add(:base, t('errors.login_required', "Users must have at least one login"))
render :json => @pseudonym.errors, :status => :bad_request
elsif @pseudonym.destroy
api_request? ?
render(:json => pseudonym_json(@pseudonym, @current_user, session)) :
render(:json => @pseudonym)
else
render :json => @pseudonym.errors, :status => :bad_request
end
end
protected
def context_is_root_account?
if @context.root_account?
true
else
render(:json => { 'message' => 'Action must be called on a root account.' }, :status => :bad_request)
false
end
end
def find_authentication_provider
return true unless params[:pseudonym][:authentication_provider_id]
params[:pseudonym][:authentication_provider] = @domain_root_account.
authentication_providers.active.
find(params[:pseudonym][:authentication_provider_id])
end
def update_pseudonym_from_params
# you have to at least attempt something recognized...
if params[:pseudonym].slice(:unique_id, :password, :sis_user_id, :authentication_provider, :integration_id).blank?
render json: nil, status: :bad_request
return false
end
# perform updates (if they have permission
# to make them). silently ignore unrecognized fields.
# note: make sure sis_user_id is updated (if happening)
# before password, since it may affect the :change_password permissions
has_right_if_requests_change(:unique_id, :update) do
@pseudonym.unique_id = params[:pseudonym][:unique_id]
end or return false
has_right_if_requests_change(:authentication_provider, :update) do
@pseudonym.authentication_provider = params[:pseudonym][:authentication_provider]
end or return false
has_right_if_requests_change(:sis_user_id, :manage_sis) do
# convert "" -> nil for sis_user_id
@pseudonym.sis_user_id = params[:pseudonym][:sis_user_id].presence
end or return false
has_right_if_requests_change(:integration_id, :manage_sis) do
# convert "" -> nil for integration_id
@pseudonym.integration_id = params[:pseudonym][:integration_id].presence
end or return false
# give a 400 instead of a 401 if it doesn't make sense to change the password
if params[:pseudonym].key?(:password) && !@pseudonym.passwordable?
@pseudonym.errors.add(:password, 'password can only be set for Canvas authentication')
respond_to do |format|
format.html { render(params[:action] == 'edit' ? :edit : :new) }
format.json { render json: @pseudonym.errors, status: :bad_request }
end
return false
end
has_right_if_requests_change(:password, :change_password) do
@pseudonym.password = params[:pseudonym][:password]
@pseudonym.password_confirmation = params[:pseudonym][:password_confirmation]
end or return false
end
private
def has_right_if_requests_change(key, right)
return true unless params[:pseudonym].key?(key.to_sym)
if @pseudonym.grants_right?(@current_user, right.to_sym)
yield
true
else
render_unauthorized_action
false
end
end
end