From 6363909c0006ecfcd85f5d853fcd1eb6c481ea7d Mon Sep 17 00:00:00 2001 From: Anthony Johnson Date: Tue, 20 Jan 2015 11:08:49 -0500 Subject: [PATCH 01/10] initial --- README.md | 25 +++ grails-app/conf/BootStrap.groovy | 6 + grails-app/conf/UrlMappings.groovy | 5 + .../netflix/ice/DashboardController.groovy | 60 +++++- .../com/netflix/ice/LoginController.groovy | 120 +++++++++++ grails-app/views/login/error.gsp | 31 +++ grails-app/views/login/failure.gsp | 31 +++ grails-app/views/login/logout.gsp | 28 +++ .../ice/basic/BasicAccountService.java | 34 ++- .../netflix/ice/common/AccountService.java | 16 ++ .../com/netflix/ice/common/BaseConfig.java | 19 ++ src/java/com/netflix/ice/common/Config.java | 2 +- .../com/netflix/ice/common/IceOptions.java | 10 + .../com/netflix/ice/common/IceSession.java | 194 ++++++++++++++++++ .../com/netflix/ice/login/LoginConfig.java | 103 ++++++++++ .../com/netflix/ice/login/LoginMethod.java | 109 ++++++++++ .../ice/login/LoginMethodException.java | 26 +++ .../com/netflix/ice/login/LoginOptions.java | 71 +++++++ .../com/netflix/ice/login/LoginResponse.java | 56 +++++ src/java/com/netflix/ice/login/Logout.java | 51 +++++ .../com/netflix/ice/login/Passphrase.java | 83 ++++++++ .../netflix/ice/login/views/passphrase.gsp | 13 ++ 22 files changed, 1079 insertions(+), 14 deletions(-) create mode 100644 grails-app/controllers/com/netflix/ice/LoginController.groovy create mode 100644 grails-app/views/login/error.gsp create mode 100644 grails-app/views/login/failure.gsp create mode 100644 grails-app/views/login/logout.gsp create mode 100644 src/java/com/netflix/ice/common/BaseConfig.java create mode 100644 src/java/com/netflix/ice/common/IceSession.java create mode 100644 src/java/com/netflix/ice/login/LoginConfig.java create mode 100644 src/java/com/netflix/ice/login/LoginMethod.java create mode 100644 src/java/com/netflix/ice/login/LoginMethodException.java create mode 100644 src/java/com/netflix/ice/login/LoginOptions.java create mode 100644 src/java/com/netflix/ice/login/LoginResponse.java create mode 100644 src/java/com/netflix/ice/login/Logout.java create mode 100644 src/java/com/netflix/ice/login/Passphrase.java create mode 100644 src/java/com/netflix/ice/login/views/passphrase.gsp diff --git a/README.md b/README.md index 0238a00a..6042872b 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,31 @@ Options with * require writing your own code. You may also want to show your organization's throughput metric alongside usage and cost. You can choose to implement interface ThroughputMetricService, or you can simply use the existing BasicThroughputMetricService. Using BasicThroughputMetricService requires the throughput metric data to be stores monthly in files with names like _2013_04, _2013_05. Data in files should be delimited by new lines. is specified when you create BasicThroughputMetricService instance. +## Authentication + +A Framework exists for supplying authentication plugins. The following properties are required: + + # Turn Logging On/Off + ice.login=true + + # Logging Classes, comma delimited + ice.login.classes=com.netflix.ice.login.Passphrase + + # Logging Names, comma delmited. These map to a handler above + # The name here will expose an http endpoint. + # http://.../ice/login/handler/passphrase + ice.login.endpoints=passphrase + + # Passphrase for the Passphrase Implementation + ice.login.passphrase=rar + + # Default Endpoint(where /login/ takes us) + ice.login.default_endpoint=passphrase + # Login Log file(audit log) + ice.login.log=/some/path + +Passphrase is simply a reference implementation that guards your ice data with a passphrase(ice.login.passphrase). To create your own login handler, you can extend the LoginMethod. + ##Support Please use the [Ice Google Group](https://groups.google.com/d/forum/iceusers) for general questions and discussion. diff --git a/grails-app/conf/BootStrap.groovy b/grails-app/conf/BootStrap.groovy index 44e978bf..a61425a6 100644 --- a/grails-app/conf/BootStrap.groovy +++ b/grails-app/conf/BootStrap.groovy @@ -16,6 +16,7 @@ import com.netflix.ice.reader.ReaderConfig import com.netflix.ice.processor.ProcessorConfig +import com.netflix.ice.login.LoginConfig import com.netflix.ice.JSONConverter import org.apache.commons.lang.StringUtils; import org.slf4j.Logger @@ -52,6 +53,7 @@ class BootStrap { private ReaderConfig readerConfig; private ProcessorConfig processorConfig; + private LoginConfig loginConfig; def init = { servletContext -> if (initialized) { @@ -233,6 +235,10 @@ class BootStrap { readerConfig.start(); } + if ("true".equals(prop.getProperty("ice.login"))) { + loginConfig = new LoginConfig(prop) + } + initialized = true; } catch (Exception e) { diff --git a/grails-app/conf/UrlMappings.groovy b/grails-app/conf/UrlMappings.groovy index 349297ff..03914820 100644 --- a/grails-app/conf/UrlMappings.groovy +++ b/grails-app/conf/UrlMappings.groovy @@ -17,6 +17,11 @@ class UrlMappings { static mappings = { + "/login/" { controller = "login" } + "/login/handler/$login_action" { + controller = "login" + action = "handler" + } "/$controller/$action?/$id?" {} "/" { controller = "dashboard"} "500" (view: '/error') diff --git a/grails-app/controllers/com/netflix/ice/DashboardController.groovy b/grails-app/controllers/com/netflix/ice/DashboardController.groovy index 39488d8b..33fbe63f 100644 --- a/grails-app/controllers/com/netflix/ice/DashboardController.groovy +++ b/grails-app/controllers/com/netflix/ice/DashboardController.groovy @@ -34,11 +34,14 @@ import org.joda.time.DateTime import org.joda.time.Interval import com.netflix.ice.tag.Tag import com.netflix.ice.reader.*; +import com.netflix.ice.login.LoginConfig; import com.google.common.collect.Lists import com.google.common.collect.Sets import com.google.common.collect.Maps import org.json.JSONObject import com.netflix.ice.common.ConsolidateType +import com.netflix.ice.common.IceSession +import com.netflix.ice.common.AccountService import org.joda.time.Hours import org.apache.commons.lang.StringUtils import com.netflix.ice.common.AwsUtils @@ -64,20 +67,33 @@ class DashboardController { return managers; } + def beforeInterceptor = { + LoginConfig lc = LoginConfig.getInstance(); + if ( lc != null && lc.loginEnable ) + { + request["iceSession"] = new IceSession(session); + if (! request["iceSession"].isAuthenticated()) { + redirect(controller: "login") + } + } + } + def index = { redirect(action: "summary") } def getAccounts = { TagGroupManager tagGroupManager = getManagers().getTagGroupManager(null); - Collection data = tagGroupManager == null ? [] : tagGroupManager.getAccounts(new TagLists()); + IceSession sess = request["iceSession"]; + Collection data = tagGroupManager == null ? [] : tagGroupManager.getAccounts(new TagLists(), sess); def result = [status: 200, data: data] render result as JSON } def getRegions = { - List accounts = getConfig().accountService.getAccounts(listParams("account")); + IceSession sess = request["iceSession"]; + List accounts = getConfig().accountService.getAccounts(listParams("account"), sess); TagGroupManager tagGroupManager = getManagers().getTagGroupManager(null); Collection data = tagGroupManager == null ? [] : tagGroupManager.getRegions(new TagLists(accounts)); @@ -87,7 +103,8 @@ class DashboardController { } def getZones = { - List accounts = getConfig().accountService.getAccounts(listParams("account")); + IceSession sess = request["iceSession"]; + List accounts = getConfig().accountService.getAccounts(listParams("account"), sess); List regions = Region.getRegions(listParams("region")); TagGroupManager tagGroupManager = getManagers().getTagGroupManager(null); @@ -119,7 +136,8 @@ class DashboardController { def getProducts = { Object o = params; - List accounts = getConfig().accountService.getAccounts(listParams("account")); + IceSession sess = request["iceSession"]; + List accounts = getConfig().accountService.getAccounts(listParams("account"), sess); List regions = Region.getRegions(listParams("region")); List zones = Zone.getZones(listParams("zone")); List operations = Operation.getOperations(listParams("operation")); @@ -168,7 +186,8 @@ class DashboardController { } def getResourceGroups = { - List accounts = getConfig().accountService.getAccounts(listParams("account")); + IceSession sess = request["iceSession"]; + List accounts = getConfig().accountService.getAccounts(listParams("account"), sess); List regions = Region.getRegions(listParams("region")); List zones = Zone.getZones(listParams("zone")); List products = getConfig().productService.getProducts(listParams("product")); @@ -188,7 +207,8 @@ class DashboardController { def getOperations = { def text = request.reader.text; JSONObject query = (JSONObject)JSON.parse(text); - List accounts = getConfig().accountService.getAccounts(listParams(query, "account")); + IceSession sess = request["iceSession"]; + List accounts = getConfig().accountService.getAccounts(listParams(query, "account"), sess); List regions = Region.getRegions(listParams(query, "region")); List zones = Zone.getZones(listParams(query, "zone")); List products = getConfig().productService.getProducts(listParams(query, "product")); @@ -230,7 +250,8 @@ class DashboardController { def getUsageTypes = { def text = request.reader.text; JSONObject query = (JSONObject)JSON.parse(text); - List accounts = getConfig().accountService.getAccounts(listParams(query, "account")); + IceSession sess = request["iceSession"]; + List accounts = getConfig().accountService.getAccounts(listParams(query, "account"), sess); List regions = Region.getRegions(listParams(query, "region")); List zones = Zone.getZones(listParams(query, "zone")); List products = getConfig().productService.getProducts(listParams(query, "product")); @@ -317,6 +338,28 @@ class DashboardController { def getData = { def text = request.reader.text; JSONObject query = (JSONObject)JSON.parse(text); + + LoginConfig lc = LoginConfig.getInstance(); + AccountService accountService = getConfig().accountService; + // Apply Data Restrictions if configured + if ( lc != null && lc.loginEnable ) + { + //ensure query is constrained to our session accounts + IceSession sess = request["iceSession"]; + String accounts = (String)query.opt("account"); + if (accounts == null || accounts.length() == 0) { + StringBuilder csvString = new StringBuilder(); + String delim=""; + for (String allowedAccount : sess.allowedAccounts()) { + csvString.append(delim); + String allowedAccountName = accountService.getAccountById(allowedAccount); + csvString.append(allowedAccountName); + delim=","; + } + query.put("account", csvString.toString()); + } + + } def result = doGetData(query); render result as JSON @@ -397,7 +440,8 @@ class DashboardController { boolean showsps = query.getBoolean("showsps"); boolean factorsps = query.getBoolean("factorsps"); AggregateType aggregate = AggregateType.valueOf(query.getString("aggregate")); - List accounts = getConfig().accountService.getAccounts(listParams(query, "account")); + IceSession sess = request["iceSession"]; + List accounts = getConfig().accountService.getAccounts(listParams(query, "account"), sess); List regions = Region.getRegions(listParams(query, "region")); List zones = Zone.getZones(listParams(query, "zone")); List products = getConfig().productService.getProducts(listParams(query, "product")); diff --git a/grails-app/controllers/com/netflix/ice/LoginController.groovy b/grails-app/controllers/com/netflix/ice/LoginController.groovy new file mode 100644 index 00000000..340dafed --- /dev/null +++ b/grails-app/controllers/com/netflix/ice/LoginController.groovy @@ -0,0 +1,120 @@ +/* + * + * Copyright 2013 Netflix, Inc. + * + * 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 com.netflix.ice + +import java.io.FileInputStream +import groovy.text.SimpleTemplateEngine +import grails.converters.JSON +import org.apache.commons.io.IOUtils +import org.joda.time.format.DateTimeFormatter +import org.joda.time.format.DateTimeFormat +import org.joda.time.DateTimeZone +import org.joda.time.DateTime +import org.joda.time.Interval +import com.netflix.ice.tag.Tag +import com.netflix.ice.login.* +import com.netflix.ice.common.IceSession +import com.google.common.collect.Lists +import com.google.common.collect.Sets +import com.google.common.collect.Maps +import org.json.JSONObject + + +class LoginController { + private static LoginConfig config = LoginConfig.getInstance(); + + private static LoginConfig getConfig() { + if (config == null) { + config = LoginConfig.getInstance(); + } + return config; + } + + def handler = { + if (config.loginEnable == false) { + redirect(controller: "dashboard") + } + LoginMethod loginMethod = config.loginMethods.get(params.login_action); + if (loginMethod == null) { + redirect(action: "error"); + } + LoginResponse loginResponse = loginMethod.processLogin(request); + + if (loginResponse.redirectTo != null) { + redirect(url: loginResponse.redirectTo); + } else if (loginResponse.loggedOut) { + redirect(action: "logout"); + } else if (loginResponse.loginSuccess) { + IceSession iceSession = new IceSession(session); + iceSession.authenticate(new Boolean(true)); + iceSession.setAllowTime(loginResponse.loginStart, loginResponse.loginEnd); + if (iceSession.authenticated) { //ensure we are good + redirect(controller: "dashboard"); + } else { + redirect(action: "failure"); + } + } else if (loginResponse.loginFailed) { + redirect(action: "failure"); + } else if (loginResponse.renderData) { + render(text: loginResponse.renderData, contentType: loginResponse.contentType) + } else if (loginResponse.templateFile) { + // Fetch the template into memory + FileInputStream inputStream = new FileInputStream(loginResponse.templateFile); + String templateData = "" + try { + templateData = IOUtils.toString(inputStream); + } finally { + inputStream.close(); + } + SimpleTemplateEngine engine = new SimpleTemplateEngine() + String processedText = engine.createTemplate(templateData).make(loginResponse.templateBindings) + render(text: processedText, contentType: loginResponse.contentType) + } else { + redirect(action: "error"); + } + } + + /** A Login Failure, pass in the config so that we can give a configurable + * message + */ + def failure = { + [loginConfig: getConfig()] + } + + /** A Login Error(code issues perhaps) */ + def error = { + [loginConfig: getConfig()] + } + + /** A Login Logout */ + def logout = { + [loginConfig: getConfig()] + } + + /** + * Redirect Authentication request to the appropriate place. + */ + def index = { + getConfig(); + if (config.loginEnable == false) { + redirect(controller: "dashboard") + } else { + redirect(uri: "/login/handler/" + config.loginDefaultEndpoint) + } + } +} diff --git a/grails-app/views/login/error.gsp b/grails-app/views/login/error.gsp new file mode 100644 index 00000000..f32547a5 --- /dev/null +++ b/grails-app/views/login/error.gsp @@ -0,0 +1,31 @@ +<%-- + + Copyright 2013 Netflix, Inc. + + 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. + +--%> + +<%@ page contentType="text/html;charset=UTF-8" %> + + + + Authentication Error + + + Authentication Error.

+${loginConfig.noAccessMessage}

+ +Click here to try again. + + diff --git a/grails-app/views/login/failure.gsp b/grails-app/views/login/failure.gsp new file mode 100644 index 00000000..6e49025e --- /dev/null +++ b/grails-app/views/login/failure.gsp @@ -0,0 +1,31 @@ +<%-- + + Copyright 2013 Netflix, Inc. + + 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. + +--%> + +<%@ page contentType="text/html;charset=UTF-8" %> + + + + Login Failure + + + A Login Failure occurred.

+${loginConfig.noAccessMessage}

