Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import gg.modl.backend.audit.service.AdminDatabaseBrowserService;
import gg.modl.backend.audit.service.AuditService;
import gg.modl.backend.audit.service.StaffPerformanceService;
import gg.modl.backend.infrastructure.exception.ForbiddenException;
import gg.modl.backend.infrastructure.authorization.PanelAccessRule;
import gg.modl.backend.infrastructure.authorization.RequiresPanelPermission;
import gg.modl.backend.infrastructure.exception.ValidationException;
import gg.modl.backend.infrastructure.rest.RESTMappingV1;
Expand Down Expand Up @@ -52,7 +52,6 @@ public class AuditController {
private final AuditService auditService;
private final AdminDatabaseBrowserService adminDatabaseBrowserService;
private final StaffPerformanceService staffPerformanceService;
private final PermissionService permissionService;
private final RealtimeEventPublisher realtimeEventPublisher;

@GetMapping("/staff-performance")
Expand Down Expand Up @@ -98,14 +97,14 @@ public ResponseEntity<PunishmentAuditListResponse> getPunishments(
}

@GetMapping("/database/{table}")
@RequiresPanelPermission(rule = PanelAccessRule.SUPER_ADMIN)
public ResponseEntity<AuditDatabaseTableResponse> getDatabaseTable(
@PathVariable String table,
@RequestParam(defaultValue = "100") @Min(RequestValidationLimits.PAGINATION_LIMIT_MIN) @Max(RequestValidationLimits.PAGINATION_LIMIT_MAX) int limit,
@RequestParam(defaultValue = "0") @Min(0) int skip,
HttpServletRequest request
) {
Server server = RequestUtil.getRequestServer(request);
requireSuperAdmin(server, request);

if (!AdminDatabaseBrowserService.ALLOWED_TABLES.contains(table)) {
throw new ValidationException("Invalid table name");
Expand All @@ -116,14 +115,13 @@ public ResponseEntity<AuditDatabaseTableResponse> getDatabaseTable(
}

@PostMapping("/punishments/{id}/rollback")
@RequiresPanelPermission(PermissionService.ADMIN_AUDIT_ROLLBACK)
@RequiresPanelPermission(rule = PanelAccessRule.SUPER_ADMIN, supersedesPermissions = PermissionService.ADMIN_AUDIT_ROLLBACK)
public ResponseEntity<AuditRollbackResponse> rollbackPunishment(
@PathVariable String id,
@RequestBody(required = false) RollbackRequest rollbackRequest,
HttpServletRequest request
) {
Server server = RequestUtil.getRequestServer(request);
requireSuperAdmin(server, request);
String performerUsername = RequestUtil.getCurrentUsername(request);

String reason = rollbackRequest != null && rollbackRequest.hasReason()
Expand All @@ -139,14 +137,13 @@ public ResponseEntity<AuditRollbackResponse> rollbackPunishment(
}

@PostMapping("/staff/{username}/rollback-all")
@RequiresPanelPermission(PermissionService.ADMIN_AUDIT_ROLLBACK)
@RequiresPanelPermission(rule = PanelAccessRule.SUPER_ADMIN, supersedesPermissions = PermissionService.ADMIN_AUDIT_ROLLBACK)
public ResponseEntity<AuditBulkOperationResponse> rollbackAllByStaff(
@PathVariable String username,
@RequestBody(required = false) RollbackRequest rollbackRequest,
HttpServletRequest request
) {
Server server = RequestUtil.getRequestServer(request);
requireSuperAdmin(server, request);
String performerUsername = RequestUtil.getCurrentUsername(request);

String reason = rollbackRequest != null && rollbackRequest.hasReason()
Expand All @@ -159,14 +156,13 @@ public ResponseEntity<AuditBulkOperationResponse> rollbackAllByStaff(
}

@PostMapping("/staff/{username}/rollback-date-range")
@RequiresPanelPermission(PermissionService.ADMIN_AUDIT_ROLLBACK)
@RequiresPanelPermission(rule = PanelAccessRule.SUPER_ADMIN, supersedesPermissions = PermissionService.ADMIN_AUDIT_ROLLBACK)
public ResponseEntity<AuditBulkOperationResponse> rollbackByDateRange(
@PathVariable String username,
@RequestBody DateRangeRollbackRequest rollbackRequest,
HttpServletRequest request
) {
Server server = RequestUtil.getRequestServer(request);
requireSuperAdmin(server, request);
String performerUsername = RequestUtil.getCurrentUsername(request);

Date startDate = AuditProtoMapper.toDate(rollbackRequest.getStartDate());
Expand All @@ -191,14 +187,12 @@ public ResponseEntity<AuditBulkOperationResponse> rollbackByDateRange(
}

@PostMapping("/punishments/bulk-pardon")
@RequiresPanelPermission(PermissionService.ADMIN_AUDIT_ROLLBACK)
@RequiresPanelPermission(rule = PanelAccessRule.SUPER_ADMIN, supersedesPermissions = PermissionService.ADMIN_AUDIT_ROLLBACK)
public ResponseEntity<AuditBulkOperationResponse> bulkPardon(
@RequestBody BulkPunishmentActionRequest actionRequest,
HttpServletRequest request
) {
Server server = RequestUtil.getRequestServer(request);
requireSuperAdmin(server, request);

String performerUsername = RequestUtil.getCurrentUsername(request);
int count = auditService.bulkPardonByType(
server, actionRequest.getTypeOrdinalsList(), actionRequest.getReason(), performerUsername);
Expand All @@ -209,13 +203,12 @@ public ResponseEntity<AuditBulkOperationResponse> bulkPardon(
}

@PostMapping("/punishments/bulk-set-expiration")
@RequiresPanelPermission(PermissionService.ADMIN_AUDIT_ROLLBACK)
@RequiresPanelPermission(rule = PanelAccessRule.SUPER_ADMIN, supersedesPermissions = PermissionService.ADMIN_AUDIT_ROLLBACK)
public ResponseEntity<AuditBulkOperationResponse> bulkSetExpiration(
@RequestBody BulkPunishmentActionRequest actionRequest,
HttpServletRequest request
) {
Server server = RequestUtil.getRequestServer(request);
requireSuperAdmin(server, request);

if (!actionRequest.hasNewDurationMs()) {
throw new ValidationException("newDurationMs is required for set-expiration");
Expand All @@ -231,13 +224,6 @@ public ResponseEntity<AuditBulkOperationResponse> bulkSetExpiration(
true, count, "Successfully updated expiration for " + count + " punishments"));
}

private void requireSuperAdmin(Server server, HttpServletRequest request) {
String email = RequestUtil.getSessionEmail(request);
if (!permissionService.isSuperAdmin(server, email)) {
throw new ForbiddenException("Only super admins can perform this action");
}
}

private void invalidateAudit(Server server) {
realtimeEventPublisher.invalidatePanel(server, PanelResource.PANEL_RESOURCE_AUDIT);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
import gg.modl.backend.auth.session.SessionService;
import gg.modl.backend.infrastructure.rest.RESTMappingV1;
import gg.modl.backend.infrastructure.rest.RequestUtil;
import gg.modl.backend.role.data.StaffRole;
import gg.modl.backend.role.service.PermissionService;
import gg.modl.backend.role.service.RoleAuthorization;
import gg.modl.backend.server.data.Server;
Expand Down Expand Up @@ -57,6 +56,7 @@ public class PanelAuthController {
private final StaffProfileService staffProfileService;
private final StaffLookupCache staffLookupCache;
private final PermissionService permissionService;
private final RoleAuthorization roleAuthorization;
private final CookieUtil cookieUtil;
private final EmailChangeService emailChangeService;

Expand Down Expand Up @@ -158,7 +158,7 @@ public ResponseEntity<?> updateProfile(
return ResponseEntity.status(404).body(PanelAuthProtoMapper.toAuthResponse(false, "Staff member not found"));
}
Staff staff = result.get();
String role = isSuperAdmin ? RoleAuthorization.SUPER_ADMIN_ROLE_NAME : permissionService.resolveRoleName(server, staff.getRoleId());
String role = isSuperAdmin ? RoleAuthorization.SUPER_ADMIN_ROLE_NAME : permissionService.effectiveRoleName(server, staff);
String minecraftUsername = minecraftUsernameOrPanel(staff);
return ResponseEntity.ok(PanelAuthProtoMapper.toProfileResponse(
staff.getId(), staff.getEmail(), staff.getUsername(), role, minecraftUsername, staff.getLanguage(), staff.getDateFormat()));
Expand Down Expand Up @@ -214,7 +214,7 @@ public ResponseEntity<?> getCurrentUser(HttpServletRequest request) {

if (staffOpt.isPresent()) {
Staff staff = staffOpt.get();
String role = isSuperAdmin ? RoleAuthorization.SUPER_ADMIN_ROLE_NAME : permissionService.resolveRoleName(server, staff.getRoleId());
String role = isSuperAdmin ? RoleAuthorization.SUPER_ADMIN_ROLE_NAME : permissionService.effectiveRoleName(server, staff);
String minecraftUsername = minecraftUsernameOrPanel(staff);
return ResponseEntity.ok(PanelAuthProtoMapper.toProfileResponse(
staff.getId(), staff.getEmail(), staff.getUsername(), role, minecraftUsername, staff.getLanguage(), staff.getDateFormat()));
Expand Down Expand Up @@ -321,20 +321,8 @@ public ResponseEntity<PanelPermissionsResponse> getUserPermissions(HttpServletRe
}

Server server = RequestUtil.getRequestServer(request);
List<String> permissions = roleAuthorization.effectivePermissionIds(server, roleAuthorization.panelPerformer(server, email));

if (permissionService.isSuperAdmin(server, email)) {
return ResponseEntity.ok(PanelAuthProtoMapper.toPermissionsResponse(permissionService.getAllPermissionIds(server)));
}

Optional<Staff> staffOpt = staffLookupCache.findByEmail(server, email);
if (staffOpt.isEmpty()) {
return ResponseEntity.ok(PanelAuthProtoMapper.toPermissionsResponse(List.of()));
}

String roleId = RoleAuthorization.effectiveRoleId(server, staffOpt.get());
Optional<StaffRole> roleOpt = permissionService.getRoleById(server, roleId);

return roleOpt.map(staffRole -> ResponseEntity.ok(PanelAuthProtoMapper.toPermissionsResponse(staffRole.getPermissions())))
.orElseGet(() -> ResponseEntity.ok(PanelAuthProtoMapper.toPermissionsResponse(List.of())));
return ResponseEntity.ok(PanelAuthProtoMapper.toPermissionsResponse(permissions));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@

import gg.modl.backend.billing.service.BillingService;
import gg.modl.backend.billing.service.UsageTrackingService;
import gg.modl.backend.infrastructure.authorization.PanelAccessRule;
import gg.modl.backend.infrastructure.authorization.RequiresPanelPermission;
import gg.modl.backend.infrastructure.rest.RESTMappingV1;
import gg.modl.backend.infrastructure.rest.RequestUtil;
import gg.modl.backend.role.service.PermissionService;
import gg.modl.backend.server.data.Server;
import gg.modl.proto.modl.v1.BillingStatusResponse;
import gg.modl.proto.modl.v1.CancelResponse;
Expand All @@ -29,7 +31,8 @@

@RestController
@RequestMapping(RESTMappingV1.PANEL_BILLING)
@RequiresPanelPermission(view = "admin.settings.view.billing", modify = "admin.settings.modify.billing")
@RequiresPanelPermission(rule = PanelAccessRule.SUPER_ADMIN,
supersedesPermissions = {PermissionService.ADMIN_SETTINGS_VIEW_BILLING, PermissionService.ADMIN_SETTINGS_MODIFY_BILLING})
@RequiredArgsConstructor
public class PanelBillingController {
private final BillingService billingService;
Expand All @@ -39,50 +42,40 @@ public class PanelBillingController {
public ResponseEntity<CheckoutSessionResponse> createCheckoutSession(HttpServletRequest request) {
billingService.requireStripeConfigured();
Server server = RequestUtil.getRequestServer(request);
billingService.requireSuperAdmin(server, RequestUtil.getSessionEmail(request));

return ResponseEntity.ok(PanelBillingProtoMapper.toCheckoutSessionResponse(billingService.createCheckoutSession(server)));
}

@PostMapping("/portal-session")
public ResponseEntity<PortalSessionResponse> createPortalSession(HttpServletRequest request) {
billingService.requireStripeConfigured();
Server server = RequestUtil.getRequestServer(request);
billingService.requireSuperAdmin(server, RequestUtil.getSessionEmail(request));

return ResponseEntity.ok(PanelBillingProtoMapper.toPortalSessionResponse(billingService.createPortalSession(server)));
}

@PostMapping("/cancel")
public ResponseEntity<CancelResponse> cancelSubscription(HttpServletRequest request) {
billingService.requireStripeConfigured();
Server server = RequestUtil.getRequestServer(request);
billingService.requireSuperAdmin(server, RequestUtil.getSessionEmail(request));

return ResponseEntity.ok(PanelBillingProtoMapper.toCancelResponse(billingService.cancelSubscription(server)));
}

@PostMapping("/resubscribe")
public ResponseEntity<ResubscribeResponse> resubscribe(HttpServletRequest request) {
billingService.requireStripeConfigured();
Server server = RequestUtil.getRequestServer(request);
billingService.requireSuperAdmin(server, RequestUtil.getSessionEmail(request));

return ResponseEntity.ok(PanelBillingProtoMapper.toResubscribeResponse(billingService.resubscribe(server)));
}

@GetMapping("/status")
public ResponseEntity<BillingStatusResponse> getBillingStatus(HttpServletRequest request) {
Server server = RequestUtil.getRequestServer(request);
billingService.requireSuperAdmin(server, RequestUtil.getSessionEmail(request));
billingService.reconcileBillingStatus(server);
return ResponseEntity.ok(PanelBillingProtoMapper.toBillingStatusResponse(billingService.getBillingStatus(server)));
}

@GetMapping("/usage")
public ResponseEntity<UsageResponse> getUsage(HttpServletRequest request) {
Server server = RequestUtil.getRequestServer(request);
billingService.requireSuperAdmin(server, RequestUtil.getSessionEmail(request));
return ResponseEntity.ok(PanelBillingProtoMapper.toUsageResponse(usageTrackingService.getUsage(server)));
}

Expand All @@ -93,8 +86,6 @@ public ResponseEntity<UsageBillingSettingsResponse> updateUsageBillingSettings(
) {
billingService.requireStripeConfigured();
Server server = RequestUtil.getRequestServer(request);
billingService.requireSuperAdmin(server, RequestUtil.getSessionEmail(request));

return ResponseEntity.ok(PanelBillingProtoMapper.toUsageBillingSettingsResponse(
usageTrackingService.updateUsageBillingSettings(server, settingsRequest.getEnabled())));
}
Expand All @@ -105,7 +96,6 @@ public ResponseEntity<UpdateStorageLimitResponse> updateStorageLimit(
HttpServletRequest request
) {
Server server = RequestUtil.getRequestServer(request);
billingService.requireSuperAdmin(server, RequestUtil.getSessionEmail(request));

long maxStorageLimitBytes = body.getMaxStorageLimitBytes();
usageTrackingService.updateStorageLimit(server, maxStorageLimitBytes);
Expand All @@ -119,7 +109,6 @@ public ResponseEntity<UpdateOverageLimitsResponse> updateOverageLimits(
HttpServletRequest request
) {
Server server = RequestUtil.getRequestServer(request);
billingService.requireSuperAdmin(server, RequestUtil.getSessionEmail(request));

int maxStorageOverageGB = body.hasMaxStorageOverageGbValue() ? body.getMaxStorageOverageGbValue() : body.getMaxStorageOverageGb();
int maxAiOverageRequests = body.hasMaxAiOverageRequestsValue() ? body.getMaxAiOverageRequestsValue() : body.getMaxAiOverageRequests();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@
import com.stripe.model.checkout.Session;
import gg.modl.backend.infrastructure.exception.ConflictException;
import gg.modl.backend.infrastructure.exception.ExternalServiceException;
import gg.modl.backend.infrastructure.exception.ForbiddenException;
import gg.modl.backend.infrastructure.exception.ResourceNotFoundException;
import gg.modl.backend.billing.dto.response.BillingStatusResponse;
import gg.modl.backend.billing.dto.response.CancelResponse;
import gg.modl.backend.billing.dto.response.CheckoutSessionResponse;
import gg.modl.backend.billing.dto.response.PortalSessionResponse;
import gg.modl.backend.billing.dto.response.ResubscribeResponse;
import gg.modl.backend.role.service.PermissionService;
import gg.modl.backend.server.data.Server;
import gg.modl.backend.server.data.ServerPlan;
import gg.modl.backend.server.data.SubscriptionStatus;
Expand All @@ -28,20 +26,13 @@
public class BillingService {
private final StripeService stripeService;
private final ServerMutationHelper serverMutationHelper;
private final PermissionService permissionService;

public void requireStripeConfigured() {
if (!stripeService.isConfigured()) {
throw new ExternalServiceException("Billing service unavailable. Stripe not configured.");
}
}

public void requireSuperAdmin(Server server, String email) {
if (email == null || !permissionService.isSuperAdmin(server, email)) {
throw new ForbiddenException("Only the super admin can manage billing");
}
}

public void syncCustomerEmail(Server server, String newEmail) {
String customerId = server.getStripeCustomerId();
if (!stripeService.isConfigured() || customerId == null || customerId.isBlank()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,17 @@ public List<PanelAccessPolicy> resolvePolicies(HttpServletRequest request) {
}

public Optional<PanelAccessPolicy> resolve(HandlerMethod handlerMethod) {
return Optional.ofNullable(findAnnotation(handlerMethod)).map(PanelAccessPolicyResolver::toPolicy);
return resolveAnnotation(handlerMethod).map(PanelAccessPolicyResolver::toPolicy);
}

public Optional<RequiresPanelPermission> resolveAnnotation(HandlerMethod handlerMethod) {
RequiresPanelPermission methodAnnotation = handlerMethod.getMethodAnnotation(RequiresPanelPermission.class);
return methodAnnotation != null ? Optional.of(methodAnnotation) : resolveTypeAnnotation(handlerMethod);
}

public Optional<RequiresPanelPermission> resolveTypeAnnotation(HandlerMethod handlerMethod) {
return Optional.ofNullable(
AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), RequiresPanelPermission.class));
}

private List<PanelAccessPolicy> siblingPolicies(RequestMappingHandlerMapping handlerMapping, HttpServletRequest request) {
Expand All @@ -71,20 +81,13 @@ private boolean ensureParsedRequestPath(RequestMappingHandlerMapping handlerMapp
return true;
}

private static RequiresPanelPermission findAnnotation(HandlerMethod handlerMethod) {
RequiresPanelPermission methodAnnotation = handlerMethod.getMethodAnnotation(RequiresPanelPermission.class);
if (methodAnnotation != null) {
return methodAnnotation;
}
return AnnotatedElementUtils.findMergedAnnotation(handlerMethod.getBeanType(), RequiresPanelPermission.class);
}

private static PanelAccessPolicy toPolicy(RequiresPanelPermission annotation) {
return switch (annotation.rule()) {
case PERMIT_ALL -> PermitAllPolicy.INSTANCE;
case PLAYER_ACCESS -> PlayerAccessPolicy.INSTANCE;
case PUNISHMENT_TYPE_ACCESS -> PunishmentTypeAccessPolicy.INSTANCE;
case APPEAL_REPLY -> AppealReplyPolicy.INSTANCE;
case SUPER_ADMIN -> SuperAdminOnlyPolicy.INSTANCE;
case REQUIRE_PERMISSION -> new ReadWritePermissionPolicy(viewPermission(annotation), modifyPermission(annotation));
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@ public enum PanelAccessRule {
PERMIT_ALL,
PLAYER_ACCESS,
PUNISHMENT_TYPE_ACCESS,
APPEAL_REPLY
APPEAL_REPLY,
SUPER_ADMIN
}
Loading
Loading