Merge pull request #4269 from stianst/dockerdockerdocker
KEYCLOAK-3592 Docker auth implementation
This commit is contained in:
commit
c9bc321d2a
80 changed files with 4619 additions and 210 deletions
|
@ -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;
|
||||
|
||||
|
|
|
@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -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 + '\'' +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -53,4 +53,8 @@ public interface ProviderFactory<T extends Provider> {
|
|||
|
||||
public String getId();
|
||||
|
||||
default int order() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
233
services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java
Executable file → Normal file
233
services/src/main/java/org/keycloak/protocol/saml/profile/ecp/authenticator/HttpBasicAuthenticator.java
Executable file → Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
|
23
services/src/main/resources/DockerComposeYamlReadme.md
Normal file
23
services/src/main/resources/DockerComposeYamlReadme.md
Normal 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.
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -16,4 +16,5 @@
|
|||
#
|
||||
|
||||
org.keycloak.protocol.oidc.OIDCLoginProtocolFactory
|
||||
org.keycloak.protocol.saml.SamlProtocolFactory
|
||||
org.keycloak.protocol.saml.SamlProtocolFactory
|
||||
org.keycloak.protocol.docker.DockerAuthV2ProtocolFactory
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
15
services/src/test/resources/docker-compose-expected.yaml
Normal file
15
services/src/test/resources/docker-compose-expected.yaml
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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])$");
|
||||
|
||||
}
|
|
@ -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)));
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
<container qualifier="auth-server-undertow" mode="suite" >
|
||||
<configuration>
|
||||
<property name="enabled">${auth.server.undertow} && ! ${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>
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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-----
|
|
@ -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-----
|
|
@ -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-----
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
@ -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 \
|
||||
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue