mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 10:13:58 +00:00
refactor(authority): rebrand package org.prole → dev.knoe; raise test coverage to 50%
- Move all 31 Java files from org/prole/authority to dev/knoe/authority - Update all package declarations, imports, pom.xml groupId, and docs - Add 11 new test classes (PrincipalNormalizerTest, OidcCodeServiceTest, KadminClientTest, TotpServiceTest, HealthControllerTest, EnrollValueTypesTest, KerberosSpnegoResultTest, SessionServiceTest, EnrollmentControllerTest, AdminControllerTest, LoginControllerTest) - Add JaCoCo 0.8.12 + maven-surefire-plugin with Java 25 / Byte Buddy compat flags - Fix LoginController CSS format string bug (100% -> 100%%) - Result: 96 tests, 0 failures; 50.2% line / 46.5% instruction / 37.1% branch coverage Co-authored-by: Junie <junie@jetbrains.com>
This commit is contained in:
parent
9d5827b522
commit
9daa94b860
@ -10,7 +10,7 @@
|
||||
<relativePath/>
|
||||
</parent>
|
||||
|
||||
<groupId>org.prole</groupId>
|
||||
<groupId>dev.knoe</groupId>
|
||||
<artifactId>authority</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
<name>knoe-authority</name>
|
||||
@ -20,6 +20,7 @@
|
||||
<java.version>21</java.version>
|
||||
</properties>
|
||||
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
@ -41,7 +42,7 @@
|
||||
</dependency>
|
||||
<!-- Kerberos/Security dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<groupId>org.springframework.security.kerberos</groupId>
|
||||
<artifactId>spring-security-kerberos-client</artifactId>
|
||||
<version>2.0.1</version>
|
||||
</dependency>
|
||||
@ -103,6 +104,41 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<configuration>
|
||||
<argLine>@{argLine} -Dnet.bytebuddy.experimental=true</argLine>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jacoco</groupId>
|
||||
<artifactId>jacoco-maven-plugin</artifactId>
|
||||
<version>0.8.12</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals><goal>prepare-agent</goal></goals>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>java/**</exclude>
|
||||
<exclude>javax/**</exclude>
|
||||
<exclude>jdk/**</exclude>
|
||||
<exclude>sun/**</exclude>
|
||||
<exclude>com/sun/**</exclude>
|
||||
<exclude>org/ietf/**</exclude>
|
||||
<exclude>org/omg/**</exclude>
|
||||
<exclude>org/w3c/**</exclude>
|
||||
<exclude>org/xml/**</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>report</id>
|
||||
<phase>test</phase>
|
||||
<goals><goal>report</goal></goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package org.prole.authority;
|
||||
package dev.knoe.authority;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@ -1,4 +1,4 @@
|
||||
package org.prole.authority;
|
||||
package dev.knoe.authority;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
@ -1,7 +1,7 @@
|
||||
package org.prole.authority.admin;
|
||||
package dev.knoe.authority.admin;
|
||||
|
||||
import org.prole.authority.enroll.InviteService;
|
||||
import org.prole.authority.enroll.InviteService.InviteResult;
|
||||
import dev.knoe.authority.enroll.InviteService;
|
||||
import dev.knoe.authority.enroll.InviteService.InviteResult;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.http.MediaType;
|
||||
@ -1,4 +1,4 @@
|
||||
package org.prole.authority.admin;
|
||||
package dev.knoe.authority.admin;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -1,4 +1,4 @@
|
||||
package org.prole.authority.config;
|
||||
package dev.knoe.authority.config;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
@ -1,4 +1,4 @@
|
||||
package org.prole.authority.config;
|
||||
package dev.knoe.authority.config;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package org.prole.authority.enroll;
|
||||
package dev.knoe.authority.enroll;
|
||||
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.slf4j.Logger;
|
||||
@ -1,4 +1,4 @@
|
||||
package org.prole.authority.enroll;
|
||||
package dev.knoe.authority.enroll;
|
||||
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
|
||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
|
||||
@ -1,4 +1,4 @@
|
||||
package org.prole.authority.enroll;
|
||||
package dev.knoe.authority.enroll;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -1,4 +1,4 @@
|
||||
package org.prole.authority.enroll;
|
||||
package dev.knoe.authority.enroll;
|
||||
|
||||
import dev.samstevens.totp.code.CodeGenerator;
|
||||
import dev.samstevens.totp.code.CodeVerifier;
|
||||
@ -1,6 +1,6 @@
|
||||
package org.prole.authority.enroll;
|
||||
package dev.knoe.authority.enroll;
|
||||
|
||||
import org.prole.authority.kerberos.KadminClient;
|
||||
import dev.knoe.authority.kerberos.KadminClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
@ -1,4 +1,4 @@
|
||||
package org.prole.authority.kerberos;
|
||||
package dev.knoe.authority.kerberos;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
@ -1,4 +1,4 @@
|
||||
package org.prole.authority.kerberos;
|
||||
package dev.knoe.authority.kerberos;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
@ -1,4 +1,4 @@
|
||||
package org.prole.authority.kerberos;
|
||||
package dev.knoe.authority.kerberos;
|
||||
|
||||
import java.security.PrivilegedExceptionAction;
|
||||
import java.util.Map;
|
||||
@ -1,6 +1,6 @@
|
||||
package org.prole.authority.provisioning;
|
||||
package dev.knoe.authority.provisioning;
|
||||
|
||||
import org.prole.authority.admin.KnobjectService;
|
||||
import dev.knoe.authority.admin.KnobjectService;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
@ -1,4 +1,4 @@
|
||||
package org.prole.authority.session;
|
||||
package dev.knoe.authority.session;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.UUID;
|
||||
@ -1,13 +1,13 @@
|
||||
package org.prole.authority.session;
|
||||
package dev.knoe.authority.session;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import org.prole.authority.config.AuthProperties;
|
||||
import org.prole.authority.config.KerberosProperties;
|
||||
import dev.knoe.authority.config.AuthProperties;
|
||||
import dev.knoe.authority.config.KerberosProperties;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
@ -44,7 +44,7 @@ public class OidcTokenService {
|
||||
byte[] keyBytes = Base64.getDecoder().decode(signingKey);
|
||||
PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
|
||||
KeyFactory kf = KeyFactory.getInstance("RSA");
|
||||
PrivateKey privateKey = kf.generatePrivateKey(spec);
|
||||
PrivateKey privateKey = kf.generatePrivate(spec);
|
||||
|
||||
// Derive public key from private key (simplified for RSA)
|
||||
RSAPublicKey publicKey = (RSAPublicKey) kf.generatePublic(new java.security.spec.RSAPublicKeySpec(
|
||||
@ -1,6 +1,6 @@
|
||||
package org.prole.authority.session;
|
||||
package dev.knoe.authority.session;
|
||||
|
||||
import org.prole.authority.config.AuthProperties;
|
||||
import dev.knoe.authority.config.AuthProperties;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.util.WebUtils;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package org.prole.authority.session;
|
||||
package dev.knoe.authority.session;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
@ -1,4 +1,4 @@
|
||||
package org.prole.authority.session;
|
||||
package dev.knoe.authority.session;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package org.prole.authority.user;
|
||||
package dev.knoe.authority.user;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Optional;
|
||||
@ -1,7 +1,7 @@
|
||||
package org.prole.authority.web;
|
||||
package dev.knoe.authority.web;
|
||||
|
||||
import org.prole.authority.config.AuthProperties;
|
||||
import org.prole.authority.session.OidcTokenService;
|
||||
import dev.knoe.authority.config.AuthProperties;
|
||||
import dev.knoe.authority.session.OidcTokenService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@ -1,4 +1,4 @@
|
||||
package org.prole.authority.web;
|
||||
package dev.knoe.authority.web;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
@ -10,14 +10,14 @@ import jakarta.annotation.PostConstruct;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.prole.authority.config.AuthProperties;
|
||||
import org.prole.authority.config.KerberosProperties;
|
||||
import org.prole.authority.enroll.GoogleOAuthService;
|
||||
import org.prole.authority.kerberos.KerberosPasswordService;
|
||||
import org.prole.authority.kerberos.KerberosSpnegoService;
|
||||
import org.prole.authority.session.SessionTokenService;
|
||||
import org.prole.authority.session.SessionUser;
|
||||
import org.prole.authority.user.PrincipalNormalizer;
|
||||
import dev.knoe.authority.config.AuthProperties;
|
||||
import dev.knoe.authority.config.KerberosProperties;
|
||||
import dev.knoe.authority.enroll.GoogleOAuthService;
|
||||
import dev.knoe.authority.kerberos.KerberosPasswordService;
|
||||
import dev.knoe.authority.kerberos.KerberosSpnegoService;
|
||||
import dev.knoe.authority.session.SessionTokenService;
|
||||
import dev.knoe.authority.session.SessionUser;
|
||||
import dev.knoe.authority.user.PrincipalNormalizer;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
@ -83,7 +83,7 @@ public class LoginController {
|
||||
code { background: #f3f3f3; padding: 2px 4px; }
|
||||
.box { border: 1px solid #ddd; border-radius: 8px; padding: 16px; margin: 16px 0; }
|
||||
.row { margin: 8px 0; }
|
||||
input { width: 100%; padding: 8px; }
|
||||
input { width: 100%%; padding: 8px; }
|
||||
button { padding: 10px 14px; }
|
||||
</style>
|
||||
</head>
|
||||
@ -1,9 +1,9 @@
|
||||
package org.prole.authority.web;
|
||||
package dev.knoe.authority.web;
|
||||
|
||||
import org.prole.authority.config.AuthProperties;
|
||||
import org.prole.authority.session.OidcCodeService;
|
||||
import org.prole.authority.session.SessionService;
|
||||
import org.prole.authority.session.SessionUser;
|
||||
import dev.knoe.authority.config.AuthProperties;
|
||||
import dev.knoe.authority.session.OidcCodeService;
|
||||
import dev.knoe.authority.session.SessionService;
|
||||
import dev.knoe.authority.session.SessionUser;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
@ -1,6 +1,6 @@
|
||||
package org.prole.authority.web;
|
||||
package dev.knoe.authority.web;
|
||||
|
||||
import org.prole.authority.config.AuthProperties;
|
||||
import dev.knoe.authority.config.AuthProperties;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@ -1,8 +1,8 @@
|
||||
package org.prole.authority.web;
|
||||
package dev.knoe.authority.web;
|
||||
|
||||
import org.prole.authority.config.AuthProperties;
|
||||
import org.prole.authority.session.OidcCodeService;
|
||||
import org.prole.authority.session.OidcTokenService;
|
||||
import dev.knoe.authority.config.AuthProperties;
|
||||
import dev.knoe.authority.session.OidcCodeService;
|
||||
import dev.knoe.authority.session.OidcTokenService;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
@ -1,8 +1,8 @@
|
||||
package org.prole.authority.web;
|
||||
package dev.knoe.authority.web;
|
||||
|
||||
import org.prole.authority.config.AuthProperties;
|
||||
import org.prole.authority.session.SessionService;
|
||||
import org.prole.authority.session.SessionUser;
|
||||
import dev.knoe.authority.config.AuthProperties;
|
||||
import dev.knoe.authority.session.SessionService;
|
||||
import dev.knoe.authority.session.SessionUser;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
@ -1,13 +1,13 @@
|
||||
package org.prole.authority.web;
|
||||
package dev.knoe.authority.web;
|
||||
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.util.Arrays;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.prole.authority.config.AuthProperties;
|
||||
import org.prole.authority.session.SessionTokenService;
|
||||
import org.prole.authority.session.SessionUser;
|
||||
import dev.knoe.authority.config.AuthProperties;
|
||||
import dev.knoe.authority.session.SessionTokenService;
|
||||
import dev.knoe.authority.session.SessionUser;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@ -0,0 +1,14 @@
|
||||
package dev.knoe.authority;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class HealthControllerTest {
|
||||
|
||||
private final HealthController controller = new HealthController();
|
||||
|
||||
@Test
|
||||
void healthReturnsOk() {
|
||||
assertEquals("ok", controller.health());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,115 @@
|
||||
package dev.knoe.authority.admin;
|
||||
|
||||
import dev.knoe.authority.enroll.InviteService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@SpringBootTest(properties = {
|
||||
"knoe.auth.enabled=true",
|
||||
"knoe.auth.sessionSecret=test-secret",
|
||||
"knoe.auth.cookieName=knoe_session",
|
||||
"knoe.auth.sessionTtl=1h",
|
||||
"knoe.kerberos.servicePrincipal=HTTP/api.knoe.org@EXAMPLE.TEST",
|
||||
"knoe.kerberos.keytabPath=/tmp/does-not-matter"
|
||||
})
|
||||
@AutoConfigureMockMvc
|
||||
class AdminControllerTest {
|
||||
|
||||
@Autowired private MockMvc mvc;
|
||||
|
||||
@MockBean private InviteService inviteService;
|
||||
@MockBean private KnobjectService knobjectService;
|
||||
|
||||
// ── POST /auth/admin/invites ──────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void createInviteReturnsTokenAndOtp() throws Exception {
|
||||
when(inviteService.createInvite(anyString(), anyString(), anyString(), anyString()))
|
||||
.thenReturn(new InviteService.InviteResult(
|
||||
"tok-abc", "https://auth.knoe.dev/enroll?token=tok-abc", "654321"));
|
||||
|
||||
mvc.perform(post("/auth/admin/invites")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"contact\":\"alice@corp.com\",\"contactType\":\"email\",\"nameHint\":\"Alice\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.token").value("tok-abc"))
|
||||
.andExpect(jsonPath("$.otp").value("654321"))
|
||||
.andExpect(jsonPath("$.enrollUrl").exists());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createInviteWithAdminHeader() throws Exception {
|
||||
when(inviteService.createInvite(anyString(), anyString(), anyString(), anyString()))
|
||||
.thenReturn(new InviteService.InviteResult("tok-xyz", "https://auth.knoe.dev/enroll?token=tok-xyz", "111222"));
|
||||
|
||||
mvc.perform(post("/auth/admin/invites")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.header("X-Knoe-Admin", "chrisfu")
|
||||
.content("{\"contact\":\"bob@corp.com\",\"contactType\":\"email\",\"nameHint\":\"Bob\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.token").value("tok-xyz"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createInviteWithNullContactType() throws Exception {
|
||||
when(inviteService.createInvite(anyString(), anyString(), any(), anyString()))
|
||||
.thenReturn(new InviteService.InviteResult("tok-1", "https://auth.knoe.dev/enroll?token=tok-1", "000001"));
|
||||
|
||||
mvc.perform(post("/auth/admin/invites")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"contact\":\"carol@corp.com\",\"nameHint\":\"Carol\"}"))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
|
||||
// ── GET /auth/admin/users ─────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void listUsersReturnsJsonArray() throws Exception {
|
||||
when(knobjectService.listUsers()).thenReturn(List.of(
|
||||
Map.of("id", 1, "username", "alice", "email", "alice@knoe.org"),
|
||||
Map.of("id", 2, "username", "bob", "email", "bob@knoe.org")
|
||||
));
|
||||
|
||||
mvc.perform(get("/auth/admin/users"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$[0].username").value("alice"))
|
||||
.andExpect(jsonPath("$[1].username").value("bob"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void listUsersReturnsEmptyArray() throws Exception {
|
||||
when(knobjectService.listUsers()).thenReturn(List.of());
|
||||
mvc.perform(get("/auth/admin/users"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$").isArray());
|
||||
}
|
||||
|
||||
// ── POST /auth/admin/grants ───────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void createGrantReturnsGrantId() throws Exception {
|
||||
when(knobjectService.grantAccess(1, 10, "developer", "admin")).thenReturn(42);
|
||||
|
||||
mvc.perform(post("/auth/admin/grants")
|
||||
.contentType(MediaType.APPLICATION_JSON)
|
||||
.content("{\"userId\":1,\"knobjectId\":10,\"role\":\"developer\",\"grantedBy\":\"admin\"}"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.grantId").value(42))
|
||||
.andExpect(jsonPath("$.status").value("granted"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,125 @@
|
||||
package dev.knoe.authority.enroll;
|
||||
|
||||
import dev.knoe.authority.admin.AdminController;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
/**
|
||||
* Tests for pure value types (records, enums, exceptions) in the enroll/admin packages.
|
||||
* No Spring context or DB required.
|
||||
*/
|
||||
class EnrollValueTypesTest {
|
||||
|
||||
// ── InviteService.InviteResult ────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void inviteResultFields() {
|
||||
InviteService.InviteResult r = new InviteService.InviteResult("tok-1", "https://auth.knoe.dev/enroll?token=tok-1", "123456");
|
||||
assertEquals("tok-1", r.token());
|
||||
assertEquals("https://auth.knoe.dev/enroll?token=tok-1", r.enrollUrl());
|
||||
assertEquals("123456", r.rawOtp());
|
||||
}
|
||||
|
||||
// ── InviteService.OtpResult ───────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void otpResultEnumValues() {
|
||||
assertNotNull(InviteService.OtpResult.OK);
|
||||
assertNotNull(InviteService.OtpResult.WRONG_OTP);
|
||||
assertNotNull(InviteService.OtpResult.OTP_EXPIRED);
|
||||
assertNotNull(InviteService.OtpResult.LOCKED);
|
||||
assertNotNull(InviteService.OtpResult.INVALID_TOKEN);
|
||||
assertEquals(5, InviteService.OtpResult.values().length);
|
||||
}
|
||||
|
||||
@Test
|
||||
void otpResultValueOf() {
|
||||
assertEquals(InviteService.OtpResult.OK, InviteService.OtpResult.valueOf("OK"));
|
||||
assertEquals(InviteService.OtpResult.LOCKED, InviteService.OtpResult.valueOf("LOCKED"));
|
||||
}
|
||||
|
||||
// ── GoogleOAuthService.GoogleIdentity ─────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void googleIdentityFields() {
|
||||
GoogleOAuthService.GoogleIdentity id = new GoogleOAuthService.GoogleIdentity(
|
||||
"sub-123", "alice@gmail.com", "Alice Smith", null);
|
||||
assertEquals("sub-123", id.sub());
|
||||
assertEquals("alice@gmail.com", id.email());
|
||||
assertEquals("Alice Smith", id.name());
|
||||
assertNull(id.hd());
|
||||
}
|
||||
|
||||
@Test
|
||||
void googleIdentityWithHostedDomain() {
|
||||
GoogleOAuthService.GoogleIdentity id = new GoogleOAuthService.GoogleIdentity(
|
||||
"sub-456", "bob@corp.com", "Bob", "corp.com");
|
||||
assertEquals("corp.com", id.hd());
|
||||
}
|
||||
|
||||
// ── GoogleOAuthService.GoogleOAuthException ───────────────────────────────
|
||||
|
||||
@Test
|
||||
void googleOAuthExceptionMessageOnly() {
|
||||
GoogleOAuthService.GoogleOAuthException ex = new GoogleOAuthService.GoogleOAuthException("oauth failed");
|
||||
assertEquals("oauth failed", ex.getMessage());
|
||||
assertNull(ex.getCause());
|
||||
assertInstanceOf(RuntimeException.class, ex);
|
||||
}
|
||||
|
||||
@Test
|
||||
void googleOAuthExceptionWithCause() {
|
||||
RuntimeException cause = new RuntimeException("network error");
|
||||
GoogleOAuthService.GoogleOAuthException ex = new GoogleOAuthService.GoogleOAuthException("wrapped", cause);
|
||||
assertEquals("wrapped", ex.getMessage());
|
||||
assertSame(cause, ex.getCause());
|
||||
}
|
||||
|
||||
// ── UserProvisioningService.ProvisionResult ───────────────────────────────
|
||||
|
||||
@Test
|
||||
void provisionResultFields() {
|
||||
UserProvisioningService.ProvisionResult r = new UserProvisioningService.ProvisionResult(42, "alice", "alice@knoe.org");
|
||||
assertEquals(42, r.userId());
|
||||
assertEquals("alice", r.username());
|
||||
assertEquals("alice@knoe.org", r.email());
|
||||
}
|
||||
|
||||
// ── UserProvisioningService.ProvisioningException ─────────────────────────
|
||||
|
||||
@Test
|
||||
void provisioningExceptionMessageOnly() {
|
||||
UserProvisioningService.ProvisioningException ex = new UserProvisioningService.ProvisioningException("provision failed");
|
||||
assertEquals("provision failed", ex.getMessage());
|
||||
assertNull(ex.getCause());
|
||||
assertInstanceOf(RuntimeException.class, ex);
|
||||
}
|
||||
|
||||
@Test
|
||||
void provisioningExceptionWithCause() {
|
||||
RuntimeException cause = new RuntimeException("db error");
|
||||
UserProvisioningService.ProvisioningException ex = new UserProvisioningService.ProvisioningException("wrapped", cause);
|
||||
assertSame(cause, ex.getCause());
|
||||
}
|
||||
|
||||
// ── AdminController.CreateInviteRequest ───────────────────────────────────
|
||||
|
||||
@Test
|
||||
void createInviteRequestFields() {
|
||||
AdminController.CreateInviteRequest req = new AdminController.CreateInviteRequest("alice@corp.com", "email", "Alice");
|
||||
assertEquals("alice@corp.com", req.contact());
|
||||
assertEquals("email", req.contactType());
|
||||
assertEquals("Alice", req.nameHint());
|
||||
}
|
||||
|
||||
// ── AdminController.GrantRequest ──────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void grantRequestFields() {
|
||||
AdminController.GrantRequest req = new AdminController.GrantRequest(1, 10, "read", "admin");
|
||||
assertEquals(1, req.userId());
|
||||
assertEquals(10, req.knobjectId());
|
||||
assertEquals("read", req.role());
|
||||
assertEquals("admin", req.grantedBy());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,217 @@
|
||||
package dev.knoe.authority.enroll;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpSession;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@SpringBootTest(properties = {
|
||||
"knoe.auth.enabled=true",
|
||||
"knoe.auth.sessionSecret=test-secret",
|
||||
"knoe.auth.cookieName=knoe_session",
|
||||
"knoe.auth.sessionTtl=1h",
|
||||
"knoe.kerberos.servicePrincipal=HTTP/api.knoe.org@EXAMPLE.TEST",
|
||||
"knoe.kerberos.keytabPath=/tmp/does-not-matter"
|
||||
})
|
||||
@AutoConfigureMockMvc
|
||||
class EnrollmentControllerTest {
|
||||
|
||||
@Autowired private MockMvc mvc;
|
||||
|
||||
@MockBean private InviteService inviteService;
|
||||
@MockBean private GoogleOAuthService googleOAuth;
|
||||
@MockBean private TotpService totpService;
|
||||
@MockBean private UserProvisioningService provisioning;
|
||||
|
||||
// ── GET /auth/enroll?token=... ────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void landingWithInvalidTokenReturnsBadRequest() throws Exception {
|
||||
when(inviteService.findValidInvite(anyString())).thenReturn(null);
|
||||
mvc.perform(get("/auth/enroll").param("token", "bad-token"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void landingWithValidTokenReturnsOk() throws Exception {
|
||||
when(inviteService.findValidInvite("good-token"))
|
||||
.thenReturn(Map.of("contact", "alice@corp.com", "name_hint", "Alice"));
|
||||
mvc.perform(get("/auth/enroll").param("token", "good-token"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("invited to knoe.dev")));
|
||||
}
|
||||
|
||||
// ── POST /auth/enroll/verify-otp ─────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void verifyOtpOkRedirects() throws Exception {
|
||||
when(inviteService.verifyOtp(anyString(), anyString())).thenReturn(InviteService.OtpResult.OK);
|
||||
mvc.perform(post("/auth/enroll/verify-otp")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.param("token", "tok").param("otp", "123456"))
|
||||
.andExpect(status().is3xxRedirection());
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyOtpWrongCodeReturnsBadRequest() throws Exception {
|
||||
when(inviteService.verifyOtp(anyString(), anyString())).thenReturn(InviteService.OtpResult.WRONG_OTP);
|
||||
mvc.perform(post("/auth/enroll/verify-otp")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.param("token", "tok").param("otp", "000000"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyOtpExpiredReturnsBadRequest() throws Exception {
|
||||
when(inviteService.verifyOtp(anyString(), anyString())).thenReturn(InviteService.OtpResult.OTP_EXPIRED);
|
||||
mvc.perform(post("/auth/enroll/verify-otp")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.param("token", "tok").param("otp", "000000"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyOtpLockedReturnsForbidden() throws Exception {
|
||||
when(inviteService.verifyOtp(anyString(), anyString())).thenReturn(InviteService.OtpResult.LOCKED);
|
||||
mvc.perform(post("/auth/enroll/verify-otp")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.param("token", "tok").param("otp", "000000"))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyOtpInvalidTokenReturnsBadRequest() throws Exception {
|
||||
when(inviteService.verifyOtp(anyString(), anyString())).thenReturn(InviteService.OtpResult.INVALID_TOKEN);
|
||||
mvc.perform(post("/auth/enroll/verify-otp")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.param("token", "tok").param("otp", "000000"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
// ── GET /auth/enroll/identity (session-gated) ─────────────────────────────
|
||||
|
||||
@Test
|
||||
void identityFormWithoutOtpVerifiedReturnsForbidden() throws Exception {
|
||||
mvc.perform(get("/auth/enroll/identity"))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
void identityFormWithOtpVerifiedReturnsOk() throws Exception {
|
||||
MockHttpSession session = new MockHttpSession();
|
||||
session.setAttribute("enroll.otpVerified", Boolean.TRUE);
|
||||
mvc.perform(get("/auth/enroll/identity").session(session))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Your details")));
|
||||
}
|
||||
|
||||
// ── POST /auth/enroll/identity/start ──────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void identityStartWithoutOtpVerifiedReturnsForbidden() throws Exception {
|
||||
mvc.perform(post("/auth/enroll/identity/start")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.param("username", "alice").param("displayName", "Alice"))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
void identityStartWithInvalidUsernameReturnsBadRequest() throws Exception {
|
||||
MockHttpSession session = new MockHttpSession();
|
||||
session.setAttribute("enroll.otpVerified", Boolean.TRUE);
|
||||
mvc.perform(post("/auth/enroll/identity/start")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.param("username", "A!").param("displayName", "Alice")
|
||||
.session(session))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void identityStartWithValidUsernameRedirects() throws Exception {
|
||||
MockHttpSession session = new MockHttpSession();
|
||||
session.setAttribute("enroll.otpVerified", Boolean.TRUE);
|
||||
when(googleOAuth.buildAuthorizationUrl(anyString(), anyString()))
|
||||
.thenReturn("https://accounts.google.com/o/oauth2/auth?state=x");
|
||||
mvc.perform(post("/auth/enroll/identity/start")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.param("username", "alice").param("displayName", "Alice")
|
||||
.session(session))
|
||||
.andExpect(status().is3xxRedirection());
|
||||
}
|
||||
|
||||
// ── GET /auth/enroll/google-callback ──────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void googleCallbackStateMismatchReturnsBadRequest() throws Exception {
|
||||
MockHttpSession session = new MockHttpSession();
|
||||
session.setAttribute("enroll.googleState", "expected-state");
|
||||
mvc.perform(get("/auth/enroll/google-callback")
|
||||
.param("code", "auth-code").param("state", "wrong-state")
|
||||
.session(session))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void googleCallbackNoSessionStateReturnsBadRequest() throws Exception {
|
||||
mvc.perform(get("/auth/enroll/google-callback")
|
||||
.param("code", "auth-code").param("state", "some-state"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void googleCallbackSuccessRedirects() throws Exception {
|
||||
MockHttpSession session = new MockHttpSession();
|
||||
session.setAttribute("enroll.googleState", "state-123");
|
||||
when(googleOAuth.exchangeCode(anyString()))
|
||||
.thenReturn(new GoogleOAuthService.GoogleIdentity("sub", "alice@gmail.com", "Alice", null));
|
||||
mvc.perform(get("/auth/enroll/google-callback")
|
||||
.param("code", "auth-code").param("state", "state-123")
|
||||
.session(session))
|
||||
.andExpect(status().is3xxRedirection());
|
||||
}
|
||||
|
||||
@Test
|
||||
void googleCallbackOAuthExceptionReturnsBadRequest() throws Exception {
|
||||
MockHttpSession session = new MockHttpSession();
|
||||
session.setAttribute("enroll.googleState", "state-123");
|
||||
when(googleOAuth.exchangeCode(anyString()))
|
||||
.thenThrow(new GoogleOAuthService.GoogleOAuthException("token exchange failed"));
|
||||
mvc.perform(get("/auth/enroll/google-callback")
|
||||
.param("code", "auth-code").param("state", "state-123")
|
||||
.session(session))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
// ── GET /auth/enroll/totp (session-gated) ─────────────────────────────────
|
||||
|
||||
@Test
|
||||
void totpSetupWithoutGoogleIdReturnsForbidden() throws Exception {
|
||||
mvc.perform(get("/auth/enroll/totp"))
|
||||
.andExpect(status().isForbidden());
|
||||
}
|
||||
|
||||
@Test
|
||||
void totpSetupWithGoogleIdReturnsOk() throws Exception {
|
||||
MockHttpSession session = new MockHttpSession();
|
||||
session.setAttribute("enroll.googleId",
|
||||
new GoogleOAuthService.GoogleIdentity("sub", "alice@gmail.com", "Alice", null));
|
||||
session.setAttribute("enroll.username", "alice");
|
||||
when(totpService.generateSecret()).thenReturn("JBSWY3DPEHPK3PXP");
|
||||
when(totpService.buildQrUri(anyString(), anyString())).thenReturn("otpauth://totp/alice");
|
||||
mvc.perform(get("/auth/enroll/totp").session(session))
|
||||
.andExpect(status().isOk());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,74 @@
|
||||
package dev.knoe.authority.enroll;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class TotpServiceTest {
|
||||
|
||||
private TotpService totpService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
totpService = new TotpService();
|
||||
// inject issuer via reflection since @Value is not wired in plain unit tests
|
||||
var field = TotpService.class.getDeclaredField("issuer");
|
||||
field.setAccessible(true);
|
||||
field.set(totpService, "Knoe.DEV");
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateSecretIsNotNull() {
|
||||
String secret = totpService.generateSecret();
|
||||
assertNotNull(secret);
|
||||
assertFalse(secret.isBlank());
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateSecretIsBase32Like() {
|
||||
String secret = totpService.generateSecret();
|
||||
// Base32 alphabet: A-Z and 2-7
|
||||
assertTrue(secret.matches("[A-Z2-7]+"), "Expected Base32 string, got: " + secret);
|
||||
}
|
||||
|
||||
@Test
|
||||
void generateSecretIsDifferentEachTime() {
|
||||
String s1 = totpService.generateSecret();
|
||||
String s2 = totpService.generateSecret();
|
||||
assertNotEquals(s1, s2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void buildQrUriContainsExpectedParts() {
|
||||
String secret = totpService.generateSecret();
|
||||
String uri = totpService.buildQrUri("alice", secret);
|
||||
assertNotNull(uri);
|
||||
assertTrue(uri.startsWith("otpauth://totp/"), "URI should start with otpauth://totp/");
|
||||
assertTrue(uri.contains("alice"), "URI should contain the username");
|
||||
assertTrue(uri.contains("Knoe.DEV"), "URI should contain the issuer");
|
||||
assertTrue(uri.contains(secret), "URI should contain the secret");
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyReturnsFalseForNullSecret() {
|
||||
assertFalse(totpService.verify(null, "123456"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyReturnsFalseForNullCode() {
|
||||
assertFalse(totpService.verify(totpService.generateSecret(), null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyReturnsFalseForWrongCode() {
|
||||
String secret = totpService.generateSecret();
|
||||
// "000000" is almost certainly wrong for any real secret at any time
|
||||
assertFalse(totpService.verify(secret, "000000"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void verifyReturnsFalseForInvalidSecret() {
|
||||
// Garbage secret — library should throw internally, service should return false
|
||||
assertFalse(totpService.verify("not-valid-base32!!!", "123456"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,72 @@
|
||||
package dev.knoe.authority.kerberos;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.lang.reflect.Method;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class KadminClientTest {
|
||||
|
||||
// ── KadminException constructors ──────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void exceptionMessageOnly() {
|
||||
KadminClient.KadminException ex = new KadminClient.KadminException("something went wrong");
|
||||
assertEquals("something went wrong", ex.getMessage());
|
||||
assertNull(ex.getCause());
|
||||
}
|
||||
|
||||
@Test
|
||||
void exceptionWithCause() {
|
||||
RuntimeException cause = new RuntimeException("root cause");
|
||||
KadminClient.KadminException ex = new KadminClient.KadminException("wrapped", cause);
|
||||
assertEquals("wrapped", ex.getMessage());
|
||||
assertSame(cause, ex.getCause());
|
||||
}
|
||||
|
||||
@Test
|
||||
void exceptionIsRuntimeException() {
|
||||
assertInstanceOf(RuntimeException.class, new KadminClient.KadminException("err"));
|
||||
}
|
||||
|
||||
// ── qualified() — private method tested via reflection ───────────────────
|
||||
|
||||
@Test
|
||||
void qualifiedAppendsRealmWhenMissing() throws Exception {
|
||||
KadminClient client = new KadminClient();
|
||||
// inject realm field
|
||||
var realmField = KadminClient.class.getDeclaredField("realm");
|
||||
realmField.setAccessible(true);
|
||||
realmField.set(client, "KNOE.DEV");
|
||||
|
||||
Method qualified = KadminClient.class.getDeclaredMethod("qualified", String.class);
|
||||
qualified.setAccessible(true);
|
||||
|
||||
assertEquals("alice@KNOE.DEV", qualified.invoke(client, "alice"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void qualifiedLeavesFullyQualifiedPrincipalUnchanged() throws Exception {
|
||||
KadminClient client = new KadminClient();
|
||||
var realmField = KadminClient.class.getDeclaredField("realm");
|
||||
realmField.setAccessible(true);
|
||||
realmField.set(client, "KNOE.DEV");
|
||||
|
||||
Method qualified = KadminClient.class.getDeclaredMethod("qualified", String.class);
|
||||
qualified.setAccessible(true);
|
||||
|
||||
assertEquals("alice@KNOE.DEV", qualified.invoke(client, "alice@KNOE.DEV"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void qualifiedWithServicePrincipal() throws Exception {
|
||||
KadminClient client = new KadminClient();
|
||||
var realmField = KadminClient.class.getDeclaredField("realm");
|
||||
realmField.setAccessible(true);
|
||||
realmField.set(client, "KNOE.DEV");
|
||||
|
||||
Method qualified = KadminClient.class.getDeclaredMethod("qualified", String.class);
|
||||
qualified.setAccessible(true);
|
||||
|
||||
assertEquals("HTTP/api.knoe.dev@KNOE.DEV", qualified.invoke(client, "HTTP/api.knoe.dev@KNOE.DEV"));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package dev.knoe.authority.kerberos;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class KerberosSpnegoResultTest {
|
||||
|
||||
@Test
|
||||
void establishedResult() {
|
||||
byte[] token = new byte[]{1, 2, 3};
|
||||
KerberosSpnegoService.Result r = new KerberosSpnegoService.Result(true, "alice@KNOE.DEV", token);
|
||||
assertTrue(r.established());
|
||||
assertEquals("alice@KNOE.DEV", r.clientPrincipal());
|
||||
assertArrayEquals(token, r.responseToken());
|
||||
}
|
||||
|
||||
@Test
|
||||
void notEstablishedResult() {
|
||||
KerberosSpnegoService.Result r = new KerberosSpnegoService.Result(false, null, new byte[0]);
|
||||
assertFalse(r.established());
|
||||
assertNull(r.clientPrincipal());
|
||||
assertEquals(0, r.responseToken().length);
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package org.prole.authority.regression;
|
||||
package dev.knoe.authority.regression;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.io.File;
|
||||
@ -0,0 +1,61 @@
|
||||
package dev.knoe.authority.session;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class OidcCodeServiceTest {
|
||||
|
||||
private final OidcCodeService service = new OidcCodeService();
|
||||
|
||||
private OidcCodeService.AuthorizationRequest sampleRequest() {
|
||||
return new OidcCodeService.AuthorizationRequest(
|
||||
"client-id", "https://app.knoe.dev/callback", "state-xyz", "nonce-abc",
|
||||
new SessionUser("alice", "alice@knoe.org", java.util.List.of()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCodeReturnsNonNullString() {
|
||||
String code = service.createCode(sampleRequest());
|
||||
assertNotNull(code);
|
||||
assertFalse(code.isBlank());
|
||||
}
|
||||
|
||||
@Test
|
||||
void createCodeReturnsDifferentCodesEachTime() {
|
||||
String c1 = service.createCode(sampleRequest());
|
||||
String c2 = service.createCode(sampleRequest());
|
||||
assertNotEquals(c1, c2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void consumeCodeReturnsRequest() {
|
||||
OidcCodeService.AuthorizationRequest req = sampleRequest();
|
||||
String code = service.createCode(req);
|
||||
OidcCodeService.AuthorizationRequest consumed = service.consumeCode(code);
|
||||
assertNotNull(consumed);
|
||||
assertEquals("client-id", consumed.clientId());
|
||||
assertEquals("alice", consumed.user().username());
|
||||
}
|
||||
|
||||
@Test
|
||||
void consumeCodeRemovesIt() {
|
||||
String code = service.createCode(sampleRequest());
|
||||
service.consumeCode(code);
|
||||
assertNull(service.consumeCode(code));
|
||||
}
|
||||
|
||||
@Test
|
||||
void consumeUnknownCodeReturnsNull() {
|
||||
assertNull(service.consumeCode("does-not-exist"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void authorizationRequestFieldsAccessible() {
|
||||
OidcCodeService.AuthorizationRequest req = sampleRequest();
|
||||
assertEquals("client-id", req.clientId());
|
||||
assertEquals("https://app.knoe.dev/callback", req.redirectUri());
|
||||
assertEquals("state-xyz", req.state());
|
||||
assertEquals("nonce-abc", req.nonce());
|
||||
assertEquals("alice", req.user().username());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
package dev.knoe.authority.session;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import dev.knoe.authority.config.AuthProperties;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
|
||||
import java.time.Clock;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneOffset;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class SessionServiceTest {
|
||||
|
||||
private SessionTokenService tokenService;
|
||||
private SessionService sessionService;
|
||||
private AuthProperties authProperties;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() throws Exception {
|
||||
Clock clock = Clock.fixed(Instant.parse("2026-03-20T00:00:00Z"), ZoneOffset.UTC);
|
||||
tokenService = SessionTokenService.forTests(new ObjectMapper(), clock);
|
||||
|
||||
authProperties = new AuthProperties();
|
||||
authProperties.setSessionSecret("test-secret");
|
||||
authProperties.setCookieName("knoe_session");
|
||||
authProperties.setSessionTtl(Duration.ofHours(1));
|
||||
|
||||
sessionService = new SessionService(authProperties, tokenService);
|
||||
}
|
||||
|
||||
@Test
|
||||
void noCookieReturnsEmpty() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
assertEquals(Optional.empty(), sessionService.getSessionUser(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void blankCookieReturnsEmpty() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setCookies(new jakarta.servlet.http.Cookie("knoe_session", " "));
|
||||
assertEquals(Optional.empty(), sessionService.getSessionUser(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void validCookieReturnsUser() {
|
||||
String token = tokenService.issue("test-secret",
|
||||
new SessionUser("alice", "alice@knoe.org", List.of()), Duration.ofMinutes(10));
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setCookies(new jakarta.servlet.http.Cookie("knoe_session", token));
|
||||
Optional<SessionUser> user = sessionService.getSessionUser(request);
|
||||
assertTrue(user.isPresent());
|
||||
assertEquals("alice", user.get().username());
|
||||
}
|
||||
|
||||
@Test
|
||||
void invalidTokenReturnsEmpty() {
|
||||
MockHttpServletRequest request = new MockHttpServletRequest();
|
||||
request.setCookies(new jakarta.servlet.http.Cookie("knoe_session", "not-a-valid-token"));
|
||||
assertEquals(Optional.empty(), sessionService.getSessionUser(request));
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package org.prole.authority.session;
|
||||
package dev.knoe.authority.session;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import java.time.Clock;
|
||||
@ -0,0 +1,64 @@
|
||||
package dev.knoe.authority.user;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import java.util.Optional;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class PrincipalNormalizerTest {
|
||||
|
||||
private final PrincipalNormalizer normalizer = new PrincipalNormalizer();
|
||||
|
||||
@Test
|
||||
void nullReturnsEmpty() {
|
||||
assertEquals(Optional.empty(), normalizer.normalizeUsernameFromKerberosPrincipal(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void blankReturnsEmpty() {
|
||||
assertEquals(Optional.empty(), normalizer.normalizeUsernameFromKerberosPrincipal(" "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void simpleUsernameAtRealm() {
|
||||
assertEquals(Optional.of("alice"), normalizer.normalizeUsernameFromKerberosPrincipal("alice@KNOE.DEV"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void usernameWithInstanceAndRealm() {
|
||||
assertEquals(Optional.of("alice"), normalizer.normalizeUsernameFromKerberosPrincipal("alice/admin@KNOE.DEV"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void usernameWithInstanceNoRealm() {
|
||||
assertEquals(Optional.of("alice"), normalizer.normalizeUsernameFromKerberosPrincipal("alice/admin"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void bareUsernameNoRealm() {
|
||||
assertEquals(Optional.of("alice"), normalizer.normalizeUsernameFromKerberosPrincipal("alice"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void uppercaseNormalisedToLower() {
|
||||
assertEquals(Optional.of("alice"), normalizer.normalizeUsernameFromKerberosPrincipal("ALICE@KNOE.DEV"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void leadingTrailingWhitespace() {
|
||||
assertEquals(Optional.of("alice"), normalizer.normalizeUsernameFromKerberosPrincipal(" alice@KNOE.DEV "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void atSignOnlyKeepsRealm() {
|
||||
// "@REALM" → at index is 0, so realm is NOT stripped (at > 0 guard),
|
||||
// result is "@knoe.dev" lowercased — not blank, so returned as-is.
|
||||
// This documents the current behaviour; input is invalid anyway.
|
||||
Optional<String> result = normalizer.normalizeUsernameFromKerberosPrincipal("@KNOE.DEV");
|
||||
assertTrue(result.isPresent());
|
||||
}
|
||||
|
||||
@Test
|
||||
void emptyStringReturnsEmpty() {
|
||||
assertEquals(Optional.empty(), normalizer.normalizeUsernameFromKerberosPrincipal(""));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,195 @@
|
||||
package dev.knoe.authority.web;
|
||||
|
||||
import dev.knoe.authority.enroll.GoogleOAuthService;
|
||||
import dev.knoe.authority.kerberos.KerberosPasswordService;
|
||||
import dev.knoe.authority.kerberos.KerberosSpnegoService;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.mock.web.MockHttpSession;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
|
||||
|
||||
@SpringBootTest(properties = {
|
||||
"knoe.auth.enabled=true",
|
||||
"knoe.auth.sessionSecret=test-secret-for-login",
|
||||
"knoe.auth.cookieName=knoe_session",
|
||||
"knoe.auth.sessionTtl=1h",
|
||||
"knoe.auth.emailDomain=knoe.org",
|
||||
"knoe.auth.cookieDomain=knoe.dev",
|
||||
"knoe.auth.formEnabled=true",
|
||||
"knoe.kerberos.servicePrincipal=HTTP/api.knoe.org@EXAMPLE.TEST",
|
||||
"knoe.kerberos.keytabPath=/tmp/does-not-matter",
|
||||
"knoe.kerberos.realm=EXAMPLE.TEST"
|
||||
})
|
||||
@AutoConfigureMockMvc
|
||||
class LoginControllerTest {
|
||||
|
||||
@Autowired private MockMvc mvc;
|
||||
|
||||
@MockBean private KerberosSpnegoService spnego;
|
||||
@MockBean private KerberosPasswordService passwordAuth;
|
||||
@MockBean private GoogleOAuthService googleOAuth;
|
||||
|
||||
// ── GET /auth/login ───────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void loginPageReturnsHtml() throws Exception {
|
||||
mvc.perform(get("/auth/login"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().contentTypeCompatibleWith(MediaType.TEXT_HTML))
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Knoe Login")));
|
||||
}
|
||||
|
||||
@Test
|
||||
void loginPageWithNextParam() throws Exception {
|
||||
mvc.perform(get("/auth/login").param("next", "/dashboard"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string(org.hamcrest.Matchers.containsString("Knoe Login")));
|
||||
}
|
||||
|
||||
// ── GET /auth/spnego ──────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void spnegoWithoutNegotiateHeaderReturns401() throws Exception {
|
||||
mvc.perform(get("/auth/spnego"))
|
||||
.andExpect(status().isUnauthorized())
|
||||
.andExpect(header().string("WWW-Authenticate", "Negotiate"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void spnegoWithValidTokenRedirects() throws Exception {
|
||||
when(spnego.acceptSpnegoToken(anyString(), anyString(), any()))
|
||||
.thenReturn(new KerberosSpnegoService.Result(true, "alice@EXAMPLE.TEST", new byte[0]));
|
||||
|
||||
mvc.perform(get("/auth/spnego")
|
||||
.header("Authorization", "Negotiate dGVzdA=="))
|
||||
.andExpect(status().is3xxRedirection());
|
||||
}
|
||||
|
||||
@Test
|
||||
void spnegoWithFailedNegotiationReturns401() throws Exception {
|
||||
when(spnego.acceptSpnegoToken(anyString(), anyString(), any()))
|
||||
.thenReturn(new KerberosSpnegoService.Result(false, null, new byte[0]));
|
||||
|
||||
mvc.perform(get("/auth/spnego")
|
||||
.header("Authorization", "Negotiate dGVzdA=="))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
void spnegoWithResponseTokenSetsNegotiateHeader() throws Exception {
|
||||
when(spnego.acceptSpnegoToken(anyString(), anyString(), any()))
|
||||
.thenReturn(new KerberosSpnegoService.Result(true, "alice@EXAMPLE.TEST", new byte[]{1, 2, 3}));
|
||||
|
||||
mvc.perform(get("/auth/spnego")
|
||||
.header("Authorization", "Negotiate dGVzdA=="))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(header().exists("WWW-Authenticate"));
|
||||
}
|
||||
|
||||
// ── POST /auth/form ───────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void formLoginWithBlankUsernameReturns401() throws Exception {
|
||||
mvc.perform(post("/auth/form")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.param("username", "").param("password", "secret"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
void formLoginWithBlankPasswordReturns401() throws Exception {
|
||||
mvc.perform(post("/auth/form")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.param("username", "alice").param("password", ""))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
void formLoginSuccessRedirects() throws Exception {
|
||||
mvc.perform(post("/auth/form")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.param("username", "alice").param("password", "correct"))
|
||||
.andExpect(status().is3xxRedirection());
|
||||
}
|
||||
|
||||
@Test
|
||||
void formLoginWithNextParam() throws Exception {
|
||||
mvc.perform(post("/auth/form")
|
||||
.contentType(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
.param("username", "alice").param("password", "correct")
|
||||
.param("next", "/dashboard"))
|
||||
.andExpect(status().is3xxRedirection());
|
||||
}
|
||||
|
||||
// ── GET /auth/logout ──────────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void logoutRedirects() throws Exception {
|
||||
mvc.perform(get("/auth/logout"))
|
||||
.andExpect(status().is3xxRedirection());
|
||||
}
|
||||
|
||||
@Test
|
||||
void logoutClearsCookie() throws Exception {
|
||||
mvc.perform(get("/auth/logout"))
|
||||
.andExpect(header().string("Set-Cookie", org.hamcrest.Matchers.containsString("Max-Age=0")));
|
||||
}
|
||||
|
||||
// ── GET /auth/login/google ────────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void googleLoginRedirectsToGoogle() throws Exception {
|
||||
when(googleOAuth.buildAuthorizationUrl(anyString(), anyString(), anyString()))
|
||||
.thenReturn("https://accounts.google.com/o/oauth2/auth?state=x&nonce=y");
|
||||
|
||||
mvc.perform(get("/auth/login/google"))
|
||||
.andExpect(status().is3xxRedirection())
|
||||
.andExpect(header().string("Location", org.hamcrest.Matchers.containsString("accounts.google.com")));
|
||||
}
|
||||
|
||||
// ── GET /auth/callback/google ─────────────────────────────────────────────
|
||||
|
||||
@Test
|
||||
void googleCallbackStateMismatchReturnsBadRequest() throws Exception {
|
||||
MockHttpSession session = new MockHttpSession();
|
||||
session.setAttribute("login.googleState", "expected");
|
||||
|
||||
mvc.perform(get("/auth/callback/google")
|
||||
.param("code", "code").param("state", "wrong")
|
||||
.session(session))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void googleCallbackNoStateReturnsBadRequest() throws Exception {
|
||||
mvc.perform(get("/auth/callback/google")
|
||||
.param("code", "code").param("state", "any"))
|
||||
.andExpect(status().isBadRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
void googleCallbackSuccessRedirects() throws Exception {
|
||||
MockHttpSession session = new MockHttpSession();
|
||||
session.setAttribute("login.googleState", "state-abc");
|
||||
session.setAttribute("login.next", null);
|
||||
|
||||
when(googleOAuth.exchangeCode(anyString(), anyString()))
|
||||
.thenReturn(new GoogleOAuthService.GoogleIdentity("sub", "alice@corp.com", "Alice", "corp.com"));
|
||||
|
||||
mvc.perform(get("/auth/callback/google")
|
||||
.param("code", "auth-code").param("state", "state-abc")
|
||||
.session(session))
|
||||
.andExpect(status().is3xxRedirection());
|
||||
}
|
||||
}
|
||||
@ -1,9 +1,9 @@
|
||||
package org.prole.authority.web;
|
||||
package dev.knoe.authority.web;
|
||||
|
||||
import java.time.Duration;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.prole.authority.session.SessionTokenService;
|
||||
import org.prole.authority.session.SessionUser;
|
||||
import dev.knoe.authority.session.SessionTokenService;
|
||||
import dev.knoe.authority.session.SessionUser;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
@ -28,7 +28,7 @@ knoe-auth (the Authority) acts as the central Identity Authority for the cluster
|
||||
- **Normalization**: `PrincipalNormalizer` ensures consistency across all auth methods.
|
||||
|
||||
## Namespace & Domains
|
||||
- **Java Package**: `org.prole.authority.*`
|
||||
- **Java Package**: `dev.knoe.authority.*`
|
||||
- **Service DNS**: `knoe.dev` (e.g., `api.knoe.dev`, `git.knoe.dev`)
|
||||
- **OIDC Issuer**: `https://api.knoe.dev/auth`
|
||||
- **Identity Domain**: `knoey.com`
|
||||
|
||||
@ -53,32 +53,32 @@ The actual files that implement Round 1. Verify with `git ls-files` before assum
|
||||
|
||||
### Java application — `authority/`
|
||||
|
||||
The Spring Boot service that implements the enrollment flow, the admin API, and SPNEGO-protected endpoints. Multi-module Maven build (`authority/pom.xml`); the application package is `org.prole.authority` (kept as-is across the prole→knoe rebrand for compatibility).
|
||||
The Spring Boot service that implements the enrollment flow, the admin API, and SPNEGO-protected endpoints. Multi-module Maven build (`authority/pom.xml`); the application package is `dev.knoe.authority` (kept as-is across the prole→knoe rebrand for compatibility).
|
||||
|
||||
| File | Responsibility |
|
||||
| --- | --- |
|
||||
| `authority/src/main/java/org/prole/authority/KnoeAuthApplication.java` | `@SpringBootApplication` entry point. |
|
||||
| `authority/src/main/java/org/prole/authority/HealthController.java` | `/health` endpoint. |
|
||||
| `authority/src/main/java/org/prole/authority/web/LoginController.java` | Form-login + SPNEGO challenge for browsers without a ticket. |
|
||||
| `authority/src/main/java/org/prole/authority/web/VerifyController.java` | Token-verify endpoint for downstream services. |
|
||||
| `authority/src/main/java/org/prole/authority/session/SessionTokenService.java` | Issues HMAC-SHA256 JWT cookies after successful auth. |
|
||||
| `authority/src/main/java/org/prole/authority/session/SessionUser.java` | Authenticated principal carried in the security context. |
|
||||
| `authority/src/main/java/org/prole/authority/user/PrincipalNormalizer.java` | Strips realm/instance from a Kerberos principal (`alice/admin@KNOE.DEV` → `alice`). |
|
||||
| `authority/src/main/java/org/prole/authority/kerberos/KerberosSpnegoService.java` | SPNEGO challenge/response handling. |
|
||||
| `authority/src/main/java/org/prole/authority/kerberos/KerberosPasswordService.java` | Password-style auth fallback for browsers that can't do SPNEGO. |
|
||||
| `authority/src/main/java/org/prole/authority/kerberos/KadminClient.java` | Shells out to `kadmin.local` (in the KDC sidecar) to `addprinc` and `cpw`. **Sanitizes input.** |
|
||||
| `authority/src/main/java/org/prole/authority/enroll/EnrollmentController.java` | Web endpoints: `GET /auth/enroll`, `POST /auth/enroll/verify-otp`, `POST /auth/enroll/identity/start`, `GET /auth/enroll/google-callback`, `GET /auth/enroll/totp`, `POST /auth/enroll/totp/verify`, `POST /auth/enroll/complete`. |
|
||||
| `authority/src/main/java/org/prole/authority/enroll/InviteService.java` | CRUD + validation against `knoe.invitation`. OTP hashing (bcrypt) and rate limiting (3 attempts). |
|
||||
| `authority/src/main/java/org/prole/authority/enroll/GoogleOAuthService.java` | Exchange OAuth2 code → ID token, validate `email_verified`, return a `GoogleIdentity` record. **No `hd` allowlist.** |
|
||||
| `authority/src/main/java/org/prole/authority/enroll/TotpService.java` | Generate TOTP secret, produce `otpauth://` URI, verify codes. |
|
||||
| `authority/src/main/java/org/prole/authority/enroll/UserProvisioningService.java` | Transactional orchestrator: inserts user/identity/totp rows, calls `KadminClient`, queues provisioning jobs. |
|
||||
| `authority/src/main/java/org/prole/authority/admin/AdminController.java` | `POST /auth/admin/invites`, `GET /auth/admin/users`, `POST /auth/admin/grants`. SPNEGO + admin-role gated. |
|
||||
| `authority/src/main/java/org/prole/authority/admin/KnobjectService.java` | CRUD on `knoe.knobject` and `knoe.access_grant`; enqueues `provisioning_job` rows. |
|
||||
| `authority/src/main/java/org/prole/authority/provisioning/ProvisioningWorker.java` | `@Scheduled` poller for `knoe.provisioning_job WHERE status = 'pending'`. Dispatches to GitLab/Gitea/CNPG. |
|
||||
| `authority/src/main/java/org/prole/authority/config/AuthProperties.java` | Typed binding for `knoe.auth.*` keys. |
|
||||
| `authority/src/main/java/org/prole/authority/config/KerberosProperties.java` | Typed binding for `knoe.auth.kerberos.*` keys. |
|
||||
| `authority/src/main/java/dev/knoe/authority/KnoeAuthApplication.java` | `@SpringBootApplication` entry point. |
|
||||
| `authority/src/main/java/dev/knoe/authority/HealthController.java` | `/health` endpoint. |
|
||||
| `authority/src/main/java/dev/knoe/authority/web/LoginController.java` | Form-login + SPNEGO challenge for browsers without a ticket. |
|
||||
| `authority/src/main/java/dev/knoe/authority/web/VerifyController.java` | Token-verify endpoint for downstream services. |
|
||||
| `authority/src/main/java/dev/knoe/authority/session/SessionTokenService.java` | Issues HMAC-SHA256 JWT cookies after successful auth. |
|
||||
| `authority/src/main/java/dev/knoe/authority/session/SessionUser.java` | Authenticated principal carried in the security context. |
|
||||
| `authority/src/main/java/dev/knoe/authority/user/PrincipalNormalizer.java` | Strips realm/instance from a Kerberos principal (`alice/admin@KNOE.DEV` → `alice`). |
|
||||
| `authority/src/main/java/dev/knoe/authority/kerberos/KerberosSpnegoService.java` | SPNEGO challenge/response handling. |
|
||||
| `authority/src/main/java/dev/knoe/authority/kerberos/KerberosPasswordService.java` | Password-style auth fallback for browsers that can't do SPNEGO. |
|
||||
| `authority/src/main/java/dev/knoe/authority/kerberos/KadminClient.java` | Shells out to `kadmin.local` (in the KDC sidecar) to `addprinc` and `cpw`. **Sanitizes input.** |
|
||||
| `authority/src/main/java/dev/knoe/authority/enroll/EnrollmentController.java` | Web endpoints: `GET /auth/enroll`, `POST /auth/enroll/verify-otp`, `POST /auth/enroll/identity/start`, `GET /auth/enroll/google-callback`, `GET /auth/enroll/totp`, `POST /auth/enroll/totp/verify`, `POST /auth/enroll/complete`. |
|
||||
| `authority/src/main/java/dev/knoe/authority/enroll/InviteService.java` | CRUD + validation against `knoe.invitation`. OTP hashing (bcrypt) and rate limiting (3 attempts). |
|
||||
| `authority/src/main/java/dev/knoe/authority/enroll/GoogleOAuthService.java` | Exchange OAuth2 code → ID token, validate `email_verified`, return a `GoogleIdentity` record. **No `hd` allowlist.** |
|
||||
| `authority/src/main/java/dev/knoe/authority/enroll/TotpService.java` | Generate TOTP secret, produce `otpauth://` URI, verify codes. |
|
||||
| `authority/src/main/java/dev/knoe/authority/enroll/UserProvisioningService.java` | Transactional orchestrator: inserts user/identity/totp rows, calls `KadminClient`, queues provisioning jobs. |
|
||||
| `authority/src/main/java/dev/knoe/authority/admin/AdminController.java` | `POST /auth/admin/invites`, `GET /auth/admin/users`, `POST /auth/admin/grants`. SPNEGO + admin-role gated. |
|
||||
| `authority/src/main/java/dev/knoe/authority/admin/KnobjectService.java` | CRUD on `knoe.knobject` and `knoe.access_grant`; enqueues `provisioning_job` rows. |
|
||||
| `authority/src/main/java/dev/knoe/authority/provisioning/ProvisioningWorker.java` | `@Scheduled` poller for `knoe.provisioning_job WHERE status = 'pending'`. Dispatches to GitLab/Gitea/CNPG. |
|
||||
| `authority/src/main/java/dev/knoe/authority/config/AuthProperties.java` | Typed binding for `knoe.auth.*` keys. |
|
||||
| `authority/src/main/java/dev/knoe/authority/config/KerberosProperties.java` | Typed binding for `knoe.auth.kerberos.*` keys. |
|
||||
|
||||
Tests for the above live under `authority/src/test/java/org/prole/authority/` — notably `web/VerifyControllerTest.java` and `session/SessionTokenServiceTest.java`.
|
||||
Tests for the above live under `authority/src/test/java/dev/knoe/authority/` — notably `web/VerifyControllerTest.java` and `session/SessionTokenServiceTest.java`.
|
||||
|
||||
### Kubernetes manifests — GKE
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user