KEYCLOAK-3414 Support for client registration from trusted hosts
This commit is contained in:
parent
a8fb988e31
commit
0520d465c1
33 changed files with 1219 additions and 34 deletions
|
@ -0,0 +1,60 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.representations.idm;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class ClientRegistrationTrustedHostRepresentation {
|
||||
|
||||
String hostName;
|
||||
Integer count;
|
||||
Integer remainingCount;
|
||||
|
||||
public static ClientRegistrationTrustedHostRepresentation create(String hostName, int count, int remainingCount) {
|
||||
ClientRegistrationTrustedHostRepresentation rep = new ClientRegistrationTrustedHostRepresentation();
|
||||
rep.setHostName(hostName);
|
||||
rep.setCount(count);
|
||||
rep.setRemainingCount(remainingCount);
|
||||
return rep;
|
||||
}
|
||||
|
||||
public String getHostName() {
|
||||
return hostName;
|
||||
}
|
||||
|
||||
public void setHostName(String hostName) {
|
||||
this.hostName = hostName;
|
||||
}
|
||||
|
||||
public Integer getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public void setCount(Integer count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public Integer getRemainingCount() {
|
||||
return remainingCount;
|
||||
}
|
||||
|
||||
public void setRemainingCount(Integer remainingCount) {
|
||||
this.remainingCount = remainingCount;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.admin.client.resource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.keycloak.representations.idm.ClientRegistrationTrustedHostRepresentation;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface ClientRegistrationTrustedHostResource {
|
||||
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
Response create(ClientRegistrationTrustedHostRepresentation config);
|
||||
|
||||
@PUT
|
||||
@Path("{hostname}")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
Response update(final @PathParam("hostname") String hostName, ClientRegistrationTrustedHostRepresentation config);
|
||||
|
||||
@GET
|
||||
@Path("{hostname}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
ClientRegistrationTrustedHostRepresentation get(final @PathParam("hostname") String hostName);
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
List<ClientRegistrationTrustedHostRepresentation> list();
|
||||
|
||||
@DELETE
|
||||
@Path("{hostname}")
|
||||
void delete(final @PathParam("hostname") String hostName);
|
||||
|
||||
}
|
|
@ -148,6 +148,9 @@ public interface RealmResource {
|
|||
@Path("clients-initial-access")
|
||||
ClientInitialAccessResource clientInitialAccess();
|
||||
|
||||
@Path("clients-trusted-hosts")
|
||||
public ClientRegistrationTrustedHostResource clientRegistrationTrustedHost();
|
||||
|
||||
@Path("partialImport")
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
|
|
|
@ -34,7 +34,7 @@ Q: Does the OP have a .well-known/openid-configuration endpoint?
|
|||
A: Yes
|
||||
|
||||
Q: Do the provider support dynamic client registration?
|
||||
A: No (just for easier start)
|
||||
A: No (See below for how to run with dynamic client registration)
|
||||
|
||||
Q: redirect_uris
|
||||
Non-editable value: https://op.certification.openid.net:60720/authz_cb
|
||||
|
@ -62,6 +62,21 @@ Nothing filled
|
|||
4) After setup, you will be redirected to the testing application. Something like `https://op.certification.openid.net:60720/` and can run individual tests.
|
||||
Some tests require some manual actions (eg. delete cookies). The conformance testsuite should guide you.
|
||||
|
||||
Run conformance testsuite with Dynamic client registration
|
||||
----------------------------------------------------------
|
||||
1) The steps are similar to above, however for question:
|
||||
|
||||
Q: Do the provider support dynamic client registration?
|
||||
The answer will be: Yes
|
||||
|
||||
Then you don't need to configure redirect_uris, client_id and client_secret.
|
||||
|
||||
2) With the setup from previous point, OIDC Conformance testsuite will dynamically register new client in Keycloak. But you also need to allow the anonymous
|
||||
client registration requests from the OIDC conformance to register clients.
|
||||
|
||||
So you need to login to Keycloak admin console and in tab "Initial Access Tokens" for realm master, you need to fill new trusted host. Fill the hostname "op.certification.openid.net" and enable big
|
||||
count of registrations for it (1000 or so) as running each test will register new client.
|
||||
|
||||
|
||||
Update the openshift cartridge with latest Keycloak
|
||||
---------------------------------------------------
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.models.cache.infinispan;
|
|||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.common.enums.SslRequired;
|
||||
import org.keycloak.common.util.StringPropertyReplacer;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.models.*;
|
||||
import org.keycloak.models.cache.infinispan.entities.CachedRealm;
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.sessions.infinispan;
|
||||
|
||||
import org.infinispan.Cache;
|
||||
import org.keycloak.models.ClientRegistrationTrustedHostModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.sessions.infinispan.entities.ClientRegistrationTrustedHostEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class ClientRegistrationTrustedHostAdapter implements ClientRegistrationTrustedHostModel {
|
||||
|
||||
private final KeycloakSession session;
|
||||
private final InfinispanUserSessionProvider provider;
|
||||
private final Cache<String, SessionEntity> cache;
|
||||
private final RealmModel realm;
|
||||
private final ClientRegistrationTrustedHostEntity entity;
|
||||
|
||||
public ClientRegistrationTrustedHostAdapter(KeycloakSession session, InfinispanUserSessionProvider provider, Cache<String, SessionEntity> cache, RealmModel realm, ClientRegistrationTrustedHostEntity entity) {
|
||||
this.session = session;
|
||||
this.provider = provider;
|
||||
this.cache = cache;
|
||||
this.realm = realm;
|
||||
this.entity = entity;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RealmModel getRealm() {
|
||||
return realm;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHostName() {
|
||||
return entity.getHostName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return entity.getCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setCount(int count) {
|
||||
entity.setCount(count);
|
||||
update();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRemainingCount() {
|
||||
return entity.getRemainingCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setRemainingCount(int remainingCount) {
|
||||
entity.setRemainingCount(remainingCount);
|
||||
update();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void decreaseRemainingCount() {
|
||||
entity.setRemainingCount(entity.getRemainingCount() - 1);
|
||||
update();
|
||||
}
|
||||
|
||||
void update() {
|
||||
provider.getTx().replace(cache, entity.getId(), entity);
|
||||
}
|
||||
}
|
|
@ -23,9 +23,11 @@ import org.jboss.logging.Logger;
|
|||
import org.keycloak.common.util.Time;
|
||||
import org.keycloak.models.ClientInitialAccessModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientRegistrationTrustedHostModel;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakTransaction;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
|
@ -33,12 +35,14 @@ import org.keycloak.models.UserSessionProvider;
|
|||
import org.keycloak.models.UserLoginFailureModel;
|
||||
import org.keycloak.models.session.UserSessionPersisterProvider;
|
||||
import org.keycloak.models.sessions.infinispan.entities.ClientInitialAccessEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.ClientRegistrationTrustedHostEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.LoginFailureEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.LoginFailureKey;
|
||||
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.UserSessionEntity;
|
||||
import org.keycloak.models.sessions.infinispan.stream.ClientInitialAccessPredicate;
|
||||
import org.keycloak.models.sessions.infinispan.stream.ClientRegistrationTrustedHostPredicate;
|
||||
import org.keycloak.models.sessions.infinispan.stream.ClientSessionPredicate;
|
||||
import org.keycloak.models.sessions.infinispan.stream.Comparators;
|
||||
import org.keycloak.models.sessions.infinispan.stream.Mappers;
|
||||
|
@ -537,6 +541,11 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
return entity != null ? new ClientInitialAccessAdapter(session, this, cache, realm, entity) : null;
|
||||
}
|
||||
|
||||
ClientRegistrationTrustedHostAdapter wrap(RealmModel realm, ClientRegistrationTrustedHostEntity entity) {
|
||||
Cache<String, SessionEntity> cache = getCache(false);
|
||||
return entity != null ? new ClientRegistrationTrustedHostAdapter(session, this, cache, realm, entity) : null;
|
||||
}
|
||||
|
||||
|
||||
UserLoginFailureModel wrap(LoginFailureKey key, LoginFailureEntity entity) {
|
||||
return entity != null ? new UserLoginFailureAdapter(this, loginFailureCache, key, entity) : null;
|
||||
|
@ -729,6 +738,63 @@ public class InfinispanUserSessionProvider implements UserSessionProvider {
|
|||
return list;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRegistrationTrustedHostModel createClientRegistrationTrustedHostModel(RealmModel realm, String hostName, int count) {
|
||||
if (getClientRegistrationTrustedHostModel(realm, hostName) != null) {
|
||||
throw new ModelDuplicateException("Client registration already exists for this realm and hostName");
|
||||
}
|
||||
|
||||
String id = computeClientRegistrationTrustedHostEntityId(realm, hostName);
|
||||
|
||||
ClientRegistrationTrustedHostEntity entity = new ClientRegistrationTrustedHostEntity();
|
||||
entity.setId(id);
|
||||
entity.setHostName(hostName);
|
||||
entity.setRealm(realm.getId());
|
||||
entity.setCount(count);
|
||||
entity.setRemainingCount(count);
|
||||
|
||||
tx.put(sessionCache, id, entity);
|
||||
|
||||
return wrap(realm, entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientRegistrationTrustedHostModel getClientRegistrationTrustedHostModel(RealmModel realm, String hostName) {
|
||||
String id = computeClientRegistrationTrustedHostEntityId(realm, hostName);
|
||||
|
||||
Cache<String, SessionEntity> cache = getCache(false);
|
||||
ClientRegistrationTrustedHostEntity entity = (ClientRegistrationTrustedHostEntity) cache.get(id);
|
||||
|
||||
// If created in this transaction
|
||||
if (entity == null) {
|
||||
entity = (ClientRegistrationTrustedHostEntity) tx.get(cache, id);
|
||||
}
|
||||
|
||||
return wrap(realm, entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void removeClientRegistrationTrustedHostModel(RealmModel realm, String hostName) {
|
||||
String id = computeClientRegistrationTrustedHostEntityId(realm, hostName);
|
||||
tx.remove(getCache(false), id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ClientRegistrationTrustedHostModel> listClientRegistrationTrustedHosts(RealmModel realm) {
|
||||
Iterator<Map.Entry<String, SessionEntity>> itr = sessionCache.entrySet().stream().filter(ClientRegistrationTrustedHostPredicate.create(realm.getId())).iterator();
|
||||
List<ClientRegistrationTrustedHostModel> list = new LinkedList<>();
|
||||
while (itr.hasNext()) {
|
||||
list.add(wrap(realm, (ClientRegistrationTrustedHostEntity) itr.next().getValue()));
|
||||
}
|
||||
return list;
|
||||
}
|
||||
|
||||
private static final String CLIENT_REG_TRUSTED_HOST_ID_PREFIX = "reg:::";
|
||||
|
||||
private String computeClientRegistrationTrustedHostEntityId(RealmModel realm, String hostName) {
|
||||
return CLIENT_REG_TRUSTED_HOST_ID_PREFIX + realm.getId() + ":::" + hostName;
|
||||
}
|
||||
|
||||
class InfinispanKeycloakTransaction implements KeycloakTransaction {
|
||||
|
||||
private boolean active;
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.sessions.infinispan.entities;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class ClientRegistrationTrustedHostEntity extends SessionEntity {
|
||||
|
||||
private String hostName;
|
||||
|
||||
private int count;
|
||||
|
||||
private int remainingCount;
|
||||
|
||||
public String getHostName() {
|
||||
return hostName;
|
||||
}
|
||||
|
||||
public void setHostName(String hostName) {
|
||||
this.hostName = hostName;
|
||||
}
|
||||
|
||||
public int getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
public void setCount(int count) {
|
||||
this.count = count;
|
||||
}
|
||||
|
||||
public int getRemainingCount() {
|
||||
return remainingCount;
|
||||
}
|
||||
|
||||
public void setRemainingCount(int remainingCount) {
|
||||
this.remainingCount = remainingCount;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models.sessions.infinispan.stream;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.Map;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import org.keycloak.models.sessions.infinispan.entities.ClientRegistrationTrustedHostEntity;
|
||||
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class ClientRegistrationTrustedHostPredicate implements Predicate<Map.Entry<String, SessionEntity>>, Serializable {
|
||||
|
||||
public static ClientRegistrationTrustedHostPredicate create(String realm) {
|
||||
return new ClientRegistrationTrustedHostPredicate(realm);
|
||||
}
|
||||
|
||||
private ClientRegistrationTrustedHostPredicate(String realm) {
|
||||
this.realm = realm;
|
||||
}
|
||||
|
||||
private String realm;
|
||||
|
||||
|
||||
@Override
|
||||
public boolean test(Map.Entry<String, SessionEntity> entry) {
|
||||
SessionEntity e = entry.getValue();
|
||||
|
||||
if (!realm.equals(e.getRealm())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!(e instanceof ClientRegistrationTrustedHostEntity)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
|
@ -19,6 +19,7 @@ package org.keycloak.models.jpa;
|
|||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.StringPropertyReplacer;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.common.enums.SslRequired;
|
||||
import org.keycloak.jose.jwk.JWKBuilder;
|
||||
|
|
|
@ -35,4 +35,5 @@
|
|||
|
||||
<include file="META-INF/jpa-changelog-authz-master.xml"/>
|
||||
<include file="META-INF/jpa-changelog-2.1.0.xml"/>
|
||||
<include file="META-INF/jpa-changelog-2.2.0.xml"/>
|
||||
</databaseChangeLog>
|
||||
|
|
|
@ -21,6 +21,7 @@ import com.mongodb.DBObject;
|
|||
import com.mongodb.QueryBuilder;
|
||||
|
||||
import org.keycloak.common.util.MultivaluedHashMap;
|
||||
import org.keycloak.common.util.StringPropertyReplacer;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
|
||||
import org.keycloak.common.enums.SslRequired;
|
||||
|
|
|
@ -128,6 +128,11 @@ public enum ResourceType {
|
|||
*/
|
||||
, CLIENT_INITIAL_ACCESS_MODEL
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
, CLIENT_REGISTRATION_TRUSTED_HOST_MODEL
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.models;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public interface ClientRegistrationTrustedHostModel {
|
||||
|
||||
RealmModel getRealm();
|
||||
|
||||
String getHostName();
|
||||
|
||||
int getCount();
|
||||
void setCount(int count);
|
||||
|
||||
int getRemainingCount();
|
||||
void setRemainingCount(int remainingCount);
|
||||
void decreaseRemainingCount();
|
||||
|
||||
}
|
|
@ -82,6 +82,11 @@ public interface UserSessionProvider extends Provider {
|
|||
void removeClientInitialAccessModel(RealmModel realm, String id);
|
||||
List<ClientInitialAccessModel> listClientInitialAccess(RealmModel realm);
|
||||
|
||||
ClientRegistrationTrustedHostModel createClientRegistrationTrustedHostModel(RealmModel realm, String hostName, int count);
|
||||
ClientRegistrationTrustedHostModel getClientRegistrationTrustedHostModel(RealmModel realm, String hostName);
|
||||
void removeClientRegistrationTrustedHostModel(RealmModel realm, String hostName);
|
||||
List<ClientRegistrationTrustedHostModel> listClientRegistrationTrustedHosts(RealmModel realm);
|
||||
|
||||
void close();
|
||||
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import org.keycloak.events.EventBuilder;
|
|||
import org.keycloak.events.EventType;
|
||||
import org.keycloak.models.ClientInitialAccessModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientRegistrationTrustedHostModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
|
@ -64,6 +65,11 @@ public abstract class AbstractClientRegistrationProvider implements ClientRegist
|
|||
initialAccessModel.decreaseRemainingCount();
|
||||
}
|
||||
|
||||
if (auth.isRegistrationHostTrusted()) {
|
||||
ClientRegistrationTrustedHostModel trustedHost = auth.getTrustedHostModel();
|
||||
trustedHost.decreaseRemainingCount();
|
||||
}
|
||||
|
||||
event.client(client.getClientId()).success();
|
||||
return client;
|
||||
} catch (ModelDuplicateException e) {
|
||||
|
|
|
@ -18,8 +18,6 @@
|
|||
package org.keycloak.services.clientregistration;
|
||||
|
||||
import org.jboss.resteasy.spi.Failure;
|
||||
import org.jboss.resteasy.spi.NotFoundException;
|
||||
import org.jboss.resteasy.spi.UnauthorizedException;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.authentication.AuthenticationProcessor;
|
||||
|
@ -30,14 +28,13 @@ import org.keycloak.events.EventBuilder;
|
|||
import org.keycloak.models.AdminRoles;
|
||||
import org.keycloak.models.ClientInitialAccessModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.ClientRegistrationTrustedHostModel;
|
||||
import org.keycloak.models.Constants;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
|
||||
import org.keycloak.representations.JsonWebToken;
|
||||
import org.keycloak.services.ErrorResponseException;
|
||||
import org.keycloak.services.ForbiddenException;
|
||||
import org.keycloak.services.ServicesLogger;
|
||||
import org.keycloak.util.TokenUtil;
|
||||
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
|
@ -58,6 +55,8 @@ public class ClientRegistrationAuth {
|
|||
private JsonWebToken jwt;
|
||||
private ClientInitialAccessModel initialAccessModel;
|
||||
|
||||
private ClientRegistrationTrustedHostModel trustedHostModel;
|
||||
|
||||
public ClientRegistrationAuth(KeycloakSession session, EventBuilder event) {
|
||||
this.session = session;
|
||||
this.event = event;
|
||||
|
@ -69,12 +68,15 @@ public class ClientRegistrationAuth {
|
|||
|
||||
String authorizationHeader = session.getContext().getRequestHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
|
||||
if (authorizationHeader == null) {
|
||||
throw unauthorized("Missing Authorization header");
|
||||
|
||||
// Try trusted hosts
|
||||
trustedHostModel = ClientRegistrationHostUtils.getTrustedHost(session.getContext().getConnection().getRemoteAddr(), session, realm);
|
||||
return;
|
||||
}
|
||||
|
||||
String[] split = authorizationHeader.split(" ");
|
||||
if (!split[0].equalsIgnoreCase("bearer")) {
|
||||
throw unauthorized("Invalid Authorization header. Expected type: Bearer");
|
||||
return;
|
||||
}
|
||||
|
||||
ClientRegistrationTokenUtils.TokenVerification tokenVerification = ClientRegistrationTokenUtils.verifyToken(realm, uri, split[1]);
|
||||
|
@ -91,6 +93,10 @@ public class ClientRegistrationAuth {
|
|||
}
|
||||
}
|
||||
|
||||
public boolean isRegistrationHostTrusted() {
|
||||
return trustedHostModel != null;
|
||||
}
|
||||
|
||||
private boolean isBearerToken() {
|
||||
return jwt != null && TokenUtil.TOKEN_TYPE_BEARER.equals(jwt.getType());
|
||||
}
|
||||
|
@ -106,7 +112,10 @@ public class ClientRegistrationAuth {
|
|||
public void requireCreate() {
|
||||
init();
|
||||
|
||||
if (isBearerToken()) {
|
||||
if (isRegistrationHostTrusted()) {
|
||||
// Client registrations from trusted hosts
|
||||
return;
|
||||
} else if (isBearerToken()) {
|
||||
if (hasRole(AdminRoles.MANAGE_CLIENTS, AdminRoles.CREATE_CLIENT)) {
|
||||
return;
|
||||
} else {
|
||||
|
@ -120,7 +129,7 @@ public class ClientRegistrationAuth {
|
|||
}
|
||||
}
|
||||
|
||||
throw unauthorized("Not authorized to view client. Maybe bad token type.");
|
||||
throw unauthorized("Not authenticated to view client. Host not trusted and Token is missing or invalid.");
|
||||
}
|
||||
|
||||
public void requireView(ClientModel client) {
|
||||
|
@ -147,7 +156,7 @@ public class ClientRegistrationAuth {
|
|||
}
|
||||
}
|
||||
|
||||
throw unauthorized("Not authorized to view client. Maybe bad token type.");
|
||||
throw unauthorized("Not authorized to view client. Missing or invalid token or bad client credentials.");
|
||||
}
|
||||
|
||||
public void requireUpdate(ClientModel client) {
|
||||
|
@ -168,13 +177,17 @@ public class ClientRegistrationAuth {
|
|||
}
|
||||
}
|
||||
|
||||
throw unauthorized("Not authorized to update client. Maybe bad token type.");
|
||||
throw unauthorized("Not authorized to update client. Missing or invalid token.");
|
||||
}
|
||||
|
||||
public ClientInitialAccessModel getInitialAccessModel() {
|
||||
return initialAccessModel;
|
||||
}
|
||||
|
||||
public ClientRegistrationTrustedHostModel getTrustedHostModel() {
|
||||
return trustedHostModel;
|
||||
}
|
||||
|
||||
private boolean hasRole(String... role) {
|
||||
try {
|
||||
Map<String, Object> otherClaims = jwt.getOtherClaims();
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.services.clientregistration;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.UnknownHostException;
|
||||
import java.util.List;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.models.ClientRegistrationTrustedHostModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class ClientRegistrationHostUtils {
|
||||
|
||||
private static final Logger logger = Logger.getLogger(ClientRegistrationHostUtils.class);
|
||||
|
||||
/**
|
||||
* @return null if host from request is not trusted. Otherwise return trusted host model
|
||||
*/
|
||||
public static ClientRegistrationTrustedHostModel getTrustedHost(String hostAddress, KeycloakSession session, RealmModel realm) {
|
||||
logger.debugf("Verifying remote host : %s", hostAddress);
|
||||
|
||||
List<ClientRegistrationTrustedHostModel> trustedHosts = session.sessions().listClientRegistrationTrustedHosts(realm);
|
||||
|
||||
for (ClientRegistrationTrustedHostModel realmTrustedHost : trustedHosts) {
|
||||
try {
|
||||
if (realmTrustedHost.getRemainingCount() <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String realmHostIPAddress = InetAddress.getByName(realmTrustedHost.getHostName()).getHostAddress();
|
||||
logger.debugf("Trying host '%s' of address '%s'", realmTrustedHost.getHostName(), realmHostIPAddress);
|
||||
if (realmHostIPAddress.equals(hostAddress)) {
|
||||
logger.debugf("Successfully verified host : %s", realmTrustedHost.getHostName());
|
||||
return realmTrustedHost;
|
||||
}
|
||||
} catch (UnknownHostException uhe) {
|
||||
logger.debugf("Unknown host from realm configuration: %s", realmTrustedHost.getHostName());
|
||||
}
|
||||
}
|
||||
|
||||
logger.debugf("Failed to verify remote host : %s", hostAddress);
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package org.keycloak.services.clientregistration;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class ClientRegistrationUriUtils {
|
||||
}
|
|
@ -0,0 +1,183 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.services.resources.admin;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import javax.ws.rs.Consumes;
|
||||
import javax.ws.rs.DELETE;
|
||||
import javax.ws.rs.GET;
|
||||
import javax.ws.rs.POST;
|
||||
import javax.ws.rs.PUT;
|
||||
import javax.ws.rs.Path;
|
||||
import javax.ws.rs.PathParam;
|
||||
import javax.ws.rs.Produces;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.HttpHeaders;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
import org.jboss.resteasy.spi.NotFoundException;
|
||||
import org.keycloak.events.admin.OperationType;
|
||||
import org.keycloak.events.admin.ResourceType;
|
||||
import org.keycloak.models.ClientRegistrationTrustedHostModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.ModelDuplicateException;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.representations.idm.ClientRegistrationTrustedHostRepresentation;
|
||||
import org.keycloak.services.ErrorResponse;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class ClientRegistrationTrustedHostResource {
|
||||
|
||||
private final RealmAuth auth;
|
||||
private final RealmModel realm;
|
||||
private final AdminEventBuilder adminEvent;
|
||||
|
||||
@Context
|
||||
protected KeycloakSession session;
|
||||
|
||||
@Context
|
||||
protected UriInfo uriInfo;
|
||||
|
||||
public ClientRegistrationTrustedHostResource(RealmModel realm, RealmAuth auth, AdminEventBuilder adminEvent) {
|
||||
this.auth = auth;
|
||||
this.realm = realm;
|
||||
this.adminEvent = adminEvent.resource(ResourceType.CLIENT_REGISTRATION_TRUSTED_HOST_MODEL);
|
||||
|
||||
auth.init(RealmAuth.Resource.CLIENT);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new initial access token.
|
||||
*
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
@POST
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response create(ClientRegistrationTrustedHostRepresentation config) {
|
||||
auth.requireManage();
|
||||
|
||||
if (config.getHostName() == null) {
|
||||
return ErrorResponse.error("hostName not provided in config", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
int count = config.getCount() != null ? config.getCount() : 1;
|
||||
|
||||
try {
|
||||
ClientRegistrationTrustedHostModel clientRegTrustedHostModel = session.sessions().createClientRegistrationTrustedHostModel(realm, config.getHostName(), count);
|
||||
|
||||
adminEvent.operation(OperationType.CREATE).resourcePath(uriInfo, clientRegTrustedHostModel.getHostName()).representation(config).success();
|
||||
|
||||
return Response.created(uriInfo.getAbsolutePathBuilder().path(clientRegTrustedHostModel.getHostName()).build()).build();
|
||||
} catch (ModelDuplicateException mde) {
|
||||
return ErrorResponse.exists(mde.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a new initial access token.
|
||||
*
|
||||
* @param config
|
||||
* @return
|
||||
*/
|
||||
@PUT
|
||||
@Path("{hostname}")
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public Response update(final @PathParam("hostname") String hostName, ClientRegistrationTrustedHostRepresentation config) {
|
||||
auth.requireManage();
|
||||
|
||||
if (config.getHostName() == null || !hostName.equals(config.getHostName())) {
|
||||
return ErrorResponse.error("hostName not provided in config or not compatible", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (config.getCount() == null) {
|
||||
return ErrorResponse.error("count needs to be available", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
if (config.getRemainingCount() != null && config.getRemainingCount() > config.getCount()) {
|
||||
return ErrorResponse.error("remainingCount can't be bigger than count", Response.Status.BAD_REQUEST);
|
||||
}
|
||||
|
||||
ClientRegistrationTrustedHostModel hostModel = session.sessions().getClientRegistrationTrustedHostModel(realm, config.getHostName());
|
||||
if (hostModel == null) {
|
||||
return ErrorResponse.error("hostName record not found", Response.Status.NOT_FOUND);
|
||||
}
|
||||
|
||||
hostModel.setCount(config.getCount());
|
||||
hostModel.setRemainingCount(config.getRemainingCount());
|
||||
|
||||
adminEvent.operation(OperationType.UPDATE).resourcePath(uriInfo).representation(config).success();
|
||||
return Response.noContent().build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an initial access token.
|
||||
*
|
||||
* @param hostName
|
||||
* @return
|
||||
*/
|
||||
@GET
|
||||
@Path("{hostname}")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public ClientRegistrationTrustedHostRepresentation getConfig(final @PathParam("hostname") String hostName) {
|
||||
auth.requireView();
|
||||
|
||||
ClientRegistrationTrustedHostModel hostModel = session.sessions().getClientRegistrationTrustedHostModel(realm, hostName);
|
||||
if (hostModel == null) {
|
||||
throw new NotFoundException("hostName record not found");
|
||||
}
|
||||
|
||||
return wrap(hostModel);
|
||||
}
|
||||
|
||||
@GET
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
public List<ClientRegistrationTrustedHostRepresentation> list() {
|
||||
auth.requireView();
|
||||
|
||||
List<ClientRegistrationTrustedHostModel> models = session.sessions().listClientRegistrationTrustedHosts(realm);
|
||||
List<ClientRegistrationTrustedHostRepresentation> reps = new LinkedList<>();
|
||||
for (ClientRegistrationTrustedHostModel m : models) {
|
||||
ClientRegistrationTrustedHostRepresentation r = wrap(m);
|
||||
reps.add(r);
|
||||
}
|
||||
return reps;
|
||||
}
|
||||
|
||||
@DELETE
|
||||
@Path("{hostname}")
|
||||
public void delete(final @PathParam("hostname") String hostName) {
|
||||
auth.requireManage();
|
||||
|
||||
session.sessions().removeClientRegistrationTrustedHostModel(realm, hostName);
|
||||
adminEvent.operation(OperationType.DELETE).resourcePath(uriInfo).success();
|
||||
}
|
||||
|
||||
private ClientRegistrationTrustedHostRepresentation wrap(ClientRegistrationTrustedHostModel model) {
|
||||
return ClientRegistrationTrustedHostRepresentation.create(model.getHostName(), model.getCount(), model.getRemainingCount());
|
||||
}
|
||||
}
|
|
@ -207,6 +207,19 @@ public class RealmAdminResource {
|
|||
return resource;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Base path for managing client initial access tokens
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
@Path("clients-trusted-hosts")
|
||||
public ClientRegistrationTrustedHostResource getClientRegistrationTrustedHost() {
|
||||
ClientRegistrationTrustedHostResource resource = new ClientRegistrationTrustedHostResource(realm, auth, adminEvent);
|
||||
ResteasyProviderFactory.getInstance().injectProperties(resource);
|
||||
return resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base path for managing components under this realm.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.testsuite.admin;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.resource.ClientRegistrationTrustedHostResource;
|
||||
import org.keycloak.events.admin.OperationType;
|
||||
import org.keycloak.events.admin.ResourceType;
|
||||
import org.keycloak.representations.idm.ClientRegistrationTrustedHostRepresentation;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.util.AdminEventPaths;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
public class ClientRegTrustedHostTest extends AbstractAdminTest {
|
||||
|
||||
|
||||
private ClientRegistrationTrustedHostResource resource;
|
||||
|
||||
@Before
|
||||
public void before() {
|
||||
resource = realm.clientRegistrationTrustedHost();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInitialAccessTokens() {
|
||||
|
||||
// Successfully create "localhost1" rep
|
||||
ClientRegistrationTrustedHostRepresentation rep = new ClientRegistrationTrustedHostRepresentation();
|
||||
rep.setHostName("localhost1");
|
||||
rep.setCount(5);
|
||||
|
||||
Response res = resource.create(rep);
|
||||
assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.clientRegistrationTrustedHostPath("localhost1"), rep, ResourceType.CLIENT_REGISTRATION_TRUSTED_HOST_MODEL);
|
||||
res.close();
|
||||
|
||||
// Failed to create conflicting rep "localhost1" again
|
||||
res = resource.create(rep);
|
||||
Assert.assertEquals(409, res.getStatus());
|
||||
assertAdminEvents.assertEmpty();
|
||||
res.close();
|
||||
|
||||
// Successfully create "localhost2" rep
|
||||
rep = new ClientRegistrationTrustedHostRepresentation();
|
||||
rep.setHostName("localhost2");
|
||||
rep.setCount(10);
|
||||
|
||||
res = resource.create(rep);
|
||||
assertAdminEvents.assertEvent(realmId, OperationType.CREATE, AdminEventPaths.clientRegistrationTrustedHostPath("localhost2"), rep, ResourceType.CLIENT_REGISTRATION_TRUSTED_HOST_MODEL);
|
||||
res.close();
|
||||
|
||||
// Get "localhost1"
|
||||
rep = resource.get("localhost1");
|
||||
assertRep(rep, "localhost1", 5, 5);
|
||||
|
||||
// Update "localhost1"
|
||||
rep.setCount(7);
|
||||
rep.setRemainingCount(7);
|
||||
resource.update("localhost1", rep);
|
||||
assertAdminEvents.assertEvent(realmId, OperationType.UPDATE, AdminEventPaths.clientRegistrationTrustedHostPath("localhost1"), rep, ResourceType.CLIENT_REGISTRATION_TRUSTED_HOST_MODEL);
|
||||
|
||||
// Get all
|
||||
List<ClientRegistrationTrustedHostRepresentation> alls = resource.list();
|
||||
Assert.assertEquals(2, alls.size());
|
||||
assertRep(findByHost(alls, "localhost1"), "localhost1", 7, 7);
|
||||
assertRep(findByHost(alls, "localhost2"), "localhost2", 10, 10);
|
||||
|
||||
// Delete "localhost1"
|
||||
resource.delete("localhost1");
|
||||
assertAdminEvents.assertEvent(realmId, OperationType.DELETE, AdminEventPaths.clientRegistrationTrustedHostPath("localhost1"), ResourceType.CLIENT_REGISTRATION_TRUSTED_HOST_MODEL);
|
||||
|
||||
// Get all and check just "localhost2" available
|
||||
alls = resource.list();
|
||||
Assert.assertEquals(1, alls.size());
|
||||
assertRep(alls.get(0), "localhost2", 10, 10);
|
||||
}
|
||||
|
||||
private ClientRegistrationTrustedHostRepresentation findByHost(List<ClientRegistrationTrustedHostRepresentation> list, String hostName) {
|
||||
for (ClientRegistrationTrustedHostRepresentation rep : list) {
|
||||
if (hostName.equals(rep.getHostName())) {
|
||||
return rep;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void assertRep(ClientRegistrationTrustedHostRepresentation rep, String expectedHost, int expectedCount, int expectedRemaining) {
|
||||
Assert.assertEquals(expectedHost, rep.getHostName());
|
||||
Assert.assertEquals(expectedCount, rep.getCount().intValue());
|
||||
Assert.assertEquals(expectedRemaining, rep.getRemainingCount().intValue());
|
||||
}
|
||||
}
|
|
@ -20,7 +20,6 @@ package org.keycloak.testsuite.client;
|
|||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.OAuthErrorException;
|
||||
import org.keycloak.client.registration.Auth;
|
||||
import org.keycloak.client.registration.ClientRegistrationException;
|
||||
import org.keycloak.client.registration.HttpErrorException;
|
||||
|
@ -28,13 +27,15 @@ import org.keycloak.common.util.CollectionUtil;
|
|||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||
import org.keycloak.representations.idm.ClientInitialAccessCreatePresentation;
|
||||
import org.keycloak.representations.idm.ClientInitialAccessPresentation;
|
||||
import org.keycloak.representations.idm.OAuth2ErrorRepresentation;
|
||||
import org.keycloak.representations.idm.ClientRegistrationTrustedHostRepresentation;
|
||||
import org.keycloak.representations.oidc.OIDCClientRepresentation;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
|
@ -50,11 +51,16 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
|
|||
reg.auth(Auth.token(token));
|
||||
}
|
||||
|
||||
public OIDCClientRepresentation create() throws ClientRegistrationException {
|
||||
private OIDCClientRepresentation createRep() {
|
||||
OIDCClientRepresentation client = new OIDCClientRepresentation();
|
||||
client.setClientName("RegistrationAccessTokenTest");
|
||||
client.setClientUri("http://root");
|
||||
client.setRedirectUris(Collections.singletonList("http://redirect"));
|
||||
return client;
|
||||
}
|
||||
|
||||
public OIDCClientRepresentation create() throws ClientRegistrationException {
|
||||
OIDCClientRepresentation client = createRep();
|
||||
|
||||
OIDCClientRepresentation response = reg.oidc().create(client);
|
||||
|
||||
|
@ -62,21 +68,41 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
public void testMissingToken() throws Exception {
|
||||
public void testCreateWithTrustedHost() throws Exception {
|
||||
reg.auth(null);
|
||||
|
||||
OIDCClientRepresentation client = new OIDCClientRepresentation();
|
||||
client.setClientName("RegistrationAccessTokenTest");
|
||||
client.setClientUri("http://root");
|
||||
client.setRedirectUris(Collections.singletonList("http://redirect"));
|
||||
OIDCClientRepresentation client = createRep();
|
||||
|
||||
// Failed to create client
|
||||
try {
|
||||
reg.oidc().create(client);
|
||||
Assert.fail("Not expected to successfuly register client");
|
||||
} catch (ClientRegistrationException expected) {
|
||||
HttpErrorException httpEx = (HttpErrorException) expected.getCause();
|
||||
Assert.assertEquals(401, httpEx.getStatusLine().getStatusCode());
|
||||
}
|
||||
|
||||
// Create trusted host entry
|
||||
Response response = adminClient.realm(REALM_NAME).clientRegistrationTrustedHost().create(ClientRegistrationTrustedHostRepresentation.create("localhost", 2, 2));
|
||||
Assert.assertEquals(201, response.getStatus());
|
||||
|
||||
// Successfully register client
|
||||
reg.oidc().create(client);
|
||||
|
||||
// Just one remaining available
|
||||
ClientRegistrationTrustedHostRepresentation rep = adminClient.realm(REALM_NAME).clientRegistrationTrustedHost().get("localhost");
|
||||
Assert.assertEquals(1, rep.getRemainingCount().intValue());
|
||||
|
||||
// Successfully register client2
|
||||
reg.oidc().create(client);
|
||||
|
||||
// Failed to create 3rd client
|
||||
try {
|
||||
reg.oidc().create(client);
|
||||
Assert.fail("Not expected to successfuly register client");
|
||||
} catch (ClientRegistrationException expected) {
|
||||
HttpErrorException httpEx = (HttpErrorException) expected.getCause();
|
||||
Assert.assertEquals(401, httpEx.getStatusLine().getStatusCode());
|
||||
Assert.assertEquals(OAuthErrorException.INVALID_TOKEN, httpEx.toErrorRepresentation().getError());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -317,6 +317,15 @@ public class AdminEventPaths {
|
|||
return uri.toString();
|
||||
}
|
||||
|
||||
// CLIENT REGISTRATION TRUSTED HOSTS
|
||||
|
||||
public static String clientRegistrationTrustedHostPath(String hostName) {
|
||||
URI uri = UriBuilder.fromUri("").path(RealmResource.class, "clientRegistrationTrustedHost")
|
||||
.path(ClientInitialAccessResource.class, "delete")
|
||||
.build(hostName);
|
||||
return uri.toString();
|
||||
}
|
||||
|
||||
// GROUPS
|
||||
|
||||
public static String groupsPath() {
|
||||
|
|
|
@ -511,12 +511,25 @@ remainingCount=Remaining Count
|
|||
created=Created
|
||||
back=Back
|
||||
initial-access-tokens=Initial Access Tokens
|
||||
initial-access-tokens.tooltip=Initial Access Tokens for dynamic registrations of clients. Request with those tokens can be sent from any host.
|
||||
add-initial-access-tokens=Add Initial Access Token
|
||||
initial-access-token=Initial Access Token
|
||||
initial-access.copyPaste.tooltip=Copy/paste the initial access token before navigating away from this page as it's not posible to retrieve later
|
||||
continue=Continue
|
||||
initial-access-token.confirm.title=Copy Initial Access Token
|
||||
initial-access-token.confirm.text=Please copy and paste the initial access token before confirming as it can't be retrieved later
|
||||
no-initial-access-available=No Initial Access Tokens available
|
||||
|
||||
trusted-hosts-legend=Trusted Hosts For Client Registrations
|
||||
trusted-hosts-legend.tooltip=Hosts, which are trusted for client registrations. Client registration requests from those hosts can be sent even without initial access token. The amount of client registrations from particular host can be limited to specified count.
|
||||
no-client-trusted-hosts-available=No Trusted Hosts available
|
||||
add-client-reg-trusted-host=Add Trusted Host
|
||||
hostname=Hostname
|
||||
client-reg-hostname.tooltip=Fully-Qualified Hostname or IP Address. Client registration requests from this host/address will be trusted and allowed to register new client.
|
||||
client-reg-count.tooltip=Allowed count of client registration requests from particular host. You need to restart this once the limit is reached.
|
||||
client-reg-remainingCount.tooltip=Remaining count of client registration requests from this host. You need to restart this once the limit is reached.
|
||||
reset-remaining-count=Reset Remaining Count
|
||||
|
||||
|
||||
client-templates=Client Templates
|
||||
client-templates.tooltip=Client templates allow you to define common configuration that is shared between multiple clients
|
||||
|
|
|
@ -208,6 +208,9 @@ module.config([ '$routeProvider', function($routeProvider) {
|
|||
},
|
||||
clientInitialAccess : function(ClientInitialAccessLoader) {
|
||||
return ClientInitialAccessLoader();
|
||||
},
|
||||
clientRegTrustedHosts : function(ClientRegistrationTrustedHostListLoader) {
|
||||
return ClientRegistrationTrustedHostListLoader();
|
||||
}
|
||||
},
|
||||
controller : 'ClientInitialAccessCtrl'
|
||||
|
@ -221,6 +224,30 @@ module.config([ '$routeProvider', function($routeProvider) {
|
|||
},
|
||||
controller : 'ClientInitialAccessCreateCtrl'
|
||||
})
|
||||
.when('/realms/:realm/client-reg-trusted-hosts/create', {
|
||||
templateUrl : resourceUrl + '/partials/client-reg-trusted-host-create.html',
|
||||
resolve : {
|
||||
realm : function(RealmLoader) {
|
||||
return RealmLoader();
|
||||
},
|
||||
clientRegTrustedHost : function() {
|
||||
return {};
|
||||
}
|
||||
},
|
||||
controller : 'ClientRegistrationTrustedHostDetailCtrl'
|
||||
})
|
||||
.when('/realms/:realm/client-reg-trusted-hosts/:hostname', {
|
||||
templateUrl : resourceUrl + '/partials/client-reg-trusted-host-detail.html',
|
||||
resolve : {
|
||||
realm : function(RealmLoader) {
|
||||
return RealmLoader();
|
||||
},
|
||||
clientRegTrustedHost : function(ClientRegistrationTrustedHostLoader) {
|
||||
return ClientRegistrationTrustedHostLoader();
|
||||
}
|
||||
},
|
||||
controller : 'ClientRegistrationTrustedHostDetailCtrl'
|
||||
})
|
||||
.when('/realms/:realm/keys-settings', {
|
||||
templateUrl : resourceUrl + '/partials/realm-keys.html',
|
||||
resolve : {
|
||||
|
|
|
@ -2149,9 +2149,23 @@ module.controller('AuthenticationConfigCreateCtrl', function($scope, realm, flow
|
|||
|
||||
});
|
||||
|
||||
module.controller('ClientInitialAccessCtrl', function($scope, realm, clientInitialAccess, ClientInitialAccess, Dialog, Notifications, $route) {
|
||||
module.controller('ClientInitialAccessCtrl', function($scope, realm, clientInitialAccess, clientRegTrustedHosts, ClientInitialAccess, ClientRegistrationTrustedHost, Dialog, Notifications, $route, $location) {
|
||||
$scope.realm = realm;
|
||||
$scope.clientInitialAccess = clientInitialAccess;
|
||||
$scope.clientRegTrustedHosts = clientRegTrustedHosts;
|
||||
|
||||
$scope.updateHost = function(hostname) {
|
||||
$location.url('/realms/' + realm.realm + '/client-reg-trusted-hosts/' + hostname);
|
||||
};
|
||||
|
||||
$scope.removeHost = function(hostname) {
|
||||
Dialog.confirmDelete(hostname, 'trusted host for client registration', function() {
|
||||
ClientRegistrationTrustedHost.remove({ realm: realm.realm, hostname: hostname }, function() {
|
||||
Notifications.success("The trusted host for client registration was deleted.");
|
||||
$route.reload();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.remove = function(id) {
|
||||
Dialog.confirmDelete(id, 'initial access token', function() {
|
||||
|
@ -2163,6 +2177,57 @@ module.controller('ClientInitialAccessCtrl', function($scope, realm, clientIniti
|
|||
}
|
||||
});
|
||||
|
||||
module.controller('ClientRegistrationTrustedHostDetailCtrl', function($scope, realm, clientRegTrustedHost, ClientRegistrationTrustedHost, Dialog, Notifications, $route, $location) {
|
||||
$scope.realm = realm;
|
||||
|
||||
$scope.create = !clientRegTrustedHost.hostName;
|
||||
$scope.changed = false;
|
||||
|
||||
if ($scope.create) {
|
||||
$scope.count = 5;
|
||||
} else {
|
||||
$scope.hostName = clientRegTrustedHost.hostName;
|
||||
$scope.count = clientRegTrustedHost.count;
|
||||
$scope.remainingCount = clientRegTrustedHost.remainingCount;
|
||||
}
|
||||
|
||||
$scope.save = function() {
|
||||
if ($scope.create) {
|
||||
ClientRegistrationTrustedHost.save({
|
||||
realm: realm.realm
|
||||
}, { hostName: $scope.hostName, count: $scope.count, remainingCount: $scope.count }, function (data) {
|
||||
Notifications.success("The trusted host was created.");
|
||||
$location.url('/realms/' + realm.realm + '/client-reg-trusted-hosts/' + $scope.hostName);
|
||||
});
|
||||
} else {
|
||||
ClientRegistrationTrustedHost.update({
|
||||
realm: realm.realm, hostname: $scope.hostName
|
||||
}, { hostName: $scope.hostName, count: $scope.count, remainingCount: $scope.count }, function (data) {
|
||||
Notifications.success("The trusted host was updated.");
|
||||
$route.reload();
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
$scope.cancel = function() {
|
||||
$location.url('/realms/' + realm.realm + '/client-initial-access');
|
||||
};
|
||||
|
||||
$scope.resetRemainingCount = function() {
|
||||
$scope.save();
|
||||
}
|
||||
|
||||
$scope.$watch('count', function(newVal, oldVal) {
|
||||
if (oldVal == newVal) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scope.changed = true;
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
module.controller('ClientInitialAccessCreateCtrl', function($scope, realm, ClientInitialAccess, TimeUnit, Dialog, $location, $translate) {
|
||||
$scope.expirationUnit = 'Days';
|
||||
$scope.expiration = TimeUnit.toUnit(0, $scope.expirationUnit);
|
||||
|
|
|
@ -510,6 +510,23 @@ module.factory('ClientInitialAccessLoader', function(Loader, ClientInitialAccess
|
|||
});
|
||||
});
|
||||
|
||||
module.factory('ClientRegistrationTrustedHostListLoader', function(Loader, ClientRegistrationTrustedHost, $route) {
|
||||
return Loader.query(ClientRegistrationTrustedHost, function() {
|
||||
return {
|
||||
realm: $route.current.params.realm
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
module.factory('ClientRegistrationTrustedHostLoader', function(Loader, ClientRegistrationTrustedHost, $route) {
|
||||
return Loader.get(ClientRegistrationTrustedHost, function() {
|
||||
return {
|
||||
realm: $route.current.params.realm,
|
||||
hostname : $route.current.params.hostname
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -298,6 +298,18 @@ module.factory('ClientInitialAccess', function($resource) {
|
|||
});
|
||||
});
|
||||
|
||||
module.factory('ClientRegistrationTrustedHost', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/clients-trusted-hosts/:hostname', {
|
||||
realm : '@realm',
|
||||
hostname : '@hostname'
|
||||
}, {
|
||||
update : {
|
||||
method : 'PUT'
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
module.factory('ClientProtocolMapper', function($resource) {
|
||||
return $resource(authUrl + '/admin/realms/:realm/clients/:client/protocol-mappers/models/:id', {
|
||||
|
|
|
@ -1,6 +1,59 @@
|
|||
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||
<kc-tabs-realm></kc-tabs-realm>
|
||||
|
||||
<div class="form-group">
|
||||
<legend>{{:: 'trusted-hosts-legend' | translate}}</legend>
|
||||
<kc-tooltip>{{:: 'trusted-hosts-legend.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="kc-table-actions" colspan="6">
|
||||
<div class="form-inline">
|
||||
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<input type="text" placeholder="{{:: 'search.placeholder' | translate}}" data-ng-model="search1.host" class="form-control search" onkeyup="if(event.keyCode == 13){$(this).next('I').click();}">
|
||||
<div class="input-group-addon">
|
||||
<i class="fa fa-search" type="submit"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="pull-right" data-ng-show="access.manageClients">
|
||||
<a id="createClientTrustedHost" class="btn btn-default" href="#/realms/{{realm.realm}}/client-reg-trusted-hosts/create">{{:: 'create' | translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<tr data-ng-hide="clientRegTrustedHosts.length == 0">
|
||||
<th>{{:: 'hostname' | translate}}</th>
|
||||
<th>{{:: 'count' | translate}}</th>
|
||||
<th>{{:: 'remainingCount' | translate}}</th>
|
||||
<th colspan="2">{{:: 'actions' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="host in clientRegTrustedHosts | filter:search1 | orderBy:'timestamp'">
|
||||
<td>{{host.hostName}}</td>
|
||||
<td>{{host.count}}</td>
|
||||
<td>{{host.remainingCount}}</td>
|
||||
<td class="kc-action-cell" data-ng-click="updateHost(host.hostName)">{{:: 'update' | translate}}</td>
|
||||
<td class="kc-action-cell" data-ng-click="removeHost(host.hostName)">{{:: 'delete' | translate}}</td>
|
||||
</tr>
|
||||
<tr data-ng-show="(clientRegTrustedHosts | filter:search1).length == 0">
|
||||
<td class="text-muted" colspan="3" data-ng-show="search1.host">{{:: 'no-results' | translate}}</td>
|
||||
<td class="text-muted" colspan="3" data-ng-hide="search1.host">{{:: 'no-client-trusted-hosts-available' | translate}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<legend>{{:: 'initial-access-tokens' | translate}}</legend>
|
||||
<kc-tooltip>{{:: 'initial-access-tokens.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
<table class="table table-striped table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -8,7 +61,7 @@
|
|||
<div class="form-inline">
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<input type="text" placeholder="{{:: 'search.placeholder' | translate}}" data-ng-model="search.id" class="form-control search" onkeyup="if(event.keyCode == 13){$(this).next('I').click();}">
|
||||
<input type="text" placeholder="{{:: 'search.placeholder' | translate}}" data-ng-model="search2.id" class="form-control search" onkeyup="if(event.keyCode == 13){$(this).next('I').click();}">
|
||||
<div class="input-group-addon">
|
||||
<i class="fa fa-search" type="submit"></i>
|
||||
</div>
|
||||
|
@ -21,7 +74,7 @@
|
|||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<tr data-ng-hide="clients.length == 0">
|
||||
<tr data-ng-hide="clientInitialAccess.length == 0">
|
||||
<th>{{:: 'id' | translate}}</th>
|
||||
<th>{{:: 'created' | translate}}</th>
|
||||
<th>{{:: 'expires' | translate}}</th>
|
||||
|
@ -31,7 +84,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr ng-repeat="ia in clientInitialAccess | filter:search | orderBy:'timestamp'">
|
||||
<tr ng-repeat="ia in clientInitialAccess | filter:search2 | orderBy:'timestamp'">
|
||||
<td>{{ia.id}}</td>
|
||||
<td>{{(ia.timestamp * 1000)|date:'shortDate'}} {{(ia.timestamp * 1000)|date:'mediumTime'}}</td>
|
||||
<td><span data-ng-show="ia.expiration > 0">{{((ia.timestamp + ia.expiration) * 1000)|date:'shortDate'}} {{((ia.timestamp + ia.expiration) * 1000)|date:'mediumTime'}}</span></td>
|
||||
|
@ -39,9 +92,9 @@
|
|||
<td>{{ia.remainingCount}}</td>
|
||||
<td class="kc-action-cell" data-ng-click="remove(ia.id)">{{:: 'delete' | translate}}</td>
|
||||
</tr>
|
||||
<tr data-ng-show="(clients | filter:search).length == 0">
|
||||
<td class="text-muted" colspan="3" data-ng-show="search.clientId">{{:: 'no-results' | translate}}</td>
|
||||
<td class="text-muted" colspan="3" data-ng-hide="search.clientId">{{:: 'no-clients-available' | translate}}</td>
|
||||
<tr data-ng-show="(clientInitialAccess | filter:search2).length == 0">
|
||||
<td class="text-muted" colspan="3" data-ng-show="search2.id">{{:: 'no-results' | translate}}</td>
|
||||
<td class="text-muted" colspan="3" data-ng-hide="search2.id">{{:: 'no-initial-access-available' | translate}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
<!--
|
||||
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
~ and other contributors as indicated by the @author tags.
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="#/realms/{{realm.realm}}/client-initial-access">{{:: 'initial-access-tokens' | translate}}</a></li>
|
||||
<li>{{:: 'add-client-reg-trusted-host' | translate}}</li>
|
||||
</ol>
|
||||
|
||||
<h1>{{:: 'add-client-reg-trusted-host' | translate}}</h1>
|
||||
|
||||
<form class="form-horizontal" name="createForm" novalidate kc-read-only="!access.manageRealm">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="hostName">{{:: 'hostname' | translate}} </label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="text" id="hostName" name="hostName" data-ng-model="hostName">
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'client-reg-hostname.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="count">{{:: 'count' | translate}} </label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="text" id="count" name="count" required min="1" max="10000" data-ng-model="count">
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'client-reg-count.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
|
||||
<button kc-save>{{:: 'save' | translate}}</button>
|
||||
<button kc-cancel data-ng-click="cancel()">{{:: 'cancel' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<kc-menu></kc-menu>
|
|
@ -0,0 +1,64 @@
|
|||
<!--
|
||||
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
|
||||
~ and other contributors as indicated by the @author tags.
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
|
||||
<div class="col-sm-9 col-md-10 col-sm-push-3 col-md-push-2">
|
||||
|
||||
<ol class="breadcrumb">
|
||||
<li><a href="#/realms/{{realm.realm}}/client-initial-access">{{:: 'initial-access-tokens' | translate}}</a></li>
|
||||
<li>{{hostName}}</li>
|
||||
</ol>
|
||||
|
||||
<h1>{{hostName}}</h1>
|
||||
|
||||
<form class="form-horizontal" name="createForm" novalidate kc-read-only="!access.manageRealm">
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="hostName">{{:: 'hostname' | translate}} </label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="text" id="hostName" name="hostName" data-ng-model="hostName" readonly>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'client-reg-hostname.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="count">{{:: 'count' | translate}} </label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="text" id="count" name="count" required min="1" max="10000" data-ng-model="count">
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'client-reg-count.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="col-md-2 control-label" for="remainingCount">{{:: 'remainingCount' | translate}} </label>
|
||||
<div class="col-sm-6">
|
||||
<input class="form-control" type="text" id="remainingCount" name="remainingCount" data-ng-model="remainingCount" readonly>
|
||||
</div>
|
||||
<kc-tooltip>{{:: 'client-reg-remainingCount.tooltip' | translate}}</kc-tooltip>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="col-md-10 col-md-offset-2" data-ng-show="access.manageRealm">
|
||||
<button class="btn btn-primary" data-ng-click="resetRemainingCount()" data-ng-hide="changed">{{:: 'reset-remaining-count' | translate}}</button>
|
||||
<button data-ng-show="changed" kc-save>{{:: 'save' | translate}}</button>
|
||||
<button kc-cancel data-ng-click="cancel()">{{:: 'cancel' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
|
||||
<kc-menu></kc-menu>
|
|
@ -25,6 +25,7 @@
|
|||
|| path[2] == 'theme-settings'
|
||||
|| path[2] == 'token-settings'
|
||||
|| path[2] == 'cache-settings'
|
||||
|| path[2] == 'client-initial-access'
|
||||
|| path[2] == 'defense'
|
||||
|| path[2] == 'keys-settings' || path[2] == 'smtp-settings' || path[2] == 'ldap-settings' || path[2] == 'auth-settings') && path[3] != 'clients') && 'active'">
|
||||
<a href="#/realms/{{realm.realm}}"><span class="pficon pficon-settings"></span> {{:: 'realm-settings' | translate}}</a>
|
||||
|
|
Loading…
Reference in a new issue