[add] initial commit

This commit is contained in:
2021-02-24 03:22:52 -05:00
commit c8c91b21d9
57 changed files with 2707 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
class ApplicationController < ActionController::API
end

View File

@@ -0,0 +1,220 @@
# 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: <prefix>.<name_id>.<api_key>
# 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

View File