[KEYCLOAK-14346] Base URL for applications is broken

This commit is contained in:
Douglas Palmer 2020-06-22 11:46:35 -07:00 committed by Bruno Oliveira da Silva
parent 76717134ba
commit 1434f14663
5 changed files with 56 additions and 10 deletions

View file

@ -10,7 +10,9 @@ public class ClientRepresentation {
private boolean userConsentRequired; private boolean userConsentRequired;
private boolean inUse; private boolean inUse;
private boolean offlineAccess; private boolean offlineAccess;
private String rootUrl;
private String baseUrl; private String baseUrl;
private String effectiveUrl;
private ConsentRepresentation consent; private ConsentRepresentation consent;
public String getClientId() { public String getClientId() {
@ -61,6 +63,14 @@ public class ClientRepresentation {
this.offlineAccess = offlineAccess; this.offlineAccess = offlineAccess;
} }
public String getRootUrl() {
return rootUrl;
}
public void setRootUrl(String rootUrl) {
this.rootUrl = rootUrl;
}
public String getBaseUrl() { public String getBaseUrl() {
return baseUrl; return baseUrl;
} }
@ -69,6 +79,14 @@ public class ClientRepresentation {
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
} }
public String getEffectiveUrl() {
return effectiveUrl;
}
public void setEffectiveUrl(String effectiveUrl) {
this.effectiveUrl = effectiveUrl;
}
public ConsentRepresentation getConsent() { public ConsentRepresentation getConsent() {
return consent; return consent;
} }

View file

@ -42,6 +42,7 @@ import org.keycloak.services.managers.Auth;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.Cors; import org.keycloak.services.resources.Cors;
import org.keycloak.services.resources.account.resources.ResourcesService; import org.keycloak.services.resources.account.resources.ResourcesService;
import org.keycloak.services.util.ResolveRelative;
import org.keycloak.storage.ReadOnlyException; import org.keycloak.storage.ReadOnlyException;
import javax.ws.rs.Consumes; import javax.ws.rs.Consumes;
@ -291,7 +292,9 @@ public class AccountRestService {
representation.setUserConsentRequired(model.isConsentRequired()); representation.setUserConsentRequired(model.isConsentRequired());
representation.setInUse(inUseClients.contains(model.getClientId())); representation.setInUse(inUseClients.contains(model.getClientId()));
representation.setOfflineAccess(offlineClients.contains(model.getClientId())); representation.setOfflineAccess(offlineClients.contains(model.getClientId()));
representation.setRootUrl(model.getRootUrl());
representation.setBaseUrl(model.getBaseUrl()); representation.setBaseUrl(model.getBaseUrl());
representation.setEffectiveUrl(ResolveRelative.resolveRelativeUri(session, model.getRootUrl(), model.getBaseUrl()));
UserConsentModel consentModel = consents.get(model.getClientId()); UserConsentModel consentModel = consents.get(model.getClientId());
if(consentModel != null) { if(consentModel != null) {
representation.setConsent(modelToRepresentation(consentModel)); representation.setConsent(modelToRepresentation(consentModel));

View file

@ -53,6 +53,7 @@ import org.keycloak.representations.idm.RequiredActionProviderSimpleRepresentati
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.account.AccountCredentialResource; import org.keycloak.services.resources.account.AccountCredentialResource;
import org.keycloak.services.resources.account.AccountCredentialResource.PasswordUpdate; import org.keycloak.services.resources.account.AccountCredentialResource.PasswordUpdate;
import org.keycloak.services.util.ResolveRelative;
import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.admin.authentication.AbstractAuthenticationTest; import org.keycloak.testsuite.admin.authentication.AbstractAuthenticationTest;
import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude; import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude;
@ -643,8 +644,8 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
Map<String, ClientRepresentation> apps = applications.stream().collect(Collectors.toMap(x -> x.getClientId(), x -> x)); Map<String, ClientRepresentation> apps = applications.stream().collect(Collectors.toMap(x -> x.getClientId(), x -> x));
Assert.assertThat(apps.keySet(), containsInAnyOrder("in-use-client", "always-display-client")); Assert.assertThat(apps.keySet(), containsInAnyOrder("in-use-client", "always-display-client"));
assertClientRep(apps.get("in-use-client"), "In Use Client", null, false, true, false, inUseClientAppUri); assertClientRep(apps.get("in-use-client"), "In Use Client", null, false, true, false, null, inUseClientAppUri);
assertClientRep(apps.get("always-display-client"), "Always Display Client", null, false, false, false, alwaysDisplayClientAppUri); assertClientRep(apps.get("always-display-client"), "Always Display Client", null, false, false, false, null, alwaysDisplayClientAppUri);
} }
@Test @Test
@ -666,7 +667,7 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
Map<String, ClientRepresentation> apps = applications.stream().collect(Collectors.toMap(x -> x.getClientId(), x -> x)); Map<String, ClientRepresentation> apps = applications.stream().collect(Collectors.toMap(x -> x.getClientId(), x -> x));
Assert.assertThat(apps.keySet(), containsInAnyOrder("offline-client", "always-display-client")); Assert.assertThat(apps.keySet(), containsInAnyOrder("offline-client", "always-display-client"));
assertClientRep(apps.get("offline-client"), "Offline Client", null, false, true, true, offlineClientAppUri); assertClientRep(apps.get("offline-client"), "Offline Client", null, false, true, true, null, offlineClientAppUri);
} }
@Test @Test
@ -705,21 +706,44 @@ public class AccountRestServiceTest extends AbstractRestServiceTest {
Assert.assertThat(apps.keySet(), containsInAnyOrder(appId, "always-display-client")); Assert.assertThat(apps.keySet(), containsInAnyOrder(appId, "always-display-client"));
ClientRepresentation app = apps.get(appId); ClientRepresentation app = apps.get(appId);
assertClientRep(app, null, "A third party application", true, false, false, "http://localhost:8180/auth/realms/master/app/auth"); assertClientRep(app, null, "A third party application", true, false, false, null, "http://localhost:8180/auth/realms/master/app/auth");
assertFalse(app.getConsent().getGrantedScopes().isEmpty()); assertFalse(app.getConsent().getGrantedScopes().isEmpty());
ConsentScopeRepresentation grantedScope = app.getConsent().getGrantedScopes().get(0); ConsentScopeRepresentation grantedScope = app.getConsent().getGrantedScopes().get(0);
assertEquals(clientScopeRepresentation.getId(), grantedScope.getId()); assertEquals(clientScopeRepresentation.getId(), grantedScope.getId());
assertEquals(clientScopeRepresentation.getName(), grantedScope.getName()); assertEquals(clientScopeRepresentation.getName(), grantedScope.getName());
} }
private void assertClientRep(ClientRepresentation clientRep, String name, String description, boolean userConsentRequired, boolean inUse, boolean offlineAccess, String baseUrl) { @Test
public void listApplicationsWithRootUrl() throws Exception {
oauth.clientId("root-url-client");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("password", "view-applications-access", "password");
Assert.assertNull(tokenResponse.getErrorDescription());
TokenUtil token = new TokenUtil("view-applications-access", "password");
List<ClientRepresentation> applications = SimpleHttp
.doGet(getAccountUrl("applications"), httpClient)
.header("Accept", "application/json")
.auth(token.getToken())
.asJson(new TypeReference<List<ClientRepresentation>>() {
});
assertFalse(applications.isEmpty());
Map<String, ClientRepresentation> apps = applications.stream().collect(Collectors.toMap(x -> x.getClientId(), x -> x));
Assert.assertThat(apps.keySet(), containsInAnyOrder("root-url-client", "always-display-client"));
assertClientRep(apps.get("root-url-client"), null, null, false, true, false, "http://localhost:8180/foo/bar", "/baz");
}
private void assertClientRep(ClientRepresentation clientRep, String name, String description, boolean userConsentRequired, boolean inUse, boolean offlineAccess, String rootUrl, String baseUrl) {
assertNotNull(clientRep); assertNotNull(clientRep);
assertEquals(name, clientRep.getClientName()); assertEquals(name, clientRep.getClientName());
assertEquals(description, clientRep.getDescription()); assertEquals(description, clientRep.getDescription());
assertEquals(userConsentRequired, clientRep.isUserConsentRequired()); assertEquals(userConsentRequired, clientRep.isUserConsentRequired());
assertEquals(inUse, clientRep.isInUse()); assertEquals(inUse, clientRep.isInUse());
assertEquals(offlineAccess, clientRep.isOfflineAccess()); assertEquals(offlineAccess, clientRep.isOfflineAccess());
assertEquals(rootUrl, clientRep.getRootUrl());
assertEquals(baseUrl, clientRep.getBaseUrl()); assertEquals(baseUrl, clientRep.getBaseUrl());
assertEquals(ResolveRelative.resolveRelativeUri(null, null, rootUrl, baseUrl), clientRep.getEffectiveUrl());
} }
@Test @Test

