Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IDP-initiated sso REST handler #51830

Merged
merged 13 commits into from
Feb 18, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,43 @@

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.cluster.metadata.IndexNameExpressionResolver;
import org.elasticsearch.cluster.node.DiscoveryNodes;
import org.elasticsearch.cluster.service.ClusterService;
import org.elasticsearch.common.io.stream.NamedWriteableRegistry;
import org.elasticsearch.common.settings.ClusterSettings;
import org.elasticsearch.common.settings.IndexScopedSettings;
import org.elasticsearch.common.settings.Setting;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.settings.SettingsFilter;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.env.Environment;
import org.elasticsearch.env.NodeEnvironment;
import org.elasticsearch.plugins.ActionPlugin;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestHandler;
import org.elasticsearch.script.ScriptService;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.watcher.ResourceWatcherService;
import org.elasticsearch.xpack.core.ssl.X509KeyPairSettings;
import org.elasticsearch.xpack.idp.action.SamlInitiateSingleSignOnAction;
import org.elasticsearch.xpack.idp.action.TransportSamlInitiateSingleSignOnAction;
import org.elasticsearch.xpack.idp.rest.action.RestSamlInitiateSingleSignOnAction;
import org.elasticsearch.xpack.idp.saml.idp.CloudIdp;
import org.elasticsearch.xpack.idp.saml.support.SamlInit;
import org.elasticsearch.xpack.idp.saml.support.SamlUtils;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;

