8000 Admin v2: Mask client secrets for view-only users by poliglots · Pull Request #52731 · keycloak/keycloak · GitHub
[go: up one dir, main page]

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
import org.keycloak.models.jpa.entities.RoleEntity;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.StripSecretsUtilsV2;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.LoginProtocolFactory;
import org.keycloak.protocol.oidc.OIDCClientSecretConfigWrapper;
Expand Down Expand Up @@ -412,7 +413,11 @@ protected void populate(ClientModel model, BaseClientRepresentation resource) {
@Override
protected BaseClientRepresentation createResourceTypeInstance(ClientModel model, List<String> attributes,
List<String> excludedAttributes) {
return populateFromSchema(getSchema(model.getProtocol()), model, attributes, excludedAttributes);
BaseClientRepresentation result = populateFromSchema(getSchema(model.getProtocol()), model, attributes, excludedAttributes);
if (!permissions.clients().canManage(model)) {
result = StripSecretsUtilsV2.stripSecrets(session, result);
}
return result;
}

private static <R extends BaseClientRepresentation> R populateFromSchema(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,57 @@ public void getClient() {
assertTrue(ex.getMessage().contains("HTTP 403 Forbidden"));
}

/**
* GET /clients/{client} - client secret masking for view-only users.
* View-only users should see masked secrets, matching v1 behavior.
*/
@Test
public void getClientSecretMaskedForViewOnly() {
String testClientId = "test-client";

// view-clients: should get masked secret, not plaintext
OIDCClientRepresentation viewRep = (OIDCClientRepresentation) getClientsApi(viewClientsAdminClient).client(testClientId).getClient();
assertThat(viewRep.getAuth().getSecret(), is("**********"));

// manage-clients: should get the real secret
OIDCClientRepresentation manageRep = (OIDCClientRepresentation) getClientsApi(manageClientsAdminClient).client(testClientId).getClient();
assertThat(manageRep.getAuth().getSecret(), is("test-secret"));
}

/**
* GET /clients - client secret masking for view-only users.
*/
@Test
public void getClientListSecretMaskedForViewOnly() {
// view-clients: list should have masked secrets
try (var response = getClientsApi(viewClientsAdminClient).getClients()) {
var viewClients = response.toList();
assertThat(viewClients.size(), greaterThan(0));

OIDCClientRepresentation testClient = viewClients.stream()
.filter(r -> r instanceof OIDCClientRepresentation)
.map(r -> (OIDCClientRepresentation) r)
.filter(r -> "test-client".equals(r.getClientId()))
.findFirst()
.orElseThrow(() -> new AssertionError("test-client not found in list"));

assertThat(testClient.getAuth().getSecret(), is("**********"));
}

// manage-clients: list should have the real secret
try (var manageResponse = getClientsApi(manageClientsAdminClient).getClients()) {
var manageClients = manageResponse.toList();
OIDCClientRepresentation testClientManaged = manageClients.stream()
.filter(r -> r instanceof OIDCClientRepresentation)
.map(r -> (OIDCClientRepresentation) r)
.filter(r -> "test-client".equals(r.getClientId()))
.findFirst()
.orElseThrow(() -> new AssertionError("test-client not found in list"));

assertThat(testClientManaged.getAuth().getSecret(), is("test-secret"));
}
}

/**
* GET /clients/{client} (client == null)
* Permissions: if auth.clients().canList() return 404, else return 403
Expand Down
Loading
0