chore: delete dead auth code — users/roles/JWTs/ForwardAuth live in Logto now
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,60 +0,0 @@
|
||||
package net.siegeln.cameleer.saas.auth;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtService jwtService;
|
||||
|
||||
public JwtAuthenticationFilter(JwtService jwtService) {
|
||||
this.jwtService = jwtService;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
|
||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
String token = authHeader.substring(7);
|
||||
|
||||
if (!jwtService.isTokenValid(token)) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
String email = jwtService.extractEmail(token);
|
||||
var userId = jwtService.extractUserId(token);
|
||||
var roles = jwtService.extractRoles(token);
|
||||
|
||||
var authorities = roles.stream()
|
||||
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
|
||||
.toList();
|
||||
|
||||
var authentication = new UsernamePasswordAuthenticationToken(
|
||||
email, userId, authorities
|
||||
);
|
||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
@@ -1,120 +0,0 @@
|
||||
package net.siegeln.cameleer.saas.auth;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import net.siegeln.cameleer.saas.config.JwtConfig;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.security.Signature;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class JwtService {
|
||||
|
||||
private final JwtConfig jwtConfig;
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
public JwtService(JwtConfig jwtConfig) {
|
||||
this.jwtConfig = jwtConfig;
|
||||
this.objectMapper = new ObjectMapper();
|
||||
}
|
||||
|
||||
public String generateToken(UserEntity user) {
|
||||
try {
|
||||
String header = base64UrlEncode(objectMapper.writeValueAsBytes(
|
||||
Map.of("alg", "EdDSA", "typ", "JWT")
|
||||
));
|
||||
|
||||
Instant now = Instant.now();
|
||||
Map<String, Object> payload = new LinkedHashMap<>();
|
||||
payload.put("sub", user.getEmail());
|
||||
payload.put("uid", user.getId().toString());
|
||||
payload.put("name", user.getName());
|
||||
payload.put("roles", user.getRoles().stream()
|
||||
.map(RoleEntity::getName)
|
||||
.toList());
|
||||
payload.put("iat", now.getEpochSecond());
|
||||
payload.put("exp", now.getEpochSecond() + jwtConfig.getExpirationSeconds());
|
||||
|
||||
String payloadEncoded = base64UrlEncode(objectMapper.writeValueAsBytes(payload));
|
||||
|
||||
String signingInput = header + "." + payloadEncoded;
|
||||
Signature sig = Signature.getInstance("Ed25519");
|
||||
sig.initSign(jwtConfig.getPrivateKey());
|
||||
sig.update(signingInput.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
String signature = base64UrlEncode(sig.sign());
|
||||
|
||||
return signingInput + "." + signature;
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to generate JWT", e);
|
||||
}
|
||||
}
|
||||
|
||||
public String extractEmail(String token) {
|
||||
Map<String, Object> payload = parsePayload(token);
|
||||
return (String) payload.get("sub");
|
||||
}
|
||||
|
||||
public UUID extractUserId(String token) {
|
||||
Map<String, Object> payload = parsePayload(token);
|
||||
return UUID.fromString((String) payload.get("uid"));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public Set<String> extractRoles(String token) {
|
||||
Map<String, Object> payload = parsePayload(token);
|
||||
List<String> roles = (List<String>) payload.get("roles");
|
||||
return roles.stream().collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
public boolean isTokenValid(String token) {
|
||||
try {
|
||||
String[] parts = token.split("\\.");
|
||||
if (parts.length != 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String signingInput = parts[0] + "." + parts[1];
|
||||
byte[] signatureBytes = base64UrlDecode(parts[2]);
|
||||
|
||||
Signature sig = Signature.getInstance("Ed25519");
|
||||
sig.initVerify(jwtConfig.getPublicKey());
|
||||
sig.update(signingInput.getBytes(java.nio.charset.StandardCharsets.UTF_8));
|
||||
|
||||
if (!sig.verify(signatureBytes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Map<String, Object> payload = parsePayload(token);
|
||||
long exp = ((Number) payload.get("exp")).longValue();
|
||||
return Instant.now().getEpochSecond() < exp;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Map<String, Object> parsePayload(String token) {
|
||||
try {
|
||||
String[] parts = token.split("\\.");
|
||||
byte[] payloadBytes = base64UrlDecode(parts[1]);
|
||||
return objectMapper.readValue(payloadBytes, new TypeReference<>() {});
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("Failed to parse JWT payload", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String base64UrlEncode(byte[] data) {
|
||||
return Base64.getUrlEncoder().withoutPadding().encodeToString(data);
|
||||
}
|
||||
|
||||
private byte[] base64UrlDecode(String data) {
|
||||
return Base64.getUrlDecoder().decode(data);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
package net.siegeln.cameleer.saas.auth;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "permissions")
|
||||
public class PermissionEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@Column(name = "name", nullable = false, unique = true, length = 100)
|
||||
private String name;
|
||||
|
||||
@Column(name = "description")
|
||||
private String description;
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package net.siegeln.cameleer.saas.auth;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.JoinTable;
|
||||
import jakarta.persistence.ManyToMany;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "roles")
|
||||
public class RoleEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@Column(name = "name", nullable = false, unique = true, length = 50)
|
||||
private String name;
|
||||
|
||||
@Column(name = "description")
|
||||
private String description;
|
||||
|
||||
@Column(name = "built_in", nullable = false)
|
||||
private boolean builtIn;
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
@ManyToMany(fetch = FetchType.EAGER)
|
||||
@JoinTable(
|
||||
name = "role_permissions",
|
||||
joinColumns = @JoinColumn(name = "role_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "permission_id")
|
||||
)
|
||||
private Set<PermissionEntity> permissions = new HashSet<>();
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
if (createdAt == null) {
|
||||
createdAt = Instant.now();
|
||||
}
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public boolean isBuiltIn() {
|
||||
return builtIn;
|
||||
}
|
||||
|
||||
public void setBuiltIn(boolean builtIn) {
|
||||
this.builtIn = builtIn;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public Set<PermissionEntity> getPermissions() {
|
||||
return permissions;
|
||||
}
|
||||
|
||||
public void setPermissions(Set<PermissionEntity> permissions) {
|
||||
this.permissions = permissions;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package net.siegeln.cameleer.saas.auth;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface RoleRepository extends JpaRepository<RoleEntity, UUID> {
|
||||
|
||||
Optional<RoleEntity> findByName(String name);
|
||||
}
|
||||
@@ -1,122 +0,0 @@
|
||||
package net.siegeln.cameleer.saas.auth;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.FetchType;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.JoinTable;
|
||||
import jakarta.persistence.ManyToMany;
|
||||
import jakarta.persistence.PrePersist;
|
||||
import jakarta.persistence.PreUpdate;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class UserEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@Column(name = "email", nullable = false, unique = true)
|
||||
private String email;
|
||||
|
||||
@Column(name = "password", nullable = false)
|
||||
private String password;
|
||||
|
||||
@Column(name = "name", nullable = false)
|
||||
private String name;
|
||||
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
private String status = "ACTIVE";
|
||||
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private Instant createdAt;
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
private Instant updatedAt;
|
||||
|
||||
@ManyToMany(fetch = FetchType.EAGER)
|
||||
@JoinTable(
|
||||
name = "user_roles",
|
||||
joinColumns = @JoinColumn(name = "user_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "role_id")
|
||||
)
|
||||
private Set<RoleEntity> roles = new HashSet<>();
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
Instant now = Instant.now();
|
||||
if (createdAt == null) {
|
||||
createdAt = now;
|
||||
}
|
||||
if (updatedAt == null) {
|
||||
updatedAt = now;
|
||||
}
|
||||
}
|
||||
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = Instant.now();
|
||||
}
|
||||
|
||||
public UUID getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getEmail() {
|
||||
return email;
|
||||
}
|
||||
|
||||
public void setEmail(String email) {
|
||||
this.email = email;
|
||||
}
|
||||
|
||||
public String getPassword() {
|
||||
return password;
|
||||
}
|
||||
|
||||
public void setPassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Instant getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
|
||||
public Instant getUpdatedAt() {
|
||||
return updatedAt;
|
||||
}
|
||||
|
||||
public Set<RoleEntity> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
|
||||
public void setRoles(Set<RoleEntity> roles) {
|
||||
this.roles = roles;
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
package net.siegeln.cameleer.saas.auth;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<UserEntity, UUID> {
|
||||
|
||||
Optional<UserEntity> findByEmail(String email);
|
||||
|
||||
boolean existsByEmail(String email);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package net.siegeln.cameleer.saas.config;
|
||||
|
||||
import net.siegeln.cameleer.saas.auth.JwtService;
|
||||
import net.siegeln.cameleer.saas.tenant.TenantService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
|
||||
@RestController
|
||||
public class ForwardAuthController {
|
||||
|
||||
private final JwtService jwtService;
|
||||
private final TenantService tenantService;
|
||||
|
||||
public ForwardAuthController(JwtService jwtService, TenantService tenantService) {
|
||||
this.jwtService = jwtService;
|
||||
this.tenantService = tenantService;
|
||||
}
|
||||
|
||||
@GetMapping("/auth/verify")
|
||||
public ResponseEntity<Void> verify(HttpServletRequest request) {
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
|
||||
String token = authHeader.substring(7);
|
||||
|
||||
if (jwtService.isTokenValid(token)) {
|
||||
String email = jwtService.extractEmail(token);
|
||||
var userId = jwtService.extractUserId(token);
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header("X-User-Id", userId.toString())
|
||||
.header("X-User-Email", email)
|
||||
.build();
|
||||
}
|
||||
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package net.siegeln.cameleer.saas.config;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.util.Base64;
|
||||
|
||||
@Component
|
||||
public class JwtConfig {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(JwtConfig.class);
|
||||
|
||||
@Value("${cameleer.jwt.expiration:86400}")
|
||||
private long expirationSeconds = 86400;
|
||||
|
||||
@Value("${cameleer.jwt.private-key-path:}")
|
||||
private String privateKeyPath = "";
|
||||
|
||||
@Value("${cameleer.jwt.public-key-path:}")
|
||||
private String publicKeyPath = "";
|
||||
|
||||
private KeyPair keyPair;
|
||||
|
||||
@PostConstruct
|
||||
public void init() throws NoSuchAlgorithmException, IOException, InvalidKeySpecException {
|
||||
if (privateKeyPath.isEmpty() || publicKeyPath.isEmpty()) {
|
||||
log.warn("No Ed25519 key files configured — generating ephemeral keys (dev mode)");
|
||||
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("Ed25519");
|
||||
this.keyPair = keyGen.generateKeyPair();
|
||||
} else {
|
||||
log.info("Loading Ed25519 keys from {} and {}", privateKeyPath, publicKeyPath);
|
||||
PrivateKey privateKey = loadPrivateKey(Path.of(privateKeyPath));
|
||||
PublicKey publicKey = loadPublicKey(Path.of(publicKeyPath));
|
||||
this.keyPair = new KeyPair(publicKey, privateKey);
|
||||
}
|
||||
}
|
||||
|
||||
private PrivateKey loadPrivateKey(Path path) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
|
||||
String pem = Files.readString(path)
|
||||
.replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.replace("-----END PRIVATE KEY-----", "")
|
||||
.replaceAll("\\s+", "");
|
||||
byte[] decoded = Base64.getDecoder().decode(pem);
|
||||
return KeyFactory.getInstance("Ed25519").generatePrivate(new PKCS8EncodedKeySpec(decoded));
|
||||
}
|
||||
|
||||
private PublicKey loadPublicKey(Path path) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
|
||||
String pem = Files.readString(path)
|
||||
.replace("-----BEGIN PUBLIC KEY-----", "")
|
||||
.replace("-----END PUBLIC KEY-----", "")
|
||||
.replaceAll("\\s+", "");
|
||||
byte[] decoded = Base64.getDecoder().decode(pem);
|
||||
return KeyFactory.getInstance("Ed25519").generatePublic(new X509EncodedKeySpec(decoded));
|
||||
}
|
||||
|
||||
public PrivateKey getPrivateKey() {
|
||||
return keyPair.getPrivate();
|
||||
}
|
||||
|
||||
public PublicKey getPublicKey() {
|
||||
return keyPair.getPublic();
|
||||
}
|
||||
|
||||
public long getExpirationSeconds() {
|
||||
return expirationSeconds;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_users_email ON users (email);
|
||||
CREATE INDEX idx_users_status ON users (status);
|
||||
@@ -1,25 +0,0 @@
|
||||
CREATE TABLE roles (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(50) NOT NULL UNIQUE,
|
||||
description VARCHAR(255),
|
||||
built_in BOOLEAN NOT NULL DEFAULT false,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE TABLE permissions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
name VARCHAR(100) NOT NULL UNIQUE,
|
||||
description VARCHAR(255)
|
||||
);
|
||||
|
||||
CREATE TABLE role_permissions (
|
||||
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||
permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (role_id, permission_id)
|
||||
);
|
||||
|
||||
CREATE TABLE user_roles (
|
||||
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (user_id, role_id)
|
||||
);
|
||||
@@ -1,37 +0,0 @@
|
||||
-- Permissions
|
||||
INSERT INTO permissions (id, name, description) VALUES
|
||||
('00000000-0000-0000-0000-000000000001', 'tenant:manage', 'Full tenant administration'),
|
||||
('00000000-0000-0000-0000-000000000002', 'billing:manage', 'Manage billing and subscriptions'),
|
||||
('00000000-0000-0000-0000-000000000003', 'team:manage', 'Manage team members and roles'),
|
||||
('00000000-0000-0000-0000-000000000004', 'apps:manage', 'Deploy, configure, and manage applications'),
|
||||
('00000000-0000-0000-0000-000000000005', 'apps:deploy', 'Deploy and promote applications'),
|
||||
('00000000-0000-0000-0000-000000000006', 'secrets:manage', 'Create and rotate secrets'),
|
||||
('00000000-0000-0000-0000-000000000007', 'observe:read', 'View traces, topology, dashboards'),
|
||||
('00000000-0000-0000-0000-000000000008', 'observe:debug', 'Use debugger and replay'),
|
||||
('00000000-0000-0000-0000-000000000009', 'settings:manage', 'Manage tenant settings');
|
||||
|
||||
-- Roles
|
||||
INSERT INTO roles (id, name, description, built_in) VALUES
|
||||
('10000000-0000-0000-0000-000000000001', 'OWNER', 'Full tenant admin including billing and deletion', true),
|
||||
('10000000-0000-0000-0000-000000000002', 'ADMIN', 'Manage apps, secrets, team. No billing.', true),
|
||||
('10000000-0000-0000-0000-000000000003', 'DEVELOPER', 'Deploy apps, view traces, use debugger.', true),
|
||||
('10000000-0000-0000-0000-000000000004', 'VIEWER', 'Read-only access to dashboards and traces.', true);
|
||||
|
||||
-- Owner: all permissions
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT '10000000-0000-0000-0000-000000000001', id FROM permissions;
|
||||
|
||||
-- Admin: everything except billing and tenant management
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT '10000000-0000-0000-0000-000000000002', id FROM permissions
|
||||
WHERE name NOT IN ('tenant:manage', 'billing:manage');
|
||||
|
||||
-- Developer: apps, secrets, observe (including debug)
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT '10000000-0000-0000-0000-000000000003', id FROM permissions
|
||||
WHERE name IN ('apps:deploy', 'secrets:manage', 'observe:read', 'observe:debug');
|
||||
|
||||
-- Viewer: observe read-only
|
||||
INSERT INTO role_permissions (role_id, permission_id)
|
||||
SELECT '10000000-0000-0000-0000-000000000004', id FROM permissions
|
||||
WHERE name = 'observe:read';
|
||||
@@ -1,104 +0,0 @@
|
||||
package net.siegeln.cameleer.saas.auth;
|
||||
|
||||
import net.siegeln.cameleer.saas.config.JwtConfig;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class JwtServiceTest {
|
||||
|
||||
private JwtService jwtService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
JwtConfig config = new JwtConfig();
|
||||
config.init();
|
||||
jwtService = new JwtService(config);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateToken_producesValidJwt() {
|
||||
UserEntity user = createUser("test@example.com", "OWNER");
|
||||
|
||||
String token = jwtService.generateToken(user);
|
||||
|
||||
assertNotNull(token);
|
||||
String[] parts = token.split("\\.");
|
||||
assertEquals(3, parts.length, "JWT should have 3 parts separated by dots");
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractEmail_returnsCorrectEmail() {
|
||||
UserEntity user = createUser("test@example.com", "OWNER");
|
||||
|
||||
String token = jwtService.generateToken(user);
|
||||
String email = jwtService.extractEmail(token);
|
||||
|
||||
assertEquals("test@example.com", email);
|
||||
}
|
||||
|
||||
@Test
|
||||
void isTokenValid_returnsTrueForValidToken() {
|
||||
UserEntity user = createUser("test@example.com", "OWNER");
|
||||
|
||||
String token = jwtService.generateToken(user);
|
||||
|
||||
assertTrue(jwtService.isTokenValid(token));
|
||||
}
|
||||
|
||||
@Test
|
||||
void isTokenValid_returnsFalseForTamperedToken() {
|
||||
UserEntity user = createUser("test@example.com", "OWNER");
|
||||
|
||||
String token = jwtService.generateToken(user);
|
||||
// Tamper with the last 5 characters of the signature
|
||||
String tampered = token.substring(0, token.length() - 5) + "XXXXX";
|
||||
|
||||
assertFalse(jwtService.isTokenValid(tampered));
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractRoles_returnsUserRoles() {
|
||||
UserEntity user = createUser("test@example.com", "OWNER");
|
||||
|
||||
String token = jwtService.generateToken(user);
|
||||
var roles = jwtService.extractRoles(token);
|
||||
|
||||
assertNotNull(roles);
|
||||
assertTrue(roles.contains("OWNER"));
|
||||
assertEquals(1, roles.size());
|
||||
}
|
||||
|
||||
@Test
|
||||
void extractUserId_returnsCorrectId() {
|
||||
UserEntity user = createUser("test@example.com", "OWNER");
|
||||
|
||||
String token = jwtService.generateToken(user);
|
||||
UUID extractedId = jwtService.extractUserId(token);
|
||||
|
||||
assertEquals(user.getId(), extractedId);
|
||||
}
|
||||
|
||||
private UserEntity createUser(String email, String roleName) {
|
||||
var role = new RoleEntity();
|
||||
role.setName(roleName);
|
||||
var user = new UserEntity();
|
||||
user.setEmail(email);
|
||||
user.setName("Test User");
|
||||
user.getRoles().add(role);
|
||||
try {
|
||||
var idField = UserEntity.class.getDeclaredField("id");
|
||||
idField.setAccessible(true);
|
||||
idField.set(user, UUID.randomUUID());
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user