+ +Click here to try again. + + diff --git a/grails-app/views/login/logout.gsp b/grails-app/views/login/logout.gsp new file mode 100644 index 00000000..1919a496 --- /dev/null +++ b/grails-app/views/login/logout.gsp @@ -0,0 +1,28 @@ +<%-- + + Copyright 2013 Netflix, Inc. + + 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. + +--%> + +<%@ page contentType="text/html;charset=UTF-8" %> + + + + Logged Out + + + You have been logged out. + + diff --git a/src/java/com/netflix/ice/basic/BasicAccountService.java b/src/java/com/netflix/ice/basic/BasicAccountService.java index be136906..9ad8df0c 100644 --- a/src/java/com/netflix/ice/basic/BasicAccountService.java +++ b/src/java/com/netflix/ice/basic/BasicAccountService.java @@ -17,6 +17,7 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import com.netflix.ice.common.IceSession; import com.netflix.ice.common.AccountService; import com.netflix.ice.tag.Account; import com.netflix.ice.tag.Zone; @@ -30,11 +31,11 @@ public class BasicAccountService implements AccountService { Logger logger = LoggerFactory.getLogger(getClass()); - private Map accountsById = Maps.newConcurrentMap(); - private Map accountsByName = Maps.newConcurrentMap(); - private Map> reservationAccounts = Maps.newHashMap(); - private Map reservationAccessRoles = Maps.newHashMap(); - private Map reservationAccessExternalIds = Maps.newHashMap(); + protected Map accountsById = Maps.newConcurrentMap(); + protected Map accountsByName = Maps.newConcurrentMap(); + protected Map> reservationAccounts = Maps.newHashMap(); + protected Map reservationAccessRoles = Maps.newHashMap(); + protected Map reservationAccessExternalIds = Maps.newHashMap(); public BasicAccountService(List accounts, Map> reservationAccounts, Map reservationAccessRoles, Map reservationAccessExternalIds) { @@ -47,6 +48,13 @@ public BasicAccountService(List accounts, Map> r } } + public Account getAccountById(String accountId, IceSession session) { + if (session != null && (! session.allowedAccount(accountId))) { + return null; + } + return getAccountById(accountId); + } + public Account getAccountById(String accountId) { Account account = accountsById.get(accountId); if (account == null) { @@ -80,6 +88,22 @@ public List getAccounts(List accountNames) { return result; } + public List getAccounts(List accountNames, IceSession session) { + List result = Lists.newArrayList(); + for (String name: accountNames) { + Account account = accountsByName.get(name); + if (account == null) { + logger.error("Got a null account looking up " + name); + account = getAccountByName(name); + } + if (session != null && ! session.allowedAccount(account.id)) { + continue; + } + result.add(account); + } + return result; + } + public Map> getReservationAccounts() { return reservationAccounts; } diff --git a/src/java/com/netflix/ice/common/AccountService.java b/src/java/com/netflix/ice/common/AccountService.java index 81f17e10..5bcea9ae 100644 --- a/src/java/com/netflix/ice/common/AccountService.java +++ b/src/java/com/netflix/ice/common/AccountService.java @@ -31,6 +31,14 @@ public interface AccountService { */ Account getAccountById(String accountId); + /** + * Get account by AWS id. The AWS id is usually an un-readable 12 digit string. + * @param accountId + * @param session + * @return Account object associated with the account id + */ + Account getAccountById(String accountId, IceSession session); + /** * Get account by account name. The account name is a user defined readable string. * @param accountName @@ -45,6 +53,14 @@ public interface AccountService { */ List getAccounts(List accountNames); + /** + * Get a list of accounts from given account names. + * @param accountNames + * @param session + * @return List of accounts + */ + List getAccounts(List accountNames, IceSession session); + /** * If you don't have reserved instances, you can return an empty map. * @return Map of accounts. The keys are owner accounts, the values are list of borrowing accounts. diff --git a/src/java/com/netflix/ice/common/BaseConfig.java b/src/java/com/netflix/ice/common/BaseConfig.java new file mode 100644 index 00000000..953e3fe8 --- /dev/null +++ b/src/java/com/netflix/ice/common/BaseConfig.java @@ -0,0 +1,19 @@ +/* + * + * 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 com.netflix.ice.common; + +public interface BaseConfig { +} diff --git a/src/java/com/netflix/ice/common/Config.java b/src/java/com/netflix/ice/common/Config.java index 73992e03..b5812faf 100644 --- a/src/java/com/netflix/ice/common/Config.java +++ b/src/java/com/netflix/ice/common/Config.java @@ -23,7 +23,7 @@ import java.util.Properties; -public abstract class Config { +public abstract class Config implements BaseConfig { public final String workS3BucketName; public final String workS3BucketPrefix; diff --git a/src/java/com/netflix/ice/common/IceOptions.java b/src/java/com/netflix/ice/common/IceOptions.java index c7d9bca9..c663717c 100644 --- a/src/java/com/netflix/ice/common/IceOptions.java +++ b/src/java/com/netflix/ice/common/IceOptions.java @@ -154,4 +154,14 @@ public class IceOptions { * from email to use when test flag is enabled. */ public static final String NUM_WEEKS_FOR_WEEKLYEMAILS = "ice.weeklyCostEmails_numWeeks"; + + /** + * Prefix/Namespace for login related configuration items. + */ + public static final String LOGIN_PREFIX = "ice.login"; + + /** + * true/false for using Login. + */ + public static final String LOGIN_ENABLE = "ice.login"; } diff --git a/src/java/com/netflix/ice/common/IceSession.java b/src/java/com/netflix/ice/common/IceSession.java new file mode 100644 index 00000000..00b3ccda --- /dev/null +++ b/src/java/com/netflix/ice/common/IceSession.java @@ -0,0 +1,194 @@ +/* + * + * Copyright 2013 Netflix, Inc. + * + * 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 com.netflix.ice.common; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.util.List; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import javax.servlet.http.HttpSession; + +/** +* An IceSession is our interfact to an HttpServlet Session +*/ +public class IceSession { + private static final Logger logger = LoggerFactory.getLogger(IceSession.class); + private final String USER_NAME = "user_name"; + private final String AUTHENTICATED_SESSION_KEY = "authenticated"; + private final String ADMIN_SESSION_KEY = "admin"; + private final String ALLOWED_ACCOUNT_SESSION_PREFIX_KEY = "allowed_account"; + private final String ALLOWED_ACCOUNTS = "allowed_accounts"; + private final String START_DATE = "start_date"; + private final String END_DATE = "end_date"; + private final HttpSession session; + + public IceSession(HttpSession session) { + this.session = session; + } + + /** + * Auth or DeAuth this session. + * @parm authd + */ + public void authenticate(Boolean authd) { + session.setAttribute("authenticated", authd); + } + + public String username() { + return (String)session.getAttribute(USER_NAME); + } + + public void setUsername(String username) { + session.setAttribute(USER_NAME, username); + } + + /** + * Is this session authenticated? + */ + public Boolean isAuthenticated() { + logger.debug("isAuthenticated?"); + Boolean authd = (Boolean)session.getAttribute(AUTHENTICATED_SESSION_KEY); + if (authd != null && authd && withinAllowTime()) { + return true; + } + return false; + } + + /** + * Set the time at which this session is valid. This is required. + * @param notBefore + * @param notAfter + */ + public void setAllowTime(Date notBefore, Date notAfter) { + if (notBefore != null && notAfter != null) { + logger.info("Allow Time: " + notBefore.toString() + " to " + notAfter.toString()); + } else { + logger.info("Set Allow Time to null"); + } + session.setAttribute(START_DATE, notBefore); + session.setAttribute(END_DATE, notAfter); + } + + /* + * Has this Session expired? + */ + public boolean withinAllowTime() { + logger.debug("Within Allow Time?"); + Date notBefore = (Date)session.getAttribute(START_DATE); + Date notAfter = (Date)session.getAttribute(END_DATE); + if (notBefore == null || notAfter == null) { + logger.error("Session has no allow time"); + return false; + } + Date now = new Date(); + if ((now.after(notBefore)) && (now.before(notAfter))) { + return true; + } + logger.info(now.toString() + " is not between " + notBefore.toString() + " - " + notAfter.toString()); + return false; + } + + + /** + * 100% invalidate this session so it cannot be used for login. + */ + public void voidSession() { + logger.info("Void Session!"); + authenticate(false); + session.setAttribute(ADMIN_SESSION_KEY, new Boolean(false)); + List allowedAccounts = (List)session.getAttribute(ALLOWED_ACCOUNTS); + if (allowedAccounts != null) { + Iterator iter = allowedAccounts.iterator(); + while (iter.hasNext()) { + String allowedAccount = iter.next(); + revokeAccount(allowedAccount); + iter.remove(); + } + } + + setAllowTime(null,null); + } + + /** + * Give access to all Account Data. + */ + public void allowAllAccounts() { + session.setAttribute(ADMIN_SESSION_KEY,new Boolean(true)); + } + + /** + * Get a list of Accounts that this session can view + */ + public List allowedAccounts() { + List allowedAccounts = (List)session.getAttribute(ALLOWED_ACCOUNTS); + if (allowedAccounts == null) { + return new ArrayList(); + } + return allowedAccounts; + } + + /** + * Revoke accountId's data for this session? + * @param accountId + */ + public void revokeAccount(String accountId) { + session.removeAttribute(ALLOWED_ACCOUNT_SESSION_PREFIX_KEY + accountId); + + } + + /** + * Is this an Admin session? + */ + public boolean isAdmin() { + return ((Boolean)session.getAttribute(ADMIN_SESSION_KEY)).booleanValue(); + } + + /** + * Is accountId's data allowed for this session? + * @param accountId + */ + public boolean allowedAccount(String accountId) { + Boolean allowedAll = (Boolean)session.getAttribute(ADMIN_SESSION_KEY); + if (allowedAll != null && allowedAll.booleanValue()) + { + return true; + } + + Boolean allowedAccount = (Boolean)session.getAttribute(ALLOWED_ACCOUNT_SESSION_PREFIX_KEY + accountId); + if (allowedAccount != null && allowedAccount.booleanValue()) { + return true; + } + return false; + } + + /** + * Revoke accountId's data for this session? + * @param accountId + */ + public void allowAccount(String accountId) { + List allowedAccounts = (List)session.getAttribute(ALLOWED_ACCOUNTS); + if (allowedAccounts == null) { + allowedAccounts = new ArrayList(); + } + allowedAccounts.add(accountId); + session.setAttribute(ALLOWED_ACCOUNTS, allowedAccounts); + session.setAttribute(ALLOWED_ACCOUNT_SESSION_PREFIX_KEY + accountId, new Boolean(true)); + } +} diff --git a/src/java/com/netflix/ice/login/LoginConfig.java b/src/java/com/netflix/ice/login/LoginConfig.java new file mode 100644 index 00000000..f53e2054 --- /dev/null +++ b/src/java/com/netflix/ice/login/LoginConfig.java @@ -0,0 +1,103 @@ +/* + * 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 com.netflix.ice.login; + +import com.netflix.ice.common.*; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.Interval; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.lang.Boolean; +import java.util.Collection; +import java.util.Properties; +import java.util.Map; +import java.util.HashMap; +import java.lang.Class; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.ClassNotFoundException; +import java.lang.NoSuchMethodException; + +/** + * Configuration class for Login Features. + */ +public class LoginConfig implements BaseConfig { + private static LoginConfig instance; + private static final Logger logger = LoggerFactory.getLogger(LoginConfig.class); + + public final String loginClasses; + public final String noAccessMessage; + public final String loginLogFile; + public final String loginEndpoints; + public boolean loginEnable = false; + public final String loginDefaultEndpoint; + public final Map loginMethods = new HashMap(); + + /** + * @param properties (required) + */ + public LoginConfig(Properties properties) { + loginEnable = Boolean.parseBoolean(properties.getProperty(IceOptions.LOGIN_ENABLE)); + loginLogFile = properties.getProperty(LoginOptions.LOGIN_LOG); + loginClasses = properties.getProperty(LoginOptions.LOGIN_CLASSES); + loginEndpoints = properties.getProperty(LoginOptions.LOGIN_ENDPOINTS); + loginDefaultEndpoint = properties.getProperty(LoginOptions.LOGIN_DEFAULT); + noAccessMessage = properties.getProperty(LoginOptions.NO_ACCESS_MESSAGE); + + loadLoginPlugins(loginEndpoints, loginClasses, properties); + LoginConfig.instance = this; + } + + /** + * Load Plugins based on config. + */ + private void loadLoginPlugins(String endpoints, String classes, Properties properties) { + String[] endpoints_arr = endpoints.split(","); + String[] classes_arr = classes.split(","); + try { + for(int i=0;i + */ + public static final String LOGIN_ENDPOINTS = "ice.login.endpoints"; + + /** + * Simple passphrase for allowing Authentication + */ + public static final String LOGIN_PASSPHRASE = "ice.login.passphrase"; + + /** + * Message to display when a user fails access. Nice + * directions to get them access + */ + public static final String NO_ACCESS_MESSAGE = "ice.login.no_access_message"; + + /** + * Audit log location + */ + public static final String LOGIN_LOG = "ice.login.log"; + +} + diff --git a/src/java/com/netflix/ice/login/LoginResponse.java b/src/java/com/netflix/ice/login/LoginResponse.java new file mode 100644 index 00000000..77ca9050 --- /dev/null +++ b/src/java/com/netflix/ice/login/LoginResponse.java @@ -0,0 +1,56 @@ +/* + * 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 com.netflix.ice.login; + +import java.io.File; +import java.util.Map; +import java.util.Date; + +/** +* Simple Response Object directs the Login Controller how to handle +* the login request. +*/ +public class LoginResponse +{ + /** Was the Login Successful */ + public boolean loginSuccess=false; + + /** Did the Login Fail */ + public boolean loginFailed=false; + + /** Did we log the user out */ + public boolean loggedOut=false; + + /** Re-direct to a controller */ + public String redirectTo=null; + + /** A template File to render */ + public File templateFile=null; + + /** Raw data to render */ + public String renderData=null; + + /** templateFile or renderData mime-type */ + public String contentType=null; + + /** Variables to pass to templateFile or renderData */ + public Map templateBindings; + + /** When to allow this login(required) */ + public Date loginStart; + + /** When to end this login(required) */ + public Date loginEnd; +} diff --git a/src/java/com/netflix/ice/login/Logout.java b/src/java/com/netflix/ice/login/Logout.java new file mode 100644 index 00000000..55220dcd --- /dev/null +++ b/src/java/com/netflix/ice/login/Logout.java @@ -0,0 +1,51 @@ +/* + * 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 com.netflix.ice.login; + +import com.netflix.ice.common.IceOptions; +import com.netflix.ice.common.IceSession; + +import javax.servlet.http.HttpServletRequest; +import java.util.Collection; +import java.util.Properties; +import java.util.Map; +import java.util.Calendar; +import java.util.Date; +import java.io.File; +import java.net.URL; + + +/** + * Simple Login Method to logout a session + */ +public class Logout extends LoginMethod { + + public Logout(Properties properties) throws LoginMethodException { + super(properties); + } + + public String propertyName(String name) { + return null; + } + + public LoginResponse processLogin(HttpServletRequest request) throws LoginMethodException { + IceSession session = new IceSession(request.getSession()); + session.voidSession(); + LoginResponse lr = new LoginResponse(); + lr.loggedOut = true; + return lr; + } +} + diff --git a/src/java/com/netflix/ice/login/Passphrase.java b/src/java/com/netflix/ice/login/Passphrase.java new file mode 100644 index 00000000..80f7a341 --- /dev/null +++ b/src/java/com/netflix/ice/login/Passphrase.java @@ -0,0 +1,83 @@ +/* + * 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 com.netflix.ice.login; + +import com.netflix.ice.common.IceOptions; +import com.netflix.ice.common.IceSession; + +import javax.servlet.http.HttpServletRequest; +import java.util.Collection; +import java.util.Properties; +import java.util.Map; +import java.util.Calendar; +import java.util.Date; +import java.io.File; +import java.net.URL; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** +* Simple Login Method to protect via a config Passphrase. This is more of +* a reference implementation. +*/ +public class Passphrase extends LoginMethod { + Logger logger = LoggerFactory.getLogger(getClass()); + public final String passphrase; + public final String PASSPHRASE_PREFIX = propertyPrefix("passphrase"); + public Passphrase(Properties properties) throws LoginMethodException { + super(properties); + passphrase = properties.getProperty(LoginOptions.LOGIN_PASSPHRASE); + } + + public String propertyName(String name) { + return PASSPHRASE_PREFIX + "." + name; + } + + public LoginResponse processLogin(HttpServletRequest request) throws LoginMethodException { + + LoginResponse lr = new LoginResponse(); + String userPassphrase = (String)request.getParameter("passphrase"); + IceSession iceSession = new IceSession(request.getSession()); + + if (userPassphrase == null) { + /** embedded view simply to give a reference for how this would + * be done with a self-contained, jar'd login plugin. + */ + URL viewUrl = this.getClass().getResource("/com/netflix/ice/login/views/passphrase.gsp"); + try { + lr.templateFile=new File(viewUrl.toURI()); + lr.contentType="text/html"; + } catch(Exception e) { + logger.error("Bad Resource " + viewUrl); + } + } else if (userPassphrase.equals(passphrase)) { + iceSession.setUsername("Passphrase"); + whitelistAllAccounts(iceSession); + // allow user + lr.loginSuccess=true; + Date now = new Date(); + lr.loginStart=now; + Calendar cal = Calendar.getInstance(); + cal.setTime(now); + cal.add(Calendar.DATE, 1); //valid for one day + lr.loginEnd=cal.getTime(); + } else { + lr.loginFailed=true; + } + return lr; + } +} + diff --git a/src/java/com/netflix/ice/login/views/passphrase.gsp b/src/java/com/netflix/ice/login/views/passphrase.gsp new file mode 100644 index 00000000..9eeab25f --- /dev/null +++ b/src/java/com/netflix/ice/login/views/passphrase.gsp @@ -0,0 +1,13 @@ + + + + + +Whats the Password!
+
+ Passphrase: + +
+ + + From 260ee117c790be147c3786f99cd54a48afd78cd6 Mon Sep 17 00:00:00 2001 From: Anthony Johnson Date: Tue, 24 Feb 2015 14:43:17 -0500 Subject: [PATCH 02/10] catch null pointer for summary view. Add missing code for Login Framework. --- grails-app/conf/BuildConfig.groovy | 12 +- .../netflix/ice/DashboardController.groovy | 4 +- .../ice/basic/BasicTagGroupManager.java | 86 +++++- .../com/netflix/ice/common/IceSession.java | 17 +- src/java/com/netflix/ice/login/saml/Saml.java | 276 +++++++++++++++++ .../netflix/ice/login/saml/SamlConfig.java | 85 +++++ .../netflix/ice/login/saml/SamlMetaData.java | 291 ++++++++++++++++++ .../netflix/ice/login/saml/SamlOptions.java | 91 ++++++ .../com/netflix/ice/login/saml/metadata.xml | 46 +++ .../netflix/ice/login/saml/saml_config.xml | 38 +++ .../netflix/ice/reader/TagGroupManager.java | 8 +- 11 files changed, 933 insertions(+), 21 deletions(-) create mode 100644 src/java/com/netflix/ice/login/saml/Saml.java create mode 100644 src/java/com/netflix/ice/login/saml/SamlConfig.java create mode 100644 src/java/com/netflix/ice/login/saml/SamlMetaData.java create mode 100644 src/java/com/netflix/ice/login/saml/SamlOptions.java create mode 100644 src/java/com/netflix/ice/login/saml/metadata.xml create mode 100644 src/java/com/netflix/ice/login/saml/saml_config.xml diff --git a/grails-app/conf/BuildConfig.groovy b/grails-app/conf/BuildConfig.groovy index 79c307b5..62a65847 100644 --- a/grails-app/conf/BuildConfig.groovy +++ b/grails-app/conf/BuildConfig.groovy @@ -61,7 +61,6 @@ grails.project.dependency.resolution = { } dependencies { - compile( // Amazon Web Services programmatic interface 'com.amazonaws:aws-java-sdk:1.9.12', @@ -77,6 +76,8 @@ grails.project.dependency.resolution = { // Extra collection types and utilities 'commons-collections:commons-collections:3.2.1', + 'org.apache.commons:commons-io:1.3.2', + // Easier Java from of the Apache Foundation 'commons-lang:commons-lang:2.4', @@ -100,7 +101,6 @@ grails.project.dependency.resolution = { 'jfree:jfreechart:1.0.13', 'org.json:json:20090211', 'org.mapdb:mapdb:0.9.1' - ) { // Exclude superfluous and dangerous transitive dependencies excludes( // Some libraries bring older versions of JUnit as a transitive dependency and that can interfere @@ -108,11 +108,17 @@ grails.project.dependency.resolution = { 'junit', 'mockito-core', + 'xercesImpl' ) } + compile( + 'org.opensaml:opensaml:2.6.1' + ) { + excludes 'xercesImpl' + } } plugins { - build ":tomcat:$grailsVersion" + build ":tomcat:2.2.1" } } diff --git a/grails-app/controllers/com/netflix/ice/DashboardController.groovy b/grails-app/controllers/com/netflix/ice/DashboardController.groovy index 33fbe63f..4edde64f 100644 --- a/grails-app/controllers/com/netflix/ice/DashboardController.groovy +++ b/grails-app/controllers/com/netflix/ice/DashboardController.groovy @@ -680,7 +680,9 @@ class DashboardController { result.interval = consolidateType.millis; } else { - result.time = new IntRange(0, data.values().iterator().next().length - 1).collect { interval.getStart().plusMonths(it).getMillis() } + if (data.values().size() > 0) { + result.time = new IntRange(0, data.values().iterator().next().length - 1).collect { interval.getStart().plusMonths(it).getMillis() } + } } return result; } diff --git a/src/java/com/netflix/ice/basic/BasicTagGroupManager.java b/src/java/com/netflix/ice/basic/BasicTagGroupManager.java index d3630e7e..1b8cdf02 100644 --- a/src/java/com/netflix/ice/basic/BasicTagGroupManager.java +++ b/src/java/com/netflix/ice/basic/BasicTagGroupManager.java @@ -23,6 +23,7 @@ import com.netflix.ice.common.AwsUtils; import com.netflix.ice.common.Poller; import com.netflix.ice.common.TagGroup; +import com.netflix.ice.common.IceSession; import com.netflix.ice.processor.TagGroupWriter; import com.netflix.ice.reader.ReaderConfig; import com.netflix.ice.reader.TagGroupManager; @@ -45,6 +46,7 @@ public class BasicTagGroupManager extends Poller implements TagGroupManager { private File file; private TreeMap> tagGroups; private TreeMap> tagGroupsWithResourceGroups; + private TreeMap>> tagGroupsWithResourceGroupsByAccount; private Interval totalInterval; BasicTagGroupManager(Product product) { @@ -76,6 +78,38 @@ protected void poll() throws IOException { this.tagGroups = tagGroups; this.tagGroupsWithResourceGroups = tagGroupsWithResourceGroups; logger.info("done reading " + file); + + // segregate out by Account + logger.info("Split Tag Group Resources by Account"); + TreeMap>> tagGroupsWithResourceGroupsByAccount = new TreeMap>>(); + // iterate all months + for (Map.Entry> entry : tagGroupsWithResourceGroups.entrySet()) { + Long millis = entry.getKey(); + logger.info("Process " + millis); + Collection tagGroupsEntry = entry.getValue(); + // each tagGroup for a month + for(TagGroup tg : tagGroupsEntry) { + String accountName = tg.account.name; + logger.info("Process " + tg.resourceGroup + " for " + tg.account.name); + TreeMap> accountEntry = tagGroupsWithResourceGroupsByAccount.get(accountName); + if (accountEntry == null) { //initialize + logger.info("Initialize " + accountName + " TagGroup TreeMap"); + accountEntry = new TreeMap>(); + tagGroupsWithResourceGroupsByAccount.put(accountName, accountEntry); + } + Collection tagGroupCollection = accountEntry.get(millis); + if (tagGroupCollection == null) { //initialize + logger.info("Initialize " + accountName + " TagGroup Collection"); + tagGroupCollection = new ArrayList(); + accountEntry.put(millis, tagGroupCollection); + } + tagGroupCollection.add(tg); + } + } + logger.info("Finished Spliting Tag Group Resources by Account"); + + this.tagGroupsWithResourceGroups = tagGroupsWithResourceGroups; + this.tagGroupsWithResourceGroupsByAccount = tagGroupsWithResourceGroupsByAccount; } finally { in.close(); @@ -121,6 +155,23 @@ private Set getTagGroupsWithResourceGroupsInRange(Collection mon return tagGroupsInRange; } + private Set getAccountTagGroupsWithResourceGroupsInRange(Collection accounts, Collection monthMillis) { + Set tagGroupsInRange = Sets.newHashSet(); + for (String account : accounts) { + logger.info("Get TagGroupsWithResourceGroups for " + account + " " + monthMillis.toString()); + TreeMap> accountTagGroupsWithResourceGroups = tagGroupsWithResourceGroupsByAccount.get(account); + if (accountTagGroupsWithResourceGroups == null) + continue; + for (Long monthMilli: monthMillis) { + Collection tagGroups = accountTagGroupsWithResourceGroups.get(monthMilli); + if (tagGroups == null) + continue; + tagGroupsInRange.addAll(tagGroups); + } + } + return tagGroupsInRange; + } + private Collection getMonthMillis(Interval interval) { Set result = Sets.newTreeSet(); for (Long milli: tagGroups.keySet()) { @@ -132,13 +183,20 @@ private Collection getMonthMillis(Interval interval) { return result; } - public Collection getAccounts(Interval interval, TagLists tagLists) { + public Collection getAccounts(Interval interval, TagLists tagLists, IceSession session) { Set result = Sets.newTreeSet(); Set tagGroupsInRange = getTagGroupsInRange(getMonthMillis(interval)); for (TagGroup tagGroup: tagGroupsInRange) { - if (tagLists.contains(tagGroup)) - result.add(tagGroup.account); + if (tagLists.contains(tagGroup)) { + Account acct = tagGroup.account; + if (session != null && session.allowedAccount(acct.id) == false) { + logger.debug("Session not allowed to view " + acct.id); + continue; //don't allow a view to this account + } + logger.debug("Adding " + acct.id); + result.add(acct); + } } return result; @@ -207,9 +265,17 @@ public Collection getUsageTypes(Interval interval, TagLists tagLists) return result; } - public Collection getResourceGroups(Interval interval, TagLists tagLists) { + public Collection getResourceGroups(Interval interval, TagLists tagLists, IceSession session) { Set result = Sets.newTreeSet(); - Set tagGroupsInRange = getTagGroupsWithResourceGroupsInRange(getMonthMillis(interval)); + Set tagGroupsInRange; + if (session == null || session.isAdmin()) + { + tagGroupsInRange = getTagGroupsWithResourceGroupsInRange(getMonthMillis(interval)); + } + else + { + tagGroupsInRange = getAccountTagGroupsWithResourceGroupsInRange(session.allowedAccounts(), getMonthMillis(interval)); + } for (TagGroup tagGroup: tagGroupsInRange) { if (tagLists.contains(tagGroup) && tagGroup.resourceGroup != null) @@ -219,8 +285,8 @@ public Collection getResourceGroups(Interval interval, TagLists t return result; } - public Collection getAccounts(TagLists tagLists) { - return this.getAccounts(totalInterval, tagLists); + public Collection getAccounts(TagLists tagLists, IceSession session) { + return this.getAccounts(totalInterval, tagLists, session); } public Collection getRegions(TagLists tagLists) { @@ -244,7 +310,7 @@ public Collection getUsageTypes(TagLists tagLists) { } public Collection getResourceGroups(TagLists tagLists) { - return this.getResourceGroups(totalInterval, tagLists); + return this.getResourceGroups(totalInterval, tagLists, null); } public Interval getOverlapInterval(Interval interval) { @@ -262,7 +328,7 @@ public Map getTagListsMap(Interval interval, TagLists tagLists, T List groupByTags = Lists.newArrayList(); switch (groupBy) { case Account: - groupByTags.addAll(getAccounts(interval, tagListsForTag)); + groupByTags.addAll(getAccounts(interval, tagListsForTag, null)); break; case Region: groupByTags.addAll(getRegions(interval, tagListsForTag)); @@ -280,7 +346,7 @@ public Map getTagListsMap(Interval interval, TagLists tagLists, T groupByTags.addAll(getUsageTypes(interval, tagListsForTag)); break; case ResourceGroup: - groupByTags.addAll(getResourceGroups(interval, tagListsForTag)); + groupByTags.addAll(getResourceGroups(interval, tagListsForTag, null)); break; } if (groupBy == TagType.Operation && !forReservation) { diff --git a/src/java/com/netflix/ice/common/IceSession.java b/src/java/com/netflix/ice/common/IceSession.java index 00b3ccda..5185d372 100644 --- a/src/java/com/netflix/ice/common/IceSession.java +++ b/src/java/com/netflix/ice/common/IceSession.java @@ -48,7 +48,8 @@ public IceSession(HttpSession session) { * @parm authd */ public void authenticate(Boolean authd) { - session.setAttribute("authenticated", authd); + logger.info("authenticate: " + authd); + session.setAttribute(AUTHENTICATED_SESSION_KEY, authd); } public String username() { @@ -65,10 +66,18 @@ public void setUsername(String username) { public Boolean isAuthenticated() { logger.debug("isAuthenticated?"); Boolean authd = (Boolean)session.getAttribute(AUTHENTICATED_SESSION_KEY); - if (authd != null && authd && withinAllowTime()) { - return true; + + if (authd == null) { + logger.error("User has no authentication entry"); + return false; + } else if (! authd.booleanValue()) { + logger.error("User has been explicitly denied - " + authd); + return false; + } else if (! withinAllowTime()) { + logger.error("User's allow time has expired"); + return false; } - return false; + return true; } /** diff --git a/src/java/com/netflix/ice/login/saml/Saml.java b/src/java/com/netflix/ice/login/saml/Saml.java new file mode 100644 index 00000000..cabec0bf --- /dev/null +++ b/src/java/com/netflix/ice/login/saml/Saml.java @@ -0,0 +1,276 @@ +/* + * 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 com.netflix.ice.login.saml; +import com.netflix.ice.login.*; +import com.netflix.ice.common.IceOptions; +import com.netflix.ice.common.IceSession; +import javax.servlet.http.HttpServletRequest; +import java.util.Collection; +import java.util.Enumeration; +import java.util.Properties; +import java.util.Map; +import java.util.List; +import java.util.ArrayList; +import java.util.Date; +import org.joda.time.DateTime; +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.net.URL; +import java.io.StringReader; +import org.apache.commons.io.FileUtils; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.opensaml.saml2.core.Attribute; +import org.opensaml.saml2.core.AttributeStatement; +import java.security.cert.CertificateFactory; +import java.security.cert.Certificate; +import java.security.KeyFactory; +import java.security.cert.X509Certificate; +import java.security.spec.X509EncodedKeySpec; +import java.security.PublicKey; +//import org.opensaml.xml.signature.PublicKey; +import org.opensaml.xml.security.credential.Credential; +import org.opensaml.xml.security.credential.BasicCredential; +import org.opensaml.xml.security.credential.UsageType; +import org.opensaml.xml.security.CriteriaSet; +import org.opensaml.xml.security.criteria.UsageCriteria; +import org.opensaml.xml.security.criteria.EntityIDCriteria; +import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.saml2.metadata.IDPSSODescriptor; +import org.opensaml.security.MetadataCriteria; +import javax.xml.namespace.QName; +import javax.xml.validation.Schema; + +import org.opensaml.xml.security.SecurityHelper; +import org.opensaml.saml2.core.Assertion; +import org.opensaml.xml.XMLObject; +import org.opensaml.common.xml.SAMLSchemaBuilder; +import org.opensaml.xml.parse.BasicParserPool; +import org.opensaml.DefaultBootstrap; +import org.opensaml.xml.ConfigurationException; +import org.opensaml.saml2.metadata.provider.AbstractMetadataProvider; +import org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider; +import org.opensaml.security.MetadataCredentialResolver; +import org.opensaml.xml.security.keyinfo.KeyInfoCredentialResolver; +import org.opensaml.xml.signature.impl.ExplicitKeySignatureTrustEngine; +import org.opensaml.Configuration; +import org.opensaml.saml2.encryption.Decrypter; +import org.opensaml.saml2.core.EncryptedAssertion; +import org.opensaml.xml.encryption.DecryptionException; +import org.opensaml.xml.util.Base64; +import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.opensaml.xml.encryption.InlineEncryptedKeyResolver; +import org.opensaml.xml.io.UnmarshallerFactory; +import org.opensaml.xml.io.Unmarshaller; +import org.opensaml.saml2.core.Response; +import org.opensaml.xml.signature.Signature; +import org.opensaml.xml.signature.SignatureValidator; +import org.opensaml.xml.validation.ValidationException; +import org.opensaml.security.SAMLSignatureProfileValidator; + + +/** + * SAML Plugin + */ +public class Saml extends LoginMethod { + + public final String SAML_PREFIX=propertyPrefix("saml"); + + private static final Logger logger = LoggerFactory.getLogger(Saml.class); + + private final SamlConfig config; + + private List trustedSigningCerts=new ArrayList(); + + public String propertyName(String name) { + return SAML_PREFIX + "." + name; + } + + public Saml(Properties properties) throws LoginMethodException { + super(properties); + config = new SamlConfig(properties); + for(String signingCert : config.trustedSigningCerts) { + try { + FileInputStream fis = new FileInputStream(new File(signingCert)); + CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); + Certificate cert = certFactory.generateCertificate(fis); + trustedSigningCerts.add(cert); + } catch(IOException ioe) { + logger.error("Error reading public key " + signingCert + ":" + ioe.toString()); + } catch(Exception e) { + logger.error("Error decoding public key " + signingCert + ":" + e.toString()); + } + } + + try { + DefaultBootstrap.bootstrap(); + } catch(ConfigurationException ce) { + throw new LoginMethodException("Failure to init OpenSAML: " + ce.toString()); + } + } + + public LoginResponse processLogin(HttpServletRequest request) throws LoginMethodException { + IceSession iceSession = new IceSession(request.getSession()); + iceSession.voidSession(); //a second login request voids anything previous + logger.info("Saml::processLogin"); + LoginResponse lr = new LoginResponse(); + String assertion = (String)request.getParameter("SAMLResponse"); + if (assertion == null) { + lr.redirectTo=config.singleSignOnUrl; + return lr; + } + logger.trace("Received SAML Assertion: " + assertion); + try + { + // 1.1 2.0 schemas + Schema schema = SAMLSchemaBuilder.getSAML11Schema(); + + //get parser pool manager + BasicParserPool parserPoolManager = new BasicParserPool(); + parserPoolManager.setNamespaceAware(true); + parserPoolManager.setIgnoreElementContentWhitespace(true); + parserPoolManager.setSchema(schema); + + String data = new String(Base64.decode(assertion)); + logger.info("Decoded SAML Assertion: " + data); + + StringReader reader = new StringReader(data); + Document document = parserPoolManager.parse(reader); + Element documentRoot = document.getDocumentElement(); + + QName qName= new QName(documentRoot.getNamespaceURI(), documentRoot.getLocalName(), documentRoot.getPrefix()); + + //get an unmarshaller + Unmarshaller unmarshaller = Configuration.getUnmarshallerFactory().getUnmarshaller(documentRoot); + + //unmarshall using the document root element + XMLObject xmlObj = unmarshaller.unmarshall(documentRoot); + Response response = (Response)xmlObj; + for(Assertion myAssertion : response.getAssertions()) + { + if (! myAssertion.isSigned()) { + logger.error("SAML Assertion not signed" ); + throw new LoginMethodException("SAML Assertions must be signed by a trusted provider"); + } + + Signature assertionSignature = myAssertion.getSignature(); + SAMLSignatureProfileValidator profVal = new SAMLSignatureProfileValidator(); + + logger.info("Validating SAML Assertion" ); + // will throw a ValidationException + profVal.validate(assertionSignature); + + //Credential signCred = assertionSignature.getSigningCredential(); + boolean goodSignature = false; + for(Certificate trustedCert : trustedSigningCerts) { + BasicCredential cred = new BasicCredential(); + cred.setPublicKey(trustedCert.getPublicKey()); + SignatureValidator validator = new SignatureValidator(cred); + try { + validator.validate(assertionSignature); + } catch(ValidationException ve) { + /* Not a good key! */ + logger.debug("Not signed by " + trustedCert.toString()); + continue; + } + logger.info("Assertion trusted from " + trustedCert.toString()); + processAssertion(iceSession, myAssertion, lr); + goodSignature = true; + break; + } + + if (goodSignature) { + lr.loginSuccess=true; + } + + } + } catch(org.xml.sax.SAXException saxe) { + logger.error(saxe.toString()); + } catch(org.opensaml.xml.parse.XMLParserException xmlpe) { + logger.error(xmlpe.toString()); + } catch(org.opensaml.xml.io.UnmarshallingException uee) { + logger.error(uee.toString()); + } catch(org.opensaml.xml.validation.ValidationException ve) { + throw new LoginMethodException("SAML Assertion Signature was not usable: " + ve.toString()); + } + return lr; + } + + /** + * Process an assertion and setup our session attributes + */ + private void processAssertion(IceSession iceSession, Assertion assertion, LoginResponse lr) throws LoginMethodException { + boolean foundAnAccount=false; + iceSession.voidSession(); + for(AttributeStatement as : assertion.getAttributeStatements()) { + // iterate once to assure we set the username first + for(Attribute attr : as.getAttributes()) { + if (attr.getName().equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name")) { + for(XMLObject groupXMLObj : attr.getAttributeValues()) { + String username = groupXMLObj.getDOM().getTextContent(); + iceSession.setUsername(username); + } + } + } + // iterate again for everything else + for(Attribute attr : as.getAttributes()) { + if (attr.getName().equals("com.netflix.ice.account")) { + for(XMLObject groupXMLObj : attr.getAttributeValues()) { + String allowedAccount = groupXMLObj.getDOM().getTextContent(); + if (allowedAccount.equals(config.allAccounts) ) { + whitelistAllAccounts(iceSession); + foundAnAccount=true; + logger.info("Found Allow All Accounts: " + allowedAccount); + break; + } else { + if (whitelistAccount(iceSession, allowedAccount)) { + foundAnAccount=true; + logger.info("Found Account: " + allowedAccount); + } + } + } + } + } + } + + //require at least one account + if (! foundAnAccount) { + lr.loginFailed=true; + //throw new LoginMethodException("SAML Assertion must give at least one Account as part of the Assertion"); + return; + } + + //set expiration date + DateTime startDate = assertion.getConditions().getNotBefore(); + DateTime endDate = assertion.getConditions().getNotOnOrAfter(); + if (startDate == null || endDate == null) { + throw new LoginMethodException("Assertion must state an expiration date"); + } + // Clocks may not be synchronized. + startDate = startDate.minusMinutes(2); + endDate = endDate.plusMinutes(2); + logger.info(startDate.toCalendar(null).getTime().toString()); + logger.info(endDate.toCalendar(null).getTime().toString()); + lr.loginStart = startDate.toCalendar(null).getTime(); + lr.loginEnd = endDate.toCalendar(null).getTime(); + } +} + diff --git a/src/java/com/netflix/ice/login/saml/SamlConfig.java b/src/java/com/netflix/ice/login/saml/SamlConfig.java new file mode 100644 index 00000000..fa88c532 --- /dev/null +++ b/src/java/com/netflix/ice/login/saml/SamlConfig.java @@ -0,0 +1,85 @@ +/* + * 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 com.netflix.ice.login.saml; + +import com.netflix.ice.login.*; +import com.netflix.ice.common.*; +import org.joda.time.DateTime; +import org.joda.time.DateTimeZone; +import org.joda.time.Interval; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.apache.commons.io.FileUtils; + +import java.lang.Boolean; +import java.util.Collection; +import java.util.Properties; +import java.util.Map; +import java.util.List; +import java.util.ArrayList; +import java.util.HashMap; +import java.io.File; +import java.io.IOException; + +/** + * COnfiguration class for UI login. + */ +public class SamlConfig implements BaseConfig { + private static final Logger logger = LoggerFactory.getLogger(SamlConfig.class); + + public final List trustedSigningCerts = new ArrayList(); + public final List trustedMetadata = new ArrayList(); + + private final String keystore; + private final String keystorePassword; + private final String keyAlias; + private final String keyPassword; + public final String organizationName; + public final String organizationDisplayName; + public final String organizationUrl; + public final String signInUrl; + public final String serviceName; + public final String allAccounts; + public final String singleSignOnUrl; + + public SamlConfig(Properties properties) { + loadSigningCerts(properties); + keystore = properties.getProperty(SamlOptions.KEYSTORE); + keystorePassword = properties.getProperty(SamlOptions.KEYSTORE_PASSWORD); + keyAlias = properties.getProperty(SamlOptions.KEY_ALIAS); + keyPassword = properties.getProperty(SamlOptions.KEY_PASSWORD); + organizationName = properties.getProperty(SamlOptions.ORGANIZATION_NAME); + organizationDisplayName = properties.getProperty(SamlOptions.ORGANIZATION_DISPLAY_NAME); + organizationUrl = properties.getProperty(SamlOptions.ORGANIZATION_URL); + signInUrl = properties.getProperty(SamlOptions.SIGNIN_URL); + serviceName = properties.getProperty(SamlOptions.SERVICE_NAME); + allAccounts = properties.getProperty(SamlOptions.ALL_ACCOUNTS); + singleSignOnUrl = properties.getProperty(SamlOptions.SINGLE_SIGN_ON_URL); + loadSigningCerts(properties); + + } + + private void loadSigningCerts(Properties properties) { + String trustedCerts = properties.getProperty(SamlOptions.TRUSTED_SIGNING_CERTS); + if (trustedCerts == null) { + logger.warn("No Trusted Certs found"); + return; + } + String[] certLocations = trustedCerts.split(","); + for(String certLocation : certLocations) { + trustedSigningCerts.add(certLocation); + } + } +} diff --git a/src/java/com/netflix/ice/login/saml/SamlMetaData.java b/src/java/com/netflix/ice/login/saml/SamlMetaData.java new file mode 100644 index 00000000..ecf54d05 --- /dev/null +++ b/src/java/com/netflix/ice/login/saml/SamlMetaData.java @@ -0,0 +1,291 @@ +/* + * 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 com.netflix.ice.login.saml; + +import com.netflix.ice.login.*; +import com.netflix.ice.common.IceOptions; + +import javax.servlet.http.HttpServletRequest; +import java.util.Collection; +import java.util.Properties; +import java.util.Map; +import java.util.HashMap; +import java.io.File; +import java.net.URL; +import java.io.StringWriter; +import java.io.IOException; + +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import javax.xml.namespace.QName; +import javax.xml.validation.Schema; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import javax.xml.transform.*; +import javax.xml.parsers.*; +import javax.xml.parsers.ParserConfigurationException; + +import org.opensaml.saml2.metadata.*; +import org.opensaml.xml.security.keyinfo.*; +import org.opensaml.xml.security.credential.*; +import org.opensaml.xml.security.x509.*; +import org.opensaml.xml.*; + +import groovy.text.SimpleTemplateEngine; +import org.opensaml.DefaultBootstrap; +import org.opensaml.xml.ConfigurationException; +import org.opensaml.saml2.metadata.provider.MetadataProvider; +import org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider; +import org.opensaml.security.MetadataCredentialResolver; +import org.opensaml.xml.security.keyinfo.KeyInfoCredentialResolver; +import org.opensaml.xml.signature.impl.ExplicitKeySignatureTrustEngine; +import org.opensaml.Configuration; +import org.opensaml.saml1.core.Assertion; +import org.opensaml.saml2.encryption.Decrypter; +import org.opensaml.saml2.core.EncryptedAssertion; +import org.opensaml.xml.encryption.DecryptionException; +import org.opensaml.xml.util.Base64; +import org.opensaml.saml2.metadata.provider.MetadataProviderException; +import org.opensaml.xml.encryption.InlineEncryptedKeyResolver; +import org.opensaml.xml.io.Marshaller; +import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.xml.security.SecurityException; + +import java.io.FileInputStream; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * SAML MetaData Plugin. Provides MetaData for idPs + */ +public class SamlMetaData extends LoginMethod { + + public final String SAML_PREFIX=propertyPrefix("saml"); + Logger logger = LoggerFactory.getLogger(getClass()); + + public String metadataXML = null; + public final String keystore; + public final String keystorePassword; + public final String keyAlias; + public final String keyPassword; + public final String orgName; + public final String orgDisplayName; + public final String orgUrl; + public final String signinUrl; + public final String serviceName; + + public String propertyName(String name) { + return SAML_PREFIX + "." + name; + } + + public SamlMetaData(Properties properties) throws LoginMethodException { + super(properties); + keystore=propertyValue("keystore"); + keystorePassword=propertyValue("keystore_password"); + keyAlias=propertyValue("key_alias"); + keyPassword=propertyValue("key_password"); + orgName=propertyValue("org_name"); + orgDisplayName=propertyValue("org_display_name"); + orgUrl=propertyValue("org_url"); + signinUrl=propertyValue("signin_url"); + serviceName=propertyValue("service_name"); + } + + public LoginResponse processLogin(HttpServletRequest request) throws LoginMethodException { + LoginResponse lr = new LoginResponse(); + if (metadataXML == null) { + generateMetadata(); + } + lr.renderData = metadataXML; + lr.contentType = "application/samlmetadata+xml"; + return lr; + } + + /** + * Wow! + * from: http://mylifewithjava.blogspot.com/2012/02/generating-metadata-with-opensaml.html + */ + private synchronized void generateMetadata() { + if (metadataXML != null) { return; } + + EntityDescriptor spEntityDescriptor = createSAMLObject(EntityDescriptor.class); + spEntityDescriptor.setEntityID("netflix ice"); + + Organization organization = createSAMLObject(Organization.class); + + OrganizationDisplayName samlOrgDisplayName = createSAMLObject(OrganizationDisplayName.class); + samlOrgDisplayName.setName(new LocalizedString(orgDisplayName, "en")); + organization.getDisplayNames().add(samlOrgDisplayName); + + OrganizationName samlOrgName = createSAMLObject(OrganizationName.class); + samlOrgName.setName(new LocalizedString(orgName, "en")); + organization.getOrganizationNames().add(samlOrgName); + + OrganizationURL samlOrgUrl = createSAMLObject(OrganizationURL.class); + samlOrgUrl.setURL(new LocalizedString(orgUrl,"en")); + organization.getURLs().add(samlOrgUrl); + + spEntityDescriptor.setOrganization(organization); + + SPSSODescriptor spSSODescriptor = createSAMLObject(SPSSODescriptor.class); + spSSODescriptor.setWantAssertionsSigned(true); + + X509KeyInfoGeneratorFactory keyInfoGeneratorFactory = new X509KeyInfoGeneratorFactory(); + keyInfoGeneratorFactory.setEmitEntityCertificate(true); + KeyInfoGenerator keyInfoGenerator = keyInfoGeneratorFactory.newInstance(); + + //spSSODescriptor.setAuthnRequestsSigned(true); + //KeyDescriptor encKeyDescriptor = createSAMLObject(KeyDescriptor.class); + + //encKeyDescriptor.setUse(UsageType.ENCRYPTION); //Set usage + + //try { + // encKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(null)); + //} catch (SecurityException e) { + // logger.error(e.getMessage(), e); + //} + + //spSSODescriptor.getKeyDescriptors().add(encKeyDescriptor); + + KeyDescriptor signKeyDescriptor = createSAMLObject(KeyDescriptor.class); + + signKeyDescriptor.setUse(UsageType.SIGNING); //Set usage + + try { + BasicX509Credential creds = new BasicX509Credential(); + creds.setEntityCertificate(signingKey()); + signKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(creds)); + } catch (SecurityException e) { + logger.error(e.getMessage(), e); + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + + spSSODescriptor.getKeyDescriptors().add(signKeyDescriptor); + NameIDFormat nameIDFormat = createSAMLObject(NameIDFormat.class); + nameIDFormat.setFormat("urn:oasis:names:tc:SAML:2.0:nameid-format:transient"); + spSSODescriptor.getNameIDFormats().add(nameIDFormat); + AssertionConsumerService assertionConsumerService = createSAMLObject(AssertionConsumerService.class); + assertionConsumerService.setIndex(0); + //assertionConsumerService.setBinding(SAMLConstants.SAML2_ARTIFACT_BINDING_URI); + assertionConsumerService.setBinding(SAMLConstants.SAML2_POST_BINDING_URI); + + assertionConsumerService.setLocation(signinUrl); + spSSODescriptor.getAssertionConsumerServices().add(assertionConsumerService); + spSSODescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS); + + spEntityDescriptor.getRoleDescriptors().add(spSSODescriptor); + + AttributeConsumingService service = createSAMLObject(AttributeConsumingService.class); + ServiceName name = createSAMLObject(ServiceName.class); + name.setName(new LocalizedString(serviceName, "en")); + service.getNames().add(name); + service.setIndex(0); + service.setIsDefault(true); + + RequestedAttribute accountAttr = createSAMLObject(RequestedAttribute.class); + accountAttr.setFriendlyName("AccountID"); + accountAttr.setName("com.netflix.ice.account"); + + service.getRequestAttributes().add(accountAttr); + + spSSODescriptor.getAttributeConsumingServices().add(service); + + DocumentBuilder builder = null; + DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); + try { + builder = factory.newDocumentBuilder(); + } catch(javax.xml.parsers.ParserConfigurationException pce) { + logger.error(pce.toString()); + } + Document document = builder.newDocument(); + Marshaller out = Configuration.getMarshallerFactory().getMarshaller(spEntityDescriptor); + + try { + out.marshall(spEntityDescriptor, document); + } catch(org.opensaml.xml.io.MarshallingException me) { + logger.error(me.toString()); + } + Transformer transformer = null; + try { + transformer = TransformerFactory.newInstance().newTransformer(); + } catch(javax.xml.transform.TransformerConfigurationException tce) { + logger.error(tce.toString()); + } + + StringWriter stringWriter = new StringWriter(); + + try { + StreamResult streamResult = new StreamResult(stringWriter); + + DOMSource source = new DOMSource(document); + transformer.transform(source, streamResult); + stringWriter.close(); + } catch(IOException ioe) { + logger.error(ioe.toString()); + return; + } catch(javax.xml.transform.TransformerException te) { + logger.error(te.toString()); + return; + } + metadataXML = stringWriter.toString(); + } + + /** + * from: http://mylifewithjava.blogspot.no/2011/04/convenience-methods-for-opensaml.html + */ + public static T createSAMLObject(final Class clazz) { + XMLObjectBuilderFactory builderFactory = Configuration.getBuilderFactory(); + QName defaultElementName = null; + try { + defaultElementName = (QName)clazz.getDeclaredField("DEFAULT_ELEMENT_NAME").get(null); + } catch(IllegalAccessException iae) { + System.out.println(iae.toString()); + return null; + } catch(java.lang.NoSuchFieldException nsfe) { + System.out.println(nsfe.toString()); + return null; + } + T object = (T)builderFactory.getBuilder(defaultElementName).buildObject(defaultElementName); + return object; + } + + + private X509Certificate signingKey() throws Exception { + FileInputStream is = new FileInputStream(keystore); + + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(is, keystorePassword.toCharArray()); + + Key key = keystore.getKey(keyAlias, keyPassword.toCharArray()); + System.out.println(key.toString()); + if (key instanceof PrivateKey) { + System.out.println("Got Here"); + // Get certificate of public key + X509Certificate cert = (X509Certificate)keystore.getCertificate(keyAlias); + return cert; + } + return null; + } +} + diff --git a/src/java/com/netflix/ice/login/saml/SamlOptions.java b/src/java/com/netflix/ice/login/saml/SamlOptions.java new file mode 100644 index 00000000..ec98c413 --- /dev/null +++ b/src/java/com/netflix/ice/login/saml/SamlOptions.java @@ -0,0 +1,91 @@ +/* + * + * Copyright 2013 Netflix, Inc. + * + * 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 com.netflix.ice.login.saml; + +import com.netflix.ice.login.LoginOptions; + +public class SamlOptions { + + /** + * Base for all our SAML properties + */ + public static final String SAML = LoginOptions.LOGIN + ".saml"; + + /** + * Signin-Url for our service + */ + public static final String SIGNIN_URL = SAML + ".signing_url"; + + /** + * Property for Service Name + */ + public static final String SERVICE_NAME = SAML + ".service_name"; + + /** + * Property for organization name. + */ + public static final String ORGANIZATION_NAME = SAML + ".organization_name"; + + /** + * Property for organization display name. + */ + public static final String ORGANIZATION_DISPLAY_NAME = SAML + ".organization_display_name"; + + /** + * Property for organization url + */ + public static final String ORGANIZATION_URL = SAML + ".organization_url"; + + /** + * Property for Keystore where we can find certificates + */ + public static final String KEYSTORE = SAML + ".keystore"; + + /** + * Property for Keystore Password + */ + public static final String KEYSTORE_PASSWORD = SAML + ".keystore_password"; + + /** + * Property for Keystore Key alias + */ + public static final String KEY_ALIAS = SAML + ".key_alias"; + + /** + * Property for Keystore Key password + */ + public static final String KEY_PASSWORD = SAML + ".key_password"; + + /** + * Property for Keystore Key password + */ + public static final String TRUSTED_SIGNING_CERTS = SAML + ".trusted_signing_certs"; + + /** + * Property for special account text to allows access to all accounts. + * This would be supplied in the saml assertion account attribute. + */ + public static final String ALL_ACCOUNTS = SAML + ".all_accounts"; + + /** + * Property for where to re-direct someone when they need to provide some + * SAML creds + */ + public static final String SINGLE_SIGN_ON_URL = SAML + ".single_sign_on_url"; + +} diff --git a/src/java/com/netflix/ice/login/saml/metadata.xml b/src/java/com/netflix/ice/login/saml/metadata.xml new file mode 100644 index 00000000..b0e738d1 --- /dev/null +++ b/src/java/com/netflix/ice/login/saml/metadata.xml @@ -0,0 +1,46 @@ + + + + + + + +${public_key} + + + + + +urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + +urn:oasis:names:tc:SAML:2.0:nameid-format:persistent + + +NetFlix Ice Single Sign-On + + + + + + + + + + + + + + + + + + + + + +${org_name} +${org_display_name} +${org_url} + + diff --git a/src/java/com/netflix/ice/login/saml/saml_config.xml b/src/java/com/netflix/ice/login/saml/saml_config.xml new file mode 100644 index 00000000..07b946aa --- /dev/null +++ b/src/java/com/netflix/ice/login/saml/saml_config.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/java/com/netflix/ice/reader/TagGroupManager.java b/src/java/com/netflix/ice/reader/TagGroupManager.java index 83d72755..285dae8e 100644 --- a/src/java/com/netflix/ice/reader/TagGroupManager.java +++ b/src/java/com/netflix/ice/reader/TagGroupManager.java @@ -18,6 +18,7 @@ package com.netflix.ice.reader; import com.netflix.ice.tag.*; +import com.netflix.ice.common.IceSession; import org.joda.time.Interval; import java.util.Collection; @@ -33,7 +34,7 @@ public interface TagGroupManager { * @param tagLists * @return collection of accounts */ - Collection getAccounts(TagLists tagLists); + Collection getAccounts(TagLists tagLists, IceSession session); /** * Get all regions that meet query in tagLists. @@ -83,7 +84,7 @@ public interface TagGroupManager { * @param tagLists * @return collection of accounts */ - Collection getAccounts(Interval interval, TagLists tagLists); + Collection getAccounts(Interval interval, TagLists tagLists, IceSession session); /** * Get all regions that meet query in tagLists and in specifed interval. @@ -129,9 +130,10 @@ public interface TagGroupManager { * Get all resource groups that meet query in tagLists and in specifed interval. * @param interval * @param tagLists + * @param session * @return collection of resource groups */ - Collection getResourceGroups(Interval interval, TagLists tagLists); + Collection getResourceGroups(Interval interval, TagLists tagLists, IceSession session); /** * Get overlapping interval From 9dff19607f7cc5af4ca01dfd0a05f666b7e210f5 Mon Sep 17 00:00:00 2001 From: Anthony Johnson Date: Wed, 25 Feb 2015 00:44:57 -0500 Subject: [PATCH 03/10] Refactor SAML authentication to use pac4j library --- grails-app/conf/BuildConfig.groovy | 8 +- src/java/com/netflix/ice/login/saml/Saml.java | 182 +++---------- .../netflix/ice/login/saml/SamlConfig.java | 38 +-- .../netflix/ice/login/saml/SamlMetaData.java | 247 ++---------------- .../netflix/ice/login/saml/SamlOptions.java | 27 +- 5 files changed, 73 insertions(+), 429 deletions(-) diff --git a/grails-app/conf/BuildConfig.groovy b/grails-app/conf/BuildConfig.groovy index 62a65847..b6ed17bf 100644 --- a/grails-app/conf/BuildConfig.groovy +++ b/grails-app/conf/BuildConfig.groovy @@ -100,7 +100,9 @@ grails.project.dependency.resolution = { 'org.codehaus.woodstox:wstx-asl:3.2.9', 'jfree:jfreechart:1.0.13', 'org.json:json:20090211', - 'org.mapdb:mapdb:0.9.1' + 'org.mapdb:mapdb:0.9.1', + 'org.pac4j:pac4j-core:1.6.0', + 'org.pac4j:pac4j-saml:1.6.0' ) { // Exclude superfluous and dangerous transitive dependencies excludes( // Some libraries bring older versions of JUnit as a transitive dependency and that can interfere @@ -108,7 +110,9 @@ grails.project.dependency.resolution = { 'junit', 'mockito-core', - 'xercesImpl' + 'xercesImpl', + 'jcl-over-slf4j', + 'log4j-over-slf4j' ) } compile( diff --git a/src/java/com/netflix/ice/login/saml/Saml.java b/src/java/com/netflix/ice/login/saml/Saml.java index cabec0bf..d8c3bffc 100644 --- a/src/java/com/netflix/ice/login/saml/Saml.java +++ b/src/java/com/netflix/ice/login/saml/Saml.java @@ -17,6 +17,7 @@ import com.netflix.ice.common.IceOptions; import com.netflix.ice.common.IceSession; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.util.Collection; import java.util.Enumeration; import java.util.Properties; @@ -32,61 +33,20 @@ import java.io.StringReader; import org.apache.commons.io.FileUtils; -import org.w3c.dom.Document; -import org.w3c.dom.Element; +import org.pac4j.saml.credentials.Saml2Credentials; +import org.pac4j.saml.profile.Saml2Profile; +import org.pac4j.core.exception.RequiresHttpAction; +import org.pac4j.core.client.RedirectAction; +import org.pac4j.core.client.BaseClient; +import org.pac4j.saml.client.Saml2Client; +import org.pac4j.core.context.J2ERequestContext; +import org.pac4j.core.context.J2EContext; +import org.pac4j.core.context.WebContext; +import org.opensaml.common.xml.SAMLConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import org.opensaml.saml2.core.Attribute; -import org.opensaml.saml2.core.AttributeStatement; -import java.security.cert.CertificateFactory; -import java.security.cert.Certificate; -import java.security.KeyFactory; -import java.security.cert.X509Certificate; -import java.security.spec.X509EncodedKeySpec; -import java.security.PublicKey; -//import org.opensaml.xml.signature.PublicKey; -import org.opensaml.xml.security.credential.Credential; -import org.opensaml.xml.security.credential.BasicCredential; -import org.opensaml.xml.security.credential.UsageType; -import org.opensaml.xml.security.CriteriaSet; -import org.opensaml.xml.security.criteria.UsageCriteria; -import org.opensaml.xml.security.criteria.EntityIDCriteria; -import org.opensaml.common.xml.SAMLConstants; -import org.opensaml.saml2.metadata.IDPSSODescriptor; -import org.opensaml.security.MetadataCriteria; -import javax.xml.namespace.QName; -import javax.xml.validation.Schema; - -import org.opensaml.xml.security.SecurityHelper; -import org.opensaml.saml2.core.Assertion; -import org.opensaml.xml.XMLObject; -import org.opensaml.common.xml.SAMLSchemaBuilder; -import org.opensaml.xml.parse.BasicParserPool; -import org.opensaml.DefaultBootstrap; -import org.opensaml.xml.ConfigurationException; -import org.opensaml.saml2.metadata.provider.AbstractMetadataProvider; -import org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider; -import org.opensaml.security.MetadataCredentialResolver; -import org.opensaml.xml.security.keyinfo.KeyInfoCredentialResolver; -import org.opensaml.xml.signature.impl.ExplicitKeySignatureTrustEngine; -import org.opensaml.Configuration; -import org.opensaml.saml2.encryption.Decrypter; -import org.opensaml.saml2.core.EncryptedAssertion; -import org.opensaml.xml.encryption.DecryptionException; -import org.opensaml.xml.util.Base64; -import org.opensaml.saml2.metadata.provider.MetadataProviderException; -import org.opensaml.xml.encryption.InlineEncryptedKeyResolver; -import org.opensaml.xml.io.UnmarshallerFactory; -import org.opensaml.xml.io.Unmarshaller; -import org.opensaml.saml2.core.Response; -import org.opensaml.xml.signature.Signature; -import org.opensaml.xml.signature.SignatureValidator; -import org.opensaml.xml.validation.ValidationException; -import org.opensaml.security.SAMLSignatureProfileValidator; - - /** * SAML Plugin */ @@ -97,8 +57,7 @@ public class Saml extends LoginMethod { private static final Logger logger = LoggerFactory.getLogger(Saml.class); private final SamlConfig config; - - private List trustedSigningCerts=new ArrayList(); + private final Saml2Client client = new Saml2Client(); public String propertyName(String name) { return SAML_PREFIX + "." + name; @@ -107,24 +66,14 @@ public String propertyName(String name) { public Saml(Properties properties) throws LoginMethodException { super(properties); config = new SamlConfig(properties); - for(String signingCert : config.trustedSigningCerts) { - try { - FileInputStream fis = new FileInputStream(new File(signingCert)); - CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); - Certificate cert = certFactory.generateCertificate(fis); - trustedSigningCerts.add(cert); - } catch(IOException ioe) { - logger.error("Error reading public key " + signingCert + ":" + ioe.toString()); - } catch(Exception e) { - logger.error("Error decoding public key " + signingCert + ":" + e.toString()); - } - } - - try { - DefaultBootstrap.bootstrap(); - } catch(ConfigurationException ce) { - throw new LoginMethodException("Failure to init OpenSAML: " + ce.toString()); + if (config.serviceIdentifier != null) { + client.setSpEntityId(config.serviceIdentifier); } + client.setIdpMetadataPath(config.idpMetadataPath); + client.setCallbackUrl(config.signInUrl); + client.setKeystorePath(config.keystore); + client.setKeystorePassword(config.keystorePassword); + client.setPrivateKeyPassword(config.keyPassword); } public LoginResponse processLogin(HttpServletRequest request) throws LoginMethodException { @@ -132,91 +81,31 @@ public LoginResponse processLogin(HttpServletRequest request) throws LoginMethod iceSession.voidSession(); //a second login request voids anything previous logger.info("Saml::processLogin"); LoginResponse lr = new LoginResponse(); - String assertion = (String)request.getParameter("SAMLResponse"); - if (assertion == null) { - lr.redirectTo=config.singleSignOnUrl; - return lr; - } - logger.trace("Received SAML Assertion: " + assertion); - try - { - // 1.1 2.0 schemas - Schema schema = SAMLSchemaBuilder.getSAML11Schema(); - - //get parser pool manager - BasicParserPool parserPoolManager = new BasicParserPool(); - parserPoolManager.setNamespaceAware(true); - parserPoolManager.setIgnoreElementContentWhitespace(true); - parserPoolManager.setSchema(schema); - - String data = new String(Base64.decode(assertion)); - logger.info("Decoded SAML Assertion: " + data); - - StringReader reader = new StringReader(data); - Document document = parserPoolManager.parse(reader); - Element documentRoot = document.getDocumentElement(); + //String assertion = (String)request.getParameter("SAMLResponse"); + final WebContext context = new J2ERequestContext(request); + client.setCallbackUrl(config.signInUrl); - QName qName= new QName(documentRoot.getNamespaceURI(), documentRoot.getLocalName(), documentRoot.getPrefix()); - - //get an unmarshaller - Unmarshaller unmarshaller = Configuration.getUnmarshallerFactory().getUnmarshaller(documentRoot); - - //unmarshall using the document root element - XMLObject xmlObj = unmarshaller.unmarshall(documentRoot); - Response response = (Response)xmlObj; - for(Assertion myAssertion : response.getAssertions()) - { - if (! myAssertion.isSigned()) { - logger.error("SAML Assertion not signed" ); - throw new LoginMethodException("SAML Assertions must be signed by a trusted provider"); - } - - Signature assertionSignature = myAssertion.getSignature(); - SAMLSignatureProfileValidator profVal = new SAMLSignatureProfileValidator(); - - logger.info("Validating SAML Assertion" ); - // will throw a ValidationException - profVal.validate(assertionSignature); - - //Credential signCred = assertionSignature.getSigningCredential(); - boolean goodSignature = false; - for(Certificate trustedCert : trustedSigningCerts) { - BasicCredential cred = new BasicCredential(); - cred.setPublicKey(trustedCert.getPublicKey()); - SignatureValidator validator = new SignatureValidator(cred); - try { - validator.validate(assertionSignature); - } catch(ValidationException ve) { - /* Not a good key! */ - logger.debug("Not signed by " + trustedCert.toString()); - continue; - } - logger.info("Assertion trusted from " + trustedCert.toString()); - processAssertion(iceSession, myAssertion, lr); - goodSignature = true; - break; - } + //logger.trace("Received SAML Assertion: " + assertion); + // get SAML2 credentials + try { + Saml2Credentials credentials = client.getCredentials(context); + Saml2Profile saml2Profile = client.getUserProfile(credentials, context); + logger.info("Credentials: " + credentials.toString()); + } catch (RequiresHttpAction rha) { + try { + lr.redirectTo=client.getRedirectAction(context, false, false).getLocation(); + return lr; + } catch (RequiresHttpAction rhae) { } - if (goodSignature) { - lr.loginSuccess=true; - } - - } - } catch(org.xml.sax.SAXException saxe) { - logger.error(saxe.toString()); - } catch(org.opensaml.xml.parse.XMLParserException xmlpe) { - logger.error(xmlpe.toString()); - } catch(org.opensaml.xml.io.UnmarshallingException uee) { - logger.error(uee.toString()); - } catch(org.opensaml.xml.validation.ValidationException ve) { - throw new LoginMethodException("SAML Assertion Signature was not usable: " + ve.toString()); } return lr; } + /** * Process an assertion and setup our session attributes */ +/* private void processAssertion(IceSession iceSession, Assertion assertion, LoginResponse lr) throws LoginMethodException { boolean foundAnAccount=false; iceSession.voidSession(); @@ -272,5 +161,6 @@ private void processAssertion(IceSession iceSession, Assertion assertion, LoginR lr.loginStart = startDate.toCalendar(null).getTime(); lr.loginEnd = endDate.toCalendar(null).getTime(); } +*/ } diff --git a/src/java/com/netflix/ice/login/saml/SamlConfig.java b/src/java/com/netflix/ice/login/saml/SamlConfig.java index fa88c532..95281d7c 100644 --- a/src/java/com/netflix/ice/login/saml/SamlConfig.java +++ b/src/java/com/netflix/ice/login/saml/SamlConfig.java @@ -39,47 +39,25 @@ public class SamlConfig implements BaseConfig { private static final Logger logger = LoggerFactory.getLogger(SamlConfig.class); - public final List trustedSigningCerts = new ArrayList(); - public final List trustedMetadata = new ArrayList(); - - private final String keystore; - private final String keystorePassword; - private final String keyAlias; - private final String keyPassword; - public final String organizationName; - public final String organizationDisplayName; - public final String organizationUrl; + public final String keystore; + public final String keystorePassword; + public final String keyAlias; + public final String keyPassword; public final String signInUrl; - public final String serviceName; public final String allAccounts; public final String singleSignOnUrl; + public final String serviceIdentifier; + public final String idpMetadataPath; public SamlConfig(Properties properties) { - loadSigningCerts(properties); keystore = properties.getProperty(SamlOptions.KEYSTORE); keystorePassword = properties.getProperty(SamlOptions.KEYSTORE_PASSWORD); keyAlias = properties.getProperty(SamlOptions.KEY_ALIAS); keyPassword = properties.getProperty(SamlOptions.KEY_PASSWORD); - organizationName = properties.getProperty(SamlOptions.ORGANIZATION_NAME); - organizationDisplayName = properties.getProperty(SamlOptions.ORGANIZATION_DISPLAY_NAME); - organizationUrl = properties.getProperty(SamlOptions.ORGANIZATION_URL); + serviceIdentifier = properties.getProperty(SamlOptions.SERVICE_IDENTIFIER); signInUrl = properties.getProperty(SamlOptions.SIGNIN_URL); - serviceName = properties.getProperty(SamlOptions.SERVICE_NAME); allAccounts = properties.getProperty(SamlOptions.ALL_ACCOUNTS); singleSignOnUrl = properties.getProperty(SamlOptions.SINGLE_SIGN_ON_URL); - loadSigningCerts(properties); - - } - - private void loadSigningCerts(Properties properties) { - String trustedCerts = properties.getProperty(SamlOptions.TRUSTED_SIGNING_CERTS); - if (trustedCerts == null) { - logger.warn("No Trusted Certs found"); - return; - } - String[] certLocations = trustedCerts.split(","); - for(String certLocation : certLocations) { - trustedSigningCerts.add(certLocation); - } + idpMetadataPath = properties.getProperty(SamlOptions.IDP_METADATA_PATH); } } diff --git a/src/java/com/netflix/ice/login/saml/SamlMetaData.java b/src/java/com/netflix/ice/login/saml/SamlMetaData.java index ecf54d05..f746be75 100644 --- a/src/java/com/netflix/ice/login/saml/SamlMetaData.java +++ b/src/java/com/netflix/ice/login/saml/SamlMetaData.java @@ -27,50 +27,10 @@ import java.io.StringWriter; import java.io.IOException; -import org.w3c.dom.Document; -import org.w3c.dom.Element; -import javax.xml.namespace.QName; -import javax.xml.validation.Schema; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; -import javax.xml.transform.*; -import javax.xml.parsers.*; -import javax.xml.parsers.ParserConfigurationException; - -import org.opensaml.saml2.metadata.*; -import org.opensaml.xml.security.keyinfo.*; -import org.opensaml.xml.security.credential.*; -import org.opensaml.xml.security.x509.*; -import org.opensaml.xml.*; - -import groovy.text.SimpleTemplateEngine; -import org.opensaml.DefaultBootstrap; -import org.opensaml.xml.ConfigurationException; -import org.opensaml.saml2.metadata.provider.MetadataProvider; -import org.opensaml.saml2.metadata.provider.FilesystemMetadataProvider; -import org.opensaml.security.MetadataCredentialResolver; -import org.opensaml.xml.security.keyinfo.KeyInfoCredentialResolver; -import org.opensaml.xml.signature.impl.ExplicitKeySignatureTrustEngine; -import org.opensaml.Configuration; -import org.opensaml.saml1.core.Assertion; -import org.opensaml.saml2.encryption.Decrypter; -import org.opensaml.saml2.core.EncryptedAssertion; -import org.opensaml.xml.encryption.DecryptionException; -import org.opensaml.xml.util.Base64; -import org.opensaml.saml2.metadata.provider.MetadataProviderException; -import org.opensaml.xml.encryption.InlineEncryptedKeyResolver; -import org.opensaml.xml.io.Marshaller; +import org.pac4j.core.client.BaseClient; +import org.pac4j.saml.client.Saml2Client; import org.opensaml.common.xml.SAMLConstants; -import org.opensaml.xml.security.SecurityException; -import java.io.FileInputStream; -import java.security.Key; -import java.security.KeyPair; -import java.security.KeyStore; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.cert.Certificate; -import java.security.cert.X509Certificate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -83,16 +43,8 @@ public class SamlMetaData extends LoginMethod { public final String SAML_PREFIX=propertyPrefix("saml"); Logger logger = LoggerFactory.getLogger(getClass()); - public String metadataXML = null; - public final String keystore; - public final String keystorePassword; - public final String keyAlias; - public final String keyPassword; - public final String orgName; - public final String orgDisplayName; - public final String orgUrl; - public final String signinUrl; - public final String serviceName; + private final SamlConfig config; + private final Saml2Client client = new Saml2Client(); public String propertyName(String name) { return SAML_PREFIX + "." + name; @@ -100,192 +52,23 @@ public String propertyName(String name) { public SamlMetaData(Properties properties) throws LoginMethodException { super(properties); - keystore=propertyValue("keystore"); - keystorePassword=propertyValue("keystore_password"); - keyAlias=propertyValue("key_alias"); - keyPassword=propertyValue("key_password"); - orgName=propertyValue("org_name"); - orgDisplayName=propertyValue("org_display_name"); - orgUrl=propertyValue("org_url"); - signinUrl=propertyValue("signin_url"); - serviceName=propertyValue("service_name"); + config = new SamlConfig(properties); + if (config.serviceIdentifier != null) { + client.setSpEntityId(config.serviceIdentifier); + } + client.setIdpMetadataPath(config.idpMetadataPath); + client.setCallbackUrl(config.signInUrl); + client.setKeystorePath(config.keystore); + client.setKeystorePassword(config.keystorePassword); + client.setPrivateKeyPassword(config.keyPassword); } public LoginResponse processLogin(HttpServletRequest request) throws LoginMethodException { LoginResponse lr = new LoginResponse(); - if (metadataXML == null) { - generateMetadata(); - } - lr.renderData = metadataXML; + lr.renderData = client.printClientMetadata(); +; lr.contentType = "application/samlmetadata+xml"; return lr; } - - /** - * Wow! - * from: http://mylifewithjava.blogspot.com/2012/02/generating-metadata-with-opensaml.html - */ - private synchronized void generateMetadata() { - if (metadataXML != null) { return; } - - EntityDescriptor spEntityDescriptor = createSAMLObject(EntityDescriptor.class); - spEntityDescriptor.setEntityID("netflix ice"); - - Organization organization = createSAMLObject(Organization.class); - - OrganizationDisplayName samlOrgDisplayName = createSAMLObject(OrganizationDisplayName.class); - samlOrgDisplayName.setName(new LocalizedString(orgDisplayName, "en")); - organization.getDisplayNames().add(samlOrgDisplayName); - - OrganizationName samlOrgName = createSAMLObject(OrganizationName.class); - samlOrgName.setName(new LocalizedString(orgName, "en")); - organization.getOrganizationNames().add(samlOrgName); - - OrganizationURL samlOrgUrl = createSAMLObject(OrganizationURL.class); - samlOrgUrl.setURL(new LocalizedString(orgUrl,"en")); - organization.getURLs().add(samlOrgUrl); - - spEntityDescriptor.setOrganization(organization); - - SPSSODescriptor spSSODescriptor = createSAMLObject(SPSSODescriptor.class); - spSSODescriptor.setWantAssertionsSigned(true); - - X509KeyInfoGeneratorFactory keyInfoGeneratorFactory = new X509KeyInfoGeneratorFactory(); - keyInfoGeneratorFactory.setEmitEntityCertificate(true); - KeyInfoGenerator keyInfoGenerator = keyInfoGeneratorFactory.newInstance(); - - //spSSODescriptor.setAuthnRequestsSigned(true); - //KeyDescriptor encKeyDescriptor = createSAMLObject(KeyDescriptor.class); - - //encKeyDescriptor.setUse(UsageType.ENCRYPTION); //Set usage - - //try { - // encKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(null)); - //} catch (SecurityException e) { - // logger.error(e.getMessage(), e); - //} - - //spSSODescriptor.getKeyDescriptors().add(encKeyDescriptor); - - KeyDescriptor signKeyDescriptor = createSAMLObject(KeyDescriptor.class); - - signKeyDescriptor.setUse(UsageType.SIGNING); //Set usage - - try { - BasicX509Credential creds = new BasicX509Credential(); - creds.setEntityCertificate(signingKey()); - signKeyDescriptor.setKeyInfo(keyInfoGenerator.generate(creds)); - } catch (SecurityException e) { - logger.error(e.getMessage(), e); - } catch (Exception e) { - logger.error(e.getMessage(), e); - } - - spSSODescriptor.getKeyDescriptors().add(signKeyDescriptor); - NameIDFormat nameIDFormat = createSAMLObject(NameIDFormat.class); - nameIDFormat.setFormat("urn:oasis:names:tc:SAML:2.0:nameid-format:transient"); - spSSODescriptor.getNameIDFormats().add(nameIDFormat); - AssertionConsumerService assertionConsumerService = createSAMLObject(AssertionConsumerService.class); - assertionConsumerService.setIndex(0); - //assertionConsumerService.setBinding(SAMLConstants.SAML2_ARTIFACT_BINDING_URI); - assertionConsumerService.setBinding(SAMLConstants.SAML2_POST_BINDING_URI); - - assertionConsumerService.setLocation(signinUrl); - spSSODescriptor.getAssertionConsumerServices().add(assertionConsumerService); - spSSODescriptor.addSupportedProtocol(SAMLConstants.SAML20P_NS); - - spEntityDescriptor.getRoleDescriptors().add(spSSODescriptor); - - AttributeConsumingService service = createSAMLObject(AttributeConsumingService.class); - ServiceName name = createSAMLObject(ServiceName.class); - name.setName(new LocalizedString(serviceName, "en")); - service.getNames().add(name); - service.setIndex(0); - service.setIsDefault(true); - - RequestedAttribute accountAttr = createSAMLObject(RequestedAttribute.class); - accountAttr.setFriendlyName("AccountID"); - accountAttr.setName("com.netflix.ice.account"); - - service.getRequestAttributes().add(accountAttr); - - spSSODescriptor.getAttributeConsumingServices().add(service); - - DocumentBuilder builder = null; - DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - try { - builder = factory.newDocumentBuilder(); - } catch(javax.xml.parsers.ParserConfigurationException pce) { - logger.error(pce.toString()); - } - Document document = builder.newDocument(); - Marshaller out = Configuration.getMarshallerFactory().getMarshaller(spEntityDescriptor); - - try { - out.marshall(spEntityDescriptor, document); - } catch(org.opensaml.xml.io.MarshallingException me) { - logger.error(me.toString()); - } - Transformer transformer = null; - try { - transformer = TransformerFactory.newInstance().newTransformer(); - } catch(javax.xml.transform.TransformerConfigurationException tce) { - logger.error(tce.toString()); - } - - StringWriter stringWriter = new StringWriter(); - - try { - StreamResult streamResult = new StreamResult(stringWriter); - - DOMSource source = new DOMSource(document); - transformer.transform(source, streamResult); - stringWriter.close(); - } catch(IOException ioe) { - logger.error(ioe.toString()); - return; - } catch(javax.xml.transform.TransformerException te) { - logger.error(te.toString()); - return; - } - metadataXML = stringWriter.toString(); - } - - /** - * from: http://mylifewithjava.blogspot.no/2011/04/convenience-methods-for-opensaml.html - */ - public static T createSAMLObject(final Class clazz) { - XMLObjectBuilderFactory builderFactory = Configuration.getBuilderFactory(); - QName defaultElementName = null; - try { - defaultElementName = (QName)clazz.getDeclaredField("DEFAULT_ELEMENT_NAME").get(null); - } catch(IllegalAccessException iae) { - System.out.println(iae.toString()); - return null; - } catch(java.lang.NoSuchFieldException nsfe) { - System.out.println(nsfe.toString()); - return null; - } - T object = (T)builderFactory.getBuilder(defaultElementName).buildObject(defaultElementName); - return object; - } - - - private X509Certificate signingKey() throws Exception { - FileInputStream is = new FileInputStream(keystore); - - KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); - keystore.load(is, keystorePassword.toCharArray()); - - Key key = keystore.getKey(keyAlias, keyPassword.toCharArray()); - System.out.println(key.toString()); - if (key instanceof PrivateKey) { - System.out.println("Got Here"); - // Get certificate of public key - X509Certificate cert = (X509Certificate)keystore.getCertificate(keyAlias); - return cert; - } - return null; - } } diff --git a/src/java/com/netflix/ice/login/saml/SamlOptions.java b/src/java/com/netflix/ice/login/saml/SamlOptions.java index ec98c413..af721cfc 100644 --- a/src/java/com/netflix/ice/login/saml/SamlOptions.java +++ b/src/java/com/netflix/ice/login/saml/SamlOptions.java @@ -29,27 +29,12 @@ public class SamlOptions { /** * Signin-Url for our service */ - public static final String SIGNIN_URL = SAML + ".signing_url"; + public static final String SIGNIN_URL = SAML + ".signin_url"; /** - * Property for Service Name - */ - public static final String SERVICE_NAME = SAML + ".service_name"; - - /** - * Property for organization name. - */ - public static final String ORGANIZATION_NAME = SAML + ".organization_name"; - - /** - * Property for organization display name. - */ - public static final String ORGANIZATION_DISPLAY_NAME = SAML + ".organization_display_name"; - - /** - * Property for organization url - */ - public static final String ORGANIZATION_URL = SAML + ".organization_url"; + * Service/Entity Identifier + */ + public static final String SERVICE_IDENTIFIER = SAML + ".service_identifier"; /** * Property for Keystore where we can find certificates @@ -87,5 +72,9 @@ public class SamlOptions { * SAML creds */ public static final String SINGLE_SIGN_ON_URL = SAML + ".single_sign_on_url"; + /** + * Path to IDP Metdata + */ + public static final String IDP_METADATA_PATH = SAML + ".idp_metadata_path"; } From 3007c2dae85e1ea337716c963a55a9966f60c61e Mon Sep 17 00:00:00 2001 From: Anthony Johnson Date: Mon, 2 Mar 2015 16:34:53 -0500 Subject: [PATCH 04/10] Add missing controller --- web-app/WEB-INF/grails.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web-app/WEB-INF/grails.xml b/web-app/WEB-INF/grails.xml index c1815056..9e637955 100644 --- a/web-app/WEB-INF/grails.xml +++ b/web-app/WEB-INF/grails.xml @@ -33,10 +33,11 @@ TrackingFilters UrlMappings com.netflix.ice.DashboardController + com.netflix.ice.LoginController HibernateGrailsPlugin SpringSecurityCoreGrailsPlugin SpringSecurityLdapGrailsPlugin - \ No newline at end of file + From 9d20c5e43d2c2201775246621d62ad9f441c4792 Mon Sep 17 00:00:00 2001 From: Anthony Johnson Date: Wed, 11 Mar 2015 16:40:29 -0400 Subject: [PATCH 05/10] - Finalized pac4j implementation. - Removed login timeframes - added SAML documentation --- README.md | 46 +++++- .../com/netflix/ice/LoginController.groovy | 8 +- .../com/netflix/ice/common/IceSession.java | 42 ----- .../com/netflix/ice/login/LoginMethod.java | 3 +- .../com/netflix/ice/login/LoginResponse.java | 20 ++- .../com/netflix/ice/login/Passphrase.java | 9 +- src/java/com/netflix/ice/login/saml/Saml.java | 144 ++++++++++-------- .../netflix/ice/login/saml/SamlConfig.java | 2 + .../netflix/ice/login/saml/SamlOptions.java | 9 +- 9 files changed, 156 insertions(+), 127 deletions(-) diff --git a/README.md b/README.md index 6042872b..15d40d7a 100644 --- a/README.md +++ b/README.md @@ -251,20 +251,62 @@ A Framework exists for supplying authentication plugins. The following properti ice.login.classes=com.netflix.ice.login.Passphrase # Logging Names, comma delmited. These map to a handler above - # The name here will expose an http endpoint. + # The name here will expose an http endpoint. # http://.../ice/login/handler/passphrase ice.login.endpoints=passphrase - # Passphrase for the Passphrase Implementation + # Passphrase for the Passphrase Implementation. This would grant access + # to all data ice.login.passphrase=rar # Default Endpoint(where /login/ takes us) ice.login.default_endpoint=passphrase + # Login Log file(audit log) ice.login.log=/some/path + + # Message to be displayed if the user has no access + ice.login.no_access_message=You do not have access to view any billing data. Please see Passphrase is simply a reference implementation that guards your ice data with a passphrase(ice.login.passphrase). To create your own login handler, you can extend the LoginMethod. +### SAML Plugin + +A SAML Plugin was written that has been verified against ADFS. The SAML Assertion needs a custom attribute/claim which is named "com.netflix.ice.account" which is a list of account ids to grant access to. You can utilize the *ice.login.saml.all_accounts* to select a value that will give access to all billing data. + +Configuration Properties: + + + # SAML Login Classes. + ice.login.classes=com.netflix.ice.login.saml.Saml,com.netflix.ice.login.saml.SamlMetaData + + # Map Handlers + ice.login.endpoints=saml,metadata.xml + + # Ensure that we use SAML by default + ice.login.default_endpoint=saml + + # Path to your IDP metadata. We do not support http + ice.login.saml.idp_metadata_path=/path/to/idp_metadata + + # Our Certificate to use for Signing + ice.login.saml.keystore=/path/to/keystore + ice.login.saml.keystore_password=pac4j-demo-passwd + ice.login.saml.key_alias=pac4j-demo + ice.login.saml.key_password=pac4j-demo-passwd + + # com.netflix.ice.account attribute value that will give the user access + # to all billing data + ice.login.saml.all_accounts=ADMIN + + # Local URL for SAML sign-in. + ice.login.saml.signin_url=https://ice.domain.com/ice/login/handler/saml + + # Our service identifier. Typically the web address of the service + ice.login.saml.service_identifier=https://ice.domain.com + +A SAML attribute(com.netflix.ice.account) should contain a list of Account Ids that the user has access to. If no accounts are given then the user will be denied. If you don't wish to filter the accounts that the user has access to then you can simply issue "com.netflix.ice.account":"ADMIN" for the SAML Assertion. + ##Support Please use the [Ice Google Group](https://groups.google.com/d/forum/iceusers) for general questions and discussion. diff --git a/grails-app/controllers/com/netflix/ice/LoginController.groovy b/grails-app/controllers/com/netflix/ice/LoginController.groovy index 340dafed..6d2af809 100644 --- a/grails-app/controllers/com/netflix/ice/LoginController.groovy +++ b/grails-app/controllers/com/netflix/ice/LoginController.groovy @@ -53,16 +53,18 @@ class LoginController { if (loginMethod == null) { redirect(action: "error"); } - LoginResponse loginResponse = loginMethod.processLogin(request); + LoginResponse loginResponse = loginMethod.processLogin(request, response); - if (loginResponse.redirectTo != null) { + if (loginResponse.responded) { + // no-op + return null; + } else if (loginResponse.redirectTo != null) { redirect(url: loginResponse.redirectTo); } else if (loginResponse.loggedOut) { redirect(action: "logout"); } else if (loginResponse.loginSuccess) { IceSession iceSession = new IceSession(session); iceSession.authenticate(new Boolean(true)); - iceSession.setAllowTime(loginResponse.loginStart, loginResponse.loginEnd); if (iceSession.authenticated) { //ensure we are good redirect(controller: "dashboard"); } else { diff --git a/src/java/com/netflix/ice/common/IceSession.java b/src/java/com/netflix/ice/common/IceSession.java index 5185d372..5a9df3dd 100644 --- a/src/java/com/netflix/ice/common/IceSession.java +++ b/src/java/com/netflix/ice/common/IceSession.java @@ -35,8 +35,6 @@ public class IceSession { private final String ADMIN_SESSION_KEY = "admin"; private final String ALLOWED_ACCOUNT_SESSION_PREFIX_KEY = "allowed_account"; private final String ALLOWED_ACCOUNTS = "allowed_accounts"; - private final String START_DATE = "start_date"; - private final String END_DATE = "end_date"; private final HttpSession session; public IceSession(HttpSession session) { @@ -73,48 +71,10 @@ public Boolean isAuthenticated() { } else if (! authd.booleanValue()) { logger.error("User has been explicitly denied - " + authd); return false; - } else if (! withinAllowTime()) { - logger.error("User's allow time has expired"); - return false; } return true; } - /** - * Set the time at which this session is valid. This is required. - * @param notBefore - * @param notAfter - */ - public void setAllowTime(Date notBefore, Date notAfter) { - if (notBefore != null && notAfter != null) { - logger.info("Allow Time: " + notBefore.toString() + " to " + notAfter.toString()); - } else { - logger.info("Set Allow Time to null"); - } - session.setAttribute(START_DATE, notBefore); - session.setAttribute(END_DATE, notAfter); - } - - /* - * Has this Session expired? - */ - public boolean withinAllowTime() { - logger.debug("Within Allow Time?"); - Date notBefore = (Date)session.getAttribute(START_DATE); - Date notAfter = (Date)session.getAttribute(END_DATE); - if (notBefore == null || notAfter == null) { - logger.error("Session has no allow time"); - return false; - } - Date now = new Date(); - if ((now.after(notBefore)) && (now.before(notAfter))) { - return true; - } - logger.info(now.toString() + " is not between " + notBefore.toString() + " - " + notAfter.toString()); - return false; - } - - /** * 100% invalidate this session so it cannot be used for login. */ @@ -131,8 +91,6 @@ public void voidSession() { iter.remove(); } } - - setAllowTime(null,null); } /** diff --git a/src/java/com/netflix/ice/login/LoginMethod.java b/src/java/com/netflix/ice/login/LoginMethod.java index 94fb3f9c..ebc38587 100644 --- a/src/java/com/netflix/ice/login/LoginMethod.java +++ b/src/java/com/netflix/ice/login/LoginMethod.java @@ -15,6 +15,7 @@ package com.netflix.ice.login; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.util.Collection; import java.util.Properties; import java.util.Map; @@ -44,7 +45,7 @@ public LoginMethod(Properties properties) throws LoginMethodException { this.properties = properties; } - public abstract LoginResponse processLogin(HttpServletRequest request) throws LoginMethodException; + public abstract LoginResponse processLogin(HttpServletRequest request, HttpServletResponse response) throws LoginMethodException; public String propertyPrefix(String name) { return LoginOptions.LOGIN_PREFIX + "." + name; diff --git a/src/java/com/netflix/ice/login/LoginResponse.java b/src/java/com/netflix/ice/login/LoginResponse.java index 77ca9050..349d8b6d 100644 --- a/src/java/com/netflix/ice/login/LoginResponse.java +++ b/src/java/com/netflix/ice/login/LoginResponse.java @@ -24,6 +24,9 @@ */ public class LoginResponse { + /**Did the handler respond to this(no action from the Controller) */ + public boolean responded=false; + /** Was the Login Successful */ public boolean loginSuccess=false; @@ -48,9 +51,16 @@ public class LoginResponse /** Variables to pass to templateFile or renderData */ public Map templateBindings; - /** When to allow this login(required) */ - public Date loginStart; - - /** When to end this login(required) */ - public Date loginEnd; + public String toString() { + return "LoginResponse [" + + "responseHandled: " + responded + ", " + + "loginSuccess: " + loginSuccess + ", " + + "loginFailed: " + loginFailed + ", " + + "loggedOut: " + loggedOut + ", " + + "redirectTo: " + redirectTo + ", " + + "renderData: " + renderData + ", " + + "contentType: " + contentType + ", " + + "TemplateFile: " + templateFile + ", " + + "]"; + } } diff --git a/src/java/com/netflix/ice/login/Passphrase.java b/src/java/com/netflix/ice/login/Passphrase.java index 80f7a341..1aadb081 100644 --- a/src/java/com/netflix/ice/login/Passphrase.java +++ b/src/java/com/netflix/ice/login/Passphrase.java @@ -18,6 +18,7 @@ import com.netflix.ice.common.IceSession; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.util.Collection; import java.util.Properties; import java.util.Map; @@ -46,7 +47,7 @@ public String propertyName(String name) { return PASSPHRASE_PREFIX + "." + name; } - public LoginResponse processLogin(HttpServletRequest request) throws LoginMethodException { + public LoginResponse processLogin(HttpServletRequest request, HttpServletResponse response) throws LoginMethodException { LoginResponse lr = new LoginResponse(); String userPassphrase = (String)request.getParameter("passphrase"); @@ -68,12 +69,6 @@ public LoginResponse processLogin(HttpServletRequest request) throws LoginMethod whitelistAllAccounts(iceSession); // allow user lr.loginSuccess=true; - Date now = new Date(); - lr.loginStart=now; - Calendar cal = Calendar.getInstance(); - cal.setTime(now); - cal.add(Calendar.DATE, 1); //valid for one day - lr.loginEnd=cal.getTime(); } else { lr.loginFailed=true; } diff --git a/src/java/com/netflix/ice/login/saml/Saml.java b/src/java/com/netflix/ice/login/saml/Saml.java index d8c3bffc..48fe4450 100644 --- a/src/java/com/netflix/ice/login/saml/Saml.java +++ b/src/java/com/netflix/ice/login/saml/Saml.java @@ -13,25 +13,14 @@ * */ package com.netflix.ice.login.saml; + import com.netflix.ice.login.*; -import com.netflix.ice.common.IceOptions; import com.netflix.ice.common.IceSession; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import java.util.Collection; -import java.util.Enumeration; import java.util.Properties; -import java.util.Map; -import java.util.List; -import java.util.ArrayList; -import java.util.Date; -import org.joda.time.DateTime; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.net.URL; -import java.io.StringReader; -import org.apache.commons.io.FileUtils; +import javax.servlet.http.HttpServletRequestWrapper; +import org.opensaml.ws.message.decoder.MessageDecodingException; import org.pac4j.saml.credentials.Saml2Credentials; import org.pac4j.saml.profile.Saml2Profile; @@ -43,6 +32,8 @@ import org.pac4j.core.context.J2EContext; import org.pac4j.core.context.WebContext; import org.opensaml.common.xml.SAMLConstants; +import org.opensaml.saml2.core.Attribute; +import org.opensaml.xml.XMLObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,6 +43,23 @@ */ public class Saml extends LoginMethod { + // Grails getRequestUrl is an internal dispatch url which isn't + // very friendly to the user + private class SamlHttpServletRequest extends HttpServletRequestWrapper { + private final String requestUrl; + SamlHttpServletRequest(HttpServletRequest request, String requestUrl) { + super(request); + this.requestUrl=requestUrl; + } + + @Override + public StringBuffer getRequestURL() { + StringBuffer sb = new StringBuffer(); + sb.append(requestUrl); + return sb; + } + } + public final String SAML_PREFIX=propertyPrefix("saml"); private static final Logger logger = LoggerFactory.getLogger(Saml.class); @@ -74,30 +82,50 @@ public Saml(Properties properties) throws LoginMethodException { client.setKeystorePath(config.keystore); client.setKeystorePassword(config.keystorePassword); client.setPrivateKeyPassword(config.keyPassword); + client.setMaximumAuthenticationLifetime(Integer.parseInt(config.maximumAuthenticationLifetime)); } - public LoginResponse processLogin(HttpServletRequest request) throws LoginMethodException { + public LoginResponse processLogin(HttpServletRequest request, HttpServletResponse response) throws LoginMethodException { IceSession iceSession = new IceSession(request.getSession()); iceSession.voidSession(); //a second login request voids anything previous logger.info("Saml::processLogin"); LoginResponse lr = new LoginResponse(); - //String assertion = (String)request.getParameter("SAMLResponse"); - final WebContext context = new J2ERequestContext(request); - client.setCallbackUrl(config.signInUrl); - //logger.trace("Received SAML Assertion: " + assertion); - // get SAML2 credentials + SamlHttpServletRequest shsr = new SamlHttpServletRequest(request, config.signInUrl); + final WebContext context = new J2ERequestContext(shsr); + client.setCallbackUrl(config.signInUrl); + boolean redirect = false; try { Saml2Credentials credentials = client.getCredentials(context); Saml2Profile saml2Profile = client.getUserProfile(credentials, context); - logger.info("Credentials: " + credentials.toString()); - } catch (RequiresHttpAction rha) { + processAssertion(iceSession, credentials, lr); + } catch (NullPointerException npe) { + redirect = true; + } catch (RequiresHttpAction rha) { + redirect = true; + } catch (Exception e) { + redirect = true; + } + if (redirect) { try { - lr.redirectTo=client.getRedirectAction(context, false, false).getLocation(); - return lr; - } catch (RequiresHttpAction rhae) { } - + logger.info("Redirect user to SSO"); + if (config.singleSignOnUrl != null) { + //redirect to SSO using a static URL + lr.redirectTo=config.singleSignOnUrl; + } else { + //try redirect using Pac4j library. Not sure if this will work. + final WebContext redirect_context = new J2EContext(shsr, response); + client.redirect(redirect_context, false, false); + lr.responded = true; + } + } catch (RequiresHttpAction rhae) { + logger.error(rhae.toString()); + } + catch (NullPointerException npe) { + logger.error(npe.toString()); + } } + logger.debug("Login Response: " + lr.toString()); return lr; } @@ -105,35 +133,32 @@ public LoginResponse processLogin(HttpServletRequest request) throws LoginMethod /** * Process an assertion and setup our session attributes */ -/* - private void processAssertion(IceSession iceSession, Assertion assertion, LoginResponse lr) throws LoginMethodException { + private void processAssertion(IceSession iceSession, Saml2Credentials credentials, LoginResponse lr) throws LoginMethodException { boolean foundAnAccount=false; iceSession.voidSession(); - for(AttributeStatement as : assertion.getAttributeStatements()) { - // iterate once to assure we set the username first - for(Attribute attr : as.getAttributes()) { - if (attr.getName().equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name")) { - for(XMLObject groupXMLObj : attr.getAttributeValues()) { - String username = groupXMLObj.getDOM().getTextContent(); - iceSession.setUsername(username); - } + + for(Attribute attr : credentials.getAttributes()) { + if (attr.getName().equals("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name")) { + for (XMLObject groupXMLObj : attr.getAttributeValues()) { + String username = groupXMLObj.getDOM().getTextContent(); + iceSession.setUsername(username); } } - // iterate again for everything else - for(Attribute attr : as.getAttributes()) { - if (attr.getName().equals("com.netflix.ice.account")) { - for(XMLObject groupXMLObj : attr.getAttributeValues()) { - String allowedAccount = groupXMLObj.getDOM().getTextContent(); - if (allowedAccount.equals(config.allAccounts) ) { - whitelistAllAccounts(iceSession); + } + // iterate again for everything else + for(Attribute attr : credentials.getAttributes()) { + if (attr.getName().equals("com.netflix.ice.account")) { + for(XMLObject groupXMLObj : attr.getAttributeValues()) { + String allowedAccount = groupXMLObj.getDOM().getTextContent(); + if (allowedAccount.equals(config.allAccounts) ) { + whitelistAllAccounts(iceSession); + foundAnAccount=true; + logger.info("Found Allow All Accounts: " + allowedAccount); + break; + } else { + if (whitelistAccount(iceSession, allowedAccount)) { foundAnAccount=true; - logger.info("Found Allow All Accounts: " + allowedAccount); - break; - } else { - if (whitelistAccount(iceSession, allowedAccount)) { - foundAnAccount=true; - logger.info("Found Account: " + allowedAccount); - } + logger.info("Found Account: " + allowedAccount); } } } @@ -143,24 +168,11 @@ private void processAssertion(IceSession iceSession, Assertion assertion, LoginR //require at least one account if (! foundAnAccount) { lr.loginFailed=true; - //throw new LoginMethodException("SAML Assertion must give at least one Account as part of the Assertion"); return; + } else { + lr.loginSuccess=true; } - //set expiration date - DateTime startDate = assertion.getConditions().getNotBefore(); - DateTime endDate = assertion.getConditions().getNotOnOrAfter(); - if (startDate == null || endDate == null) { - throw new LoginMethodException("Assertion must state an expiration date"); - } - // Clocks may not be synchronized. - startDate = startDate.minusMinutes(2); - endDate = endDate.plusMinutes(2); - logger.info(startDate.toCalendar(null).getTime().toString()); - logger.info(endDate.toCalendar(null).getTime().toString()); - lr.loginStart = startDate.toCalendar(null).getTime(); - lr.loginEnd = endDate.toCalendar(null).getTime(); } -*/ -} +} diff --git a/src/java/com/netflix/ice/login/saml/SamlConfig.java b/src/java/com/netflix/ice/login/saml/SamlConfig.java index 95281d7c..56e78dd8 100644 --- a/src/java/com/netflix/ice/login/saml/SamlConfig.java +++ b/src/java/com/netflix/ice/login/saml/SamlConfig.java @@ -48,6 +48,7 @@ public class SamlConfig implements BaseConfig { public final String singleSignOnUrl; public final String serviceIdentifier; public final String idpMetadataPath; + public final String maximumAuthenticationLifetime; public SamlConfig(Properties properties) { keystore = properties.getProperty(SamlOptions.KEYSTORE); @@ -59,5 +60,6 @@ public SamlConfig(Properties properties) { allAccounts = properties.getProperty(SamlOptions.ALL_ACCOUNTS); singleSignOnUrl = properties.getProperty(SamlOptions.SINGLE_SIGN_ON_URL); idpMetadataPath = properties.getProperty(SamlOptions.IDP_METADATA_PATH); + maximumAuthenticationLifetime = properties.getProperty(SamlOptions.MAXIMUM_AUTHENTICATION_LIFETIME,"28800"); } } diff --git a/src/java/com/netflix/ice/login/saml/SamlOptions.java b/src/java/com/netflix/ice/login/saml/SamlOptions.java index af721cfc..64cfe4d5 100644 --- a/src/java/com/netflix/ice/login/saml/SamlOptions.java +++ b/src/java/com/netflix/ice/login/saml/SamlOptions.java @@ -27,7 +27,8 @@ public class SamlOptions { public static final String SAML = LoginOptions.LOGIN + ".saml"; /** - * Signin-Url for our service + * The iDP Signin-Url for our service. Use this if the SAML Redirect doesn't work + * ADFS URL looks like this: https://sso.it.here.com/adfs/ls/wia?LoginToRP=service name */ public static final String SIGNIN_URL = SAML + ".signin_url"; @@ -77,4 +78,10 @@ public class SamlOptions { */ public static final String IDP_METADATA_PATH = SAML + ".idp_metadata_path"; + /** + * Maximum amount of time that we accept a SAML Assertion + * ADFS defaults to 8 hours - + */ + public static final String MAXIMUM_AUTHENTICATION_LIFETIME = SAML + ".maximum_authentication_lifetime"; + } From cc7bbf210b126bb362f4714ef8b3b29c696894db Mon Sep 17 00:00:00 2001 From: Anthony Johnson Date: Fri, 13 Mar 2015 15:01:27 -0400 Subject: [PATCH 06/10] Update to new method definition --- src/java/com/netflix/ice/login/saml/SamlMetaData.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/java/com/netflix/ice/login/saml/SamlMetaData.java b/src/java/com/netflix/ice/login/saml/SamlMetaData.java index f746be75..0da5105b 100644 --- a/src/java/com/netflix/ice/login/saml/SamlMetaData.java +++ b/src/java/com/netflix/ice/login/saml/SamlMetaData.java @@ -18,6 +18,7 @@ import com.netflix.ice.common.IceOptions; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.util.Collection; import java.util.Properties; import java.util.Map; @@ -63,7 +64,7 @@ public SamlMetaData(Properties properties) throws LoginMethodException { client.setPrivateKeyPassword(config.keyPassword); } - public LoginResponse processLogin(HttpServletRequest request) throws LoginMethodException { + public LoginResponse processLogin(HttpServletRequest request, HttpServletResponse response) throws LoginMethodException { LoginResponse lr = new LoginResponse(); lr.renderData = client.printClientMetadata(); ; From a3d0c3e9559144d9efbcf5a7a315c4a84003f204 Mon Sep 17 00:00:00 2001 From: Anthony Johnson Date: Fri, 13 Mar 2015 20:58:16 -0400 Subject: [PATCH 07/10] Update Logout for new method declaration --- src/java/com/netflix/ice/login/Logout.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/java/com/netflix/ice/login/Logout.java b/src/java/com/netflix/ice/login/Logout.java index 55220dcd..c41b1cc7 100644 --- a/src/java/com/netflix/ice/login/Logout.java +++ b/src/java/com/netflix/ice/login/Logout.java @@ -18,6 +18,7 @@ import com.netflix.ice.common.IceSession; import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; import java.util.Collection; import java.util.Properties; import java.util.Map; @@ -40,7 +41,7 @@ public String propertyName(String name) { return null; } - public LoginResponse processLogin(HttpServletRequest request) throws LoginMethodException { + public LoginResponse processLogin(HttpServletRequest request, HttpServletResponse response) throws LoginMethodException { IceSession session = new IceSession(request.getSession()); session.voidSession(); LoginResponse lr = new LoginResponse(); From b573a27025f7a7b99a8394c302e40fa48b2890cd Mon Sep 17 00:00:00 2001 From: Anthony Johnson Date: Fri, 20 Mar 2015 14:01:44 -0400 Subject: [PATCH 08/10] initial support for pre-auth URL capturing --- .../com/netflix/ice/DashboardController.groovy | 2 ++ .../com/netflix/ice/LoginController.groovy | 10 ++++++++-- src/java/com/netflix/ice/common/IceSession.java | 17 ++++++++++++++++- src/java/com/netflix/ice/login/LoginMethod.java | 4 ++-- 4 files changed, 28 insertions(+), 5 deletions(-) diff --git a/grails-app/controllers/com/netflix/ice/DashboardController.groovy b/grails-app/controllers/com/netflix/ice/DashboardController.groovy index 4edde64f..cf770d31 100644 --- a/grails-app/controllers/com/netflix/ice/DashboardController.groovy +++ b/grails-app/controllers/com/netflix/ice/DashboardController.groovy @@ -73,6 +73,8 @@ class DashboardController { { request["iceSession"] = new IceSession(session); if (! request["iceSession"].isAuthenticated()) { + // TODO: would be nice to save the URL here + // request["iceSession"].setUrl(...) exists redirect(controller: "login") } } diff --git a/grails-app/controllers/com/netflix/ice/LoginController.groovy b/grails-app/controllers/com/netflix/ice/LoginController.groovy index 6d2af809..2e26bf9c 100644 --- a/grails-app/controllers/com/netflix/ice/LoginController.groovy +++ b/grails-app/controllers/com/netflix/ice/LoginController.groovy @@ -33,6 +33,7 @@ import com.google.common.collect.Lists import com.google.common.collect.Sets import com.google.common.collect.Maps import org.json.JSONObject +import grails.util.Holders class LoginController { @@ -47,7 +48,7 @@ class LoginController { def handler = { if (config.loginEnable == false) { - redirect(controller: "dashboard") + redirect(controller: "dashboard", absolute: true) } LoginMethod loginMethod = config.loginMethods.get(params.login_action); if (loginMethod == null) { @@ -66,7 +67,12 @@ class LoginController { IceSession iceSession = new IceSession(session); iceSession.authenticate(new Boolean(true)); if (iceSession.authenticated) { //ensure we are good - redirect(controller: "dashboard"); + if (iceSession.url != null) { + String redirectURL = "" + iceSession.url + redirect(url: redirectURL, absolute: true); + } else { + redirect(controller: "dashboard"); + } } else { redirect(action: "failure"); } diff --git a/src/java/com/netflix/ice/common/IceSession.java b/src/java/com/netflix/ice/common/IceSession.java index 5a9df3dd..2e741bae 100644 --- a/src/java/com/netflix/ice/common/IceSession.java +++ b/src/java/com/netflix/ice/common/IceSession.java @@ -30,6 +30,7 @@ */ public class IceSession { private static final Logger logger = LoggerFactory.getLogger(IceSession.class); + private final String URL = "url"; private final String USER_NAME = "user_name"; private final String AUTHENTICATED_SESSION_KEY = "authenticated"; private final String ADMIN_SESSION_KEY = "admin"; @@ -50,7 +51,21 @@ public void authenticate(Boolean authd) { session.setAttribute(AUTHENTICATED_SESSION_KEY, authd); } - public String username() { + /** + * URL to redirect user to after login + */ + public String getUrl() { + return (String)session.getAttribute(URL); + } + + /** + * URL to redirect user to after login + */ + public void setUrl(String url) { + session.setAttribute(URL, url); + } + + public String getUsername() { return (String)session.getAttribute(USER_NAME); } diff --git a/src/java/com/netflix/ice/login/LoginMethod.java b/src/java/com/netflix/ice/login/LoginMethod.java index ebc38587..08c5bbac 100644 --- a/src/java/com/netflix/ice/login/LoginMethod.java +++ b/src/java/com/netflix/ice/login/LoginMethod.java @@ -83,11 +83,11 @@ public void initLogging() { public void log(IceSession session, String message) throws RuntimeException { initLogging(); if (loginLogger != null) { - String username = session.username(); + String username = session.getUsername(); if (username == null) { throw new RuntimeException("Username not set on session, unable to properly log"); } - loginLogger.info(this.getClass().getSimpleName() + ":" + session.username() + ":" + message); + loginLogger.info(this.getClass().getSimpleName() + ":" + session.getUsername() + ":" + message); } } From a520a253496fc3a7fa6bf55ae8078bd13b0a2d7a Mon Sep 17 00:00:00 2001 From: Anthony Johnson Date: Mon, 23 Mar 2015 00:51:14 -0400 Subject: [PATCH 09/10] fix bug with admin accounts not able to see default queries. --- .../netflix/ice/DashboardController.groovy | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/grails-app/controllers/com/netflix/ice/DashboardController.groovy b/grails-app/controllers/com/netflix/ice/DashboardController.groovy index cf770d31..142d7b1d 100644 --- a/grails-app/controllers/com/netflix/ice/DashboardController.groovy +++ b/grails-app/controllers/com/netflix/ice/DashboardController.groovy @@ -350,15 +350,27 @@ class DashboardController { IceSession sess = request["iceSession"]; String accounts = (String)query.opt("account"); if (accounts == null || accounts.length() == 0) { - StringBuilder csvString = new StringBuilder(); - String delim=""; - for (String allowedAccount : sess.allowedAccounts()) { - csvString.append(delim); - String allowedAccountName = accountService.getAccountById(allowedAccount); - csvString.append(allowedAccountName); - delim=","; - } - query.put("account", csvString.toString()); + StringBuilder csvString = new StringBuilder(); + String delim=""; + // login requires explicit accounts to be defined + if (! sess.isAdmin()) { + for (String allowedAccount : sess.allowedAccounts()) { + csvString.append(delim); + String allowedAccountName = accountService.getAccountById(allowedAccount); + csvString.append(allowedAccountName); + delim = ","; + } + } else { + TagGroupManager tagGroupManager = getManagers().getTagGroupManager(null); + Collection accts = tagGroupManager == null ? [] : tagGroupManager.getAccounts(new TagLists(), sess); + for (Account account : accts) { + csvString.append(delim); + csvString.append(account.id); + delim = ","; + } + + } + query.put("account", csvString.toString()); } } From bca279d4a7b73bb0b5cd4bfea025109fff3d94ed Mon Sep 17 00:00:00 2001 From: Anthony Johnson Date: Thu, 9 Apr 2015 16:13:11 -0400 Subject: [PATCH 10/10] Remove files that are no longer needed --- .../com/netflix/ice/login/saml/metadata.xml | 46 ------------------- .../netflix/ice/login/saml/saml_config.xml | 38 --------------- 2 files changed, 84 deletions(-) delete mode 100644 src/java/com/netflix/ice/login/saml/metadata.xml delete mode 100644 src/java/com/netflix/ice/login/saml/saml_config.xml diff --git a/src/java/com/netflix/ice/login/saml/metadata.xml b/src/java/com/netflix/ice/login/saml/metadata.xml deleted file mode 100644 index b0e738d1..00000000 --- a/src/java/com/netflix/ice/login/saml/metadata.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - - - -${public_key} - - - - - -urn:oasis:names:tc:SAML:2.0:nameid-format:transient - - -urn:oasis:names:tc:SAML:2.0:nameid-format:persistent - - -NetFlix Ice Single Sign-On - - - - - - - - - - - - - - - - - - - - - -${org_name} -${org_display_name} -${org_url} - - diff --git a/src/java/com/netflix/ice/login/saml/saml_config.xml b/src/java/com/netflix/ice/login/saml/saml_config.xml deleted file mode 100644 index 07b946aa..00000000 --- a/src/java/com/netflix/ice/login/saml/saml_config.xml +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -