ip address role take 1
This commit is contained in:
@@ -5,6 +5,17 @@ class ApplicationController < ActionController::Base
|
||||
include Pundit::Authorization
|
||||
include Devise::Controllers::Helpers::ClassMethods
|
||||
|
||||
sig { returns(T.nilable(IpAddressRole)) }
|
||||
def current_ip_address_role
|
||||
@current_ip_address_role ||= IpAddressRole.for_ip(request.remote_ip)
|
||||
end
|
||||
helper_method :current_ip_address_role
|
||||
|
||||
sig { returns(T.nilable(T.any(User, IpAddressRole))) }
|
||||
def pundit_user
|
||||
current_user || current_ip_address_role
|
||||
end
|
||||
|
||||
before_action do
|
||||
if Rails.env.development? || Rails.env.staging?
|
||||
Rack::MiniProfiler.authorize_request
|
||||
@@ -16,14 +27,6 @@ class ApplicationController < ActionController::Base
|
||||
# Pundit authorization error handling
|
||||
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
|
||||
|
||||
# Returns the IpAddressRole for the current request's IP address
|
||||
# This is similar to current_user but based on IP address
|
||||
sig { returns(T.nilable(IpAddressRole)) }
|
||||
def current_ip_address_role
|
||||
@current_ip_address_role ||= IpAddressRole.for_ip(request.remote_ip)
|
||||
end
|
||||
helper_method :current_ip_address_role
|
||||
|
||||
protected
|
||||
|
||||
def prometheus_client
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
validateIpAddress,
|
||||
type ValidationResult,
|
||||
} from '../utils/ipValidation';
|
||||
|
||||
interface IpAddressInputProps {
|
||||
initialValue?: string;
|
||||
@@ -10,17 +14,11 @@ interface IpAddressInputProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
type ValidationResult = {
|
||||
isValid: boolean;
|
||||
message: string;
|
||||
type: 'ip' | 'cidr' | 'none';
|
||||
};
|
||||
|
||||
const IpAddressInput: React.FC<IpAddressInputProps> = ({
|
||||
initialValue = '',
|
||||
name = 'ip_address_role[ip_address]',
|
||||
id,
|
||||
placeholder = 'Example: 192.168.1.1 or 10.0.0.0/24',
|
||||
placeholder = 'Example: 192.168.1.1, 2001:db8::1, or 10.0.0.0/24',
|
||||
onChange,
|
||||
className = '',
|
||||
}) => {
|
||||
@@ -32,47 +30,6 @@ const IpAddressInput: React.FC<IpAddressInputProps> = ({
|
||||
});
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
|
||||
// Regular expressions for validation
|
||||
const ipv4Regex =
|
||||
/^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}$/;
|
||||
const cidrRegex =
|
||||
/^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]\d|\d)){3}\/(?:[0-9]|[1-2][0-9]|3[0-2])$/;
|
||||
|
||||
// Function to validate the input
|
||||
const validateIpAddress = (input: string): ValidationResult => {
|
||||
if (!input) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'IP address is required',
|
||||
type: 'none',
|
||||
};
|
||||
}
|
||||
|
||||
if (input.includes('/')) {
|
||||
// CIDR notation
|
||||
if (cidrRegex.test(input)) {
|
||||
return { isValid: true, message: 'Valid CIDR range', type: 'cidr' };
|
||||
} else {
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'Invalid CIDR range format',
|
||||
type: 'cidr',
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Single IP address
|
||||
if (ipv4Regex.test(input)) {
|
||||
return { isValid: true, message: 'Valid IP address', type: 'ip' };
|
||||
} else {
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'Invalid IP address format',
|
||||
type: 'ip',
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Update validation when value changes
|
||||
useEffect(() => {
|
||||
const result = validateIpAddress(value);
|
||||
@@ -100,7 +57,7 @@ const IpAddressInput: React.FC<IpAddressInputProps> = ({
|
||||
onFocus={() => setIsFocused(true)}
|
||||
onBlur={() => setIsFocused(false)}
|
||||
placeholder={placeholder}
|
||||
className={`block w-full rounded-md shadow-sm focus:ring-sky-500 sm:text-sm ${getBorderColorClass()} ${className}`}
|
||||
className={`block w-full rounded-md font-mono shadow-sm focus:ring-sky-500 sm:text-sm ${getBorderColorClass()} ${className}`}
|
||||
id={id || 'ip_address_input'}
|
||||
/>
|
||||
|
||||
@@ -150,18 +107,20 @@ const IpAddressInput: React.FC<IpAddressInputProps> = ({
|
||||
)}
|
||||
|
||||
{/* Additional helpful information */}
|
||||
<div className="mt-2 text-sm text-slate-500">
|
||||
{validation.type === 'cidr' ? (
|
||||
<span>
|
||||
CIDR notation represents an IP range (e.g., 10.0.0.0/24 includes 256
|
||||
addresses)
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
Enter a single IP address (e.g., 192.168.1.1) or an IP range in CIDR
|
||||
notation (e.g., 10.0.0.0/24)
|
||||
</span>
|
||||
)}
|
||||
<div className="mt-2 min-w-[400px] text-sm text-slate-500">
|
||||
<div className="flex items-center">
|
||||
{validation.type === 'cidr-v4' || validation.type === 'cidr-v6' ? (
|
||||
<span>
|
||||
CIDR notation represents an IP range (e.g., 10.0.0.0/24 for IPv4
|
||||
or 2001:db8::/32 for IPv6)
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
Enter a single IP address (IPv4: 192.168.1.1 or IPv6: 2001:db8::1)
|
||||
or an IP range in CIDR notation
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
128
app/javascript/bundles/UI/utils/ipValidation.ts
Normal file
128
app/javascript/bundles/UI/utils/ipValidation.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
export type ValidationResult = {
|
||||
isValid: boolean;
|
||||
message: string;
|
||||
type: 'ipv4' | 'ipv6' | 'cidr-v4' | 'cidr-v6' | 'none';
|
||||
};
|
||||
|
||||
// Utility functions for IP address validation
|
||||
const isIPv4Segment = (segment: string): boolean => {
|
||||
const num = Number(segment);
|
||||
return !isNaN(num) && num >= 0 && num <= 255 && segment === num.toString();
|
||||
};
|
||||
|
||||
const isIPv4Address = (address: string): boolean => {
|
||||
const segments = address.split('.');
|
||||
return segments.length === 4 && segments.every(isIPv4Segment);
|
||||
};
|
||||
|
||||
const isIPv6Segment = (segment: string): boolean => {
|
||||
return segment.length <= 4 && /^[0-9a-fA-F]*$/.test(segment);
|
||||
};
|
||||
|
||||
const isIPv6Address = (address: string): boolean => {
|
||||
// Handle the :: compression
|
||||
const parts = address.split('::');
|
||||
if (parts.length > 2) return false; // More than one :: is invalid
|
||||
|
||||
if (parts.length === 2) {
|
||||
const [left, right] = parts;
|
||||
const leftSegments = left ? left.split(':') : [];
|
||||
const rightSegments = right ? right.split(':') : [];
|
||||
|
||||
// Total segments should be 8 after decompression
|
||||
if (leftSegments.length + rightSegments.length > 7) return false;
|
||||
|
||||
// Validate each segment
|
||||
return (
|
||||
leftSegments.every(isIPv6Segment) && rightSegments.every(isIPv6Segment)
|
||||
);
|
||||
}
|
||||
|
||||
// No compression, should be exactly 8 segments
|
||||
const segments = address.split(':');
|
||||
return segments.length === 8 && segments.every(isIPv6Segment);
|
||||
};
|
||||
|
||||
const isCIDRPrefix = (prefix: string, isIPv6: boolean): boolean => {
|
||||
const num = Number(prefix);
|
||||
return !isNaN(num) && num >= 0 && num <= (isIPv6 ? 128 : 32);
|
||||
};
|
||||
|
||||
const validateCIDR = (input: string): ValidationResult | null => {
|
||||
const [address, prefix] = input.split('/');
|
||||
if (!prefix) return null;
|
||||
|
||||
// Check if it's IPv6 CIDR
|
||||
if (address.includes(':')) {
|
||||
if (!isIPv6Address(address) || !isCIDRPrefix(prefix, true)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'Invalid IPv6 CIDR range format',
|
||||
type: 'none',
|
||||
};
|
||||
}
|
||||
return {
|
||||
isValid: true,
|
||||
message: 'Valid IPv6 CIDR range',
|
||||
type: 'cidr-v6',
|
||||
};
|
||||
}
|
||||
|
||||
// Check if it's IPv4 CIDR
|
||||
if (!isIPv4Address(address) || !isCIDRPrefix(prefix, false)) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'Invalid IPv4 CIDR range format',
|
||||
type: 'none',
|
||||
};
|
||||
}
|
||||
return {
|
||||
isValid: true,
|
||||
message: 'Valid IPv4 CIDR range',
|
||||
type: 'cidr-v4',
|
||||
};
|
||||
};
|
||||
|
||||
export const validateIpAddress = (input: string): ValidationResult => {
|
||||
if (!input) {
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'IP address is required',
|
||||
type: 'none',
|
||||
};
|
||||
}
|
||||
|
||||
if (input.includes('/')) {
|
||||
const cidrResult = validateCIDR(input);
|
||||
if (cidrResult) return cidrResult;
|
||||
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'Invalid CIDR range format',
|
||||
type: 'none',
|
||||
};
|
||||
}
|
||||
|
||||
// Single IP address validation
|
||||
if (isIPv4Address(input)) {
|
||||
return {
|
||||
isValid: true,
|
||||
message: 'Valid IPv4 address',
|
||||
type: 'ipv4',
|
||||
};
|
||||
}
|
||||
|
||||
if (isIPv6Address(input)) {
|
||||
return {
|
||||
isValid: true,
|
||||
message: 'Valid IPv6 address',
|
||||
type: 'ipv6',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: false,
|
||||
message: 'Invalid IP address format',
|
||||
type: 'none',
|
||||
};
|
||||
};
|
||||
@@ -4,7 +4,7 @@
|
||||
class ApplicationPolicy
|
||||
extend T::Sig
|
||||
|
||||
sig { returns(T.nilable(User)) }
|
||||
sig { returns(T.nilable(T.any(User, IpAddressRole))) }
|
||||
attr_reader :user
|
||||
|
||||
sig { returns(T.untyped) }
|
||||
|
||||
@@ -38,6 +38,12 @@
|
||||
<%= request.remote_ip %>
|
||||
</span>
|
||||
</span>
|
||||
<span class="font-mono text-slate-500">
|
||||
<span>IP Address Role</span>
|
||||
<span class="font-mono text-slate-700">
|
||||
<%= current_ip_address_role ? current_ip_address_role.ip_address : "None" %>
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<div class="flex-grow"></div>
|
||||
<nav class="flex items-center space-x-4">
|
||||
|
||||
@@ -11,4 +11,8 @@ module Pundit::Authorization
|
||||
end
|
||||
def policy_scope(scope, policy_scope_class: T.unsafe(nil))
|
||||
end
|
||||
|
||||
sig { returns(Pundit::Context) }
|
||||
def pundit
|
||||
end
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user