/**
* This plugin provides the backend for an IdP built on top of Elasticsearch security features.
Expand Down Expand Up @@ -73,23 +87,47 @@ public class IdentityProviderPlugin extends Plugin implements ActionPlugin {

private final Logger logger = LogManager.getLogger();
private boolean enabled;
private Settings settings;

@Override
public Collection<Object> createComponents(Client client, ClusterService clusterService, ThreadPool threadPool,
ResourceWatcherService resourceWatcherService, ScriptService scriptService,
NamedXContentRegistry xContentRegistry, Environment environment,
NodeEnvironment nodeEnvironment, NamedWriteableRegistry namedWriteableRegistry) {
final Settings settings = environment.settings();
settings = environment.settings();
enabled = ENABLED_SETTING.get(settings);
if (enabled == false) {
return List.of();
}

SamlInit.initialize();
SamlUtils.initialize();
CloudIdp idp = new CloudIdp(environment, settings);
return List.of();
}

@Override
public List<ActionHandler<? extends ActionRequest, ? extends ActionResponse>> getActions() {

enabled = ENABLED_SETTING.get(settings);
jkakavas marked this conversation as resolved.
Show resolved Hide resolved
if (enabled == false) {
return Collections.emptyList();
}
return Collections.singletonList(
new ActionHandler<>(SamlInitiateSingleSignOnAction.INSTANCE, TransportSamlInitiateSingleSignOnAction.class)
);
}

@Override
public List<RestHandler> getRestHandlers(Settings settings, RestController restController, ClusterSettings clusterSettings,
IndexScopedSettings indexScopedSettings, SettingsFilter settingsFilter,
IndexNameExpressionResolver indexNameExpressionResolver,
Supplier<DiscoveryNodes> nodesInCluster) {
if (enabled == false) {
return Collections.emptyList();
}
return Collections.singletonList(new RestSamlInitiateSingleSignOnAction(restController));
}

@Override
public List<Setting<?>> getSettings() {
List<Setting<?>> settings = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.idp.action;

import org.elasticsearch.action.ActionType;

/**
* ActionType to create a SAML Response in the context of IDP initiated SSO for a given SP
*/
public class SamlInitiateSingleSignOnAction extends ActionType<SamlInitiateSingleSignOnResponse> {

public static final String NAME = "cluster:admin/xpack/idp/saml/init";
jkakavas marked this conversation as resolved.
Show resolved Hide resolved
public static final SamlInitiateSingleSignOnAction INSTANCE = new SamlInitiateSingleSignOnAction();

private SamlInitiateSingleSignOnAction() {
super(NAME, SamlInitiateSingleSignOnResponse::new);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.idp.action;


import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;

import static org.elasticsearch.action.ValidateActions.addValidationError;

import java.io.IOException;

public class SamlInitiateSingleSignOnRequest extends ActionRequest {

private String spEntityId;

public SamlInitiateSingleSignOnRequest(StreamInput in) throws IOException {
super(in);
spEntityId = in.readString();
}

public SamlInitiateSingleSignOnRequest() {
}

@Override
public ActionRequestValidationException validate() {
ActionRequestValidationException validationException = null;
if (Strings.isNullOrEmpty(spEntityId)) {
validationException = addValidationError("sp_entity_id is missing", null);
jkakavas marked this conversation as resolved.
Show resolved Hide resolved
}
return validationException;
}

public String getSpEntityId() {
return spEntityId;
}

public void setSpEntityId(String spEntityId) {
this.spEntityId = spEntityId;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(spEntityId);
}

@Override
public String toString() {
return getClass().getSimpleName() + "{spEntityId='" + spEntityId + "'}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.idp.action;

import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;

import java.io.IOException;

public class SamlInitiateSingleSignOnResponse extends ActionResponse {

private String redirectUrl;
private String responseBody;
private String spEntityId;

public SamlInitiateSingleSignOnResponse(StreamInput in) throws IOException {
super(in);
this.redirectUrl = in.readString();
this.responseBody = in.readString();
this.spEntityId = in.readString();
}

public SamlInitiateSingleSignOnResponse(String redirectUrl, String responseBody, String spEntityId) {
this.redirectUrl = redirectUrl;
this.responseBody = responseBody;
this.spEntityId = spEntityId;
}

public String getRedirectUrl() {
return redirectUrl;
}

public String getResponseBody() {
return responseBody;
}

public String getSpEntityId() {
return spEntityId;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
out.writeString(redirectUrl);
out.writeString(responseBody);
out.writeString(spEntityId);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.idp.action;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.HandledTransportAction;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.env.Environment;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.core.security.user.User;
import org.elasticsearch.xpack.idp.saml.authn.SuccessfulAuthenticationResponseMessageBuilder;
import org.elasticsearch.xpack.idp.saml.authn.UserServiceAuthentication;
import org.elasticsearch.xpack.idp.saml.idp.CloudIdp;
import org.elasticsearch.xpack.idp.saml.idp.SamlIdentityProvider;
import org.elasticsearch.xpack.idp.saml.sp.SamlServiceProvider;
import org.elasticsearch.xpack.idp.saml.support.SamlFactory;
import org.elasticsearch.xpack.idp.saml.support.SamlUtils;
import org.opensaml.saml.saml2.core.Response;

import java.time.Clock;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

public class TransportSamlInitiateSingleSignOnAction
extends HandledTransportAction<SamlInitiateSingleSignOnRequest, SamlInitiateSingleSignOnResponse> {
jkakavas marked this conversation as resolved.
Show resolved Hide resolved

private final ThreadPool threadPool;
private final Environment env;
private final Logger logger = LogManager.getLogger(TransportSamlInitiateSingleSignOnAction.class);

@Inject
public TransportSamlInitiateSingleSignOnAction(ThreadPool threadPool, TransportService transportService,
ActionFilters actionFilters, Environment environment) {
super(SamlInitiateSingleSignOnAction.NAME, transportService, actionFilters, SamlInitiateSingleSignOnRequest::new);
this.threadPool = threadPool;
this.env = environment;
}

@Override
protected void doExecute(Task task, SamlInitiateSingleSignOnRequest request,
ActionListener<SamlInitiateSingleSignOnResponse> listener) {
final ThreadContext threadContext = threadPool.getThreadContext();
final SamlFactory samlFactory = new SamlFactory();
final SamlIdentityProvider idp = new CloudIdp(env, env.settings());
Authentication serviceAccountAuthentication = Authentication.getAuthentication(threadContext);
// TODO: Adjust this once secondary auth code is merged in master and use the authentication object of the user
// Authentication authentication = authenticationService.getSecondaryAuth();

SamlServiceProvider sp = idp.getRegisteredServiceProviders().get(request.getSpEntityId());
jkakavas marked this conversation as resolved.
Show resolved Hide resolved
if (null == sp) {
final String message =
"Service Provider with Entity ID [" + request.getSpEntityId() + "] is not registered with this Identity Provider";
logger.debug(message);
listener.onFailure(new IllegalArgumentException(message));
return;
}
final UserServiceAuthentication user = buildUserFromAuthentication(serviceAccountAuthentication, sp);
final SuccessfulAuthenticationResponseMessageBuilder builder = new SuccessfulAuthenticationResponseMessageBuilder(samlFactory,
Clock.systemUTC(), idp);
final Response response = builder.build(user, null);
listener.onResponse(new SamlInitiateSingleSignOnResponse(user.getServiceProvider().getAssertionConsumerService().toString(),
SamlUtils.samlObjectToString(response),
user.getServiceProvider().getEntityId()));
}

private UserServiceAuthentication buildUserFromAuthentication(Authentication authentication, SamlServiceProvider sp) {
final User authenticatedUser = authentication.getUser();
//TBD Where we will be sourcing the information from, use roles for easier testing now
final Set<String> groups = new HashSet<>(Arrays.asList(authenticatedUser.roles()));
return new UserServiceAuthentication(authenticatedUser.principal(), groups, sp);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.xpack.idp.rest.action;

import org.elasticsearch.client.node.NodeClient;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.xcontent.ObjectParser;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.rest.BaseRestHandler;
import org.elasticsearch.rest.BytesRestResponse;
import org.elasticsearch.rest.RestController;
import org.elasticsearch.rest.RestRequest;

import org.elasticsearch.rest.RestResponse;
import org.elasticsearch.rest.RestStatus;
import org.elasticsearch.rest.action.RestBuilderListener;
import org.elasticsearch.xpack.idp.action.SamlInitiateSingleSignOnAction;
import org.elasticsearch.xpack.idp.action.SamlInitiateSingleSignOnRequest;
import org.elasticsearch.xpack.idp.action.SamlInitiateSingleSignOnResponse;

import java.io.IOException;

import static org.elasticsearch.rest.RestRequest.Method.POST;

public class RestSamlInitiateSingleSignOnAction extends BaseRestHandler {
static final ObjectParser<SamlInitiateSingleSignOnRequest, Void> PARSER = new ObjectParser<>("idp_init_sso",
SamlInitiateSingleSignOnRequest::new);

static {
PARSER.declareString(SamlInitiateSingleSignOnRequest::setSpEntityId, new ParseField("sp_entity_id"));
}

public RestSamlInitiateSingleSignOnAction(RestController controller) {
controller.registerHandler(
POST, "/_idp/saml/init", this
);
}

@Override
public String getName() {
return "idp_init_sso_action";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's minor, but I think this name should reference SAML since it's a SAML specific endpoint.

}

@Override
protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException {
try (XContentParser parser = request.contentParser()) {
final SamlInitiateSingleSignOnRequest initRequest = PARSER.parse(parser, null);
return channel -> client.execute(SamlInitiateSingleSignOnAction.INSTANCE, initRequest,
new RestBuilderListener<SamlInitiateSingleSignOnResponse>(channel) {
@Override
public RestResponse buildResponse(SamlInitiateSingleSignOnResponse response, XContentBuilder builder) throws Exception {
builder.startObject();
builder.field("redirect_url", response.getRedirectUrl());
builder.field("response_body", response.getResponseBody());
builder.field("sp_entity_id", response.getSpEntityId());
jkakavas marked this conversation as resolved.
Show resolved Hide resolved
builder.endObject();
return new BytesRestResponse(RestStatus.OK, builder);
}
});
}
}
}
Loading