View file

@ -200,6 +200,7 @@
"http://localhost:8180/foo/bar/*", "http://localhost:8180/foo/bar/*",
"https://localhost:8543/foo/bar/*" "https://localhost:8543/foo/bar/*"
], ],
"directAccessGrantsEnabled": true,
"secret": "password" "secret": "password"
}, },
{ {

View file

@ -58,7 +58,7 @@ export interface Consent {
} }
interface Application { interface Application {
baseUrl: string; effectiveUrl: string;
clientId: string; clientId: string;
clientName: string; clientName: string;
consent: Consent; consent: Consent;
@ -117,7 +117,7 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
<ContentPage title={Msg.localize('applicationsPageTitle')}> <ContentPage title={Msg.localize('applicationsPageTitle')}>
<DataList id="applications-list" aria-label={Msg.localize('applicationsPageTitle')}> <DataList id="applications-list" aria-label={Msg.localize('applicationsPageTitle')}>
{this.state.applications.map((application: Application, appIndex: number) => { {this.state.applications.map((application: Application, appIndex: number) => {
const appUrl: string = application.userConsentRequired ? application.baseUrl : '/auth' + application.baseUrl; const appUrl: string = application.effectiveUrl;
return ( return (
<DataListItem id={this.elementId("client-id", application)} key={'application-' + appIndex} aria-labelledby="applications-list" isExpanded={this.state.isRowOpen[appIndex]}> <DataListItem id={this.elementId("client-id", application)} key={'application-' + appIndex} aria-labelledby="applications-list" isExpanded={this.state.isRowOpen[appIndex]}>
@ -140,11 +140,11 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
<DataListCell id={this.elementId('status', application)} width={2} key={'status-' + appIndex}> <DataListCell id={this.elementId('status', application)} width={2} key={'status-' + appIndex}>
{application.inUse ? Msg.localize('inUse') : Msg.localize('notInUse')} {application.inUse ? Msg.localize('inUse') : Msg.localize('notInUse')}
</DataListCell>, </DataListCell>,
<DataListCell id={this.elementId('baseurl', application)} width={4} key={'baseUrl-' + appIndex}> <DataListCell id={this.elementId('effectiveurl', application)} width={4} key={'effectiveUrl-' + appIndex}>
<button className="pf-c-button pf-m-link" type="button" onClick={() => window.open(appUrl)}> <button className="pf-c-button pf-m-link" type="button" onClick={() => window.open(appUrl)}>
<span className="pf-c-button__icon"> <span className="pf-c-button__icon">
<i className="fas fa-link" aria-hidden="true"></i> <i className="fas fa-link" aria-hidden="true"></i>
</span>{application.baseUrl}</button> </span>{application.effectiveUrl}</button>
</DataListCell>, </DataListCell>,
]} ]}
/> />
@ -161,7 +161,7 @@ export class ApplicationsPage extends React.Component<ApplicationsPageProps, App
{application.description && {application.description &&
<GridItem><strong>{Msg.localize('description') + ': '}</strong> {application.description}</GridItem> <GridItem><strong>{Msg.localize('description') + ': '}</strong> {application.description}</GridItem>
} }
<GridItem><strong>{Msg.localize('baseUrl') + ': '}</strong> {application.baseUrl}</GridItem> <GridItem><strong>{Msg.localize('effectiveUrl') + ': '}</strong> {application.effectiveUrl}</GridItem>
{application.consent && {application.consent &&
<React.Fragment> <React.Fragment>
<GridItem span={12}> <GridItem span={12}>