mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2025-10-28 20:55:42 +00:00
feat: allow or block an ip/range of ips using labels
This commit is contained in:
@@ -42,6 +42,7 @@
|
|||||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||||
|
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||||
"unauthorizedButton": "Try again",
|
"unauthorizedButton": "Try again",
|
||||||
"untrustedRedirectTitle": "Untrusted redirect",
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
"unauthorizedResourceSubtitle": "The user with username <code>{{username}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||||
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
"unauthorizedLoginSubtitle": "The user with username <code>{{username}}</code> is not authorized to login.",
|
||||||
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
"unauthorizedGroupsSubtitle": "The user with username <code>{{username}}</code> is not in the groups required by the resource <code>{{resource}}</code>.",
|
||||||
|
"unauthorizedIpSubtitle": "Your IP address <code>{{ip}}</code> is not authorized to access the resource <code>{{resource}}</code>.",
|
||||||
"unauthorizedButton": "Try again",
|
"unauthorizedButton": "Try again",
|
||||||
"untrustedRedirectTitle": "Untrusted redirect",
|
"untrustedRedirectTitle": "Untrusted redirect",
|
||||||
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
"untrustedRedirectSubtitle": "You are trying to redirect to a domain that does not match your configured domain (<code>{{domain}}</code>). Are you sure you want to continue?",
|
||||||
|
|||||||
@@ -17,9 +17,10 @@ export const UnauthorizedPage = () => {
|
|||||||
const username = searchParams.get("username");
|
const username = searchParams.get("username");
|
||||||
const resource = searchParams.get("resource");
|
const resource = searchParams.get("resource");
|
||||||
const groupErr = searchParams.get("groupErr");
|
const groupErr = searchParams.get("groupErr");
|
||||||
|
const ip = searchParams.get("ip");
|
||||||
|
|
||||||
if (!username) {
|
if (!username && !ip) {
|
||||||
return <Navigate to="/" />;
|
return <Navigate to="/login" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -41,6 +42,10 @@ export const UnauthorizedPage = () => {
|
|||||||
i18nKey = "unauthorizedGroupsSubtitle";
|
i18nKey = "unauthorizedGroupsSubtitle";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ip) {
|
||||||
|
i18nKey = "unauthorizedIpSubtitle";
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="min-w-xs sm:min-w-sm">
|
<Card className="min-w-xs sm:min-w-sm">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
@@ -55,6 +60,7 @@ export const UnauthorizedPage = () => {
|
|||||||
values={{
|
values={{
|
||||||
username,
|
username,
|
||||||
resource,
|
resource,
|
||||||
|
ip,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
|
|||||||
@@ -351,3 +351,44 @@ func (auth *Auth) GetBasicAuth(c *gin.Context) *types.User {
|
|||||||
Password: password,
|
Password: password,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (auth *Auth) CheckIP(c *gin.Context, labels types.Labels) bool {
|
||||||
|
// Get the IP address from the request
|
||||||
|
ip := c.ClientIP()
|
||||||
|
|
||||||
|
// Check if the IP is in block list
|
||||||
|
for _, blocked := range labels.IP.Block {
|
||||||
|
res, err := utils.FilterIP(blocked, ip)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Str("item", blocked).Msg("Invalid IP/CIDR in block list")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if res {
|
||||||
|
log.Warn().Str("ip", ip).Str("item", blocked).Msg("IP is in blocked list, denying access")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For every IP in the allow list, check if the IP matches
|
||||||
|
for _, allowed := range labels.IP.Allow {
|
||||||
|
res, err := utils.FilterIP(allowed, ip)
|
||||||
|
if err != nil {
|
||||||
|
log.Warn().Err(err).Str("item", allowed).Msg("Invalid IP/CIDR in allow list")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if res {
|
||||||
|
log.Debug().Str("ip", ip).Str("item", allowed).Msg("IP is in allowed list, allowing access")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not in allowed range and allowed range is not empty, deny access
|
||||||
|
if len(labels.IP.Allow) > 0 {
|
||||||
|
log.Warn().Str("ip", ip).Msg("IP not in allow list, denying access")
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Debug().Str("ip", ip).Msg("IP not in allow or block list, allowing by default")
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|||||||
@@ -96,6 +96,38 @@ func (h *Handlers) AuthHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the IP is allowed/blocked
|
||||||
|
ip := c.ClientIP()
|
||||||
|
if !h.Auth.CheckIP(c, labels) {
|
||||||
|
log.Warn().Str("ip", ip).Msg("IP not allowed")
|
||||||
|
|
||||||
|
if proxy.Proxy == "nginx" || !isBrowser {
|
||||||
|
c.JSON(403, gin.H{
|
||||||
|
"status": 403,
|
||||||
|
"message": "Forbidden",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
values := types.UnauthorizedQuery{
|
||||||
|
Resource: strings.Split(host, ".")[0],
|
||||||
|
IP: ip,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build query
|
||||||
|
queries, err := query.Values(values)
|
||||||
|
|
||||||
|
// Handle error
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("Failed to build queries")
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", h.Config.AppURL))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/unauthorized?%s", h.Config.AppURL, queries.Encode()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Check if auth is enabled
|
// Check if auth is enabled
|
||||||
authEnabled, err := h.Auth.AuthEnabled(c, labels)
|
authEnabled, err := h.Auth.AuthEnabled(c, labels)
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ type UnauthorizedQuery struct {
|
|||||||
Username string `url:"username"`
|
Username string `url:"username"`
|
||||||
Resource string `url:"resource"`
|
Resource string `url:"resource"`
|
||||||
GroupErr bool `url:"groupErr"`
|
GroupErr bool `url:"groupErr"`
|
||||||
|
IP string `url:"ip"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proxy is the uri parameters for the proxy endpoint
|
// Proxy is the uri parameters for the proxy endpoint
|
||||||
|
|||||||
@@ -105,6 +105,12 @@ type BasicLabels struct {
|
|||||||
Password string
|
Password string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IP labels for a tinyauth protected container
|
||||||
|
type IPLabels struct {
|
||||||
|
Allow []string
|
||||||
|
Block []string
|
||||||
|
}
|
||||||
|
|
||||||
// Labels is a struct that contains the labels for a tinyauth protected container
|
// Labels is a struct that contains the labels for a tinyauth protected container
|
||||||
type Labels struct {
|
type Labels struct {
|
||||||
Users string
|
Users string
|
||||||
@@ -113,4 +119,5 @@ type Labels struct {
|
|||||||
Domain string
|
Domain string
|
||||||
Basic BasicLabels
|
Basic BasicLabels
|
||||||
OAuth OAuthLabels
|
OAuth OAuthLabels
|
||||||
|
IP IPLabels
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package utils
|
|||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -202,7 +203,7 @@ func GetLabels(labels map[string]string) (types.Labels, error) {
|
|||||||
var labelsParsed types.Labels
|
var labelsParsed types.Labels
|
||||||
|
|
||||||
// Decode the labels into the labels struct
|
// Decode the labels into the labels struct
|
||||||
err := parser.Decode(labels, &labelsParsed, "tinyauth", "tinyauth.users", "tinyauth.allowed", "tinyauth.headers", "tinyauth.domain", "tinyauth.basic", "tinyauth.oauth")
|
err := parser.Decode(labels, &labelsParsed, "tinyauth", "tinyauth.users", "tinyauth.allowed", "tinyauth.headers", "tinyauth.domain", "tinyauth.basic", "tinyauth.oauth", "tinyauth.ip")
|
||||||
|
|
||||||
// Check if there was an error
|
// Check if there was an error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -368,3 +369,39 @@ func GetBasicAuth(username string, password string) string {
|
|||||||
// Encode the auth string to base64
|
// Encode the auth string to base64
|
||||||
return base64.StdEncoding.EncodeToString([]byte(auth))
|
return base64.StdEncoding.EncodeToString([]byte(auth))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if an IP is contained in a CIDR range/matches a single IP
|
||||||
|
func FilterIP(filter string, ip string) (bool, error) {
|
||||||
|
// Convert the check IP to an IP instance
|
||||||
|
ipAddr := net.ParseIP(ip)
|
||||||
|
|
||||||
|
// Check if the filter is a CIDR range
|
||||||
|
if strings.Contains(filter, "/") {
|
||||||
|
// Parse the CIDR range
|
||||||
|
_, cidr, err := net.ParseCIDR(filter)
|
||||||
|
|
||||||
|
// Check if there was an error
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the IP is in the CIDR range
|
||||||
|
return cidr.Contains(ipAddr), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the filter as a single IP
|
||||||
|
ipFilter := net.ParseIP(filter)
|
||||||
|
|
||||||
|
// Check if the IP is valid
|
||||||
|
if ipFilter == nil {
|
||||||
|
return false, errors.New("invalid IP address in filter")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the IP matches the filter
|
||||||
|
if ipFilter.Equal(ipAddr) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the filter is not a CIDR range or a single IP, return false
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user