fix(session): Authentik answers 403 when the account may view no provider
Seen in the contract build on Authentik 2026.8.3: GET /providers/oauth2/ is refused (403), not empty, for a service account with no provider permission. The health check reports that as DOWN (unverified) with the grant to add, and the contract test expects the refusal before and after the temporary grant. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
1 parent
848378d3bc
commit
5bf86725eb
4 files changed
+30
-7
No files matched your search
@@ -202,7 +202,7 @@ config.withAppClientId("my-app").withSessionPolicy(policy);
|
||||
timeout under twice the access token, a maximum not longer than the idle timeout or over 90 days.
|
||||
- The health check needs the service account to view the app's provider and nothing else: grant
|
||||
`authentik_providers_oauth2.view_oauth2provider` as an object permission on that one provider (never
|
||||
model-wide: the provider API returns client secrets). Without it the check is DOWN ("unverified");
|
||||
model-wide: the provider API returns client secrets). Without it Authentik answers 403 and the check is DOWN ("unverified");
|
||||
a provider that differs is DEGRADED and the message names each setting in seconds.
|
||||
|
||||
## In-app help
|
||||
|
||||
@@ -32,6 +32,7 @@ import com.kingsrook.qbits.authentik.client.AuthentikTokenBroker;
|
||||
import com.kingsrook.qbits.authentik.drift.AuthentikDriftReport;
|
||||
import com.kingsrook.qbits.authentik.drift.DriftFinding;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
|
||||
import com.kingsrook.qqq.backend.core.health.HealthCategory;
|
||||
import com.kingsrook.qqq.backend.core.health.HealthCheckKind;
|
||||
import com.kingsrook.qqq.backend.core.health.HealthCheckMetaData;
|
||||
@@ -257,7 +258,19 @@ public final class AuthentikHealthChecks
|
||||
.withValue("absoluteMaximumSeconds", policy.getAbsoluteMaximumSeconds())
|
||||
.withValue("renewEverySeconds", policy.getAccessTokenSeconds());
|
||||
|
||||
List<JSONObject> providers = util.getOAuth2ProviderValidity(config.getAppClientId());
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
// Authentik answers 403 to an account that may view no provider at all, and an //
|
||||
// empty list to one that may view others but not this one: both are unverified //
|
||||
///////////////////////////////////////////////////////////////////////////////////
|
||||
List<JSONObject> providers;
|
||||
try
|
||||
{
|
||||
providers = util.getOAuth2ProviderValidity(config.getAppClientId());
|
||||
}
|
||||
catch(QPermissionDeniedException e)
|
||||
{
|
||||
providers = List.of();
|
||||
}
|
||||
if(providers.size() != 1)
|
||||
{
|
||||
return (HealthCheckOutcome.down(providers.isEmpty()
|
||||
|
||||
@@ -25,6 +25,7 @@ import com.kingsrook.qbits.authentik.AuthentikTestSupport;
|
||||
import com.kingsrook.qbits.authentik.RequiresDockerCondition;
|
||||
import com.kingsrook.qbits.authentik.client.AuthentikActionUtil;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.module.api.model.metadata.APIBackendMetaData;
|
||||
import org.json.JSONArray;
|
||||
@@ -34,12 +35,13 @@ import org.junit.jupiter.api.Order;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The session-policy comparison against a real Authentik: what the provider
|
||||
** API returns for the validity settings, and that the service account reads
|
||||
** exactly its own app's provider with an object permission and nothing
|
||||
** exactly its own app's provider with an object permission and is refused
|
||||
** without one (the least-privilege grant the health check documents).
|
||||
*******************************************************************************/
|
||||
@ExtendWith(RequiresDockerCondition.class)
|
||||
@@ -64,7 +66,7 @@ class SessionPolicyContractIT
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Without the grant the provider is invisible; with it, the three validity
|
||||
** Without the grant the read is refused; with it, the three validity
|
||||
** settings and the grant types come back and compare in seconds.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
@@ -80,7 +82,11 @@ class SessionPolicyContractIT
|
||||
AuthentikActionUtil client = new AuthentikActionUtil();
|
||||
client.setBackendMetaData((APIBackendMetaData) instance.getBackend(config.getApiBackendName()));
|
||||
|
||||
assertThat(client.getOAuth2ProviderValidity(CLIENT_ID)).as("no grant: the service account sees no provider").isEmpty();
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// seen on 2026.8.3: an account that may view no provider at all gets a 403, //
|
||||
// not an empty list (the health check reports both as "unverified") //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
assertThatThrownBy(() -> client.getOAuth2ProviderValidity(CLIENT_ID)).as("no grant: refused").isInstanceOf(QPermissionDeniedException.class);
|
||||
|
||||
String role = admin.first("/rbac/roles/?name=" + AuthentikTestStack.SERVICE_ACCOUNT).getString("pk");
|
||||
String pk = String.valueOf(admin.first("/providers/oauth2/?client_id=" + CLIENT_ID).get("pk"));
|
||||
@@ -111,6 +117,6 @@ class SessionPolicyContractIT
|
||||
{
|
||||
assertThat(admin.patch("/rbac/permissions/assigned_by_roles/" + role + "/unassign/", grant).status()).isEqualTo(204);
|
||||
}
|
||||
assertThat(client.getOAuth2ProviderValidity(CLIENT_ID)).as("grant removed").isEmpty();
|
||||
assertThatThrownBy(() -> client.getOAuth2ProviderValidity(CLIENT_ID)).as("grant removed: refused again").isInstanceOf(QPermissionDeniedException.class);
|
||||
}
|
||||
}
|
||||
@@ -197,7 +197,11 @@ class AuthentikHealthChecksTest
|
||||
page(provider, provider);
|
||||
assertThat(run("session_policy").getStatus()).isEqualTo(HealthStatus.DOWN);
|
||||
FixtureActionUtil.handler = request -> FixtureActionUtil.response(403, "{\"detail\":\"" + SECRET + "\"}");
|
||||
assertThat(run("session_policy").getStatus()).isEqualTo(HealthStatus.DOWN);
|
||||
var forbidden = run("session_policy");
|
||||
assertThat(forbidden.getStatus()).isEqualTo(HealthStatus.DOWN);
|
||||
assertThat(forbidden.getMessage()).contains("view_oauth2provider");
|
||||
FixtureActionUtil.handler = request -> FixtureActionUtil.response(500, "{}");
|
||||
assertThat(run("session_policy").getMessage()).isEqualTo("Authentik health check failed");
|
||||
|
||||
assertThat(FixtureActionUtil.REQUESTS).isNotEmpty().allSatisfy(request ->
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user