feat(sprint-3): Phase 1 — staff permissions + token revocation
- StaffPermission enum (8 granular permissions) - StaffAccount JPA entity with permissions collection - RevokedToken entity for JWT blacklisting - Flyway V3 migration (staff_accounts, staff_account_permissions, revoked_tokens) - StaffAccountRepository + RevokedTokenRepository - TokenRevocationService with Caffeine cache (60s TTL, 10k max) - StaffPermissionChecker SpEL bean (@staffPermissions.has) - PreventionOfficerChecker SpEL bean (@preventionOfficer.check) - JwtService: added jti claim + generateStaffAccessToken + extractJti/extractPermissions - JwtAuthFilter: token blacklist check via TokenRevocationService - SecurityConfig: STAFF role added to endpoint matchers - Controllers updated with @PreAuthorize for fine-grained access - TokenCleanupScheduler (daily 03:00 cleanup of expired revoked tokens) - Caffeine dependency added to cannamanage-service - Unit tests: StaffPermissionCheckerTest (7), TokenRevocationServiceTest (9)
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
package de.cannamanage.domain.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Stores revoked JWT tokens for token blacklist checking.
|
||||
* Tokens are identified by their JTI (JWT ID) claim.
|
||||
* Cleanup scheduler removes expired entries nightly.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "revoked_tokens", indexes = {
|
||||
@Index(name = "idx_revoked_tokens_jti", columnList = "jti", unique = true),
|
||||
@Index(name = "idx_revoked_tokens_user_id", columnList = "user_id"),
|
||||
@Index(name = "idx_revoked_tokens_expires_at", columnList = "expires_at")
|
||||
})
|
||||
public class RevokedToken {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
@Column(name = "id", nullable = false, updatable = false)
|
||||
private UUID id;
|
||||
|
||||
@Column(name = "jti", nullable = false, unique = true, length = 36)
|
||||
private String jti;
|
||||
|
||||
@Column(name = "user_id", nullable = false)
|
||||
private UUID userId;
|
||||
|
||||
@Column(name = "tenant_id", nullable = false)
|
||||
private UUID tenantId;
|
||||
|
||||
@Column(name = "revoked_at", nullable = false)
|
||||
private Instant revokedAt;
|
||||
|
||||
@Column(name = "expires_at", nullable = false)
|
||||
private Instant expiresAt;
|
||||
|
||||
@Column(name = "reason", length = 100)
|
||||
private String reason;
|
||||
|
||||
// --- Getters & Setters ---
|
||||
|
||||
public UUID getId() { return id; }
|
||||
public void setId(UUID id) { this.id = id; }
|
||||
|
||||
public String getJti() { return jti; }
|
||||
public void setJti(String jti) { this.jti = jti; }
|
||||
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
|
||||
public UUID getTenantId() { return tenantId; }
|
||||
public void setTenantId(UUID tenantId) { this.tenantId = tenantId; }
|
||||
|
||||
public Instant getRevokedAt() { return revokedAt; }
|
||||
public void setRevokedAt(Instant revokedAt) { this.revokedAt = revokedAt; }
|
||||
|
||||
public Instant getExpiresAt() { return expiresAt; }
|
||||
public void setExpiresAt(Instant expiresAt) { this.expiresAt = expiresAt; }
|
||||
|
||||
public String getReason() { return reason; }
|
||||
public void setReason(String reason) { this.reason = reason; }
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package de.cannamanage.domain.entity;
|
||||
|
||||
import de.cannamanage.domain.enums.StaffPermission;
|
||||
import jakarta.persistence.*;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Staff account with fine-grained permissions.
|
||||
* Links a user (STAFF role) to their granted permissions stored as JSONB.
|
||||
* One StaffAccount per user; permissions are a subset of StaffPermission enum values.
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "staff_accounts")
|
||||
public class StaffAccount extends AbstractTenantEntity {
|
||||
|
||||
@Column(name = "user_id", nullable = false, unique = true)
|
||||
private UUID userId;
|
||||
|
||||
@Column(name = "display_name", nullable = false, length = 150)
|
||||
private String displayName;
|
||||
|
||||
@ElementCollection(targetClass = StaffPermission.class, fetch = FetchType.EAGER)
|
||||
@CollectionTable(name = "staff_account_permissions",
|
||||
joinColumns = @JoinColumn(name = "staff_account_id"))
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "permission", nullable = false, length = 50)
|
||||
private Set<StaffPermission> grantedPermissions = new HashSet<>();
|
||||
|
||||
@Column(name = "is_prevention_officer", nullable = false)
|
||||
private boolean preventionOfficer = false;
|
||||
|
||||
@Column(name = "active", nullable = false)
|
||||
private boolean active = true;
|
||||
|
||||
@Column(name = "invited_at")
|
||||
private Instant invitedAt;
|
||||
|
||||
@Column(name = "activated_at")
|
||||
private Instant activatedAt;
|
||||
|
||||
// --- Getters & Setters ---
|
||||
|
||||
public UUID getUserId() { return userId; }
|
||||
public void setUserId(UUID userId) { this.userId = userId; }
|
||||
|
||||
public String getDisplayName() { return displayName; }
|
||||
public void setDisplayName(String displayName) { this.displayName = displayName; }
|
||||
|
||||
public Set<StaffPermission> getGrantedPermissions() { return grantedPermissions; }
|
||||
public void setGrantedPermissions(Set<StaffPermission> grantedPermissions) { this.grantedPermissions = grantedPermissions; }
|
||||
|
||||
public boolean isPreventionOfficer() { return preventionOfficer; }
|
||||
public void setPreventionOfficer(boolean preventionOfficer) { this.preventionOfficer = preventionOfficer; }
|
||||
|
||||
public boolean isActive() { return active; }
|
||||
public void setActive(boolean active) { this.active = active; }
|
||||
|
||||
public Instant getInvitedAt() { return invitedAt; }
|
||||
public void setInvitedAt(Instant invitedAt) { this.invitedAt = invitedAt; }
|
||||
|
||||
public Instant getActivatedAt() { return activatedAt; }
|
||||
public void setActivatedAt(Instant activatedAt) { this.activatedAt = activatedAt; }
|
||||
|
||||
public boolean hasPermission(StaffPermission permission) {
|
||||
return grantedPermissions.contains(permission);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package de.cannamanage.domain.enums;
|
||||
|
||||
/**
|
||||
* Fine-grained permissions for STAFF role users.
|
||||
* Admins implicitly have all permissions.
|
||||
* Staff members are granted a subset via their StaffAccount.
|
||||
*/
|
||||
public enum StaffPermission {
|
||||
RECORD_DISTRIBUTION,
|
||||
VIEW_MEMBER_LIST,
|
||||
VIEW_MEMBER_QUOTA,
|
||||
ADD_MEMBER,
|
||||
VIEW_STOCK,
|
||||
RECORD_STOCK_IN,
|
||||
VIEW_COMPLIANCE_REPORT,
|
||||
MANAGE_GROW_CALENDAR
|
||||
}
|
||||
Reference in New Issue
Block a user