feat(api,install): исключения fail2ban и смена своего никнейма
- установщик: `[DEFAULT] ignoreip` в jail.local — локальные сети и WireGuard исключены всегда, свой адрес добавляется флагом `--ignore-ip <ip|cidr>`; админ, часто заходящий в панель, больше не банит сам себя; - участник может менять свой никнейм на сервере без MANAGE_NICKNAMES и без проверки иерархии (тайм-аут на себя по-прежнему запрещён); - тест на смену своего ника и запрет self-тайм-аута.
This commit is contained in:
@@ -1,4 +1,10 @@
|
|||||||
# Сгенерировано deploy/install.sh — не редактируйте вручную.
|
# Сгенерировано deploy/install.sh — не редактируйте вручную.
|
||||||
|
# Исключения: локальные и доверенные адреса не банятся никогда. Админ,
|
||||||
|
# который часто перезаходит в панель, не должен блокировать сам себя
|
||||||
|
# (AGENT.md 9.6: баны за атаки, а не за работу).
|
||||||
|
[DEFAULT]
|
||||||
|
ignoreip = 127.0.0.1/8 ::1 ${FAIL2BAN_IGNOREIP}
|
||||||
|
|
||||||
# Jail для неудачных входов в приложение (AGENT.md 9.6).
|
# Jail для неудачных входов в приложение (AGENT.md 9.6).
|
||||||
[glchat-auth]
|
[glchat-auth]
|
||||||
enabled = true
|
enabled = true
|
||||||
|
|||||||
+8
-1
@@ -81,6 +81,9 @@ ADMIN_EMAIL="${ADMIN_EMAIL:-}"
|
|||||||
ENABLE_ADMIN_2FA="${ENABLE_ADMIN_2FA:-0}"
|
ENABLE_ADMIN_2FA="${ENABLE_ADMIN_2FA:-0}"
|
||||||
# Явная ротация секретов: по умолчанию секреты существующего .env сохраняются.
|
# Явная ротация секретов: по умолчанию секреты существующего .env сохраняются.
|
||||||
ROTATE_SECRETS="${ROTATE_SECRETS:-0}"
|
ROTATE_SECRETS="${ROTATE_SECRETS:-0}"
|
||||||
|
# Список адресов, которые fail2ban не банит. Локальные сети и WireGuard-туннель
|
||||||
|
# добавляются всегда: иначе администратор блокирует сам себя при работе в панели.
|
||||||
|
FAIL2BAN_IGNOREIP="${FAIL2BAN_IGNOREIP:-192.168.0.0/16 10.0.0.0/8 172.16.0.0/12}"
|
||||||
ADMIN_PASSWORD="${ADMIN_PASSWORD:-}"
|
ADMIN_PASSWORD="${ADMIN_PASSWORD:-}"
|
||||||
REGISTRATION_ENABLED="${REGISTRATION_ENABLED:-true}"
|
REGISTRATION_ENABLED="${REGISTRATION_ENABLED:-true}"
|
||||||
TURN_ENABLED="${TURN_ENABLED:-true}"
|
TURN_ENABLED="${TURN_ENABLED:-true}"
|
||||||
@@ -121,6 +124,9 @@ install.sh — установка glchat (AGENT.md 10).
|
|||||||
--email <email> email для Let's Encrypt (обязателен, если TLS включён)
|
--email <email> email для Let's Encrypt (обязателен, если TLS включён)
|
||||||
--admin <email> логин инстанс-администратора
|
--admin <email> логин инстанс-администратора
|
||||||
--password <password> пароль инстанс-администратора (иначе — интерактивный ввод)
|
--password <password> пароль инстанс-администратора (иначе — интерактивный ввод)
|
||||||
|
--ignore-ip <ip|cidr> адрес, который fail2ban никогда не банит
|
||||||
|
(можно указывать несколько раз; локальные сети
|
||||||
|
и WireGuard уже включены по умолчанию)
|
||||||
--rotate-secrets сменить секреты (MASTER_KEY, SESSION_PEPPER и др.):
|
--rotate-secrets сменить секреты (MASTER_KEY, SESSION_PEPPER и др.):
|
||||||
ломает существующие данные, нужен чистый инстанс
|
ломает существующие данные, нужен чистый инстанс
|
||||||
--admin-2fa включить 2FA инстанс-админу и напечатать секрет и
|
--admin-2fa включить 2FA инстанс-админу и напечатать секрет и
|
||||||
@@ -193,6 +199,7 @@ parse_args() {
|
|||||||
--password) ADMIN_PASSWORD="${2:?}"; shift 2 ;;
|
--password) ADMIN_PASSWORD="${2:?}"; shift 2 ;;
|
||||||
--admin-2fa) ENABLE_ADMIN_2FA=1; shift ;;
|
--admin-2fa) ENABLE_ADMIN_2FA=1; shift ;;
|
||||||
--rotate-secrets) ROTATE_SECRETS=1; shift ;;
|
--rotate-secrets) ROTATE_SECRETS=1; shift ;;
|
||||||
|
--ignore-ip) FAIL2BAN_IGNOREIP="${FAIL2BAN_IGNOREIP} ${2:?}"; shift 2 ;;
|
||||||
--instance-name) INSTANCE_NAME="${2:?}"; shift 2 ;;
|
--instance-name) INSTANCE_NAME="${2:?}"; shift 2 ;;
|
||||||
--main-guild) MAIN_GUILD_NAME="${2:?}"; shift 2 ;;
|
--main-guild) MAIN_GUILD_NAME="${2:?}"; shift 2 ;;
|
||||||
--ssh-port) SSH_PORT="${2:?}"; shift 2 ;;
|
--ssh-port) SSH_PORT="${2:?}"; shift 2 ;;
|
||||||
@@ -1091,7 +1098,7 @@ render_all_templates() {
|
|||||||
|
|
||||||
if [ "$ENABLE_FAIL2BAN" = "true" ]; then
|
if [ "$ENABLE_FAIL2BAN" = "true" ]; then
|
||||||
render_template "${DEPLOY_DIR}/fail2ban/jail.local.tpl" "${TARGET_DIR}/fail2ban/jail.local" \
|
render_template "${DEPLOY_DIR}/fail2ban/jail.local.tpl" "${TARGET_DIR}/fail2ban/jail.local" \
|
||||||
"SSH_PORT=${SSH_PORT}"
|
"SSH_PORT=${SSH_PORT}" "FAIL2BAN_IGNOREIP=${FAIL2BAN_IGNOREIP}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$ENABLE_NFTABLES" = "true" ]; then
|
if [ "$ENABLE_NFTABLES" = "true" ]; then
|
||||||
|
|||||||
@@ -729,21 +729,29 @@ func (s *Server) registerMemberRoutes(api huma.API) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := s.checkMemberHierarchy(ctx, guildID, user, resolved, targetID); err != nil {
|
// Свой никнейм участник меняет сам: иерархия и MANAGE_NICKNAMES для
|
||||||
return nil, err
|
// этого не нужны (AGENT.md 7.2, 7.17). Тайм-аут на себя — запрещён.
|
||||||
|
selfEdit := targetID == user.ID
|
||||||
|
if !selfEdit {
|
||||||
|
if err := s.checkMemberHierarchy(ctx, guildID, user, resolved, targetID); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
var (
|
var (
|
||||||
nickname *string
|
nickname *string
|
||||||
timeoutUntil *time.Time
|
timeoutUntil *time.Time
|
||||||
)
|
)
|
||||||
if input.Body.Nickname != nil {
|
if input.Body.Nickname != nil {
|
||||||
if !resolved.Has(permissions.ManageNicknames) {
|
if !selfEdit && !resolved.Has(permissions.ManageNicknames) {
|
||||||
return nil, humaErrorStatus(http.StatusForbidden, "perm.denied", "MANAGE_NICKNAMES is required")
|
return nil, humaErrorStatus(http.StatusForbidden, "perm.denied", "MANAGE_NICKNAMES is required")
|
||||||
}
|
}
|
||||||
trimmed := strings.TrimSpace(*input.Body.Nickname)
|
trimmed := strings.TrimSpace(*input.Body.Nickname)
|
||||||
nickname = &trimmed
|
nickname = &trimmed
|
||||||
}
|
}
|
||||||
if input.Body.TimeoutUntil != nil {
|
if input.Body.TimeoutUntil != nil {
|
||||||
|
if selfEdit {
|
||||||
|
return nil, humaErrorStatus(http.StatusForbidden, "perm.denied", "you cannot time yourself out")
|
||||||
|
}
|
||||||
if !resolved.Has(permissions.TimeoutMembers) {
|
if !resolved.Has(permissions.TimeoutMembers) {
|
||||||
return nil, humaErrorStatus(http.StatusForbidden, "perm.denied", "TIMEOUT_MEMBERS is required")
|
return nil, humaErrorStatus(http.StatusForbidden, "perm.denied", "TIMEOUT_MEMBERS is required")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -452,6 +452,43 @@ func TestInstanceAdminEndpointsAndLimits(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMemberCanChangeOwnNickname(t *testing.T) {
|
||||||
|
srv, _ := newTestServer(t)
|
||||||
|
ownerCookie := registerAndLogin(t, srv, "nick_owner", "nick-owner@example.com")
|
||||||
|
memberCookie := registerAndLogin(t, srv, "nick_member", "nick-member@example.com")
|
||||||
|
|
||||||
|
created := doJSON(t, srv, http.MethodPost, "/api/v1/guilds", `{"name":"Ники"}`, ownerCookie)
|
||||||
|
guild := decodeResponse[struct {
|
||||||
|
Guild struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"guild"`
|
||||||
|
}](t, created)
|
||||||
|
doJSON(t, srv, http.MethodPost, "/api/v1/guilds/"+guild.Guild.ID+"/join", "", memberCookie)
|
||||||
|
|
||||||
|
member, err := srv.auth.UserByEmail(t.Context(), "nick-member@example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UserByEmail: %v", err)
|
||||||
|
}
|
||||||
|
self := doJSON(t, srv, http.MethodPatch,
|
||||||
|
"/api/v1/guilds/"+guild.Guild.ID+"/members/"+formatSnowflake(member.ID),
|
||||||
|
`{"nickname":"себе ник"}`, memberCookie)
|
||||||
|
if self.Code != http.StatusOK {
|
||||||
|
t.Fatalf("self nickname = %d, body = %s", self.Code, self.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Тайм-аут на себя запрещён даже владельцу.
|
||||||
|
owner, err := srv.auth.UserByEmail(t.Context(), "nick-owner@example.com")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("UserByEmail: %v", err)
|
||||||
|
}
|
||||||
|
timeout := doJSON(t, srv, http.MethodPatch,
|
||||||
|
"/api/v1/guilds/"+guild.Guild.ID+"/members/"+formatSnowflake(owner.ID),
|
||||||
|
`{"timeout_until":"2030-01-01T00:00:00Z"}`, ownerCookie)
|
||||||
|
if timeout.Code != http.StatusForbidden {
|
||||||
|
t.Fatalf("self timeout = %d, want 403", timeout.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMemberTimeoutRequiresPermission(t *testing.T) {
|
func TestMemberTimeoutRequiresPermission(t *testing.T) {
|
||||||
srv, _ := newTestServer(t)
|
srv, _ := newTestServer(t)
|
||||||
ownerCookie := registerAndLogin(t, srv, "timeout_owner", "timeout-owner@example.com")
|
ownerCookie := registerAndLogin(t, srv, "timeout_owner", "timeout-owner@example.com")
|
||||||
|
|||||||
Reference in New Issue
Block a user