Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Feature/login framework #168

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <filePrefix>_2013_04, <filePrefix>_2013_05. Data in files should be delimited by new lines. <filePrefix> 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 <a href=some useful linke</a>

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.
Expand Down
6 changes: 6 additions & 0 deletions grails-app/conf/BootStrap.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -52,6 +53,7 @@ class BootStrap {

private ReaderConfig readerConfig;
private ProcessorConfig processorConfig;
private LoginConfig loginConfig;

def init = { servletContext ->
if (initialized) {
Expand Down Expand Up @@ -233,6 +235,10 @@ class BootStrap {
readerConfig.start();
}

if ("true".equals(prop.getProperty("ice.login"))) {
loginConfig = new LoginConfig(prop)
}

initialized = true;

} catch (Exception e) {
Expand Down
18 changes: 14 additions & 4 deletions grails-app/conf/BuildConfig.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@ grails.project.dependency.resolution = {
}

dependencies {

compile(
// Amazon Web Services programmatic interface
'com.amazonaws:aws-java-sdk:1.9.12',
Expand All @@ -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',
Expand All @@ -99,20 +100,29 @@ 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
// with Grails' built in JUnit
'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"
}
}
5 changes: 5 additions & 0 deletions grails-app/conf/UrlMappings.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
78 changes: 69 additions & 9 deletions grails-app/controllers/com/netflix/ice/DashboardController.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<Account> data = tagGroupManager == null ? [] : tagGroupManager.getAccounts(new TagLists());
IceSession sess = request["iceSession"];
Collection<Account> data = tagGroupManager == null ? [] : tagGroupManager.getAccounts(new TagLists(), sess);

def result = [status: 200, data: data]
render result as JSON
}

def getRegions = {
List<Account> accounts = getConfig().accountService.getAccounts(listParams("account"));
IceSession sess = request["iceSession"];
List<Account> accounts = getConfig().accountService.getAccounts(listParams("account"), sess);

TagGroupManager tagGroupManager = getManagers().getTagGroupManager(null);
Collection<Region> data = tagGroupManager == null ? [] : tagGroupManager.getRegions(new TagLists(accounts));
Expand All @@ -87,7 +105,8 @@ class DashboardController {
}

def getZones = {
List<Account> accounts = getConfig().accountService.getAccounts(listParams("account"));
IceSession sess = request["iceSession"];
List<Account> accounts = getConfig().accountService.getAccounts(listParams("account"), sess);
List<Region> regions = Region.getRegions(listParams("region"));

TagGroupManager tagGroupManager = getManagers().getTagGroupManager(null);
Expand Down Expand Up @@ -119,7 +138,8 @@ class DashboardController {

def getProducts = {
Object o = params;
List<Account> accounts = getConfig().accountService.getAccounts(listParams("account"));
IceSession sess = request["iceSession"];
List<Account> accounts = getConfig().accountService.getAccounts(listParams("account"), sess);
List<Region> regions = Region.getRegions(listParams("region"));
List<Zone> zones = Zone.getZones(listParams("zone"));
List<Operation> operations = Operation.getOperations(listParams("operation"));
Expand Down Expand Up @@ -168,7 +188,8 @@ class DashboardController {
}

def getResourceGroups = {
List<Account> accounts = getConfig().accountService.getAccounts(listParams("account"));
IceSession sess = request["iceSession"];
List<Account> accounts = getConfig().accountService.getAccounts(listParams("account"), sess);
List<Region> regions = Region.getRegions(listParams("region"));
List<Zone> zones = Zone.getZones(listParams("zone"));
List<Product> products = getConfig().productService.getProducts(listParams("product"));
Expand All @@ -188,7 +209,8 @@ class DashboardController {
def getOperations = {
def text = request.reader.text;
JSONObject query = (JSONObject)JSON.parse(text);
List<Account> accounts = getConfig().accountService.getAccounts(listParams(query, "account"));
IceSession sess = request["iceSession"];
List<Account> accounts = getConfig().accountService.getAccounts(listParams(query, "account"), sess);
List<Region> regions = Region.getRegions(listParams(query, "region"));
List<Zone> zones = Zone.getZones(listParams(query, "zone"));
List<Product> products = getConfig().productService.getProducts(listParams(query, "product"));
Expand Down Expand Up @@ -230,7 +252,8 @@ class DashboardController {
def getUsageTypes = {
def text = request.reader.text;
JSONObject query = (JSONObject)JSON.parse(text);
List<Account> accounts = getConfig().accountService.getAccounts(listParams(query, "account"));
IceSession sess = request["iceSession"];
List<Account> accounts = getConfig().accountService.getAccounts(listParams(query, "account"), sess);
List<Region> regions = Region.getRegions(listParams(query, "region"));
List<Zone> zones = Zone.getZones(listParams(query, "zone"));
List<Product> products = getConfig().productService.getProducts(listParams(query, "product"));
Expand Down Expand Up @@ -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<Account> 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
Expand Down Expand Up @@ -397,7 +454,8 @@ class DashboardController {
boolean showsps = query.getBoolean("showsps");
boolean factorsps = query.getBoolean("factorsps");
AggregateType aggregate = AggregateType.valueOf(query.getString("aggregate"));
List<Account> accounts = getConfig().accountService.getAccounts(listParams(query, "account"));
IceSession sess = request["iceSession"];
List<Account> accounts = getConfig().accountService.getAccounts(listParams(query, "account"), sess);
List<Region> regions = Region.getRegions(listParams(query, "region"));
List<Zone> zones = Zone.getZones(listParams(query, "zone"));
List<Product> products = getConfig().productService.getProducts(listParams(query, "product"));
Expand Down Expand Up @@ -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;
}
Expand Down
Loading