Merge pull request #4269 from stianst/dockerdockerdocker

KEYCLOAK-3592 Docker auth implementation
This commit is contained in:
Stian Thorgersen 2017-06-29 07:23:47 +02:00 committed by GitHub
commit c9bc321d2a
80 changed files with 4619 additions and 210 deletions

View file

@ -35,13 +35,13 @@ import java.util.Set;
public class Profile {
public enum Feature {
AUTHORIZATION, IMPERSONATION, SCRIPTS
AUTHORIZATION, IMPERSONATION, SCRIPTS, DOCKER
}
private enum ProfileValue {
PRODUCT(Feature.AUTHORIZATION, Feature.SCRIPTS),
PRODUCT(Feature.AUTHORIZATION, Feature.SCRIPTS, Feature.DOCKER),
PREVIEW,
COMMUNITY;
COMMUNITY(Feature.DOCKER);
private List<Feature> disabled;

View file

@ -0,0 +1,119 @@
package org.keycloak.representations.docker;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
/**
* Per the docker auth v2 spec, access is defined like this:
*
* {
* "type": "repository",
* "name": "samalba/my-app",
* "actions": [
* "push",
* "pull"
* ]
* }
*
*/
public class DockerAccess {
public static final int ACCESS_TYPE = 0;
public static final int REPOSITORY_NAME = 1;
public static final int PERMISSIONS = 2;
public static final String DECODE_ENCODING = "UTF-8";
@JsonProperty("type")
protected String type;
@JsonProperty("name")
protected String name;
@JsonProperty("actions")
protected List<String> actions;
public DockerAccess() {
}
public DockerAccess(final String scopeParam) {
if (scopeParam != null) {
try {
final String unencoded = URLDecoder.decode(scopeParam, DECODE_ENCODING);
final String[] parts = unencoded.split(":");
if (parts.length != 3) {
throw new IllegalArgumentException(String.format("Expecting input string to have %d parts delineated by a ':' character. " +
"Found %d parts: %s", 3, parts.length, unencoded));
}
type = parts[ACCESS_TYPE];
name = parts[REPOSITORY_NAME];
if (parts[PERMISSIONS] != null) {
actions = Arrays.asList(parts[PERMISSIONS].split(","));
}
} catch (final UnsupportedEncodingException e) {
throw new IllegalStateException("Error attempting to decode scope parameter using encoding: " + DECODE_ENCODING);
}
}
}
public String getType() {
return type;
}
public DockerAccess setType(final String type) {
this.type = type;
return this;
}
public String getName() {
return name;
}
public DockerAccess setName(final String name) {
this.name = name;
return this;
}
public List<String> getActions() {
return actions;
}
public DockerAccess setActions(final List<String> actions) {
this.actions = actions;
return this;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (!(o instanceof DockerAccess)) return false;
final DockerAccess that = (DockerAccess) o;
if (type != null ? !type.equals(that.type) : that.type != null) return false;
if (name != null ? !name.equals(that.name) : that.name != null) return false;
return actions != null ? actions.equals(that.actions) : that.actions == null;
}
@Override
public int hashCode() {
int result = type != null ? type.hashCode() : 0;
result = 31 * result + (name != null ? name.hashCode() : 0);
result = 31 * result + (actions != null ? actions.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "DockerAccess{" +
"type='" + type + '\'' +
", name='" + name + '\'' +
", actions=" + actions +
'}';
}
}

View file

@ -0,0 +1,84 @@
package org.keycloak.representations.docker;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
/**
* JSON Representation of a Docker Error in the following format:
*
*
* {
* "code": "UNAUTHORIZED",
* "message": "access to the requested resource is not authorized",
* "detail": [
* {
* "Type": "repository",
* "Name": "samalba/my-app",
* "Action": "pull"
* },
* {
* "Type": "repository",
* "Name": "samalba/my-app",
* "Action": "push"
* }
* ]
* }
*/
public class DockerError {
@JsonProperty("code")
private final String errorCode;
@JsonProperty("message")
private final String message;
@JsonProperty("detail")
private final List<DockerAccess> dockerErrorDetails;
public DockerError(final String errorCode, final String message, final List<DockerAccess> dockerErrorDetails) {
this.errorCode = errorCode;
this.message = message;
this.dockerErrorDetails = dockerErrorDetails;
}
public String getErrorCode() {
return errorCode;
}
public String getMessage() {
return message;
}
public List<DockerAccess> getDockerErrorDetails() {
return dockerErrorDetails;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (!(o instanceof DockerError)) return false;
final DockerError that = (DockerError) o;
if (errorCode != that.errorCode) return false;
if (message != null ? !message.equals(that.message) : that.message != null) return false;
return dockerErrorDetails != null ? dockerErrorDetails.equals(that.dockerErrorDetails) : that.dockerErrorDetails == null;
}
@Override
public int hashCode() {
int result = errorCode != null ? errorCode.hashCode() : 0;
result = 31 * result + (message != null ? message.hashCode() : 0);
result = 31 * result + (dockerErrorDetails != null ? dockerErrorDetails.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "DockerError{" +
"errorCode=" + errorCode +
", message='" + message + '\'' +
", dockerErrorDetails=" + dockerErrorDetails +
'}';
}
}

View file

@ -0,0 +1,38 @@
package org.keycloak.representations.docker;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
public class DockerErrorResponseToken {
@JsonProperty("errors")
private final List<DockerError> errorList;
public DockerErrorResponseToken(final List<DockerError> errorList) {
this.errorList = errorList;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (!(o instanceof DockerErrorResponseToken)) return false;
final DockerErrorResponseToken that = (DockerErrorResponseToken) o;
return errorList != null ? errorList.equals(that.errorList) : that.errorList == null;
}
@Override
public int hashCode() {
return errorList != null ? errorList.hashCode() : 0;
}
@Override
public String toString() {
return "DockerErrorResponseToken{" +
"errorList=" + errorList +
'}';
}
}

View file

@ -0,0 +1,88 @@
package org.keycloak.representations.docker;
import com.fasterxml.jackson.annotation.JsonProperty;
/**
* Creates a response understandable by the docker client in the form:
*
{
"token" : "eyJh...nSQ",
"expires_in" : 300,
"issued_at" : "2016-09-02T10:56:33Z"
}
*/
public class DockerResponse {
@JsonProperty("token")
private String token;
@JsonProperty("expires_in")
private Integer expires_in;
@JsonProperty("issued_at")
private String issued_at;
public DockerResponse() {
}
public DockerResponse(final String token, final Integer expires_in, final String issued_at) {
this.token = token;
this.expires_in = expires_in;
this.issued_at = issued_at;
}
public String getToken() {
return token;
}
public DockerResponse setToken(final String token) {
this.token = token;
return this;
}
public Integer getExpires_in() {
return expires_in;
}
public DockerResponse setExpires_in(final Integer expires_in) {
this.expires_in = expires_in;
return this;
}
public String getIssued_at() {
return issued_at;
}
public DockerResponse setIssued_at(final String issued_at) {
this.issued_at = issued_at;
return this;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (!(o instanceof DockerResponse)) return false;
final DockerResponse that = (DockerResponse) o;
if (token != null ? !token.equals(that.token) : that.token != null) return false;
if (expires_in != null ? !expires_in.equals(that.expires_in) : that.expires_in != null) return false;
return issued_at != null ? issued_at.equals(that.issued_at) : that.issued_at == null;
}
@Override
public int hashCode() {
int result = token != null ? token.hashCode() : 0;
result = 31 * result + (expires_in != null ? expires_in.hashCode() : 0);
result = 31 * result + (issued_at != null ? issued_at.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "DockerResponse{" +
"token='" + token + '\'' +
", expires_in='" + expires_in + '\'' +
", issued_at='" + issued_at + '\'' +
'}';
}
}

View file

@ -0,0 +1,97 @@
package org.keycloak.representations.docker;
import com.fasterxml.jackson.annotation.JsonProperty;
import org.keycloak.representations.JsonWebToken;
import java.util.ArrayList;
import java.util.List;
/**
* * {
* "iss": "auth.docker.com",
* "sub": "jlhawn",
* "aud": "registry.docker.com",
* "exp": 1415387315,
* "nbf": 1415387015,
* "iat": 1415387015,
* "jti": "tYJCO1c6cnyy7kAn0c7rKPgbV1H1bFws",
* "access": [
* {
* "type": "repository",
* "name": "samalba/my-app",
* "actions": [
* "push"
* ]
* }
* ]
* }
*/
public class DockerResponseToken extends JsonWebToken {
@JsonProperty("access")
protected List<DockerAccess> accessItems = new ArrayList<>();
public List<DockerAccess> getAccessItems() {
return accessItems;
}
@Override
public DockerResponseToken id(final String id) {
super.id(id);
return this;
}
@Override
public DockerResponseToken expiration(final int expiration) {
super.expiration(expiration);
return this;
}
@Override
public DockerResponseToken notBefore(final int notBefore) {
super.notBefore(notBefore);
return this;
}
@Override
public DockerResponseToken issuedNow() {
super.issuedNow();
return this;
}
@Override
public DockerResponseToken issuedAt(final int issuedAt) {
super.issuedAt(issuedAt);
return this;
}
@Override
public DockerResponseToken issuer(final String issuer) {
super.issuer(issuer);
return this;
}
@Override
public DockerResponseToken audience(final String... audience) {
super.audience(audience);
return this;
}
@Override
public DockerResponseToken subject(final String subject) {
super.subject(subject);
return this;
}
@Override
public DockerResponseToken type(final String type) {
super.type(type);
return this;
}
@Override
public DockerResponseToken issuedFor(final String issuedFor) {
super.issuedFor(issuedFor);
return this;
}
}

View file

@ -137,6 +137,7 @@ public class RealmRepresentation {
protected String directGrantFlow;
protected String resetCredentialsFlow;
protected String clientAuthenticationFlow;
protected String dockerAuthenticationFlow;
protected Map<String, String> attributes;
@ -884,6 +885,15 @@ public class RealmRepresentation {
this.clientAuthenticationFlow = clientAuthenticationFlow;
}
public String getDockerAuthenticationFlow() {
return dockerAuthenticationFlow;
}
public RealmRepresentation setDockerAuthenticationFlow(final String dockerAuthenticationFlow) {
this.dockerAuthenticationFlow = dockerAuthenticationFlow;
return this;
}
public String getKeycloakVersion() {
return keycloakVersion;
}

View file

@ -21,8 +21,18 @@ import java.util.Map;
public class ProviderRepresentation {
private int order;
private Map<String, String> operationalInfo;
public int getOrder() {
return order;
}
public void setOrder(int priorityUI) {
this.order = priorityUI;
}
public Map<String, String> getOperationalInfo() {
return operationalInfo;
}

View file

@ -1038,6 +1038,18 @@ public class RealmAdapter implements CachedRealmModel {
updated.setClientAuthenticationFlow(flow);
}
@Override
public AuthenticationFlowModel getDockerAuthenticationFlow() {
if (isUpdated()) return updated.getDockerAuthenticationFlow();
return cached.getDockerAuthenticationFlow();
}
@Override
public void setDockerAuthenticationFlow(final AuthenticationFlowModel flow) {
getDelegateForUpdate();
updated.setDockerAuthenticationFlow(flow);
}
@Override
public List<AuthenticationFlowModel> getAuthenticationFlows() {
if (isUpdated()) return updated.getAuthenticationFlows();

View file

@ -117,6 +117,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
protected AuthenticationFlowModel directGrantFlow;
protected AuthenticationFlowModel resetCredentialsFlow;
protected AuthenticationFlowModel clientAuthenticationFlow;
protected AuthenticationFlowModel dockerAuthenticationFlow;
protected boolean eventsEnabled;
protected long eventsExpiration;
@ -252,6 +253,7 @@ public class CachedRealm extends AbstractExtendableRevisioned {
directGrantFlow = model.getDirectGrantFlow();
resetCredentialsFlow = model.getResetCredentialsFlow();
clientAuthenticationFlow = model.getClientAuthenticationFlow();
dockerAuthenticationFlow = model.getDockerAuthenticationFlow();
for (ComponentModel component : model.getComponents()) {
componentsByParentAndType.add(component.getParentId() + component.getProviderType(), component);
@ -547,6 +549,10 @@ public class CachedRealm extends AbstractExtendableRevisioned {
return clientAuthenticationFlow;
}
public AuthenticationFlowModel getDockerAuthenticationFlow() {
return dockerAuthenticationFlow;
}
public List<String> getDefaultGroups() {
return defaultGroups;
}

View file

@ -1375,6 +1375,18 @@ public class RealmAdapter implements RealmModel, JpaModel<RealmEntity> {
realm.setClientAuthenticationFlow(flow.getId());
}
@Override
public AuthenticationFlowModel getDockerAuthenticationFlow() {
String flowId = realm.getDockerAuthenticationFlow();
if (flowId == null) return null;
return getAuthenticationFlowById(flowId);
}
@Override
public void setDockerAuthenticationFlow(AuthenticationFlowModel flow) {
realm.setDockerAuthenticationFlow(flow.getId());
}
@Override
public List<AuthenticationFlowModel> getAuthenticationFlows() {
return realm.getAuthenticationFlows().stream()

View file

@ -220,6 +220,8 @@ public class RealmEntity {
@Column(name="CLIENT_AUTH_FLOW")
protected String clientAuthenticationFlow;
@Column(name="DOCKER_AUTH_FLOW")
protected String dockerAuthenticationFlow;
@Column(name="INTERNATIONALIZATION_ENABLED")
@ -733,6 +735,15 @@ public class RealmEntity {
this.clientAuthenticationFlow = clientAuthenticationFlow;
}
public String getDockerAuthenticationFlow() {
return dockerAuthenticationFlow;
}
public RealmEntity setDockerAuthenticationFlow(String dockerAuthenticationFlow) {
this.dockerAuthenticationFlow = dockerAuthenticationFlow;
return this;
}
public Collection<ClientTemplateEntity> getClientTemplates() {
return clientTemplates;
}

View file

@ -15,10 +15,14 @@
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
<changeSet author="keycloak" id="3.2.0">
<addColumn tableName="REALM">
<column name="DOCKER_AUTH_FLOW" type="VARCHAR(36)">
<constraints nullable="true"/>
</column>
</addColumn>
<changeSet author="mposolda@redhat.com" id="3.2.0">
<dropPrimaryKey constraintName="CONSTRAINT_OFFL_CL_SES_PK2" tableName="OFFLINE_CLIENT_SESSION" />
<dropColumn tableName="OFFLINE_CLIENT_SESSION" columnName="CLIENT_SESSION_ID" />
<addPrimaryKey columnNames="USER_SESSION_ID,CLIENT_ID, OFFLINE_FLAG" constraintName="CONSTRAINT_OFFL_CL_SES_PK3" tableName="OFFLINE_CLIENT_SESSION"/>
@ -38,9 +42,6 @@
<addPrimaryKey columnNames="ID" constraintName="CNSTR_CLIENT_INIT_ACC_PK" tableName="CLIENT_INITIAL_ACCESS"/>
<addForeignKeyConstraint baseColumnNames="REALM_ID" baseTableName="CLIENT_INITIAL_ACCESS" constraintName="FK_CLIENT_INIT_ACC_REALM" referencedColumnNames="ID" referencedTableName="REALM"/>
</changeSet>
<changeSet author="glavoie@gmail.com" id="3.2.0.idx">
<createIndex indexName="IDX_ASSOC_POL_ASSOC_POL_ID" tableName="ASSOCIATED_POLICY">
<column name="ASSOCIATED_POLICY_ID" type="VARCHAR(36)"/>
</createIndex>

View file

@ -27,11 +27,8 @@ import org.keycloak.migration.ModelVersion;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.PasswordPolicy;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class MigrateTo3_2_0 implements Migration {
public static final ModelVersion VERSION = new ModelVersion("3.2.0");
@ -44,6 +41,10 @@ public class MigrateTo3_2_0 implements Migration {
realm.setPasswordPolicy(builder.remove(PasswordPolicy.HASH_ITERATIONS_ID).build(session));
}
if (realm.getDockerAuthenticationFlow() == null) {
DefaultAuthenticationFlows.dockerAuthenticationFlow(realm);
}
ClientModel realmAccess = realm.getClientByClientId(Constants.REALM_MANAGEMENT_CLIENT_ID);
if (realmAccess != null) {
addRoles(realmAccess);

View file

@ -42,6 +42,7 @@ public class DefaultAuthenticationFlows {
public static final String RESET_CREDENTIALS_FLOW = "reset credentials";
public static final String LOGIN_FORMS_FLOW = "forms";
public static final String SAML_ECP_FLOW = "saml ecp";
public static final String DOCKER_AUTH = "docker auth";
public static final String CLIENT_AUTHENTICATION_FLOW = "clients";
public static final String FIRST_BROKER_LOGIN_FLOW = "first broker login";
@ -58,6 +59,7 @@ public class DefaultAuthenticationFlows {
if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm);
if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, false);
if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm);
if (realm.getFlowByAlias(DOCKER_AUTH) == null) dockerAuthenticationFlow(realm);
}
public static void migrateFlows(RealmModel realm) {
if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm, true);
@ -67,6 +69,7 @@ public class DefaultAuthenticationFlows {
if (realm.getFlowByAlias(CLIENT_AUTHENTICATION_FLOW) == null) clientAuthFlow(realm);
if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, true);
if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm);
if (realm.getFlowByAlias(DOCKER_AUTH) == null) dockerAuthenticationFlow(realm);
}
public static void registrationFlow(RealmModel realm) {
@ -528,4 +531,26 @@ public class DefaultAuthenticationFlows {
realm.addAuthenticatorExecution(execution);
}
public static void dockerAuthenticationFlow(final RealmModel realm) {
AuthenticationFlowModel dockerAuthFlow = new AuthenticationFlowModel();
dockerAuthFlow.setAlias(DOCKER_AUTH);
dockerAuthFlow.setDescription("Used by Docker clients to authenticate against the IDP");
dockerAuthFlow.setProviderId("basic-flow");
dockerAuthFlow.setTopLevel(true);
dockerAuthFlow.setBuiltIn(true);
dockerAuthFlow = realm.addAuthenticationFlow(dockerAuthFlow);
realm.setDockerAuthenticationFlow(dockerAuthFlow);
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
execution.setParentFlow(dockerAuthFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("docker-http-basic-authenticator");
execution.setPriority(10);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
}
}

View file

@ -489,6 +489,7 @@ public final class KeycloakModelUtils {
if ((realmFlow = realm.getClientAuthenticationFlow()) != null && realmFlow.getId().equals(model.getId())) return true;
if ((realmFlow = realm.getDirectGrantFlow()) != null && realmFlow.getId().equals(model.getId())) return true;
if ((realmFlow = realm.getResetCredentialsFlow()) != null && realmFlow.getId().equals(model.getId())) return true;
if ((realmFlow = realm.getDockerAuthenticationFlow()) != null && realmFlow.getId().equals(model.getId())) return true;
for (IdentityProviderModel idp : realm.getIdentityProviders()) {
if (model.getId().equals(idp.getFirstBrokerLoginFlowId())) return true;

View file

@ -35,6 +35,7 @@ import org.keycloak.authorization.model.ResourceServer;
import org.keycloak.authorization.model.Scope;
import org.keycloak.authorization.policy.provider.PolicyProviderFactory;
import org.keycloak.authorization.store.ResourceStore;
import org.keycloak.common.Profile;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.Time;
import org.keycloak.component.ComponentModel;
@ -325,6 +326,7 @@ public class ModelToRepresentation {
if (realm.getDirectGrantFlow() != null) rep.setDirectGrantFlow(realm.getDirectGrantFlow().getAlias());
if (realm.getResetCredentialsFlow() != null) rep.setResetCredentialsFlow(realm.getResetCredentialsFlow().getAlias());
if (realm.getClientAuthenticationFlow() != null) rep.setClientAuthenticationFlow(realm.getClientAuthenticationFlow().getAlias());
if (realm.getDockerAuthenticationFlow() != null) rep.setDockerAuthenticationFlow(realm.getDockerAuthenticationFlow().getAlias());
List<String> defaultRoles = realm.getDefaultRoles();
if (!defaultRoles.isEmpty()) {

View file

@ -614,6 +614,18 @@ public class RepresentationToModel {
}
}
// Added in 3.2
if (rep.getDockerAuthenticationFlow() == null) {
AuthenticationFlowModel dockerAuthenticationFlow = newRealm.getFlowByAlias(DefaultAuthenticationFlows.DOCKER_AUTH);
if (dockerAuthenticationFlow == null) {
DefaultAuthenticationFlows.dockerAuthenticationFlow(newRealm);
} else {
newRealm.setDockerAuthenticationFlow(dockerAuthenticationFlow);
}
} else {
newRealm.setDockerAuthenticationFlow(newRealm.getFlowByAlias(rep.getDockerAuthenticationFlow()));
}
DefaultAuthenticationFlows.addIdentityProviderAuthenticator(newRealm, defaultProvider);
}
@ -898,6 +910,9 @@ public class RepresentationToModel {
if (rep.getClientAuthenticationFlow() != null) {
realm.setClientAuthenticationFlow(realm.getFlowByAlias(rep.getClientAuthenticationFlow()));
}
if (rep.getDockerAuthenticationFlow() != null) {
realm.setDockerAuthenticationFlow(realm.getFlowByAlias(rep.getDockerAuthenticationFlow()));
}
}
// Basic realm stuff

View file

@ -251,6 +251,9 @@ public interface RealmModel extends RoleContainerModel {
AuthenticationFlowModel getClientAuthenticationFlow();
void setClientAuthenticationFlow(AuthenticationFlowModel flow);
AuthenticationFlowModel getDockerAuthenticationFlow();
void setDockerAuthenticationFlow(AuthenticationFlowModel flow);
List<AuthenticationFlowModel> getAuthenticationFlows();
AuthenticationFlowModel getFlowByAlias(String alias);
AuthenticationFlowModel addAuthenticationFlow(AuthenticationFlowModel model);

View file

@ -53,4 +53,8 @@ public interface ProviderFactory<T extends Provider> {
public String getId();
default int order() {
return 0;
}
}

View file

@ -22,6 +22,7 @@ import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
@ -29,9 +30,11 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.LoginProtocol.Error;
import org.keycloak.services.ErrorPageException;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.AuthenticationSessionManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.services.util.AuthenticationFlowURLHelper;
@ -62,7 +65,7 @@ public abstract class AuthorizationEndpointBase {
@Context
protected HttpHeaders headers;
@Context
protected HttpRequest request;
protected HttpRequest httpRequest;
@Context
protected KeycloakSession session;
@Context
@ -84,7 +87,7 @@ public abstract class AuthorizationEndpointBase {
.setRealm(realm)
.setSession(session)
.setUriInfo(uriInfo)
.setRequest(request);
.setRequest(httpRequest);
authSession.setAuthNote(AuthenticationProcessor.CURRENT_FLOW_PATH, flowPath);
@ -147,6 +150,19 @@ public abstract class AuthorizationEndpointBase {
return realm.getBrowserFlow();
}
protected void checkSsl() {
if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
event.error(Errors.SSL_REQUIRED);
throw new ErrorPageException(session, Messages.HTTPS_REQUIRED);
}
}
protected void checkRealm() {
if (!realm.isEnabled()) {
event.error(Errors.REALM_DISABLED);
throw new ErrorPageException(session, Messages.REALM_NOT_ENABLED);
}
}
protected AuthorizationEndpointChecks getOrCreateAuthenticationSession(ClientModel client, String requestState) {
AuthenticationSessionManager manager = new AuthenticationSessionManager(session);

View file

@ -0,0 +1,184 @@
package org.keycloak.protocol.docker;
import org.jboss.logging.Logger;
import org.jboss.resteasy.specimpl.ResponseBuilderImpl;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeyManager;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.docker.mapper.DockerAuthV2AttributeMapper;
import org.keycloak.representations.docker.DockerResponse;
import org.keycloak.representations.docker.DockerResponseToken;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.TokenUtil;
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 java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Set;
public class DockerAuthV2Protocol implements LoginProtocol {
protected static final Logger logger = Logger.getLogger(DockerEndpoint.class);
public static final String LOGIN_PROTOCOL = "docker-v2";
public static final String ACCOUNT_PARAM = "account";
public static final String SERVICE_PARAM = "service";
public static final String SCOPE_PARAM = "scope";
public static final String ISSUER = "docker.iss"; // don't want to overlap with OIDC notes
public static final String ISO_8601_DATE_FORMAT = "yyyy-MM-dd'T'HH:mm:ss'Z'";
private KeycloakSession session;
private RealmModel realm;
private UriInfo uriInfo;
private HttpHeaders headers;
private EventBuilder event;
public DockerAuthV2Protocol() {
}
public DockerAuthV2Protocol(final KeycloakSession session, final RealmModel realm, final UriInfo uriInfo, final HttpHeaders headers, final EventBuilder event) {
this.session = session;
this.realm = realm;
this.uriInfo = uriInfo;
this.headers = headers;
this.event = event;
}
@Override
public LoginProtocol setSession(final KeycloakSession session) {
this.session = session;
return this;
}
@Override
public LoginProtocol setRealm(final RealmModel realm) {
this.realm = realm;
return this;
}
@Override
public LoginProtocol setUriInfo(final UriInfo uriInfo) {
this.uriInfo = uriInfo;
return this;
}
@Override
public LoginProtocol setHttpHeaders(final HttpHeaders headers) {
this.headers = headers;
return this;
}
@Override
public LoginProtocol setEventBuilder(final EventBuilder event) {
this.event = event;
return this;
}
@Override
public Response authenticated(final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) {
// First, create a base response token with realm + user values populated
final ClientModel client = clientSession.getClient();
DockerResponseToken responseToken = new DockerResponseToken()
.id(KeycloakModelUtils.generateId())
.type(TokenUtil.TOKEN_TYPE_BEARER)
.issuer(clientSession.getNote(DockerAuthV2Protocol.ISSUER))
.subject(userSession.getUser().getUsername())
.issuedNow()
.audience(client.getClientId())
.issuedFor(client.getClientId());
// since realm access token is given in seconds
final int accessTokenLifespan = realm.getAccessTokenLifespan();
responseToken.notBefore(responseToken.getIssuedAt())
.expiration(responseToken.getIssuedAt() + accessTokenLifespan);
// Next, allow mappers to decorate the token to add/remove scopes as appropriate
final ClientSessionCode<AuthenticatedClientSessionModel> accessCode = new ClientSessionCode<>(session, realm, clientSession);
final Set<ProtocolMapperModel> mappings = accessCode.getRequestedProtocolMappers();
for (final ProtocolMapperModel mapping : mappings) {
final ProtocolMapper mapper = (ProtocolMapper) session.getKeycloakSessionFactory().getProviderFactory(ProtocolMapper.class, mapping.getProtocolMapper());
if (mapper instanceof DockerAuthV2AttributeMapper) {
final DockerAuthV2AttributeMapper dockerAttributeMapper = (DockerAuthV2AttributeMapper) mapper;
if (dockerAttributeMapper.appliesTo(responseToken)) {
responseToken = dockerAttributeMapper.transformDockerResponseToken(responseToken, mapping, session, userSession, clientSession);
}
}
}
try {
// Finally, construct the response to the docker client with the token + metadata
if (event.getEvent() != null && EventType.LOGIN.equals(event.getEvent().getType())) {
final KeyManager.ActiveRsaKey activeKey = session.keys().getActiveRsaKey(realm);
final String encodedToken = new JWSBuilder()
.kid(new DockerKeyIdentifier(activeKey.getPublicKey()).toString())
.type("JWT")
.jsonContent(responseToken)
.rsa256(activeKey.getPrivateKey());
final String expiresInIso8601String = new SimpleDateFormat(ISO_8601_DATE_FORMAT).format(new Date(responseToken.getIssuedAt() * 1000L));
final DockerResponse responseEntity = new DockerResponse()
.setToken(encodedToken)
.setExpires_in(accessTokenLifespan)
.setIssued_at(expiresInIso8601String);
return new ResponseBuilderImpl().status(Response.Status.OK).header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON).entity(responseEntity).build();
} else {
logger.errorv("Unable to handle request for event type {0}. Currently only LOGIN event types are supported by docker protocol.", event.getEvent() == null ? "null" : event.getEvent().getType());
throw new ErrorResponseException("invalid_request", "Event type not supported", Response.Status.BAD_REQUEST);
}
} catch (final InstantiationException e) {
logger.errorv("Error attempting to create Key ID for Docker JOSE header: ", e.getMessage());
throw new ErrorResponseException("token_error", "Unable to construct JOSE header for JWT", Response.Status.INTERNAL_SERVER_ERROR);
}
}
@Override
public Response sendError(final AuthenticationSessionModel clientSession, final LoginProtocol.Error error) {
return new ResponseBuilderImpl().status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
@Override
public void backchannelLogout(final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) {
errorResponse(userSession, "backchannelLogout");
}
@Override
public Response frontchannelLogout(final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) {
return errorResponse(userSession, "frontchannelLogout");
}
@Override
public Response finishLogout(final UserSessionModel userSession) {
return errorResponse(userSession, "finishLogout");
}
@Override
public boolean requireReauthentication(final UserSessionModel userSession, final AuthenticationSessionModel clientSession) {
return true;
}
private Response errorResponse(final UserSessionModel userSession, final String methodName) {
logger.errorv("User {0} attempted to invoke unsupported method {1} on docker protocol.", userSession.getUser().getUsername(), methodName);
throw new ErrorResponseException("invalid_request", String.format("Attempted to invoke unsupported docker method %s", methodName), Response.Status.BAD_REQUEST);
}
@Override
public void close() {
// no-op
}
}

View file

@ -0,0 +1,86 @@
package org.keycloak.protocol.docker;
import org.keycloak.common.Profile;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientTemplateModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.AbstractLoginProtocolFactory;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.docker.mapper.AllowAllDockerProtocolMapper;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.ClientTemplateRepresentation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class DockerAuthV2ProtocolFactory extends AbstractLoginProtocolFactory implements EnvironmentDependentProviderFactory {
static List<ProtocolMapperModel> builtins = new ArrayList<>();
static List<ProtocolMapperModel> defaultBuiltins = new ArrayList<>();
static {
final ProtocolMapperModel addAllRequestedScopeMapper = new ProtocolMapperModel();
addAllRequestedScopeMapper.setName(AllowAllDockerProtocolMapper.PROVIDER_ID);
addAllRequestedScopeMapper.setProtocolMapper(AllowAllDockerProtocolMapper.PROVIDER_ID);
addAllRequestedScopeMapper.setProtocol(DockerAuthV2Protocol.LOGIN_PROTOCOL);
addAllRequestedScopeMapper.setConsentRequired(false);
addAllRequestedScopeMapper.setConfig(Collections.EMPTY_MAP);
builtins.add(addAllRequestedScopeMapper);
defaultBuiltins.add(addAllRequestedScopeMapper);
}
@Override
protected void addDefaults(final ClientModel client) {
defaultBuiltins.forEach(builtinMapper -> client.addProtocolMapper(builtinMapper));
}
@Override
public List<ProtocolMapperModel> getBuiltinMappers() {
return builtins;
}
@Override
public List<ProtocolMapperModel> getDefaultBuiltinMappers() {
return defaultBuiltins;
}
@Override
public Object createProtocolEndpoint(final RealmModel realm, final EventBuilder event) {
return new DockerV2LoginProtocolService(realm, event);
}
@Override
public void setupClientDefaults(final ClientRepresentation rep, final ClientModel newClient) {
// no-op
}
@Override
public void setupTemplateDefaults(final ClientTemplateRepresentation clientRep, final ClientTemplateModel newClient) {
// no-op
}
@Override
public LoginProtocol create(final KeycloakSession session) {
return new DockerAuthV2Protocol().setSession(session);
}
@Override
public String getId() {
return DockerAuthV2Protocol.LOGIN_PROTOCOL;
}
@Override
public boolean isSupported() {
return Profile.isFeatureEnabled(Profile.Feature.DOCKER);
}
@Override
public int order() {
return -100;
}
}

View file

@ -0,0 +1,76 @@
package org.keycloak.protocol.docker;
import org.jboss.logging.Logger;
import org.jboss.resteasy.specimpl.ResponseBuilderImpl;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.events.Errors;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticator;
import org.keycloak.representations.docker.DockerAccess;
import org.keycloak.representations.docker.DockerError;
import org.keycloak.representations.docker.DockerErrorResponseToken;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.util.Collections;
import java.util.Locale;
import java.util.Optional;
public class DockerAuthenticator extends HttpBasicAuthenticator {
private static final Logger logger = Logger.getLogger(DockerAuthenticator.class);
public static final String ID = "docker-http-basic-authenticator";
@Override
protected void notValidCredentialsAction(final AuthenticationFlowContext context, final RealmModel realm, final UserModel user) {
invalidUserAction(context, realm, user.getUsername(), context.getSession().getContext().resolveLocale(user));
}
@Override
protected void nullUserAction(final AuthenticationFlowContext context, final RealmModel realm, final String userId) {
final String localeString = Optional.ofNullable(realm.getDefaultLocale()).orElse(Locale.ENGLISH.toString());
invalidUserAction(context, realm, userId, new Locale(localeString));
}
@Override
protected void userDisabledAction(AuthenticationFlowContext context, RealmModel realm, UserModel user) {
context.getEvent().user(user);
context.getEvent().error(Errors.USER_DISABLED);
final DockerError error = new DockerError("UNAUTHORIZED","Invalid username or password.",
Collections.singletonList(new DockerAccess(context.getAuthenticationSession().getClientNote(DockerAuthV2Protocol.SCOPE_PARAM))));
context.failure(AuthenticationFlowError.USER_DISABLED, new ResponseBuilderImpl()
.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.entity(new DockerErrorResponseToken(Collections.singletonList(error)))
.build());
}
/**
* For Docker protocol the same error message will be returned for invalid credentials and incorrect user name. For SAML
* ECP, there is a different behavior for each.
*/
private void invalidUserAction(final AuthenticationFlowContext context, final RealmModel realm, final String userId, final Locale locale) {
context.getEvent().user(userId);
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
final DockerError error = new DockerError("UNAUTHORIZED","Invalid username or password.",
Collections.singletonList(new DockerAccess(context.getAuthenticationSession().getClientNote(DockerAuthV2Protocol.SCOPE_PARAM))));
context.failure(AuthenticationFlowError.INVALID_USER, new ResponseBuilderImpl()
.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
.entity(new DockerErrorResponseToken(Collections.singletonList(error)))
.build());
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return true;
}
}

View file

@ -0,0 +1,84 @@
package org.keycloak.protocol.docker;
import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.common.Profile;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.Collections;
import java.util.List;
import static org.keycloak.models.AuthenticationExecutionModel.Requirement;
public class DockerAuthenticatorFactory implements AuthenticatorFactory {
@Override
public String getHelpText() {
return "Uses HTTP Basic authentication to validate docker users, returning a docker error token on auth failure";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return Collections.emptyList();
}
@Override
public String getDisplayType() {
return "Docker Authenticator";
}
@Override
public String getReferenceCategory() {
return "docker";
}
@Override
public boolean isConfigurable() {
return false;
}
private static final Requirement[] REQUIREMENT_CHOICES = {
Requirement.REQUIRED,
};
@Override
public Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
@Override
public boolean isUserSetupAllowed() {
return false;
}
@Override
public Authenticator create(KeycloakSession session) {
return new DockerAuthenticator();
}
@Override
public void init(Config.Scope config) {
// no-op
}
@Override
public void postInit(KeycloakSessionFactory factory) {
// no-op
}
@Override
public void close() {
// no-op
}
@Override
public String getId() {
return DockerAuthenticator.ID;
}
}

View file

@ -0,0 +1,103 @@
package org.keycloak.protocol.docker;
import org.jboss.logging.Logger;
import org.keycloak.common.Profile;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.AuthorizationEndpointBase;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequest;
import org.keycloak.protocol.oidc.endpoints.request.AuthorizationEndpointRequestParserProcessor;
import org.keycloak.services.ErrorResponseException;
import org.keycloak.services.Urls;
import org.keycloak.services.util.CacheControlUtil;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.sessions.CommonClientSessionModel;
import org.keycloak.utils.ProfileHelper;
import javax.ws.rs.GET;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
/**
* Implements a docker-client understandable format.
*/
public class DockerEndpoint extends AuthorizationEndpointBase {
protected static final Logger logger = Logger.getLogger(DockerEndpoint.class);
private final EventType login;
private String account;
private String service;
private String scope;
private ClientModel client;
private AuthenticationSessionModel authenticationSession;
public DockerEndpoint(final RealmModel realm, final EventBuilder event, final EventType login) {
super(realm, event);
this.login = login;
}
@GET
public Response build() {
ProfileHelper.requireFeature(Profile.Feature.DOCKER);
final MultivaluedMap<String, String> params = uriInfo.getQueryParameters();
account = params.getFirst(DockerAuthV2Protocol.ACCOUNT_PARAM);
if (account == null) {
logger.debug("Account parameter not provided by docker auth. This is techincally required, but not actually used since " +
"username is provided by Basic auth header.");
}
service = params.getFirst(DockerAuthV2Protocol.SERVICE_PARAM);
if (service == null) {
throw new ErrorResponseException("invalid_request", "service parameter must be provided", Response.Status.BAD_REQUEST);
}
client = realm.getClientByClientId(service);
if (client == null) {
logger.errorv("Failed to lookup client given by service={0} parameter for realm: {1}.", service, realm.getName());
throw new ErrorResponseException("invalid_client", "Client specified by 'service' parameter does not exist", Response.Status.BAD_REQUEST);
}
scope = params.getFirst(DockerAuthV2Protocol.SCOPE_PARAM);
checkSsl();
checkRealm();
final AuthorizationEndpointRequest authRequest = AuthorizationEndpointRequestParserProcessor.parseRequest(event, session, client, params);
AuthorizationEndpointChecks checks = getOrCreateAuthenticationSession(client, authRequest.getState());
if (checks.response != null) {
return checks.response;
}
authenticationSession = checks.authSession;
updateAuthenticationSession();
// So back button doesn't work
CacheControlUtil.noBackButtonCacheControlHeader();
return handleBrowserAuthenticationRequest(authenticationSession, new DockerAuthV2Protocol(session, realm, uriInfo, headers, event.event(login)), false, false);
}
private void updateAuthenticationSession() {
authenticationSession.setProtocol(DockerAuthV2Protocol.LOGIN_PROTOCOL);
authenticationSession.setAction(CommonClientSessionModel.Action.AUTHENTICATE.name());
// Docker specific stuff
authenticationSession.setClientNote(DockerAuthV2Protocol.ACCOUNT_PARAM, account);
authenticationSession.setClientNote(DockerAuthV2Protocol.SERVICE_PARAM, service);
authenticationSession.setClientNote(DockerAuthV2Protocol.SCOPE_PARAM, scope);
authenticationSession.setClientNote(DockerAuthV2Protocol.ISSUER, Urls.realmIssuer(uriInfo.getBaseUri(), realm.getName()));
}
@Override
protected AuthenticationFlowModel getAuthenticationFlow() {
return realm.getDockerAuthenticationFlow();
}
@Override
protected boolean isNewRequest(final AuthenticationSessionModel authSession, final ClientModel clientFromRequest, final String requestState) {
return true;
}
}

View file

@ -0,0 +1,127 @@
package org.keycloak.protocol.docker;
import org.keycloak.models.utils.Base32;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.BinaryOperator;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collector;
import java.util.stream.Stream;
/**
* The kid field has to be in a libtrust fingerprint compatible format. Such a format can be generated by following steps:
* 1) Take the DER encoded public key which the JWT token was signed against.
* 2) Create a SHA256 hash out of it and truncate to 240bits.
* 3) Split the result into 12 base32 encoded groups with : as delimiter.
*
* Ex: "kid": "PYYO:TEWU:V7JH:26JV:AQTZ:LJC3:SXVJ:XGHA:34F2:2LAQ:ZRMK:Z7Q6"
*
* @see https://docs.docker.com/registry/spec/auth/jwt/
* @see https://github.com/docker/libtrust/blob/master/key.go#L24
*/
public class DockerKeyIdentifier {
private final String identifier;
public DockerKeyIdentifier(final Key key) throws InstantiationException {
try {
final MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
final byte[] hashed = sha256.digest(key.getEncoded());
final byte[] hashedTruncated = truncateToBitLength(240, hashed);
final String base32Id = Base32.encode(hashedTruncated);
identifier = byteStream(base32Id.getBytes()).collect(new DelimitingCollector());
} catch (final NoSuchAlgorithmException e) {
throw new InstantiationException("Could not instantiate docker key identifier, no SHA-256 algorithm available.");
}
}
// ugh.
private Stream<Byte> byteStream(final byte[] bytes) {
final Collection<Byte> colectionedBytes = new ArrayList<>();
for (final byte aByte : bytes) {
colectionedBytes.add(aByte);
}
return colectionedBytes.stream();
}
private byte[] truncateToBitLength(final int bitLength, final byte[] arrayToTruncate) {
if (bitLength % 8 != 0) {
throw new IllegalArgumentException("Bit length for truncation of byte array given as a number not divisible by 8");
}
final int numberOfBytes = bitLength / 8;
return Arrays.copyOfRange(arrayToTruncate, 0, numberOfBytes);
}
@Override
public String toString() {
return identifier;
}
@Override
public boolean equals(final Object o) {
if (this == o) return true;
if (!(o instanceof DockerKeyIdentifier)) return false;
final DockerKeyIdentifier that = (DockerKeyIdentifier) o;
return identifier != null ? identifier.equals(that.identifier) : that.identifier == null;
}
@Override
public int hashCode() {
return identifier != null ? identifier.hashCode() : 0;
}
// Could probably be generalized with size and delimiter arguments, but leaving it here for now until someone else needs it.
public static class DelimitingCollector implements Collector<Byte, StringBuilder, String> {
@Override
public Supplier<StringBuilder> supplier() {
return () -> new StringBuilder();
}
@Override
public BiConsumer<StringBuilder, Byte> accumulator() {
return ((stringBuilder, aByte) -> {
if (needsDelimiter(4, ":", stringBuilder)) {
stringBuilder.append(":");
}
stringBuilder.append(new String(new byte[]{aByte}));
});
}
private static boolean needsDelimiter(final int maxLength, final String delimiter, final StringBuilder builder) {
final int lastDelimiter = builder.lastIndexOf(delimiter);
final int charsSinceLastDelimiter = builder.length() - lastDelimiter;
return charsSinceLastDelimiter > maxLength;
}
@Override
public BinaryOperator<StringBuilder> combiner() {
return ((left, right) -> new StringBuilder(left.toString()).append(right.toString()));
}
@Override
public Function<StringBuilder, String> finisher() {
return StringBuilder::toString;
}
@Override
public Set<Characteristics> characteristics() {
return Collections.emptySet();
}
}
}

View file

@ -0,0 +1,70 @@
package org.keycloak.protocol.docker;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.common.Profile;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.services.resources.RealmsResource;
import org.keycloak.utils.ProfileHelper;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
public class DockerV2LoginProtocolService {
private final RealmModel realm;
private final TokenManager tokenManager;
private final EventBuilder event;
@Context
private UriInfo uriInfo;
@Context
private KeycloakSession session;
@Context
private HttpHeaders headers;
public DockerV2LoginProtocolService(final RealmModel realm, final EventBuilder event) {
this.realm = realm;
this.tokenManager = new TokenManager();
this.event = event;
}
public static UriBuilder authProtocolBaseUrl(final UriInfo uriInfo) {
final UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
return authProtocolBaseUrl(baseUriBuilder);
}
public static UriBuilder authProtocolBaseUrl(final UriBuilder baseUriBuilder) {
return baseUriBuilder.path(RealmsResource.class).path("{realm}/protocol/" + DockerAuthV2Protocol.LOGIN_PROTOCOL);
}
public static UriBuilder authUrl(final UriInfo uriInfo) {
final UriBuilder baseUriBuilder = uriInfo.getBaseUriBuilder();
return authUrl(baseUriBuilder);
}
public static UriBuilder authUrl(final UriBuilder baseUriBuilder) {
final UriBuilder uriBuilder = authProtocolBaseUrl(baseUriBuilder);
return uriBuilder.path(DockerV2LoginProtocolService.class, "auth");
}
/**
* Authorization endpoint
*/
@Path("auth")
public Object auth() {
ProfileHelper.requireFeature(Profile.Feature.DOCKER);
final DockerEndpoint endpoint = new DockerEndpoint(realm, event, EventType.LOGIN);
ResteasyProviderFactory.getInstance().injectProperties(endpoint);
return endpoint;
}
}

View file

@ -0,0 +1,148 @@
package org.keycloak.protocol.docker.installation;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.ClientInstallationProvider;
import org.keycloak.protocol.docker.DockerAuthV2Protocol;
import org.keycloak.protocol.docker.installation.compose.DockerComposeZipContent;
import javax.ws.rs.core.Response;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.URL;
import java.security.cert.Certificate;
import java.util.stream.Collectors;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class DockerComposeYamlInstallationProvider implements ClientInstallationProvider {
private static Logger log = Logger.getLogger(DockerComposeYamlInstallationProvider.class);
public static final String ROOT_DIR = "keycloak-docker-compose-yaml/";
@Override
public ClientInstallationProvider create(final KeycloakSession session) {
return this;
}
@Override
public void init(final Config.Scope config) {
// no-op
}
@Override
public void postInit(final KeycloakSessionFactory factory) {
// no-op
}
@Override
public void close() {
// no-op
}
@Override
public String getId() {
return "docker-v2-compose-yaml";
}
@Override
public Response generateInstallation(final KeycloakSession session, final RealmModel realm, final ClientModel client, final URI serverBaseUri) {
final ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
final ZipOutputStream zipOutput = new ZipOutputStream(byteStream);
try {
return generateInstallation(zipOutput, byteStream, session.keys().getActiveRsaKey(realm).getCertificate(), session.getContext().getAuthServerUrl().toURL(), realm.getName(), client.getClientId());
} catch (final IOException e) {
try {
zipOutput.close();
} catch (final IOException ex) {
// do nothing, already in an exception
}
try {
byteStream.close();
} catch (final IOException ex) {
// do nothing, already in an exception
}
throw new RuntimeException("Error occurred during attempt to generate docker-compose yaml installation files", e);
}
}
public Response generateInstallation(final ZipOutputStream zipOutput, final ByteArrayOutputStream byteStream, final Certificate realmCert, final URL realmBaseURl,
final String realmName, final String clientName) throws IOException {
final DockerComposeZipContent zipContent = new DockerComposeZipContent(realmCert, realmBaseURl, realmName, clientName);
zipOutput.putNextEntry(new ZipEntry(ROOT_DIR));
// Write docker compose file
zipOutput.putNextEntry(new ZipEntry(ROOT_DIR + "docker-compose.yaml"));
zipOutput.write(zipContent.getYamlFile().generateDockerComposeFileBytes());
zipOutput.closeEntry();
// Write data directory
zipOutput.putNextEntry(new ZipEntry(ROOT_DIR + zipContent.getDataDirectoryName() + "/"));
zipOutput.putNextEntry(new ZipEntry(ROOT_DIR + zipContent.getDataDirectoryName() + "/.gitignore"));
zipOutput.write("*".getBytes());
zipOutput.closeEntry();
// Write certificates
final String certsDirectory = ROOT_DIR + zipContent.getCertsDirectory().getDirectoryName() + "/";
zipOutput.putNextEntry(new ZipEntry(certsDirectory));
zipOutput.putNextEntry(new ZipEntry(certsDirectory + zipContent.getCertsDirectory().getLocalhostCertFile().getKey()));
zipOutput.write(zipContent.getCertsDirectory().getLocalhostCertFile().getValue());
zipOutput.closeEntry();
zipOutput.putNextEntry(new ZipEntry(certsDirectory + zipContent.getCertsDirectory().getLocalhostKeyFile().getKey()));
zipOutput.write(zipContent.getCertsDirectory().getLocalhostKeyFile().getValue());
zipOutput.closeEntry();
zipOutput.putNextEntry(new ZipEntry(certsDirectory + zipContent.getCertsDirectory().getIdpTrustChainFile().getKey()));
zipOutput.write(zipContent.getCertsDirectory().getIdpTrustChainFile().getValue());
zipOutput.closeEntry();
// Write README to .zip
zipOutput.putNextEntry(new ZipEntry(ROOT_DIR + "README.md"));
final String readmeContent = new BufferedReader(new InputStreamReader(DockerComposeYamlInstallationProvider.class.getResourceAsStream("/DockerComposeYamlReadme.md"))).lines().collect(Collectors.joining("\n"));
zipOutput.write(readmeContent.getBytes());
zipOutput.closeEntry();
zipOutput.close();
byteStream.close();
return Response.ok(byteStream.toByteArray(), getMediaType()).build();
}
@Override
public String getProtocol() {
return DockerAuthV2Protocol.LOGIN_PROTOCOL;
}
@Override
public String getDisplayType() {
return "Docker Compose YAML";
}
@Override
public String getHelpText() {
return "Produces a zip file that can be used to stand up a development registry on localhost";
}
@Override
public String getFilename() {
return "keycloak-docker-compose-yaml.zip";
}
@Override
public String getMediaType() {
return "application/zip";
}
@Override
public boolean isDownloadOnly() {
return true;
}
}

View file

@ -0,0 +1,81 @@
package org.keycloak.protocol.docker.installation;
import org.keycloak.Config;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.ClientInstallationProvider;
import org.keycloak.protocol.docker.DockerAuthV2Protocol;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.net.URI;
public class DockerRegistryConfigFileInstallationProvider implements ClientInstallationProvider {
@Override
public ClientInstallationProvider create(final KeycloakSession session) {
return this;
}
@Override
public void init(final Config.Scope config) {
// no-op
}
@Override
public void postInit(final KeycloakSessionFactory factory) {
// no-op
}
@Override
public void close() {
// no-op
}
@Override
public String getId() {
return "docker-v2-registry-config-file";
}
@Override
public Response generateInstallation(final KeycloakSession session, final RealmModel realm, final ClientModel client, final URI serverBaseUri) {
final StringBuilder responseString = new StringBuilder("auth:\n")
.append(" token:\n")
.append(" realm: ").append(serverBaseUri).append("/auth/realms/").append(realm.getName()).append("/protocol/").append(DockerAuthV2Protocol.LOGIN_PROTOCOL).append("/auth\n")
.append(" service: ").append(client.getClientId()).append("\n")
.append(" issuer: ").append(serverBaseUri).append("/auth/realms/").append(realm.getName()).append("\n");
return Response.ok(responseString.toString(), MediaType.TEXT_PLAIN_TYPE).build();
}
@Override
public String getProtocol() {
return DockerAuthV2Protocol.LOGIN_PROTOCOL;
}
@Override
public String getDisplayType() {
return "Registry Config File";
}
@Override
public String getHelpText() {
return "Provides a registry configuration file snippet for use with this client";
}
@Override
public String getFilename() {
return "config.yml";
}
@Override
public String getMediaType() {
return MediaType.TEXT_PLAIN;
}
@Override
public boolean isDownloadOnly() {
return false;
}
}

View file

@ -0,0 +1,81 @@
package org.keycloak.protocol.docker.installation;
import org.keycloak.Config;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.ClientInstallationProvider;
import org.keycloak.protocol.docker.DockerAuthV2Protocol;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.net.URI;
public class DockerVariableOverrideInstallationProvider implements ClientInstallationProvider {
@Override
public ClientInstallationProvider create(final KeycloakSession session) {
return this;
}
@Override
public void init(final Config.Scope config) {
// no-op
}
@Override
public void postInit(final KeycloakSessionFactory factory) {
// no-op
}
@Override
public void close() {
// no-op
}
@Override
public String getId() {
return "docker-v2-variable-override";
}
// TODO "auth" is not guaranteed to be the endpoint, fix it
@Override
public Response generateInstallation(final KeycloakSession session, final RealmModel realm, final ClientModel client, final URI serverBaseUri) {
final StringBuilder builder = new StringBuilder()
.append("-e REGISTRY_AUTH_TOKEN_REALM=").append(serverBaseUri).append("/auth/realms/").append(realm.getName()).append("/protocol/").append(DockerAuthV2Protocol.LOGIN_PROTOCOL).append("/auth \\\n")
.append("-e REGISTRY_AUTH_TOKEN_SERVICE=").append(client.getClientId()).append(" \\\n")
.append("-e REGISTRY_AUTH_TOKEN_ISSUER=").append(serverBaseUri).append("/auth/realms/").append(realm.getName()).append(" \\\n");
return Response.ok(builder.toString(), MediaType.TEXT_PLAIN_TYPE).build();
}
@Override
public String getProtocol() {
return DockerAuthV2Protocol.LOGIN_PROTOCOL;
}
@Override
public String getDisplayType() {
return "Variable Override";
}
@Override
public String getHelpText() {
return "Configures environment variable overrides, typically used with a docker-compose.yaml configuration for a docker registry";
}
@Override
public String getFilename() {
return "docker-env.txt";
}
@Override
public String getMediaType() {
return MediaType.TEXT_PLAIN;
}
@Override
public boolean isDownloadOnly() {
return false;
}
}

View file

@ -0,0 +1,37 @@
package org.keycloak.protocol.docker.installation.compose;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.util.Base64;
public final class DockerCertFileUtils {
public static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----";
public static final String END_CERT = "-----END CERTIFICATE-----";
public static final String BEGIN_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----";
public static final String END_PRIVATE_KEY = "-----END PRIVATE KEY-----";
public final static String LINE_SEPERATOR = System.getProperty("line.separator");
private DockerCertFileUtils() {
}
public static String formatCrtFileContents(final Certificate certificate) throws CertificateEncodingException {
return encodeAndPrettify(BEGIN_CERT, certificate.getEncoded(), END_CERT);
}
public static String formatPrivateKeyContents(final PrivateKey privateKey) {
return encodeAndPrettify(BEGIN_PRIVATE_KEY, privateKey.getEncoded(), END_PRIVATE_KEY);
}
public static String formatPublicKeyContents(final PublicKey publicKey) {
return encodeAndPrettify(BEGIN_CERT, publicKey.getEncoded(), END_CERT);
}
private static String encodeAndPrettify(final String header, final byte[] rawCrtText, final String footer) {
final Base64.Encoder encoder = Base64.getMimeEncoder(64, LINE_SEPERATOR.getBytes());
final String encodedCertText = new String(encoder.encode(rawCrtText));
final String prettified_cert = header + LINE_SEPERATOR + encodedCertText + LINE_SEPERATOR + footer;
return prettified_cert;
}
}

View file

@ -0,0 +1,62 @@
package org.keycloak.protocol.docker.installation.compose;
import org.keycloak.common.util.CertificateUtils;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import java.util.AbstractMap;
import java.util.Map;
public class DockerComposeCertsDirectory {
private final String directoryName;
private final Map.Entry<String, byte[]> localhostCertFile;
private final Map.Entry<String, byte[]> localhostKeyFile;
private final Map.Entry<String, byte[]> idpTrustChainFile;
public DockerComposeCertsDirectory(final String directoryName, final Certificate realmCert, final String registryCertFilename, final String registryKeyFilename, final String idpCertTrustChainFilename, final String realmName) {
this.directoryName = directoryName;
final KeyPairGenerator keyGen;
try {
keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048, new SecureRandom());
final KeyPair keypair = keyGen.generateKeyPair();
final PrivateKey privateKey = keypair.getPrivate();
final Certificate certificate = CertificateUtils.generateV1SelfSignedCertificate(keypair, realmName);
localhostCertFile = new AbstractMap.SimpleImmutableEntry<>(registryCertFilename, DockerCertFileUtils.formatCrtFileContents(certificate).getBytes());
localhostKeyFile = new AbstractMap.SimpleImmutableEntry<>(registryKeyFilename, DockerCertFileUtils.formatPrivateKeyContents(privateKey).getBytes());
idpTrustChainFile = new AbstractMap.SimpleEntry<>(idpCertTrustChainFilename, DockerCertFileUtils.formatCrtFileContents(realmCert).getBytes());
} catch (final NoSuchAlgorithmException e) {
// TODO throw error here descritively
throw new RuntimeException(e);
} catch (final CertificateEncodingException e) {
// TODO throw error here descritively
throw new RuntimeException(e);
}
}
public String getDirectoryName() {
return directoryName;
}
public Map.Entry<String, byte[]> getLocalhostCertFile() {
return localhostCertFile;
}
public Map.Entry<String, byte[]> getLocalhostKeyFile() {
return localhostKeyFile;
}
public Map.Entry<String, byte[]> getIdpTrustChainFile() {
return idpTrustChainFile;
}
}

View file

@ -0,0 +1,70 @@
package org.keycloak.protocol.docker.installation.compose;
import java.io.ByteArrayOutputStream;
import java.io.PrintWriter;
import java.net.URL;
/**
* Representation of the docker-compose.yaml file
*/
public class DockerComposeYamlFile {
private final String registryDataDirName;
private final String localCertDirName;
private final String containerCertPath;
private final String localhostCrtFileName;
private final String localhostKeyFileName;
private final String authServerTrustChainFileName;
private final URL authServerUrl;
private final String realmName;
private final String serviceId;
/**
* @param registryDataDirName Directory name to be used for both the container's storage directory, as well as the local data directory name
* @param localCertDirName Name of the (relative) local directory that holds the certs
* @param containerCertPath Path at which the local certs directory should be mounted on the container
* @param localhostCrtFileName SSL Cert file name for the registry
* @param localhostKeyFileName SSL Key file name for the registry
* @param authServerTrustChainFileName IDP trust chain, used for auth token validation
* @param authServerUrl Root URL for Keycloak, commonly something like http://localhost:8080/auth for dev environments
* @param realmName Name of the realm for which the docker client is configured
* @param serviceId Docker's Service ID, corresponds to Keycloak's client ID
*/
public DockerComposeYamlFile(final String registryDataDirName, final String localCertDirName, final String containerCertPath, final String localhostCrtFileName, final String localhostKeyFileName, final String authServerTrustChainFileName, final URL authServerUrl, final String realmName, final String serviceId) {
this.registryDataDirName = registryDataDirName;
this.localCertDirName = localCertDirName;
this.containerCertPath = containerCertPath;
this.localhostCrtFileName = localhostCrtFileName;
this.localhostKeyFileName = localhostKeyFileName;
this.authServerTrustChainFileName = authServerTrustChainFileName;
this.authServerUrl = authServerUrl;
this.realmName = realmName;
this.serviceId = serviceId;
}
public byte[] generateDockerComposeFileBytes() {
final ByteArrayOutputStream output = new ByteArrayOutputStream();
final PrintWriter writer = new PrintWriter(output);
writer.print("registry:\n");
writer.print(" image: registry:2\n");
writer.print(" ports:\n");
writer.print(" - 127.0.0.1:5000:5000\n");
writer.print(" environment:\n");
writer.print(" REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /" + registryDataDirName + "\n");
writer.print(" REGISTRY_HTTP_TLS_CERTIFICATE: " + containerCertPath + "/" + localhostCrtFileName + "\n");
writer.print(" REGISTRY_HTTP_TLS_KEY: " + containerCertPath + "/" + localhostKeyFileName + "\n");
writer.print(" REGISTRY_AUTH_TOKEN_REALM: " + authServerUrl + "/realms/" + realmName + "/protocol/docker-v2/auth\n");
writer.print(" REGISTRY_AUTH_TOKEN_SERVICE: " + serviceId + "\n");
writer.print(" REGISTRY_AUTH_TOKEN_ISSUER: " + authServerUrl + "/realms/" + realmName + "\n");
writer.print(" REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: " + containerCertPath + "/" + authServerTrustChainFileName + "\n");
writer.print(" volumes:\n");
writer.print(" - ./" + registryDataDirName + ":/" + registryDataDirName + ":z\n");
writer.print(" - ./" + localCertDirName + ":" + containerCertPath + ":z");
writer.flush();
writer.close();
return output.toByteArray();
}
}

View file

@ -0,0 +1,35 @@
package org.keycloak.protocol.docker.installation.compose;
import java.net.URL;
import java.security.cert.Certificate;
public class DockerComposeZipContent {
private final DockerComposeYamlFile yamlFile;
private final String dataDirectoryName;
private final DockerComposeCertsDirectory certsDirectory;
public DockerComposeZipContent(final Certificate realmCert, final URL realmBaseUrl, final String realmName, final String clientId) {
final String dataDirectoryName = "data";
final String certsDirectoryName = "certs";
final String registryCertFilename = "localhost.crt";
final String registryKeyFilename = "localhost.key";
final String idpCertTrustChainFilename = "localhost_trust_chain.pem";
this.yamlFile = new DockerComposeYamlFile(dataDirectoryName, certsDirectoryName, "/opt/" + certsDirectoryName, registryCertFilename, registryKeyFilename, idpCertTrustChainFilename, realmBaseUrl, realmName, clientId);
this.dataDirectoryName = dataDirectoryName;
this.certsDirectory = new DockerComposeCertsDirectory(certsDirectoryName, realmCert, registryCertFilename, registryKeyFilename, idpCertTrustChainFilename, realmName);
}
public DockerComposeYamlFile getYamlFile() {
return yamlFile;
}
public String getDataDirectoryName() {
return dataDirectoryName;
}
public DockerComposeCertsDirectory getCertsDirectory() {
return certsDirectory;
}
}

View file

@ -0,0 +1,52 @@
package org.keycloak.protocol.docker.mapper;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.docker.DockerAuthV2Protocol;
import org.keycloak.representations.docker.DockerAccess;
import org.keycloak.representations.docker.DockerResponseToken;
/**
* Populates token with requested scope. If more scopes are present than what has been requested, they will be removed.
*/
public class AllowAllDockerProtocolMapper extends DockerAuthV2ProtocolMapper implements DockerAuthV2AttributeMapper {
public static final String PROVIDER_ID = "docker-v2-allow-all-mapper";
@Override
public String getDisplayType() {
return "Allow All";
}
@Override
public String getHelpText() {
return "Allows all grants, returning the full set of requested access attributes as permitted attributes.";
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public boolean appliesTo(final DockerResponseToken responseToken) {
return true;
}
@Override
public DockerResponseToken transformDockerResponseToken(final DockerResponseToken responseToken, final ProtocolMapperModel mappingModel,
final KeycloakSession session, final UserSessionModel userSession, final AuthenticatedClientSessionModel clientSession) {
responseToken.getAccessItems().clear();
final String requestedScope = clientSession.getNote(DockerAuthV2Protocol.SCOPE_PARAM);
if (requestedScope != null) {
final DockerAccess allRequestedAccess = new DockerAccess(requestedScope);
responseToken.getAccessItems().add(allRequestedAccess);
}
return responseToken;
}
}

View file

@ -0,0 +1,15 @@
package org.keycloak.protocol.docker.mapper;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ProtocolMapperModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.representations.docker.DockerResponseToken;
public interface DockerAuthV2AttributeMapper {
boolean appliesTo(DockerResponseToken responseToken);
DockerResponseToken transformDockerResponseToken(DockerResponseToken responseToken, ProtocolMapperModel mappingModel,
KeycloakSession session, UserSessionModel userSession, AuthenticatedClientSessionModel clientSession);
}

View file

@ -0,0 +1,51 @@
package org.keycloak.protocol.docker.mapper;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.protocol.ProtocolMapper;
import org.keycloak.protocol.docker.DockerAuthV2Protocol;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.Collections;
import java.util.List;
public abstract class DockerAuthV2ProtocolMapper implements ProtocolMapper {
public static final String DOCKER_AUTH_V2_CATEGORY = "Docker Auth Mapper";
@Override
public String getProtocol() {
return DockerAuthV2Protocol.LOGIN_PROTOCOL;
}
@Override
public String getDisplayCategory() {
return DOCKER_AUTH_V2_CATEGORY;
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return Collections.emptyList();
}
@Override
public void close() {
// no-op
}
@Override
public final ProtocolMapper create(final KeycloakSession session) {
throw new UnsupportedOperationException("The create method is not supported by this mapper");
}
@Override
public void init(final Config.Scope config) {
// no-op
}
@Override
public void postInit(final KeycloakSessionFactory factory) {
// no-op
}
}

View file

@ -49,7 +49,6 @@ import org.keycloak.util.TokenUtil;
import javax.ws.rs.GET;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -169,21 +168,6 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
return this;
}
private void checkSsl() {
if (!uriInfo.getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
event.error(Errors.SSL_REQUIRED);
throw new ErrorPageException(session, Messages.HTTPS_REQUIRED);
}
}
private void checkRealm() {
if (!realm.isEnabled()) {
event.error(Errors.REALM_DISABLED);
throw new ErrorPageException(session, Messages.REALM_NOT_ENABLED);
}
}
private void checkClient(String clientId) {
if (clientId == null) {
event.error(Errors.INVALID_REQUEST);
@ -288,24 +272,24 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
private Response checkPKCEParams() {
String codeChallenge = request.getCodeChallenge();
String codeChallengeMethod = request.getCodeChallengeMethod();
// PKCE not adopted to OAuth2 Implicit Grant and OIDC Implicit Flow,
// adopted to OAuth2 Authorization Code Grant and OIDC Authorization Code Flow, Hybrid Flow
// Namely, flows using authorization code.
if (parsedResponseType.isImplicitFlow()) return null;
if (codeChallenge == null && codeChallengeMethod != null) {
logger.info("PKCE supporting Client without code challenge");
event.error(Errors.INVALID_REQUEST);
return redirectErrorToClient(parsedResponseMode, OAuthErrorException.INVALID_REQUEST, "Missing parameter: code_challenge");
}
// based on code_challenge value decide whether this client(RP) supports PKCE
if (codeChallenge == null) {
logger.debug("PKCE non-supporting Client");
return null;
}
if (codeChallengeMethod != null) {
// https://tools.ietf.org/html/rfc7636#section-4.2
// plain or S256
@ -319,13 +303,13 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
// default code_challenge_method is plane
codeChallengeMethod = OIDCLoginProtocol.PKCE_METHOD_PLAIN;
}
if (!isValidPkceCodeChallenge(codeChallenge)) {
logger.infof("PKCE supporting Client with invalid code challenge specified in PKCE, codeChallenge = %s", codeChallenge);
event.error(Errors.INVALID_REQUEST);
return redirectErrorToClient(parsedResponseMode, OAuthErrorException.INVALID_REQUEST, "Invalid parameter: code_challenge");
}
return null;
}

View file

@ -1,182 +1,122 @@
/*
* 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.protocol.saml.profile.ecp.authenticator;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.Config;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.common.util.Base64;
import org.keycloak.events.Errors;
import org.keycloak.models.AuthenticationExecutionModel.Requirement;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.ProviderConfigProperty;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.List;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class HttpBasicAuthenticator implements AuthenticatorFactory {
public class HttpBasicAuthenticator implements Authenticator {
public static final String PROVIDER_ID = "http-basic-authenticator";
private static final String BASIC = "Basic";
private static final String BASIC_PREFIX = BASIC + " ";
@Override
public String getDisplayType() {
return "HTTP Basic Authentication";
public void authenticate(final AuthenticationFlowContext context) {
final HttpRequest httpRequest = context.getHttpRequest();
final HttpHeaders httpHeaders = httpRequest.getHttpHeaders();
final String[] usernameAndPassword = getUsernameAndPassword(httpHeaders);
context.attempted();
if (usernameAndPassword != null) {
final RealmModel realm = context.getRealm();
final String username = usernameAndPassword[0];
final UserModel user = context.getSession().users().getUserByUsername(username, realm);
if (user != null) {
final String password = usernameAndPassword[1];
final boolean valid = context.getSession().userCredentialManager().isValid(realm, user, UserCredentialModel.password(password));
if (valid) {
if (user.isEnabled()) {
userSuccessAction(context, user);
} else {
userDisabledAction(context, realm, user);
}
} else {
notValidCredentialsAction(context, realm, user);
}
} else {
nullUserAction(context, realm, username);
}
}
}
protected void userSuccessAction(AuthenticationFlowContext context, UserModel user) {
context.getAuthenticationSession().setAuthenticatedUser(user);
context.success();
}
protected void userDisabledAction(AuthenticationFlowContext context, RealmModel realm, UserModel user) {
userSuccessAction(context, user);
}
protected void nullUserAction(final AuthenticationFlowContext context, final RealmModel realm, final String user) {
// no-op by default
}
protected void notValidCredentialsAction(final AuthenticationFlowContext context, final RealmModel realm, final UserModel user) {
context.getEvent().user(user);
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
context.failure(AuthenticationFlowError.INVALID_USER, Response.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.WWW_AUTHENTICATE, BASIC_PREFIX + "realm=\"" + realm.getName() + "\"")
.build());
}
private String[] getUsernameAndPassword(final HttpHeaders httpHeaders) {
final List<String> authHeaders = httpHeaders.getRequestHeader(HttpHeaders.AUTHORIZATION);
if (authHeaders == null || authHeaders.size() == 0) {
return null;
}
String credentials = null;
for (final String authHeader : authHeaders) {
if (authHeader.startsWith(BASIC_PREFIX)) {
final String[] split = authHeader.trim().split("\\s+");
if (split == null || split.length != 2) return null;
credentials = split[1];
}
}
try {
return new String(Base64.decode(credentials)).split(":");
} catch (final IOException e) {
throw new RuntimeException("Failed to parse credentials.", e);
}
}
@Override
public String getReferenceCategory() {
return null;
public void action(final AuthenticationFlowContext context) {
}
@Override
public boolean isConfigurable() {
public boolean requiresUser() {
return false;
}
@Override
public Requirement[] getRequirementChoices() {
return new Requirement[0];
}
@Override
public boolean isUserSetupAllowed() {
public boolean configuredFor(final KeycloakSession session, final RealmModel realm, final UserModel user) {
return false;
}
@Override
public String getHelpText() {
return "Validates username and password from Authorization HTTP header";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return null;
}
@Override
public Authenticator create(KeycloakSession session) {
return new Authenticator() {
private static final String BASIC = "Basic";
private static final String BASIC_PREFIX = BASIC + " ";
@Override
public void authenticate(AuthenticationFlowContext context) {
HttpRequest httpRequest = context.getHttpRequest();
HttpHeaders httpHeaders = httpRequest.getHttpHeaders();
String[] usernameAndPassword = getUsernameAndPassword(httpHeaders);
context.attempted();
if (usernameAndPassword != null) {
RealmModel realm = context.getRealm();
UserModel user = context.getSession().users().getUserByUsername(usernameAndPassword[0], realm);
if (user != null) {
String password = usernameAndPassword[1];
boolean valid = context.getSession().userCredentialManager().isValid(realm, user, UserCredentialModel.password(password));
if (valid) {
context.getAuthenticationSession().setAuthenticatedUser(user);
context.success();
} else {
context.getEvent().user(user);
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
context.failure(AuthenticationFlowError.INVALID_USER, Response.status(Response.Status.UNAUTHORIZED)
.header(HttpHeaders.WWW_AUTHENTICATE, BASIC_PREFIX + "realm=\"" + realm.getName() + "\"")
.build());
}
}
}
}
private String[] getUsernameAndPassword(HttpHeaders httpHeaders) {
List<String> authHeaders = httpHeaders.getRequestHeader(HttpHeaders.AUTHORIZATION);
if (authHeaders == null || authHeaders.size() == 0) {
return null;
}
String credentials = null;
for (String authHeader : authHeaders) {
if (authHeader.startsWith(BASIC_PREFIX)) {
String[] split = authHeader.trim().split("\\s+");
if (split == null || split.length != 2) return null;
credentials = split[1];
}
}
try {
return new String(Base64.decode(credentials)).split(":");
} catch (IOException e) {
throw new RuntimeException("Failed to parse credentials.", e);
}
}
@Override
public void action(AuthenticationFlowContext context) {
}
@Override
public boolean requiresUser() {
return false;
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return false;
}
@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
}
@Override
public void close() {
}
};
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
public void setRequiredActions(final KeycloakSession session, final RealmModel realm, final UserModel user) {
}
@ -184,9 +124,4 @@ public class HttpBasicAuthenticator implements AuthenticatorFactory {
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -0,0 +1,115 @@
/*
* 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.protocol.saml.profile.ecp.authenticator;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.Config;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.common.util.Base64;
import org.keycloak.events.Errors;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationExecutionModel.Requirement;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.ProviderConfigProperty;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.util.List;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class HttpBasicAuthenticatorFactory implements AuthenticatorFactory {
public static final String PROVIDER_ID = "http-basic-authenticator";
@Override
public String getDisplayType() {
return "HTTP Basic Authentication";
}
@Override
public String getReferenceCategory() {
return "basic";
}
@Override
public boolean isConfigurable() {
return false;
}
private static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED,
Requirement.ALTERNATIVE,
Requirement.OPTIONAL,
AuthenticationExecutionModel.Requirement.DISABLED
};
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
@Override
public boolean isUserSetupAllowed() {
return false;
}
@Override
public String getHelpText() {
return "Validates username and password from Authorization HTTP header";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return null;
}
@Override
public Authenticator create(final KeycloakSession session) {
return new HttpBasicAuthenticator();
}
@Override
public void init(final Config.Scope config) {
}
@Override
public void postInit(final KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -129,6 +129,7 @@ public class ServerInfoAdminResource {
for (String name : providerIds) {
ProviderRepresentation provider = new ProviderRepresentation();
ProviderFactory<?> pi = session.getKeycloakSessionFactory().getProviderFactory(spi.getProviderClass(), name);
provider.setOrder(pi.order());
if (ServerInfoAwareProviderFactory.class.isAssignableFrom(pi.getClass())) {
provider.setOperationalInfo(((ServerInfoAwareProviderFactory) pi).getOperationalInfo());
}

View file

@ -0,0 +1,23 @@
# Docker Compose YAML Installation
-----------------------------------
*NOTE:* This installation method is intended for development use only. Please don't ever let this anywhere near prod!
## Keycloak Realm Assumptions:
- Client configuration has not changed since the installtion files were generated. If you change your client configuration, be sure to grab a re-generated installtion .zip from the 'Installation' tab.
- Keycloak server is started with the 'docker' feature enabled. I.E. -Dkeycloak.profile.feature.docker=enabled
## Running the Installation:
- Spin up a fully functional docker registry with:
docker-compose up
- Now you can login against the registry and perform normal operations:
docker login -u $username -p $password localhost:5000
docker pull centos:7
docker tag centos:7 localhost:5000/centos:7
docker push localhost:5000/centos:7
** Remember that users for the `docker login` command must be configured and available in the keycloak realm that hosts the docker client.

View file

@ -34,6 +34,7 @@ org.keycloak.authentication.authenticators.broker.IdpConfirmLinkAuthenticatorFac
org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory
org.keycloak.authentication.authenticators.broker.IdpUsernamePasswordFormFactory
org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticatorFactory
org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticator
org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticatorFactory
org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory
org.keycloak.authentication.authenticators.x509.ValidateX509CertificateUsernameFactory
org.keycloak.protocol.docker.DockerAuthenticatorFactory

View file

@ -22,4 +22,6 @@ org.keycloak.protocol.saml.installation.SamlSPDescriptorClientInstallation
org.keycloak.protocol.saml.installation.SamlIDPDescriptorClientInstallation
org.keycloak.protocol.saml.installation.ModAuthMellonClientInstallation
org.keycloak.protocol.saml.installation.KeycloakSamlSubsystemInstallation
org.keycloak.protocol.docker.installation.DockerVariableOverrideInstallationProvider
org.keycloak.protocol.docker.installation.DockerRegistryConfigFileInstallationProvider
org.keycloak.protocol.docker.installation.DockerComposeYamlInstallationProvider

View file

@ -16,4 +16,5 @@
#
org.keycloak.protocol.oidc.OIDCLoginProtocolFactory
org.keycloak.protocol.saml.SamlProtocolFactory
org.keycloak.protocol.saml.SamlProtocolFactory
org.keycloak.protocol.docker.DockerAuthV2ProtocolFactory

View file

@ -35,4 +35,5 @@ org.keycloak.protocol.saml.mappers.GroupMembershipMapper
org.keycloak.protocol.oidc.mappers.UserClientRoleMappingMapper
org.keycloak.protocol.oidc.mappers.UserRealmRoleMappingMapper
org.keycloak.protocol.oidc.mappers.SHA256PairwiseSubMapper
org.keycloak.protocol.docker.mapper.AllowAllDockerProtocolMapper

View file

@ -0,0 +1,193 @@
package org.keycloak.procotol.docker.installation;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Ignore;
import org.junit.Test;
import org.keycloak.common.util.CertificateUtils;
import org.keycloak.common.util.PemUtils;
import org.keycloak.protocol.docker.installation.DockerComposeYamlInstallationProvider;
import javax.ws.rs.core.Response;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.SecureRandom;
import java.security.cert.Certificate;
import java.security.cert.X509Certificate;
import java.util.Optional;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.core.IsEqual.equalTo;
import static org.hamcrest.core.IsNull.notNullValue;
import static org.junit.Assert.fail;
import static org.keycloak.protocol.docker.installation.DockerComposeYamlInstallationProvider.ROOT_DIR;
public class DockerComposeYamlInstallationProviderTest {
DockerComposeYamlInstallationProvider installationProvider;
static Certificate certificate;
@BeforeClass
public static void setUp_beforeClass() throws NoSuchAlgorithmException {
final KeyPairGenerator keyGen;
keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048, new SecureRandom());
final KeyPair keypair = keyGen.generateKeyPair();
certificate = CertificateUtils.generateV1SelfSignedCertificate(keypair, "test-realm");
}
@Before
public void setUp() {
installationProvider = new DockerComposeYamlInstallationProvider();
}
private Response fireInstallationProvider() throws IOException {
ByteArrayOutputStream byteStream = null;
ZipOutputStream zipOutput = null;
byteStream = new ByteArrayOutputStream();
zipOutput = new ZipOutputStream(byteStream);
return installationProvider.generateInstallation(zipOutput, byteStream, certificate, new URL("http://localhost:8080/auth"), "docker-test", "docker-registry");
}
@Test
@Ignore // Used only for smoke testing
public void writeToRealZip() throws IOException {
final Response response = fireInstallationProvider();
final byte[] responseBytes = (byte[]) response.getEntity();
FileUtils.writeByteArrayToFile(new File("target/keycloak-docker-compose-yaml.zip"), responseBytes);
}
@Test
public void testAllTheZipThings() throws Exception {
final Response response = fireInstallationProvider();
assertThat("compose YAML returned non-ok response", response.getStatus(), equalTo(Response.Status.OK.getStatusCode()));
shouldIncludeDockerComposeYamlInZip(getZipResponseFromInstallProvider(response));
shouldIncludeReadmeInZip(getZipResponseFromInstallProvider(response));
shouldWriteBlankDataDirectoryInZip(getZipResponseFromInstallProvider(response));
shouldWriteCertDirectoryInZip(getZipResponseFromInstallProvider(response));
shouldWriteSslCertificateInZip(getZipResponseFromInstallProvider(response));
shouldWritePrivateKeyInZip(getZipResponseFromInstallProvider(response));
}
public void shouldIncludeDockerComposeYamlInZip(ZipInputStream zipInput) throws Exception {
final Optional<String> dockerComposeFileContents = getFileContents(zipInput, ROOT_DIR + "docker-compose.yaml");
assertThat("Could not find docker-compose.yaml file in zip archive response", dockerComposeFileContents.isPresent(), equalTo(true));
final boolean zipFileContentEqualsTestFile = IOUtils.contentEquals(new ByteArrayInputStream(dockerComposeFileContents.get().getBytes()), new FileInputStream("src/test/resources/docker-compose-expected.yaml"));
assertThat("Invalid docker-compose file contents: \n" + dockerComposeFileContents.get(), zipFileContentEqualsTestFile, equalTo(true));
}
public void shouldIncludeReadmeInZip(ZipInputStream zipInput) throws Exception {
final Optional<String> dockerComposeFileContents = getFileContents(zipInput, ROOT_DIR + "README.md");
assertThat("Could not find README.md file in zip archive response", dockerComposeFileContents.isPresent(), equalTo(true));
}
public void shouldWriteBlankDataDirectoryInZip(ZipInputStream zipInput) throws Exception {
ZipEntry zipEntry;
boolean dataDirFound = false;
while ((zipEntry = zipInput.getNextEntry()) != null) {
try {
if (zipEntry.getName().equals(ROOT_DIR + "data/")) {
dataDirFound = true;
assertThat("Zip entry for data directory is not the correct type", zipEntry.isDirectory(), equalTo(true));
}
} finally {
zipInput.closeEntry();
}
}
assertThat("Could not find data directory", dataDirFound, equalTo(true));
}
public void shouldWriteCertDirectoryInZip(ZipInputStream zipInput) throws Exception {
ZipEntry zipEntry;
boolean certsDirFound = false;
while ((zipEntry = zipInput.getNextEntry()) != null) {
try {
if (zipEntry.getName().equals(ROOT_DIR + "certs/")) {
certsDirFound = true;
assertThat("Zip entry for cert directory is not the correct type", zipEntry.isDirectory(), equalTo(true));
}
} finally {
zipInput.closeEntry();
}
}
assertThat("Could not find cert directory", certsDirFound, equalTo(true));
}
public void shouldWriteSslCertificateInZip(ZipInputStream zipInput) throws Exception {
final Optional<String> localhostCertificateFileContents = getFileContents(zipInput, ROOT_DIR + "certs/localhost.crt");
assertThat("Could not find localhost certificate", localhostCertificateFileContents.isPresent(), equalTo(true));
final X509Certificate x509Certificate = PemUtils.decodeCertificate(localhostCertificateFileContents.get());
assertThat("Invalid x509 given by docker-compose YAML", x509Certificate, notNullValue());
}
public void shouldWritePrivateKeyInZip(ZipInputStream zipInput) throws Exception {
final Optional<String> localhostPrivateKeyFileContents = getFileContents(zipInput, ROOT_DIR + "certs/localhost.key");
assertThat("Could not find localhost private key", localhostPrivateKeyFileContents.isPresent(), equalTo(true));
final PrivateKey privateKey = PemUtils.decodePrivateKey(localhostPrivateKeyFileContents.get());
assertThat("Invalid private Key given by docker-compose YAML", privateKey, notNullValue());
}
private ZipInputStream getZipResponseFromInstallProvider(Response response) throws IOException {
final Object responseEntity = response.getEntity();
if (!(responseEntity instanceof byte[])) {
fail("Recieved non-byte[] entity for docker-compose YAML installation response");
}
return new ZipInputStream(new ByteArrayInputStream((byte[]) responseEntity));
}
private static Optional<String> getFileContents(final ZipInputStream zipInputStream, final String fileName) throws IOException {
ZipEntry zipEntry;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
try {
if (zipEntry.getName().equals(fileName)) {
return Optional.of(readBytesToString(zipInputStream));
}
} finally {
zipInputStream.closeEntry();
}
}
// fall-through case if file name not found:
return Optional.empty();
}
private static String readBytesToString(final InputStream inputStream) throws IOException {
final ByteArrayOutputStream output = new ByteArrayOutputStream();
final byte[] buffer = new byte[4096];
int bytesRead;
try {
while ((bytesRead = inputStream.read(buffer)) != -1) {
output.write(buffer, 0, bytesRead);
}
} finally {
output.close();
}
return new String(output.toByteArray());
}
}

View file

@ -0,0 +1,41 @@
package org.keycloak.procotol.docker.installation;
import org.hamcrest.CoreMatchers;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.models.utils.Base32;
import org.keycloak.protocol.docker.DockerKeyIdentifier;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PublicKey;
import java.security.SecureRandom;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.MatcherAssert.assertThat;
/**
* Docker gets really unhappy if the key identifier is not in the format documented here:
* @see https://github.com/docker/libtrust/blob/master/key.go#L24
*/
public class DockerKeyIdentifierTest {
String keyIdentifierString;
PublicKey publicKey;
@Before
public void shouldBlah() throws Exception {
final KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA");
keyGen.initialize(2048, new SecureRandom());
final KeyPair keypair = keyGen.generateKeyPair();
publicKey = keypair.getPublic();
final DockerKeyIdentifier identifier = new DockerKeyIdentifier(publicKey);
keyIdentifierString = identifier.toString();
}
@Test
public void shoulProduceExpectedKeyFormat() {
assertThat("Every 4 chars are not delimted by colon", keyIdentifierString.matches("([\\w]{4}:){11}[\\w]{4}"), equalTo(true));
}
}

View file

@ -0,0 +1,15 @@
registry:
image: registry:2
ports:
- 127.0.0.1:5000:5000
environment:
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
REGISTRY_HTTP_TLS_CERTIFICATE: /opt/certs/localhost.crt
REGISTRY_HTTP_TLS_KEY: /opt/certs/localhost.key
REGISTRY_AUTH_TOKEN_REALM: http://localhost:8080/auth/realms/docker-test/protocol/docker-v2/auth
REGISTRY_AUTH_TOKEN_SERVICE: docker-registry
REGISTRY_AUTH_TOKEN_ISSUER: http://localhost:8080/auth/realms/docker-test
REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: /opt/certs/localhost_trust_chain.pem
volumes:
- ./data:/data:z
- ./certs:/opt/certs:z

View file

@ -61,7 +61,7 @@ More info: http://javahowto.blogspot.cz/2010/09/java-agentlibjdwp-for-attaching.
Analogically, there is the same behaviour for JBoss based app server as for auth server. The default port is set to 5006. There are app server properties.
-Dapp.server.debug.port=$PORT
-Dapp.server.debug.suspend=y
-Dapp.server.debug.suspend=y
## Testsuite logging
@ -454,7 +454,7 @@ First compile the Infinispan/JDG test server via the following command:
`mvn -Pcache-server-infinispan -f testsuite/integration-arquillian -DskipTests clean install`
or
`mvn -Pcache-server-jdg -f testsuite/integration-arquillian -DskipTests clean install`
Then you can run the tests using the following command (adjust the test specification according to your needs):
@ -466,3 +466,103 @@ or
`mvn -Pcache-server-jdg -Dtest=*.crossdc.* -pl testsuite/integration-arquillian/tests/base test`
_Someone using IntelliJ IDEA, please describe steps for that IDE_
## Run Docker Authentication test
First, validate that your machine has a valid docker installation and that it is available to the JVM running the test.
The exact steps to configure Docker depend on the operating system.
By default, the test will run against Undertow based embedded Keycloak Server, thus no distribution build is required beforehand.
The exact command line arguments depend on the operating system.
### General guidelines
If docker daemon doesn't run locally, or if you're not running on Linux, you may need
to determine the IP of the bridge interface or local interface that Docker daemon can use to connect to Keycloak Server.
Then specify that IP as additional system property called *host.ip*, for example:
-Dhost.ip=192.168.64.1
If using Docker for Mac, you can create an alias for your local network interface:
sudo ifconfig lo0 alias 10.200.10.1/24
Then pass the IP as *host.ip*:
-Dhost.ip=10.200.10.1
If you're running a Docker fork that always lists a host component of an image on `docker images` (e.g. Fedora / RHEL Docker)
use `-Ddocker.io-prefix-explicit=true` argument when running the test.
### Fedora
On Fedora one way to set up Docker server is the following:
# install docker
sudo dnf install docker
# configure docker
# remove --selinux-enabled from OPTIONS
sudo vi /etc/sysconfig/docker
# create docker group and add your user (so docker wouldn't need root permissions)
sudo groupadd docker && sudo gpasswd -a ${USER} docker && sudo systemctl restart docker
newgrp docker
# you need to login again after this
# make sure Docker is available
docker pull registry:2
You may also need to add an iptables rule to allow container to host traffic
sudo iptables -I INPUT -i docker0 -j ACCEPT
Then, run the test passing `-Ddocker.io-prefix-explicit=true`:
mvn -f testsuite/integration-arquillian/tests/base/pom.xml \
clean test \
-Dtest=DockerClientTest \
-Dkeycloak.profile.feature.docker=enabled \
-Ddocker.io-prefix-explicit=true
### macOS
On macOS all you need to do is install Docker for Mac, start it up, and check that it works:
# make sure Docker is available
docker pull registry:2
Be especially careful to restart Docker server after every sleep / suspend to ensure system clock of Docker VM is synchronized with
that of the host operating system - Docker for Mac runs inside a VM.
Then, run the test passing `-Dhost.ip=IP` where IP corresponds to en0 interface or an alias for localhost:
mvn -f testsuite/integration-arquillian/tests/base/pom.xml \
clean test \
-Dtest=DockerClientTest \
-Dkeycloak.profile.feature.docker=enabled \
-Dhost.ip=10.200.10.1
### Running Docker test against Keycloak Server distribution
Make sure to build the distribution:
mvn clean install -f distribution
Then, before running the test, setup Keycloak Server distribution for the tests:
mvn -f testsuite/integration-arquillian/servers/pom.xml \
clean install \
-Pauth-server-wildfly
When running the test, add the following arguments to the command line:
-Pauth-server-wildfly -Pauth-server-enable-disable-feature -Dfeature.name=docker -Dfeature.value=enabled

View file

@ -88,6 +88,28 @@
<artifactId>greenmail</artifactId>
<scope>compile</scope>
</dependency>
<!--<dependency>-->
<!--<groupId>com.spotify</groupId>-->
<!--<artifactId>docker-client</artifactId>-->
<!--<version>8.3.2</version>-->
<!--<scope>test</scope>-->
<!--<exclusions>-->
<!--<exclusion>-->
<!--<groupId>javax.ws.rs</groupId>-->
<!--<artifactId>javax.ws.rs-api</artifactId>-->
<!--</exclusion>-->
<!--<exclusion>-->
<!--<groupId>com.github.jnr</groupId>-->
<!--<artifactId>jnr-unixsocket</artifactId>-->
<!--</exclusion>-->
<!--</exclusions>-->
<!--</dependency>-->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.2.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>

View file

@ -25,6 +25,10 @@ import org.keycloak.common.Profile;
*/
public class ProfileAssume {
public static void assumeFeatureEnabled(Profile.Feature feature) {
Assume.assumeTrue("Ignoring test as " + feature.name() + " is not enabled", Profile.isFeatureEnabled(feature));
}
public static void assumePreview() {
Assume.assumeTrue("Ignoring test as community/preview profile is not enabled", !Profile.getName().equals("product"));
}

View file

@ -39,6 +39,7 @@ public class URLProvider extends URLResourceProvider {
protected final Logger log = Logger.getLogger(this.getClass());
public static final String BOUND_TO_ALL = "0.0.0.0";
public static final String LOCALHOST_ADDRESS = "127.0.0.1";
public static final String LOCALHOST_HOSTNAME = "localhost";
@ -59,6 +60,7 @@ public class URLProvider extends URLResourceProvider {
if (url != null) {
try {
url = fixLocalhost(url);
url = fixBoundToAll(url);
url = removeTrailingSlash(url);
if (appServerSslRequired) {
url = fixSsl(url);
@ -111,6 +113,14 @@ public class URLProvider extends URLResourceProvider {
return url;
}
public URL fixBoundToAll(URL url) throws MalformedURLException {
URL fixedUrl = url;
if (url.getHost().contains(BOUND_TO_ALL)) {
fixedUrl = new URL(fixedUrl.toExternalForm().replace(BOUND_TO_ALL, LOCALHOST_HOSTNAME));
}
return fixedUrl;
}
public URL fixLocalhost(URL url) throws MalformedURLException {
URL fixedUrl = url;
if (url.getHost().contains(LOCALHOST_ADDRESS)) {

View file

@ -19,6 +19,8 @@ package org.keycloak.testsuite.admin.authentication;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.protocol.docker.DockerAuthenticator;
import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
@ -155,6 +157,13 @@ public class InitialFlowsTest extends AbstractAuthenticationTest {
addExecInfo(execs, "OTP", "direct-grant-validate-otp", false, 0, 2, OPTIONAL, null, new String[]{REQUIRED, OPTIONAL, DISABLED});
expected.add(new FlowExecutions(flow, execs));
flow = newFlow("docker auth", "Used by Docker clients to authenticate against the IDP", "basic-flow", true, true);
addExecExport(flow, null, false, "docker-http-basic-authenticator", false, null, REQUIRED, 10);
execs = new LinkedList<>();
addExecInfo(execs, "Docker Authenticator", "docker-http-basic-authenticator", false, 0, 0, REQUIRED, null, new String[]{REQUIRED});
expected.add(new FlowExecutions(flow, execs));
flow = newFlow("first broker login", "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account",
"basic-flow", true, true);
addExecExport(flow, null, false, "idp-review-profile", false, "review profile config", REQUIRED, 10);

View file

@ -151,6 +151,7 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"Validates the password supplied as a 'password' form parameter in direct grant request");
addProviderInfo(result, "direct-grant-validate-username", "Username Validation",
"Validates the username supplied as a 'username' form parameter in direct grant request");
addProviderInfo(result, "docker-http-basic-authenticator", "Docker Authenticator", "Uses HTTP Basic authentication to validate docker users, returning a docker error token on auth failure");
addProviderInfo(result, "expected-param-authenticator", "TEST: Expected Parameter",
"You will be approved if you send query string parameter 'foo' with expected value.");
addProviderInfo(result, "http-basic-authenticator", "HTTP Basic Authentication", "Validates username and password from Authorization HTTP header");

View file

@ -0,0 +1,200 @@
package org.keycloak.testsuite.docker;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.keycloak.common.Profile;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.ProfileAssume;
import org.keycloak.testsuite.util.WaitUtils;
import org.rnorth.ducttape.ratelimits.RateLimiterBuilder;
import org.rnorth.ducttape.unreliables.Unreliables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.testcontainers.containers.BindMode;
import org.testcontainers.containers.Container;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.output.Slf4jLogConsumer;
import org.testcontainers.images.builder.ImageFromDockerfile;
import org.testcontainers.shaded.com.github.dockerjava.api.model.ContainerNetwork;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import java.util.function.Predicate;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.junit.Assume.assumeTrue;
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
public class DockerClientTest extends AbstractKeycloakTest {
public static final Logger LOGGER = LoggerFactory.getLogger(DockerClientTest.class);
public static final String REALM_ID = "docker-test-realm";
public static final String AUTH_FLOW = "docker-basic-auth-flow";
public static final String CLIENT_ID = "docker-test-client";
public static final String DOCKER_USER = "docker-user";
public static final String DOCKER_USER_PASSWORD = "password";
public static final String REGISTRY_HOSTNAME = "registry.localdomain";
public static final Integer REGISTRY_PORT = 5000;
public static final String MINIMUM_DOCKER_VERSION = "1.8.0";
public static final String IMAGE_NAME = "busybox";
private GenericContainer dockerRegistryContainer = null;
private GenericContainer dockerClientContainer = null;
private static String hostIp;
@BeforeClass
public static void verifyEnvironment() {
ProfileAssume.assumeFeatureEnabled(Profile.Feature.DOCKER);
final Optional<DockerVersion> dockerVersion = new DockerHostVersionSupplier().get();
assumeTrue("Could not determine docker version for host machine. It either is not present or accessible to the JVM running the test harness.", dockerVersion.isPresent());
assumeTrue("Docker client on host machine is not a supported version. Please upgrade and try again.", DockerVersion.COMPARATOR.compare(dockerVersion.get(), DockerVersion.parseVersionString(MINIMUM_DOCKER_VERSION)) >= 0);
LOGGER.debug("Discovered valid docker client on host. version: {}", dockerVersion);
hostIp = System.getProperty("host.ip");
if (hostIp == null) {
final Optional<String> foundHostIp = new DockerHostIpSupplier().get();
if (foundHostIp.isPresent()) {
hostIp = foundHostIp.get();
}
}
Assert.assertNotNull("Could not resolve host machine's IP address for docker adapter, and 'host.ip' system poperty not set. Client will not be able to authenticate against the keycloak server!", hostIp);
}
@Override
public void addTestRealms(final List<RealmRepresentation> testRealms) {
final RealmRepresentation dockerRealm = loadJson(getClass().getResourceAsStream("/docker-test-realm.json"), RealmRepresentation.class);
/**
* TODO fix test harness/importer NPEs when attempting to create realm from scratch.
* Need to fix those, would be preferred to do this programmatically such that we don't have to keep realm elements
* (I.E. certs, realm url) in sync with a flat file
*
* final RealmRepresentation dockerRealm = DockerTestRealmSetup.createRealm(REALM_ID);
* DockerTestRealmSetup.configureDockerAuthenticationFlow(dockerRealm, AUTH_FLOW);
*/
DockerTestRealmSetup.configureDockerRegistryClient(dockerRealm, CLIENT_ID);
DockerTestRealmSetup.configureUser(dockerRealm, DOCKER_USER, DOCKER_USER_PASSWORD);
testRealms.add(dockerRealm);
}
@Override
public void beforeAbstractKeycloakTest() throws Exception {
super.beforeAbstractKeycloakTest();
final Map<String, String> environment = new HashMap<>();
environment.put("REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY", "/tmp");
environment.put("REGISTRY_HTTP_TLS_CERTIFICATE", "/opt/certs/localhost.crt");
environment.put("REGISTRY_HTTP_TLS_KEY", "/opt/certs/localhost.key");
environment.put("REGISTRY_AUTH_TOKEN_REALM", "http://" + hostIp + ":8180/auth/realms/docker-test-realm/protocol/docker-v2/auth");
environment.put("REGISTRY_AUTH_TOKEN_SERVICE", CLIENT_ID);
environment.put("REGISTRY_AUTH_TOKEN_ISSUER", "http://" + hostIp + ":8180/auth/realms/docker-test-realm");
environment.put("REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE", "/opt/certs/docker-realm-public-key.pem");
environment.put("INSECURE_REGISTRY", "--insecure-registry " + REGISTRY_HOSTNAME + ":" + REGISTRY_PORT);
String dockerioPrefix = Boolean.parseBoolean(System.getProperty("docker.io-prefix-explicit")) ? "docker.io/" : "";
// TODO this required me to turn selinux off :(. Add BindMode options for :z and :Z. Make selinux enforcing again!
dockerRegistryContainer = new GenericContainer(dockerioPrefix + "registry:2")
.withClasspathResourceMapping("dockerClientTest/keycloak-docker-compose-yaml/certs", "/opt/certs", BindMode.READ_ONLY)
.withEnv(environment)
.withPrivilegedMode(true);
dockerRegistryContainer.start();
dockerRegistryContainer.followOutput(new Slf4jLogConsumer(LOGGER));
dockerClientContainer = new GenericContainer(
new ImageFromDockerfile()
.withDockerfileFromBuilder(dockerfileBuilder -> {
dockerfileBuilder.from("centos/systemd:latest")
.run("yum", "install", "-y", "docker", "iptables", ";", "yum", "clean", "all")
.cmd("/usr/sbin/init")
.volume("/sys/fs/cgroup")
.build();
})
)
.withClasspathResourceMapping("dockerClientTest/keycloak-docker-compose-yaml/certs/localhost.crt", "/opt/docker/certs.d/" + REGISTRY_HOSTNAME + "/localhost.crt", BindMode.READ_ONLY)
.withClasspathResourceMapping("dockerClientTest/keycloak-docker-compose-yaml/sysconfig_docker", "/etc/sysconfig/docker", BindMode.READ_WRITE)
.withPrivilegedMode(true);
final Optional<ContainerNetwork> network = dockerRegistryContainer.getContainerInfo().getNetworkSettings().getNetworks().values().stream().findFirst();
assumeTrue("Could not find a network adapter whereby the docker client container could connect to host!", network.isPresent());
dockerClientContainer.withExtraHost(REGISTRY_HOSTNAME, network.get().getIpAddress());
dockerClientContainer.start();
dockerClientContainer.followOutput(new Slf4jLogConsumer(LOGGER));
int i = 0;
String stdErr = "";
while (i++ < 30) {
log.infof("Trying to start docker service; attempt: %d", i);
stdErr = dockerClientContainer.execInContainer("systemctl", "start", "docker.service").getStderr();
if (stdErr.isEmpty()) {
break;
}
else {
log.info("systemctl failed: " + stdErr);
}
WaitUtils.pause(1000);
}
assumeTrue("Cannot start docker service!", stdErr.isEmpty());
log.info("Waiting for docker service...");
validateDockerStarted();
log.info("Docker service successfully started");
}
private void validateDockerStarted() {
final Callable<Boolean> checkStrategy = () -> {
try {
final String commandResult = dockerClientContainer.execInContainer("docker", "ps").getStderr();
return !commandResult.contains("Cannot connect");
} catch (InterruptedException e) {
throw new RuntimeException(e);
} catch (Exception e) {
return false;
}
};
Unreliables.retryUntilTrue(30, TimeUnit.SECONDS, () -> RateLimiterBuilder.newBuilder().withRate(1, TimeUnit.SECONDS).withConstantThroughput().build().getWhenReady(() -> {
try {
return checkStrategy.call();
} catch (Exception e) {
throw new RuntimeException(e);
}
}));
}
@Test
public void shouldPerformDockerAuthAgainstRegistry() throws Exception {
Container.ExecResult dockerLoginResult = dockerClientContainer.execInContainer("docker", "login", "-u", DOCKER_USER, "-p", DOCKER_USER_PASSWORD, REGISTRY_HOSTNAME + ":" + REGISTRY_PORT);
printNonEmpties(dockerLoginResult.getStdout(), dockerLoginResult.getStderr());
assertThat(dockerLoginResult.getStdout(), containsString("Login Succeeded"));
}
private static void printNonEmpties(final String... results) {
Arrays.stream(results)
.forEachOrdered(DockerClientTest::printNonEmpty);
}
private static void printNonEmpty(final String result) {
if (nullOrEmpty.negate().test(result)) {
LOGGER.info(result);
}
}
public static final Predicate<String> nullOrEmpty = string -> string == null || string.isEmpty();
}

View file

@ -0,0 +1,45 @@
package org.keycloak.testsuite.docker;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.regex.Pattern;
/**
* Docker doesn't provide a static/reliable way to grab the host machine's IP.
* <p>
* this currently just returns the first address for the bridge adapter starting with 'docker'. Not the most elegant solution,
* but I'm open to suggestions.
*
* @see https://github.com/moby/moby/issues/1143 and related issues referenced therein.
*/
public class DockerHostIpSupplier implements Supplier<Optional<String>> {
@Override
public Optional<String> get() {
final Enumeration<NetworkInterface> networkInterfaces;
try {
networkInterfaces = NetworkInterface.getNetworkInterfaces();
} catch (SocketException e) {
return Optional.empty();
}
return Collections.list(networkInterfaces).stream()
.filter(networkInterface -> networkInterface.getDisplayName().startsWith("docker"))
.flatMap(networkInterface -> Collections.list(networkInterface.getInetAddresses()).stream())
.map(InetAddress::getHostAddress)
.filter(DockerHostIpSupplier::looksLikeIpv4Address)
.findFirst();
}
public static boolean looksLikeIpv4Address(final String ip) {
return IPv4RegexPattern.matcher(ip).matches();
}
private static final Pattern IPv4RegexPattern = Pattern.compile("^(([01]?\\d\\d?|2[0-4]\\d|25[0-5])\\.){3}([01]?\\d\\d?|2[0-4]\\d|25[0-5])$");
}

View file

@ -0,0 +1,43 @@
package org.keycloak.testsuite.docker;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Collectors;
public class DockerHostVersionSupplier implements Supplier<Optional<DockerVersion>> {
private static final Logger log = LoggerFactory.getLogger(DockerHostVersionSupplier.class);
@Override
public Optional<DockerVersion> get() {
try {
Process process = new ProcessBuilder()
.command("docker", "version", "--format", "'{{.Client.Version}}'")
.start();
final BufferedReader stdout = getReader(process, Process::getInputStream);
final BufferedReader err = getReader(process, Process::getErrorStream);
int exitCode = process.waitFor();
if (exitCode == 0) {
final String versionString = stdout.lines().collect(Collectors.joining()).replaceAll("'", "");
return Optional.ofNullable(DockerVersion.parseVersionString(versionString));
}
} catch (IOException | InterruptedException e) {
log.error("Could not determine host machine's docker version: ", e);
}
return Optional.empty();
}
private static BufferedReader getReader(final Process process, final Function<Process, InputStream> streamSelector) {
return new BufferedReader(new InputStreamReader(streamSelector.apply(process)));
}
}

View file

@ -0,0 +1,87 @@
package org.keycloak.testsuite.docker;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.protocol.docker.DockerAuthV2Protocol;
import org.keycloak.protocol.docker.DockerAuthenticator;
import org.keycloak.representations.idm.AuthenticationExecutionExportRepresentation;
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
public final class DockerTestRealmSetup {
private DockerTestRealmSetup() {
}
public static RealmRepresentation createRealm(final String realmId) {
final RealmRepresentation createdRealm = new RealmRepresentation();
createdRealm.setId(UUID.randomUUID().toString());
createdRealm.setRealm(realmId);
createdRealm.setEnabled(true);
createdRealm.setAuthenticatorConfig(new ArrayList<>());
return createdRealm;
}
public static void configureDockerAuthenticationFlow(final RealmRepresentation dockerRealm, final String authFlowAlais) {
final AuthenticationFlowRepresentation dockerBasicAuthFlow = new AuthenticationFlowRepresentation();
dockerBasicAuthFlow.setId(UUID.randomUUID().toString());
dockerBasicAuthFlow.setAlias(authFlowAlais);
dockerBasicAuthFlow.setProviderId("basic-flow");
dockerBasicAuthFlow.setTopLevel(true);
dockerBasicAuthFlow.setBuiltIn(false);
final AuthenticationExecutionExportRepresentation dockerBasicAuthExecution = new AuthenticationExecutionExportRepresentation();
dockerBasicAuthExecution.setAuthenticator(DockerAuthenticator.ID);
dockerBasicAuthExecution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED.name());
dockerBasicAuthExecution.setPriority(0);
dockerBasicAuthExecution.setUserSetupAllowed(false);
dockerBasicAuthExecution.setAutheticatorFlow(false);
final List<AuthenticationExecutionExportRepresentation> authenticationExecutions = Optional.ofNullable(dockerBasicAuthFlow.getAuthenticationExecutions()).orElse(new ArrayList<>());
authenticationExecutions.add(dockerBasicAuthExecution);
dockerBasicAuthFlow.setAuthenticationExecutions(authenticationExecutions);
final List<AuthenticationFlowRepresentation> authenticationFlows = Optional.ofNullable(dockerRealm.getAuthenticationFlows()).orElse(new ArrayList<>());
authenticationFlows.add(dockerBasicAuthFlow);
dockerRealm.setAuthenticationFlows(authenticationFlows);
dockerRealm.setBrowserFlow(dockerBasicAuthFlow.getAlias());
}
public static void configureDockerRegistryClient(final RealmRepresentation dockerRealm, final String clientId) {
final ClientRepresentation dockerClient = new ClientRepresentation();
dockerClient.setClientId(clientId);
dockerClient.setProtocol(DockerAuthV2Protocol.LOGIN_PROTOCOL);
dockerClient.setEnabled(true);
final List<ClientRepresentation> clients = Optional.ofNullable(dockerRealm.getClients()).orElse(new ArrayList<>());
clients.add(dockerClient);
dockerRealm.setClients(clients);
}
public static void configureUser(final RealmRepresentation dockerRealm, final String username, final String password) {
final UserRepresentation dockerUser = new UserRepresentation();
dockerUser.setUsername(username);
dockerUser.setEnabled(true);
dockerUser.setEmail("docker-users@localhost.localdomain");
dockerUser.setFirstName("docker");
dockerUser.setLastName("user");
final CredentialRepresentation dockerUserCreds = new CredentialRepresentation();
dockerUserCreds.setType(CredentialRepresentation.PASSWORD);
dockerUserCreds.setValue(password);
dockerUser.setCredentials(Collections.singletonList(dockerUserCreds));
dockerRealm.setUsers(Collections.singletonList(dockerUser));
}
}

View file

@ -0,0 +1,99 @@
package org.keycloak.testsuite.docker;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
public class DockerVersion {
public static final Integer MAJOR_VERSION_INDEX = 0;
public static final Integer MINOR_VERSION_INDEX = 1;
public static final Integer PATCH_VERSION_INDEX = 2;
private final Integer major;
private final Integer minor;
private final Integer patch;
public static final Comparator<DockerVersion> COMPARATOR = (lhs, rhs) -> Comparator.comparing(DockerVersion::getMajor)
.thenComparing(Comparator.comparing(DockerVersion::getMinor)
.thenComparing(Comparator.comparing(DockerVersion::getPatch)))
.compare(lhs, rhs);
/**
* Major version is required. minor and patch versions will be assumed '0' if not provided.
*/
public DockerVersion(final Integer major, final Optional<Integer> minor, final Optional<Integer> patch) {
Objects.requireNonNull(major, "Invalid docker version - no major release number given");
this.major = major;
this.minor = minor.orElse(0);
this.patch = patch.orElse(0);
}
/**
* @param versionString given in the form '1.12.6'
*/
public static DockerVersion parseVersionString(final String versionString) {
Objects.requireNonNull(versionString, "Cannot parse null docker version string");
final List<Integer> versionNumberList = Arrays.stream(stripDashAndEdition(versionString).trim().split("\\."))
.map(Integer::parseInt)
.collect(Collectors.toList());
return new DockerVersion(versionNumberList.get(MAJOR_VERSION_INDEX),
Optional.ofNullable(versionNumberList.get(MINOR_VERSION_INDEX)),
Optional.ofNullable(versionNumberList.get(PATCH_VERSION_INDEX)));
}
private static String stripDashAndEdition(final String versionString) {
if (versionString.contains("-")) {
return versionString.substring(0, versionString.indexOf("-"));
}
return versionString;
}
public Integer getMajor() {
return major;
}
public Integer getMinor() {
return minor;
}
public Integer getPatch() {
return patch;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DockerVersion that = (DockerVersion) o;
if (major != null ? !major.equals(that.major) : that.major != null) return false;
if (minor != null ? !minor.equals(that.minor) : that.minor != null) return false;
return patch != null ? patch.equals(that.patch) : that.patch == null;
}
@Override
public int hashCode() {
int result = major != null ? major.hashCode() : 0;
result = 31 * result + (minor != null ? minor.hashCode() : 0);
result = 31 * result + (patch != null ? patch.hashCode() : 0);
return result;
}
@Override
public String toString() {
return "DockerVersion{" +
"major=" + major +
", minor=" + minor +
", patch=" + patch +
'}';
}
}

View file

@ -59,6 +59,7 @@ import org.keycloak.testsuite.runonserver.RunHelpers;
import org.keycloak.testsuite.runonserver.RunOnServerDeployment;
import org.keycloak.testsuite.util.OAuthClient;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT;
import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT_LINKS;
@ -216,6 +217,20 @@ public class MigrationTest extends AbstractKeycloakTest {
private void testMigrationTo3_2_0() {
assertNull(masterRealm.toRepresentation().getPasswordPolicy());
assertNull(migrationRealm.toRepresentation().getPasswordPolicy());
testDockerAuthenticationFlow(masterRealm, migrationRealm);
}
private void testDockerAuthenticationFlow(RealmResource... realms) {
for (RealmResource realm : realms) {
AuthenticationFlowRepresentation flow = null;
for (AuthenticationFlowRepresentation f : realm.flows().getFlows()) {
if (DefaultAuthenticationFlows.DOCKER_AUTH.equals(f.getAlias())) {
flow = f;
}
}
assertNotNull(flow);
}
}
private void testRoleManageAccountLinks(RealmResource... realms) {

View file

@ -50,7 +50,7 @@
<container qualifier="auth-server-undertow" mode="suite" >
<configuration>
<property name="enabled">${auth.server.undertow} &amp;&amp; ! ${auth.server.undertow.crossdc}</property>
<property name="bindAddress">localhost</property>
<property name="bindAddress">0.0.0.0</property>
<property name="adapterImplClass">org.keycloak.testsuite.arquillian.undertow.KeycloakOnUndertow</property>
<property name="bindHttpPort">${auth.server.http.port}</property>
<property name="remoteMode">${undertow.remote}</property>

View file

@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICsTCCAZkCBgFbaSTAdjANBgkqhkiG9w0BAQsFADAcMRowGAYDVQQDDBFkb2Nr
ZXItdGVzdC1yZWFsbTAeFw0xNzA0MTMyMTA2MDdaFw0yNzA0MTMyMTA3NDdaMBwx
GjAYBgNVBAMMEWRvY2tlci10ZXN0LXJlYWxtMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEAk2ZfvP3znNH5EbBd6ckiT7Eq7loqBCa5o6fdOajD2X8cjT7r
oLG4GANhu075SUrCxfcx2A+P1kBnSsyPCc3dxMmCT7BUJsYScCF88q52GIskQc7E
+eBkuIjeVmPMECLq3xhY7YONqIl47n17dEYYmVo1uRqbrVSFdSX9EDqn9vRn/7uJ
FLafdK9766Na2JMSZVKgnNsXRTtxxCjnU3LyMnNw5JdbnsfSPj1pgnOi+pTDPqlw
fcAIaG72lmhWMXaStmwO1DYsBoUd4yEnv6/dtXQkAaDr6TthX7ITliaxXPrh+YMD
AxnhV7X/PtbiFUpTaNBpSy3k87onYBiWrL44IQIDAQABMA0GCSqGSIb3DQEBCwUA
A4IBAQB2u9hP3S1bP4+FBwOLPwI3p7WrWBlt2CgwTiyuXvV7u9GLiXqCDUWZd3dS
ks9vU4Y4NdVyToY4q9YFJ3oAQXlfRw2Yi6e/0nSPpU25o52TWwREnRY98fjVy1eC
5K2GRwSu79HZKeqA0Tg/ONvGOrlYO1KPbWZGg9NcwAGeILkNdfI82w0KZTpTy+f5
ATtV30pFkDNT0gfayFmDQvw3EgcD/x0/vI3PlnHLLGprV/ZlBmFWo0vk8iUBwP1Y
bTA0XqKasITFXJaPeZWzNMCjR1NxDqlIq095uX04E5XGS6XGJKS9PanvGXidk5xM
gI7xwKE6jaxD9pspYPRgv66528Dc
-----END CERTIFICATE-----

View file

@ -0,0 +1,35 @@
-----BEGIN CERTIFICATE-----
MIIGBTCCA+2gAwIBAgIJALfo8UyCLlnkMA0GCSqGSIb3DQEBCwUAMIGYMQswCQYD
VQQGEwJVUzEXMBUGA1UECAwOTm9ydGggQ2Fyb2xpbmExEDAOBgNVBAcMB1JhbGVp
Z2gxFjAUBgNVBAoMDVJlZCBIYXQsIEluYy4xJzAlBgNVBAsMHklkZW50aXR5IGFu
ZCBBY2Nlc3MgTWFuYWdlbWVudDEdMBsGA1UEAwwUcmVnaXN0cnkubG9jYWxkb21h
aW4wHhcNMTcwNDIwMDMwNzMwWhcNMjAwMTE0MDMwNzMwWjCBmDELMAkGA1UEBhMC
VVMxFzAVBgNVBAgMDk5vcnRoIENhcm9saW5hMRAwDgYDVQQHDAdSYWxlaWdoMRYw
FAYDVQQKDA1SZWQgSGF0LCBJbmMuMScwJQYDVQQLDB5JZGVudGl0eSBhbmQgQWNj
ZXNzIE1hbmFnZW1lbnQxHTAbBgNVBAMMFHJlZ2lzdHJ5LmxvY2FsZG9tYWluMIIC
IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAyIKYO7gYA9T8PpqTf2Lad81X
cHzhiRYvvzUDgR4UD1NummWPnl2sPjdlQayM/TZ7p6gserdLjms336tvU/6GOIjv
v10uvDsFVxafuASY1tQSlrFLwF2NwavVOWlPhdlYLvOUnT/zk7fWKRFy7WXp6hD5
RAkI4+ywuhS6eiZy3wIv/04VjFGYAB1x3NfHVwSuo+cjz/UvI3sU1i0LR+aOSRoP
9GM8OBpaTxRu/vEHd3k0A2FLP3sJYzkSD6A0p+nqbMfrPKRuZEjDYvBad4KemAl2
5GRxNeZkJUk0CX2QK2cqr6xOa7598Nr+3ejv99Iiga5r2VlSSdsbV3U9j3RoZY48
J0RvSgsVeeYqE93SUsVKhSoN4UIdhiVoDCvLtuIeqfQjehowent03OwDUiYw0TeV
GqmcN54Ki6v+EWSNqY2h01wcbMuQw6PDQ/mn1pz7f/ZAt9T0fop6ml4Mg4nud9S9
b/Y9+XfuJlPKwZIgQEtrpSfLveOBmWYRu9/rSX9YtHx+pyzbWDtwrF0O9Z/pO+T4
qOMmfc2ltjzRMFKK6JZFhFVHQP0AKsxLChQrzoHr5k7Rmcn+iGtmqD4tWtzgEQvA
umhNsm4nrR92hB97yxw3WC9gGvJlBIi/swrCxiKCJDklxCZtVCmqwMFx/bzXu3pH
sKwYv3poURR9NZb7kDcCAwEAAaNQME4wHQYDVR0OBBYEFNhH71tQSivnjfCHd7pt
3Qo50DCZMB8GA1UdIwQYMBaAFNhH71tQSivnjfCHd7pt3Qo50DCZMAwGA1UdEwQF
MAMBAf8wDQYJKoZIhvcNAQELBQADggIBAGSCDF/l/ExabQ1DfoKoRCmVoslnK+M1
0TuDtfss2zqF89BPLBNBKdfp7r1OV4fp465HMpd2ovUkuijLjrIf78+I4AFEv60s
Z7NKMYEULpvBZ3RY7INr9CoNcWGvnfC/h782axjyI6ZW6I2v717FcciI6su0Eg+k
kF6+c+cVLmhKLi7hnC9mlN0JMUcOt3cBuZ8NvCHwW6VFmv8hsxt8Z18JcY6aPZE8
32XzdgcU/U9OAhv1iMEuoGAqQatCHAmA3FOpfI9LjVOxW0LZgHWKX7OEyDEZ+7Ed
DbEpD73bmTp89lvFcT0UEAcWkRpD+VSozgYEzSeNmzKks2ngl37SlG2YQ23UzgYS
alGcUEJFBmWr9pJUN+tDPzbtmlrEw9pA6xYZMTDgAQSRHGQK/5lISuzEIMR0nh3q
Hyhmamlg+zkF415gYKUwh96NgalIc+Y9B4vnSpOv7b+ZFXoubBD2Wk5oi0Ziyog0
J8YcbLQ8ZhINRvDyNv0iWHNachIzO1/N5G5H8hjibLkH+tpFBSs3uCiwTi+L/MlD
Pqc0A6Slyi8TnJJDFCDaa3xU321dkvyhGmPeqiyIK+dpJO1FI3OU0rZeGGcyc+K6
SnDRByp0HQt9W/8Aw+kXjUoI8LOYeR/7Ctd+Tqf11TDxmw9w9LSIEhiYeEJQCxTc
Dk72PkeTi1zO
-----END CERTIFICATE-----

View file

@ -0,0 +1,51 @@
-----BEGIN RSA PRIVATE KEY-----
MIIJKQIBAAKCAgEAyIKYO7gYA9T8PpqTf2Lad81XcHzhiRYvvzUDgR4UD1NummWP
nl2sPjdlQayM/TZ7p6gserdLjms336tvU/6GOIjvv10uvDsFVxafuASY1tQSlrFL
wF2NwavVOWlPhdlYLvOUnT/zk7fWKRFy7WXp6hD5RAkI4+ywuhS6eiZy3wIv/04V
jFGYAB1x3NfHVwSuo+cjz/UvI3sU1i0LR+aOSRoP9GM8OBpaTxRu/vEHd3k0A2FL
P3sJYzkSD6A0p+nqbMfrPKRuZEjDYvBad4KemAl25GRxNeZkJUk0CX2QK2cqr6xO
a7598Nr+3ejv99Iiga5r2VlSSdsbV3U9j3RoZY48J0RvSgsVeeYqE93SUsVKhSoN
4UIdhiVoDCvLtuIeqfQjehowent03OwDUiYw0TeVGqmcN54Ki6v+EWSNqY2h01wc
bMuQw6PDQ/mn1pz7f/ZAt9T0fop6ml4Mg4nud9S9b/Y9+XfuJlPKwZIgQEtrpSfL
veOBmWYRu9/rSX9YtHx+pyzbWDtwrF0O9Z/pO+T4qOMmfc2ltjzRMFKK6JZFhFVH
QP0AKsxLChQrzoHr5k7Rmcn+iGtmqD4tWtzgEQvAumhNsm4nrR92hB97yxw3WC9g
GvJlBIi/swrCxiKCJDklxCZtVCmqwMFx/bzXu3pHsKwYv3poURR9NZb7kDcCAwEA
AQKCAgEAsPuM0dGZ6O/7QmsAXEVuHqbyUkj4bh9WP8jUcgiRnkF/c+rHTPrTyQru
Znye6fZISWFI+XyGxYvgAp54osQbxxUfwWLHmL/j484FZtEv8xe33Klb+szZDiTV
DVrmJXgFvVOlTvOe1TlEYHWVYvQ89yzKSIJNBZnrGCSpwJ3lcPCmWwyaOoPezeMv
mMYhnq50VBn2Y13AoOnIJ5AUz/8yglXt1UIuajrgkcKwgnlPpOYnwgAEAmFglONQ
DNjVAY2YLTJ9ccaV5hDP3anXwHtb70kTV19NCk11AfBObT4Wniju5acKhVHcKley
9T7haXZinOLPMUcFOkmbJaRHlTMj3UgnF4k2iJJ7NyY3lAAIedlZ3EFNwpa68Roo
WClNAJIV6KYRExOZfqeRyR09loTnynPgxkMR4N4oLJHCiTtReXW5Y1HAYbT+iVHC
Ox1ob/INuZ1VoumDfn6bRqFdK8LldjBwVqRecSad/dg84BtjTB/po81aUpSRENEV
aZP+jOT9kZbybACh8FdF8u7mxgL+x7Xidng3SKRJi5whQJNmQ62QkzTFMPVXCqlO
ABsz2a/Zw7swyetg9uApoTTCeK1P0V/MrcEVTIGmcABfBYAVMBj1S2SH1xgAr20P
IR3SOpPtiNYhIIOnfyQQ3qVudsaSOAJH26I7QLnMyBqOId0Js9ECggEBAOSrGSfT
bm7OhGu1ZcTmlS17kjsUUYn1Uy30vV5e7uhpQGmr4rKVWYkNeZa5qtJossY3z+4H
9fZAqJWH2Cr/4pqnfz4GqK+qE56fFdbyHzHKLZOXZGdp9fQzlLsEi9JVYgv+nAPR
MHS7WeMTUlFc+P3pP6Btyhk/x7YfZnnlatFYlsNJVzUVdblrG6wSVZGpmxcNIeM2
UeGG78aDBZQdKUO+xuh6MFW20lU165QC1JfGE+NRawqvgSD09F3MGkEwJuD8XEBg
/rOwNUg8/ayQhd1EgRGQOiDgqfXSpsF101HPUSX/HDC41KG3gTKTc/Vw+ac5ID1r
b3PKExEXCicDgCkCggEBAOB55eVsRZHBHeBjhqemH8SxWUfSCbx17cGbs7sw95Rs
3wYci7ABC8wbvG5UDNPd3BI2IV5bJWYOlbVv+Y1FjNHamQjiSXgB3g6RzvaM0bVP
1Rvn7EvQF87XIKEdo3uHtvpSVBDHYq/DtDyE9wwaNctxBgJwThVXVYINsp+leGsD
uGVMAsUP01vMNdHJBk/ANPvYxUkDOCtlDDV8cyaFVJAq4/A1h4crv39S/6ZY/RWo
LQpYnA47pfKZzxvtDQsnVTmolQ8x4yAX5bQrpKAt/hIJhzKdeCglgVr9cq/7sNOO
kDLZzPLlFPRX1gOHTpDlucNxxlIjPh2h+3CCCPUzGV8CggEAYGmDgbczqKSKUJ96
+Tn/S93+GcrHVlOJbqbx8Qg10ugNsIA4ZPNzfMWhrls6GtzqA4kkskfI/LrmWaWd
DwQ0luBoVc6Y8PfUrdyFaMtNO8Dy1nfObYvPl9bnrrKMAXLelBAV18YrmAwmKgfL
fWKl2OivWwTvYRXzLmau3lZMY1fmuRADJO6XZEY0tKhGS9Qm/+EZmKMeguhR0HEN
uRVSgK2/T+W0227p3+OMICvRVuy9FesOJsM4vpyJK8MSjsmums3MV5iNy1VQIdUV
X9zPlCt9/9m/qH0RLARVKtxy7Ntsa4jUafaEMGseniRtj97CZC9B2KOjqj5ZK6t7
LFfdgQKCAQEAtu6gC3dQupdGYba55aXb/c8Jkx34ET2JpF3e+o3NNYgDuFdK/wPb
OVrhFIgqa/5BehXi26IruB/qoRG/rQEg4WPjkvnWJZZgAD+TChl4TOniIfu+9Yl/
3XAzhxlAQUs4MoclOwdBxTsXhrpVGefCLyjMXPBosbuaU4IWL0QJ/ivp+aMYHr/m
3shsk6nfGt7oTtU48WdOPw76BByHOr0tTM+nMfptmBpu1LQu4sFifmOvUN8lTfQO
KMZvobJtDsnfCj34O4nMLjtLVqi6YE8a3lgldXoekZj+8cfZztCuKbnkiYw1GTzW
9skd/4Ik5LBR0pTFqepOlJeM8QMHics6wQKCAQA+6RvPk2/b8OJArrFHkhNbfqpf
Sa/BvRam8azo2MGgOZWVm/yAGHvoVgOaq2H1DrrDh6qBlzZULpwFD+XeuuzYrLs2
mYr2LFZdeQtd95V7oASdM0OlFatzKPOoLrHwNc4ztwNz0sMrjTYxDG07mp/3Ixz7
koUPinV636wZUmvwHiUTlD4E2db+fslDhBUc+HV/4MXihvMSA3D8Mum9SttMABYJ
L0lBzexfVL8oyYvft/tGwV9LwrlFpzndnX6ZZvgJUqzBPx/+exuZjnTwD3N70SN+
T0TwL0tsVE5clxVdv5xlm5WIW4kQKglRoJnVB1TnpFddRRu/QD8S+e/S6G4w
-----END RSA PRIVATE KEY-----

View file

@ -0,0 +1,15 @@
registry:
image: registry:2
ports:
- 127.0.0.1:5000:5000
environment:
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
REGISTRY_HTTP_TLS_CERTIFICATE: /opt/certs/localhost.crt
REGISTRY_HTTP_TLS_KEY: /opt/certs/localhost.key
REGISTRY_AUTH_TOKEN_REALM: http://localhost:8080/auth/realms/docker-test-realm/protocol/docker-v2/auth
REGISTRY_AUTH_TOKEN_SERVICE: docker-test-client
REGISTRY_AUTH_TOKEN_ISSUER: http://localhost:8080/auth/realms/docker-test-realm
REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: /opt/certs/localhost_trust_chain.pem
volumes:
- ./data:/data:z
- ./certs:/opt/certs:z

View file

@ -0,0 +1,45 @@
# /etc/sysconfig/docker
# Modify these options if you want to change the way the docker daemon runs
OPTIONS='--selinux-enabled --log-driver=journald --signature-verification=false'
if [ -z "${DOCKER_CERT_PATH}" ]; then
DOCKER_CERT_PATH=/etc/docker
fi
# If you want to add your own registry to be used for docker search and docker
# pull use the ADD_REGISTRY option to list a set of registries, each prepended
# with --add-registry flag. The first registry added will be the first registry
# searched.
# ADD_REGISTRY='--add-registry registry.access.redhat.com'
# If you want to block registries from being used, uncomment the BLOCK_REGISTRY
# option and give it a set of registries, each prepended with --block-registry
# flag. For example adding docker.io will stop users from downloading images
# from docker.io
# BLOCK_REGISTRY='--block-registry'
# If you have a registry secured with https but do not have proper certs
# distributed, you can tell docker to not look for full authorization by
# adding the registry to the INSECURE_REGISTRY line and uncommenting it.
INSECURE_REGISTRY='--insecure-registry registry.localdomain:5000'
# On an SELinux system, if you remove the --selinux-enabled option, you
# also need to turn on the docker_transition_unconfined boolean.
# setsebool -P docker_transition_unconfined 1
# Location used for temporary files, such as those created by
# docker load and build operations. Default is /var/lib/docker/tmp
# Can be overriden by setting the following environment variable.
# DOCKER_TMPDIR=/var/tmp
# Controls the /etc/cron.daily/docker-logrotate cron job status.
# To disable, uncomment the line below.
# LOGROTATE=false
#
# docker-latest daemon can be used by starting the docker-latest unitfile.
# To use docker-latest client, uncomment below lines
#DOCKERBINARY=/usr/bin/docker-latest
#DOCKERDBINARY=/usr/bin/dockerd-latest
#DOCKER_CONTAINERD_BINARY=/usr/bin/docker-containerd-latest
#DOCKER_CONTAINERD_SHIM_BINARY=/usr/bin/docker-containerd-shim-latest

View file

@ -0,0 +1,6 @@
auth:
token:
realm: http://localhost:8080/auth/auth/realms/docker-test-realm/protocol/docker-v2/auth
service: docker-test-client
issuer: http://localhost:8080/auth/auth/realms/docker-test-realm

View file

@ -0,0 +1,4 @@
-e REGISTRY_AUTH_TOKEN_REALM=http://localhost:8080/auth/auth/realms/docker-test-realm/protocol/docker-v2/auth \
-e REGISTRY_AUTH_TOKEN_SERVICE: docker-test-client \
-e REGISTRY_AUTH_TOKEN_ISSUER: http://localhost:8080/auth/auth/realms/docker-test-realm \

View file

@ -833,6 +833,8 @@ reset-credentials=Reset Credentials
reset-credentials.tooltip=Select the flow you want to use when the user has forgotten their credentials.
client-authentication=Client Authentication
client-authentication.tooltip=Select the flow you want to use for authentication of clients.
docker-auth=Docker Authentication
docker-auth.tooptip=Select the flow you want to use for authenticatoin against a docker client.
new=New
copy=Copy
add-execution=Add execution

View file

@ -1709,8 +1709,8 @@ module.config([ '$routeProvider', function($routeProvider) {
flows : function(AuthenticationFlowsLoader) {
return AuthenticationFlowsLoader();
},
serverInfo : function(ServerInfo) {
return ServerInfo.delay;
serverInfo : function(ServerInfoLoader) {
return ServerInfoLoader();
}
},
controller : 'RealmFlowBindingCtrl'

View file

@ -814,7 +814,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
"bearer-only"
];
$scope.protocols = Object.keys(serverInfo.providers['login-protocol'].providers).sort();
$scope.protocols = serverInfo.listProviderIds('login-protocol');
$scope.templates = [ {name:'NONE'}];
for (var i = 0; i < templates.length; i++) {
@ -1240,7 +1240,7 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
});
module.controller('CreateClientCtrl', function($scope, realm, client, templates, $route, serverInfo, Client, ClientDescriptionConverter, $location, $modal, Dialog, Notifications) {
$scope.protocols = Object.keys(serverInfo.providers['login-protocol'].providers).sort();
$scope.protocols = serverInfo.listProviderIds('login-protocol');
$scope.create = true;
$scope.templates = [ {name:'NONE'}];
var templateNameMap = new Object();
@ -1915,7 +1915,7 @@ module.controller('ClientTemplateListCtrl', function($scope, realm, templates, C
});
module.controller('ClientTemplateDetailCtrl', function($scope, realm, template, $route, serverInfo, ClientTemplate, $location, $modal, Dialog, Notifications) {
$scope.protocols = Object.keys(serverInfo.providers['login-protocol'].providers).sort();
$scope.protocols = serverInfo.listProviderIds('login-protocol');
$scope.realm = realm;
$scope.create = !template.name;

View file

@ -1935,6 +1935,8 @@ module.controller('RealmFlowBindingCtrl', function($scope, flows, Current, Realm
}
}
$scope.profileInfo = serverInfo.profileInfo;
genericRealmUpdate($scope, Current, Realm, realm, serverInfo, $http, $location, Dialog, Notifications, "/realms/" + realm.realm + "/authentication/flow-bindings");
});
@ -2129,6 +2131,9 @@ module.controller('AuthenticationFlowsCtrl', function($scope, $route, realm, flo
} else if (realm.clientAuthenticationFlow == $scope.flow.alias) {
Notifications.error("Cannot remove flow, it is currently being used as the client authentication flow.");
} else if (realm.dockerAuthenticationFlow == $scope.flow.alias) {
Notifications.error("Cannot remove flow, it is currently being used as the docker authentication flow.");
} else {
AuthenticationFlows.remove({realm: realm.realm, flow: $scope.flow.id}, function () {
$location.url("/realms/" + realm.realm + '/authentication/flows/' + flows[0].alias);

View file

@ -335,8 +335,38 @@ module.service('ServerInfo', function($resource, $q, $http) {
var info = {};
var delay = $q.defer();
$http.get(authUrl + '/admin/serverinfo').success(function(data) {
function copyInfo(data, info) {
angular.copy(data, info);
info.listProviderIds = function(spi) {
var providers = info.providers[spi].providers;
var ids = Object.keys(providers);
ids.sort(function(a, b) {
var s1;
var s2;
if (providers[a].order != providers[b].order) {
s1 = providers[b].order;
s2 = providers[a].order;
} else {
s1 = a;
s2 = b;
}
if (s1 < s2) {
return -1;
} else if (s1 > s2) {
return 1;
} else {
return 0;
}
});
return ids;
}
}
$http.get(authUrl + '/admin/serverinfo').success(function(data) {
copyInfo(data, info);
delay.resolve(info);
});
@ -346,7 +376,7 @@ module.service('ServerInfo', function($resource, $q, $http) {
},
reload: function() {
$http.get(authUrl + '/admin/serverinfo').success(function(data) {
angular.copy(data, info);
copyInfo(data, info);
});
},
promise: delay.promise

View file

@ -47,7 +47,7 @@
</div>
<div class="form-group">
<label for="resetCredentials" class="col-md-2 control-label">{{:: 'client-authentication' | translate}}</label>
<label for="clientAuthentication" class="col-md-2 control-label">{{:: 'client-authentication' | translate}}</label>
<div class="col-md-2">
<div>
<select id="clientAuthentication" ng-model="realm.clientAuthenticationFlow" class="form-control" ng-options="flow.alias as flow.alias for flow in clientFlows">
@ -57,6 +57,18 @@
<kc-tooltip>{{:: 'client-authentication.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group" data-ng-show="profileInfo.disabledFeatures.indexOf('DOCKER') == -1">
<label for="dockerAuth" class="col-md-2 control-label">{{:: 'docker-auth' | translate}}</label>
<div class="col-md-2">
<div>
<select id="dockerAuth" ng-model="realm.dockerAuthenticationFlow" class="form-control" ng-options="flow.alias as flow.alias for flow in flows">
</select>
</div>
</div>
<kc-tooltip>{{:: 'docker-auth.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group" data-ng-show="access.manageRealm">
<div class="col-md-10 col-md-offset-2">
<button kc-save data-ng-disabled="!changed">{{:: 'save' | translate}}</button>

View file

@ -37,7 +37,7 @@
</div>
<kc-tooltip>{{:: 'client.enabled.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block">
<div class="form-group clearfix block" data-ng-show="protocol != 'docker-v2'">
<label class="col-md-2 control-label" for="consentRequired">{{:: 'consent-required' | translate}}</label>
<div class="col-sm-6">
<input ng-model="clientEdit.consentRequired" name="consentRequired" id="consentRequired" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
@ -239,7 +239,7 @@
<kc-tooltip>{{:: 'name-id-format.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group" data-ng-show="!clientEdit.bearerOnly">
<div class="form-group" data-ng-show="!clientEdit.bearerOnly && protocol != 'docker-v2'">
<label class="col-md-2 control-label" for="rootUrl">{{:: 'root-url' | translate}}</label>
<div class="col-sm-6">
<input class="form-control" type="text" name="rootUrl" id="rootUrl" data-ng-model="clientEdit.rootUrl">
@ -247,7 +247,7 @@
<kc-tooltip>{{:: 'root-url.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group clearfix block" data-ng-hide="clientEdit.bearerOnly || (!clientEdit.standardFlowEnabled && !clientEdit.implicitFlowEnabled)">
<div class="form-group clearfix block" data-ng-hide="clientEdit.bearerOnly || (!clientEdit.standardFlowEnabled && !clientEdit.implicitFlowEnabled) || protocol == 'docker-v2'">
<label class="col-md-2 control-label" for="newRedirectUri"><span class="required" data-ng-show="protocol != 'saml'">*</span> {{:: 'valid-redirect-uris' | translate}}</label>
<div class="col-sm-6">
@ -269,14 +269,14 @@
<kc-tooltip>{{:: 'valid-redirect-uris.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group" data-ng-show="!clientEdit.bearerOnly">
<div class="form-group" data-ng-show="!clientEdit.bearerOnly && protocol != 'docker-v2'">
<label class="col-md-2 control-label" for="baseUrl">{{:: 'base-url' | translate}}</label>
<div class="col-sm-6">
<input class="form-control" type="text" name="baseUrl" id="baseUrl" data-ng-model="clientEdit.baseUrl">
</div>
<kc-tooltip>{{:: 'base-url.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group" data-ng-hide="protocol == 'saml'">
<div class="form-group" data-ng-hide="protocol == 'saml' || protocol == 'docker-v2'">
<label class="col-md-2 control-label" for="adminUrl">{{:: 'admin-url' | translate}}</label>
<div class="col-sm-6">
<input class="form-control" type="text" name="adminUrl" id="adminUrl"

View file

@ -12,7 +12,7 @@
<a href="#/realms/{{realm.realm}}/client-templates/{{template.id}}/mappers">{{:: 'mappers' | translate}}</a>
<kc-tooltip>{{:: 'mappers.tooltip' | translate}}</kc-tooltip>
</li>
<li ng-class="{active: path[4] == 'scope-mappings'}" >
<li ng-class="{active: path[4] == 'scope-mappings'}" data-ng-show="client.protocol != 'docker-v2'">
<a href="#/realms/{{realm.realm}}/client-templates/{{template.id}}/scope-mappings">{{:: 'scope' | translate}}</a>
<kc-tooltip>{{:: 'scope.tooltip' | translate}}</kc-tooltip>
</li>

View file

@ -8,7 +8,10 @@
<ul class="nav nav-tabs" data-ng-hide="create && !path[4]">
<li ng-class="{active: !path[4]}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}">{{:: 'settings' | translate}}</a></li>
<li ng-class="{active: path[4] == 'credentials'}" data-ng-show="!disableCredentialsTab && !client.publicClient && client.protocol != 'saml'"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials">{{:: 'credentials' | translate}}</a></li>
<li ng-class="{active: path[4] == 'credentials'}"
data-ng-show="!client.publicClient && client.protocol == 'openid-connect'">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/credentials">{{:: 'credentials' | translate}}</a>
</li>
<li ng-class="{active: path[4] == 'saml'}" data-ng-show="client.protocol == 'saml' && (client.attributes['saml.client.signature'] == 'true' || client.attributes['saml.encrypt'] == 'true')"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/saml/keys">{{:: 'saml-keys' | translate}}</a></li>
<li ng-class="{active: path[4] == 'roles'}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/roles">{{:: 'roles' | translate}}</a></li>
<li ng-class="{active: path[4] == 'mappers'}" data-ng-show="!client.bearerOnly">
@ -19,8 +22,13 @@
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/scope-mappings">{{:: 'scope' | translate}}</a>
<kc-tooltip>{{:: 'scope.tooltip' | translate}}</kc-tooltip>
</li>
<li ng-class="{active: path[4] == 'authz'}" data-ng-show="serverInfo.profileInfo.disabledFeatures.indexOf('AUTHORIZATION') == -1 && !disableAuthorizationTab && client.authorizationServicesEnabled"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">{{:: 'authz-authorization' | translate}}</a></li>
<li ng-class="{active: path[4] == 'revocation'}"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/revocation">{{:: 'revocation' | translate}}</a></li>
<li ng-class="{active: path[4] == 'authz'}"
data-ng-show="serverInfo.profileInfo.previewEnabled && !disableAuthorizationTab && client.authorizationServicesEnabled">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/authz/resource-server">{{:: 'authz-authorization' |
translate}}</a></li>
<li ng-class="{active: path[4] == 'revocation'}" data-ng-show="client.protocol != 'docker-v2'"><a
href="#/realms/{{realm.realm}}/clients/{{client.id}}/revocation">{{:: 'revocation' | translate}}</a>
</li>
<!-- <li ng-class="{active: path[4] == 'identity-provider'}" data-ng-show="realm.identityFederationEnabled"><a href="#/realms/{{realm.realm}}/clients/{{client.id}}/identity-provider">Identity Provider</a></li> -->
<li ng-class="{active: path[4] == 'sessions'}" data-ng-show="!client.bearerOnly">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/sessions">{{:: 'sessions' | translate}}</a>
@ -39,7 +47,7 @@
<kc-tooltip>{{:: 'installation.tooltip' | translate}}</kc-tooltip>
</li>
<li ng-class="{active: path[4] == 'service-account-roles'}" data-ng-show="!disableServiceAccountRolesTab && client.serviceAccountsEnabled && !(client.bearerOnly || client.publicClient)">
<li ng-class="{active: path[4] == 'service-account-roles'}" data-ng-show="client.serviceAccountsEnabled && !(client.bearerOnly || client.publicClient)">
<a href="#/realms/{{realm.realm}}/clients/{{client.id}}/service-account-roles">{{:: 'service-account-roles' | translate}}</a>
<kc-tooltip>{{:: 'service-account-roles.tooltip' | translate}}</kc-tooltip>
</li>
@ -48,4 +56,4 @@
<kc-tooltip>{{:: 'manage-permissions-client.tooltip' | translate}}</kc-tooltip>
</li>
</ul>
</div>
</div>