diff --git a/README.md b/README.md index 0238a00a..15d40d7a 100644 --- a/README.md +++ b/README.md @@ -240,6 +240,73 @@ 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. 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/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/BuildConfig.groovy b/grails-app/conf/BuildConfig.groovy index 79c307b5..b6ed17bf 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', @@ -99,8 +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,11 +110,19 @@ grails.project.dependency.resolution = { 'junit', 'mockito-core', + 'xercesImpl', + 'jcl-over-slf4j', + 'log4j-over-slf4j' ) } + compile( + 'org.opensaml:opensaml:2.6.1' + ) { + excludes 'xercesImpl' + } } plugins { - build ":tomcat:$grailsVersion" + build ":tomcat:2.2.1" } } 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..142d7b1d 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,35 @@ class DashboardController { return managers; } + def beforeInterceptor = { + LoginConfig lc = LoginConfig.getInstance(); + if ( lc != null && lc.loginEnable ) + { + 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") + } + } + } + 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 +105,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 +138,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 +188,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 +209,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 +252,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 +340,40 @@ 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=""; + // 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()); + } + + } def result = doGetData(query); render result as JSON @@ -397,7 +454,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")); @@ -636,7 +694,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/grails-app/controllers/com/netflix/ice/LoginController.groovy b/grails-app/controllers/com/netflix/ice/LoginController.groovy new file mode 100644 index 00000000..2e26bf9c --- /dev/null +++ b/grails-app/controllers/com/netflix/ice/LoginController.groovy @@ -0,0 +1,128 @@ +/* + * + * 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 +import grails.util.Holders + + +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", absolute: true) + } + LoginMethod loginMethod = config.loginMethods.get(params.login_action); + if (loginMethod == null) { + redirect(action: "error"); + } + LoginResponse loginResponse = loginMethod.processLogin(request, response); + + 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)); + if (iceSession.authenticated) { //ensure we are good + if (iceSession.url != null) { + String redirectURL = "" + iceSession.url + redirect(url: redirectURL, absolute: true); + } else { + 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/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/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..2e741bae --- /dev/null +++ b/src/java/com/netflix/ice/common/IceSession.java @@ -0,0 +1,176 @@ +/* + * + * 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 URL = "url"; + 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 HttpSession session; + + public IceSession(HttpSession session) { + this.session = session; + } + + /** + * Auth or DeAuth this session. + * @parm authd + */ + public void authenticate(Boolean authd) { + logger.info("authenticate: " + authd); + session.setAttribute(AUTHENTICATED_SESSION_KEY, authd); + } + + /** + * 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); + } + + 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) { + logger.error("User has no authentication entry"); + return false; + } else if (! authd.booleanValue()) { + logger.error("User has been explicitly denied - " + authd); + return false; + } + return true; + } + + /** + * 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(); + } + } + } + + /** + * 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..349d8b6d --- /dev/null +++ b/src/java/com/netflix/ice/login/LoginResponse.java @@ -0,0 +1,66 @@ +/* + * 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 +{ + /**Did the handler respond to this(no action from the Controller) */ + public boolean responded=false; + + /** 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; + + 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/Logout.java b/src/java/com/netflix/ice/login/Logout.java new file mode 100644 index 00000000..c41b1cc7 --- /dev/null +++ b/src/java/com/netflix/ice/login/Logout.java @@ -0,0 +1,52 @@ +/* + * 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 javax.servlet.http.HttpServletResponse; +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, HttpServletResponse response) 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..1aadb081 --- /dev/null +++ b/src/java/com/netflix/ice/login/Passphrase.java @@ -0,0 +1,78 @@ +/* + * 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 javax.servlet.http.HttpServletResponse; +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, HttpServletResponse response) 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; + } else { + lr.loginFailed=true; + } + return lr; + } +} + 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..48fe4450 --- /dev/null +++ b/src/java/com/netflix/ice/login/saml/Saml.java @@ -0,0 +1,178 @@ +/* + * 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.IceSession; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Properties; +import javax.servlet.http.HttpServletRequestWrapper; +import org.opensaml.ws.message.decoder.MessageDecodingException; + +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.opensaml.saml2.core.Attribute; +import org.opensaml.xml.XMLObject; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * SAML Plugin + */ +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); + + private final SamlConfig config; + private final Saml2Client client = new Saml2Client(); + + public String propertyName(String name) { + return SAML_PREFIX + "." + name; + } + + public Saml(Properties properties) throws LoginMethodException { + super(properties); + 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); + client.setMaximumAuthenticationLifetime(Integer.parseInt(config.maximumAuthenticationLifetime)); + } + + 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(); + + 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); + processAssertion(iceSession, credentials, lr); + } catch (NullPointerException npe) { + redirect = true; + } catch (RequiresHttpAction rha) { + redirect = true; + } catch (Exception e) { + redirect = true; + } + if (redirect) { + try { + 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; + } + + + /** + * Process an assertion and setup our session attributes + */ + private void processAssertion(IceSession iceSession, Saml2Credentials credentials, LoginResponse lr) throws LoginMethodException { + boolean foundAnAccount=false; + iceSession.voidSession(); + + 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 : 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 Account: " + allowedAccount); + } + } + } + } + } + + //require at least one account + if (! foundAnAccount) { + lr.loginFailed=true; + return; + } else { + lr.loginSuccess=true; + } + + } +} + 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..56e78dd8 --- /dev/null +++ b/src/java/com/netflix/ice/login/saml/SamlConfig.java @@ -0,0 +1,65 @@ +/* + * 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 String keystore; + public final String keystorePassword; + public final String keyAlias; + public final String keyPassword; + public final String signInUrl; + public final String allAccounts; + 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); + keystorePassword = properties.getProperty(SamlOptions.KEYSTORE_PASSWORD); + keyAlias = properties.getProperty(SamlOptions.KEY_ALIAS); + keyPassword = properties.getProperty(SamlOptions.KEY_PASSWORD); + serviceIdentifier = properties.getProperty(SamlOptions.SERVICE_IDENTIFIER); + signInUrl = properties.getProperty(SamlOptions.SIGNIN_URL); + 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/SamlMetaData.java b/src/java/com/netflix/ice/login/saml/SamlMetaData.java new file mode 100644 index 00000000..0da5105b --- /dev/null +++ b/src/java/com/netflix/ice/login/saml/SamlMetaData.java @@ -0,0 +1,75 @@ +/* + * 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 javax.servlet.http.HttpServletResponse; +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.pac4j.core.client.BaseClient; +import org.pac4j.saml.client.Saml2Client; +import org.opensaml.common.xml.SAMLConstants; + + +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()); + + private final SamlConfig config; + private final Saml2Client client = new Saml2Client(); + + public String propertyName(String name) { + return SAML_PREFIX + "." + name; + } + + public SamlMetaData(Properties properties) throws LoginMethodException { + super(properties); + 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, HttpServletResponse response) throws LoginMethodException { + LoginResponse lr = new LoginResponse(); + lr.renderData = client.printClientMetadata(); +; + lr.contentType = "application/samlmetadata+xml"; + return lr; + } +} + 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..64cfe4d5 --- /dev/null +++ b/src/java/com/netflix/ice/login/saml/SamlOptions.java @@ -0,0 +1,87 @@ +/* + * + * 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"; + + /** + * 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"; + + /** + * Service/Entity Identifier + */ + public static final String SERVICE_IDENTIFIER = SAML + ".service_identifier"; + + /** + * 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"; + /** + * Path to IDP Metdata + */ + 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"; + +} 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: + +
+ + + 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 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 +