- Роль ADMIN = users.is_admin; встроенный администратор (mailserver.admins, по
умолчанию admin@) всегда ADMIN и получает роль в базе при старте.
- PUT /api/admin/mailboxes/{id}/role {ADMIN|USER}: только ADMIN; не свою, не
встроенного, общий ящик не может быть ADMIN.
- Admin panel (/admin, только ADMIN): USERS (логины и роли, MAKE ADMIN / MAKE USER),
MAILBOXES, LISTS. User panel (/settings): профиль, пароль, хранилище, вид, мои ящики.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,7 +40,12 @@ public class MailAdmins {
|
||||
return admins.contains(key) || granted.contains(key);
|
||||
}
|
||||
|
||||
/** Given / taken by Jambotron (users.is_admin) — loaded at start and kept here on every change. */
|
||||
/** Built-in administrator from the configuration (admin@ by default): the role can not be taken away. */
|
||||
public boolean isBuiltIn(String email) {
|
||||
return email != null && admins.contains(email.toLowerCase(Locale.ROOT));
|
||||
}
|
||||
|
||||
/** Role ADMIN in users.is_admin (given by an administrator in Admin panel or by Jambotron) — loaded at start, kept here on every change. */
|
||||
public void grant(String email, boolean admin) {
|
||||
String key = email.toLowerCase(Locale.ROOT);
|
||||
if (admin) {
|
||||
|
||||
@@ -6,8 +6,9 @@ import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
/** At start: mailboxes Jambotron made administrators (users.is_admin) → {@link MailAdmins}. */
|
||||
/** At start: role ADMIN (users.is_admin) → {@link MailAdmins}; the built-in administrator gets the role in the database too. */
|
||||
@Component
|
||||
public class MailAdminsLoader {
|
||||
|
||||
@@ -23,10 +24,13 @@ public class MailAdminsLoader {
|
||||
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void load() {
|
||||
users.findAll().filter(user -> user.isAdmin())
|
||||
users.findAll()
|
||||
.concatMap(user -> !user.isAdmin() && admins.isBuiltIn(user.getEmail())
|
||||
? Mono.fromSupplier(() -> { user.setAdmin(true); return user; }).flatMap(users::save) : Mono.just(user))
|
||||
.filter(user -> user.isAdmin())
|
||||
.doOnNext(user -> admins.grant(user.getEmail(), true))
|
||||
.count()
|
||||
.subscribe(n -> log.info("Mail administrators from Jambotron: {}", n),
|
||||
e -> log.error("Mail administrators from Jambotron not loaded: {}", e.getMessage()));
|
||||
.subscribe(n -> log.info("Mail administrators (role ADMIN): {}", n),
|
||||
e -> log.error("Mail administrators not loaded: {}", e.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,13 @@ public class MailboxAdminController {
|
||||
private static final Logger log = LoggerFactory.getLogger(MailboxAdminController.class);
|
||||
private static final int MIN_PASSWORD = MailDirectory.MIN_PASSWORD;
|
||||
|
||||
/** @param role ADMIN or USER; @param builtIn the administrator from the configuration — the role stays */
|
||||
public record MailboxView(Long id, String email, String displayName, boolean active, boolean admin, boolean trap,
|
||||
boolean shared, List<String> members, long usedBytes, long quotaBytes, Instant createdAt) {
|
||||
boolean shared, List<String> members, long usedBytes, long quotaBytes, Instant createdAt,
|
||||
String role, boolean builtIn) {
|
||||
}
|
||||
|
||||
public record RoleRequest(String role) {
|
||||
}
|
||||
|
||||
/** @param password empty — the server generates one and returns it once */
|
||||
@@ -171,6 +176,30 @@ public class MailboxAdminController {
|
||||
}));
|
||||
}
|
||||
|
||||
/** Role ADMIN (manages mailboxes, lists, roles — Admin panel) or USER. Not your own, not the built-in administrator. */
|
||||
@PutMapping("/{id}/role")
|
||||
public Mono<MailboxView> role(Mono<Authentication> auth, @PathVariable Long id, @RequestBody Mono<RoleRequest> body) {
|
||||
return admin(auth).flatMap(me -> body.flatMap(request -> mailbox(id).flatMap(user -> {
|
||||
if (!"ADMIN".equals(request.role()) && !"USER".equals(request.role())) {
|
||||
return Mono.error(bad("Role: ADMIN or USER"));
|
||||
}
|
||||
boolean toAdmin = "ADMIN".equals(request.role());
|
||||
if (user.getEmail().equalsIgnoreCase(me)) {
|
||||
return Mono.error(bad("You can not change your own role"));
|
||||
}
|
||||
if (!toAdmin && admins.isBuiltIn(user.getEmail())) {
|
||||
return Mono.error(bad("The built-in administrator stays ADMIN"));
|
||||
}
|
||||
if (toAdmin && user.isShared()) {
|
||||
return Mono.error(bad("A shared mailbox has no login of its own — it can not be ADMIN"));
|
||||
}
|
||||
return users.setAdmin(user, toAdmin).doOnNext(u -> {
|
||||
admins.grant(u.getEmail(), toAdmin);
|
||||
log.warn("Mailbox {} role {} given by administrator {}", u.getEmail(), request.role(), me);
|
||||
});
|
||||
}))).flatMap(this::view);
|
||||
}
|
||||
|
||||
/** Trap: what the mailbox sends is stored in its own INBOX, nothing leaves the server (staging@). */
|
||||
@PutMapping("/{id}/trap")
|
||||
public Mono<MailboxView> trap(Mono<Authentication> auth, @PathVariable Long id, @RequestBody Mono<TrapRequest> body) {
|
||||
@@ -237,7 +266,7 @@ public class MailboxAdminController {
|
||||
return mail.usedBytes(user.getId()).defaultIfEmpty(0L).zipWith(access.members(user.getId()).collectList())
|
||||
.map(pair -> new MailboxView(user.getId(), user.getEmail(), user.getDisplayName(), user.isActive(),
|
||||
admins.isAdmin(user.getEmail()), user.isTrap(), user.isShared(), pair.getT2(), pair.getT1(), user.getQuotaBytes() == null ? 0 : user.getQuotaBytes(),
|
||||
user.getCreatedAt()));
|
||||
user.getCreatedAt(), admins.isAdmin(user.getEmail()) ? "ADMIN" : "USER", admins.isBuiltIn(user.getEmail())));
|
||||
}
|
||||
|
||||
private static ResponseStatusException bad(String message) {
|
||||
|
||||
Reference in New Issue
Block a user