# frozen_string_literal: true require 'resolv' class ClientsController < ApplicationController before_action :parse_api_key_header, except: [:show_public_ipv4] before_action :authenticate_client, except: [:show_public_ipv4] before_action :authorize_perm_r, only: [:show_ipv4] before_action :authorize_perm_w, only: %i[update_ipv4 destroy_ipv4] before_action :authorize_perm_x, only: %i[create update index show destroy] before_action :client_authorized, except: [:show_public_ipv4] # URL Methods # POST /admin/client def create client_params = params.permit(:name, :description, :permission, :public_ip) return head(405) unless client_params.key?(:name) client = Client.find_by name: client_params[:name] status_code = client ? 200 : 201 # create new client or update existing if client.nil? client ||= Client.new_client(client_params) else # return 406 if request is trying to remove perm_x from last admin account to prevent lockout # TODO: move this into the model (update) return head(406) if removing_last_admin?(client, client_params) client.update_client(client_params) end api_key = client.gen_api_key client.save json_response({ name: client.name, api_key: api_key }, status_code) end # PUT /admin/client def update client_params = params.permit(:name, :description, :permission, :public_ip) return head(405) unless client_params.key?(:name) client = Client.find_by name: client_params[:name] return head(404) unless client # return 406 if request is trying to remove perm_x from last admin account to prevent lockout # TODO: move this into the model (update) return head(406) if removing_last_admin?(client, client_params) # update client client.update_client(client_params) client.save head(200) end # GET /admin/clients def index clients = [] Client.find_each do |client| clients.append admin_read_object(client) end json_response(clients) end # GET /admin/clients/:name def show # return 404 if requested client does not exist client = Client.find_by name: params[:name] return head(404) if client.nil? json_response(admin_read_object(client)) end # DELETE /admin/clients/:name def destroy client = Client.find_by(name: params[:name]) # return 404 if requested client does not exist return head(404) unless client # return 406 if request is trying to remove last admin account to prevent lockout # TODO: move this into the model (delete) return head(406) if client.admin? && Client.number_of_admins <= 1 client.delete head(200) end # PUT /client def update_ipv4 ipv4 = request_ip # return 406 if body with invalid ipv4 is present # TODO: move this into the model (update) return head(406) unless ipv4 @requesting_client.ipv4 = ipv4 @requesting_client.save json_response(client_read_object(@requesting_client)) end # DELETE /client def destroy_ipv4 prev_data = client_read_object(@requesting_client) @requesting_client.ipv4 = '' @requesting_client.save json_response(prev_data) end # GET /clients/:id def show_ipv4 # return 404 if requested client does not exist client = Client.find_by name: params[:name] return head(404) if client.nil? json_response(client_read_object(client)) end # GET /public/:id def show_public_ipv4 # check if client exist and is public else return 401 (don't leak existence) client = Client.find_by name: params[:name] return head(401) if client.nil? || !client.public_ip json_response(client_read_object(client)) end private # helper methods # respond with json object and status def json_response(object, status = :ok) render json: object, status: status end # checks if request is trying to remove the last admin def removing_last_admin?(client, params) return false unless params[:permission].is_a?(String) client.admin? && !Client.parse_perm_string(params[:permission])[:x] && Client.number_of_admins <= 1 end def client_read_object(client) { name: client.name, ipv4: client.ipv4 } end def admin_read_object(client) { name: client.name, description: client.description, permission: client.permission, public_ip: client.public_ip, ipv4: client.ipv4 } end def request_ip ipv4_params = params.permit(:ipv4) if ipv4_params.key?(:ipv4) return nil unless ipv4_params[:ipv4].match(Resolv::IPv4::Regex) ipv4_params[:ipv4] else request.remote_ip end end # before_action methods # check if the api is valid and has the appropriate prefix def parse_api_key_header # TODO: move api_key checks and formatting to helper class # check if api key is present and can be split into 3 parts: .. # NOTE: .compact is more for got measure than really necessary, but this ensures no nil values in the array api_key_header = request.headers[RynDNS::Application::API_KEY_HEADER].split('.').compact unless request.headers[RynDNS::Application::API_KEY_HEADER].nil? return head(401) if api_key_header.nil? || api_key_header.length < 3 prefix, @name_id, @api_key = api_key_header # check if prefix matches (do this securely to prevent timing attacks on the prefix) return head(403) unless ActiveSupport::SecurityUtils.secure_compare(prefix, RynDNS::Application::API_KEY_PREFIX) end # authenticate the client with the supplied api key def authenticate_client # load the request client else return 403 @requesting_client = Client.find_by name_id: @name_id return head(403) unless @requesting_client # compare api_key with hash value else return 403 return head(403) unless @requesting_client.api_key == @api_key end # check if requesting client has read permission def authorize_perm_r @authorized = @requesting_client.read? end # check if requesting client has write permission def authorize_perm_w @authorized = @requesting_client.write? end # check if requesting client has admin permission def authorize_perm_x @authorized = @requesting_client.admin? end # check if request is authorized def client_authorized return head(403) unless @authorized end end