Support for GitHub authentication

This commit is contained in:
Vincent Fiduccia 2014-12-19 17:38:58 -07:00
parent a6cdf94f93
commit c2c93d4e32
52 changed files with 1304 additions and 339 deletions

View File

@ -7,5 +7,5 @@
*/
"disableAnalytics": true,
"port": 8000,
"host": "127.0.0.1"
"host": "0.0.0.0"
}

View File

@ -49,5 +49,7 @@ app.import('bower_components/c3/c3.css');
app.import('vendor/term.js/src/term.js');
app.import('bower_components/bootstrap-multiselect/dist/js/bootstrap-multiselect.js');
app.import('bower_components/bootstrap-multiselect/dist/css/bootstrap-multiselect.css');
app.import('bower_components/bootstrap-switch/dist/js/bootstrap-switch.js');
app.import('bower_components/bootstrap-switch/dist/css/bootstrap3/bootstrap-switch.css');
module.exports = app.toTree();

View File

@ -0,0 +1,49 @@
import Ember from 'ember';
import config from 'torii/configuration';
import bootstrap from 'torii/bootstrap/torii';
export function initialize(container, application) {
application.deferReadiness();
var store = container.lookup('store:main');
// Find out if auth is enabled
store.rawRequest({
url: 'token', // Base url, which will be /v1
headers: { 'authorization': undefined }
})
.then(function(obj) {
var body = JSON.parse(obj.xhr.responseText);
var token = body.data[0];
application.set('hasAuthentication', true);
application.set('authenticationEnabled', token.security);
configureTorii(token.clientId);
return Ember.RSVP.resolve(undefined,'API supports authentication');
})
.catch(function(obj) {
application.set('hasAuthentication', false);
application.set('authenticationEnabled', false);
application.set('initError', obj);
return Ember.RSVP.resolve(undefined,'Error determining API authentication');
})
.finally(function() {
application.advanceReadiness();
});
function configureTorii(clientId) {
config.providers['github-oauth2'] = {
apiKey: clientId,
scope: 'read:org'
};
bootstrap(container);
application.inject('controller', 'torii', 'torii:main');
}
}
export default {
name: 'authentication',
after: 'store',
initialize: initialize
};

View File

@ -0,0 +1,14 @@
import SessionStorage from 'ui/utils/session-storage';
export function initialize(container, application) {
container.register('session:main', SessionStorage);
application.inject('controller', 'session', 'session:main');
application.inject('route', 'session', 'session:main');
application.inject('model', 'session', 'session:main');
}
export default {
name: 'session',
before: 'store',
initialize: initialize
};

View File

@ -1,13 +1,33 @@
import Ember from 'ember';
export function initialize(container, application) {
application.deferReadiness();
var store = container.lookup('store:main');
var session = container.lookup('session:main');
store.set('removeAfterDelete', false);
store.reopen({
baseUrl: application.apiEndpoint,
headers: function() {
var out = {
'x-api-no-challenge': 'true', // Don't send me www-authenticate headers
};
var token = session.get('token');
if ( token )
{
out['authorization'] = 'Bearer ' + token;
}
var accountId = session.get('accountId');
if ( accountId )
{
out['x-api-project-id'] = accountId;
}
return out;
}.property().volatile(),
reallyAll: store.all,
all: function(type) {
type = this.normalizeType(type);
@ -27,10 +47,6 @@ export function initialize(container, application) {
return proxy;
}
});
store.find('schema', null, {url: 'schemas'}).then(function() {
application.advanceReadiness();
});
}
export default {

View File

@ -0,0 +1,13 @@
import Ember from "ember";
export default Ember.Mixin.create({
beforeModel: function(transition) {
this._super(transition);
var isLoggedIn = (this.get('session.isLoggedIn') === '1') && this.get('session.token');
if ( this.get('app.authenticationEnabled') && !isLoggedIn )
{
transition.send('logout',transition,true);
return Ember.RSVP.reject('Not logged in');
}
}
});

View File

@ -1,19 +1,8 @@
import Ember from "ember";
export default Ember.Controller.extend({
navExpand: (parseInt($.cookie('navExpand'),10) !== 0),
error: null,
pageName: '',
navExpandChange: function() {
var inAYear = new Date();
inAYear.setYear(inAYear.getFullYear()+1);
$.cookie('navExpand', (this.get('navExpand') ? 1 : 0), {
expires: inAYear
});
}.observes('navExpand'),
requiresAuthentication: null,
absoluteEndpoint: function() {
var url = this.get('app.endpoint');
@ -23,11 +12,8 @@ export default Ember.Controller.extend({
{
url = window.location.origin + '/' + url.replace(/^\/+/,'');
}
else if ( url.charAt(url.length-1) !== '/' ) {
url = url + "/";
}
// Url must end in a single slash
// URL must end in a single slash
url = url.replace(/\/+$/,'') + '/';
return url;

View File

@ -1,257 +1,32 @@
import Ember from 'ember';
import Socket from 'ui/utils/socket';
import Util from 'ui/utils/util';
export default Ember.Route.extend({
socket: null,
actions: {
beforeModel: function() {
var err = this.get('app.initError');
if ( err )
{
this.send('error',err);
}
},
error: function(err) {
this.controller.set('error',err);
this.transitionTo('failWhale');
console.log('Application ' + err.stack);
},
setPageName: function(str) {
this.controller.set('pageName',str);
},
logout: function(transition,timedOut) {
this.set('session.isLoggedIn',0);
this.set('app.afterLoginTransition', transition);
var params = {queryParams: {}};
// Raw message from the WebSocket
wsMessage: function(/*data*/) {
//console.log('wsMessage',data);
},
// WebSocket connected
wsConnected: function(tries,msec) {
var msg = 'WebSocket connected';
if (tries > 0)
if ( timedOut )
{
msg += ' (after '+ tries + ' ' + (tries === 1 ? 'try' : 'tries');
if (msec)
{
msg += ', ' + (msec/1000) + ' sec';
params.queryParams.timedOut = true;
}
msg += ')';
}
console.log(msg);
},
// WebSocket disconnected
wsDisconnected: function() {
console.log('WebSocket disconnected');
},
wsPing: function() {
console.log('WebSocket ping');
},
/*
agentChanged: function(change) {
if (!change || !change.data || !change.data.resource)
{
return;
}
//console.log('Agent Changed:', change);
var agent = change.data.resource;
var id = agent.id;
delete agent.hosts;
var hosts = this.controllerFor('hosts');
hosts.forEach(function(host) {
if ( host.get('agent.id') === id )
{
host.get('agent').setProperties(agent);
}
});
},
*/
containerChanged: function(change) {
this._instanceChanged(change);
},
instanceChanged: function(change) {
this._instanceChanged(change);
},
mountChanged: function(change) {
var mount = change.data.resource;
var volume = this.get('store').getById('volume', mount.get('volumeId'));
if ( volume )
{
var mounts = volume.get('mounts');
if ( !Ember.isArray('mounts') )
{
mounts = [];
volume.set('mounts',mounts);
}
var existingMount = mounts.filterBy('id', mount.get('id')).get('firstObject');
if ( existingMount )
{
existingMount.setProperties(mount);
}
else
{
mounts.pushObject(mount);
}
}
this.transitionTo('login', params);
}
},
enter: function() {
var self = this;
var store = this.get('store');
var boundTypeify = store._typeify.bind(store);
var socket = Socket.create({
url: "ws://"+window.location.host + this.get('app.wsEndpoint'),
});
socket.on('message', function(event) {
var d = JSON.parse(event.data, boundTypeify);
self._trySend('wsMessage',d);
var str = d.name;
if ( d.resourceType )
{
str += ' ' + d.resourceType;
if ( d.resourceId )
{
str += ' ' + d.resourceId;
}
}
var action;
if ( d.name === 'resource.change' )
{
action = d.resourceType+'Changed';
}
else if ( d.name === 'ping' )
{
action = 'wsPing';
}
if ( action )
{
self._trySend(action,d);
}
});
socket.on('connected', function(tries, after) {
self._trySend('wsConnected', tries, after);
});
socket.on('disconnected', function() {
self._trySend('wsDisconnected', self.get('tries'));
});
this.set('socket', socket);
socket.connect();
},
exit: function() {
var socket = this.get('socket');
if ( socket )
{
socket.disconnect();
}
},
_trySend: function(/*arguments*/) {
try
{
this.send.apply(this,arguments);
}
catch (err)
{
if ( err instanceof Ember.Error && err.message.indexOf('Nothing handled the action') === 0 )
{
// Don't care
}
else
{
throw err;
}
}
},
_findInCollection: function(collectionName,id) {
var collection = this.controllerFor(collectionName);
var existing = collection.filterBy('id',id).get('firstObject');
return existing;
},
_instanceChanged: function(change) {
if (!change || !change.data || !change.data.resource)
{
return;
}
//console.log('Instance Changed:',change);
var self = this;
var instance = change.data.resource;
var id = instance.get('id');
// All the hosts
var allHosts = self.get('store').all('host');
// Host IDs the instance should be on
var expectedHostIds = [];
if ( instance.get('state') !== 'purged' )
{
expectedHostIds = (instance.get('hosts')||[]).map(function(host) {
return host.get('id');
});
}
// Host IDs it is currently on
var curHostIds = [];
allHosts.forEach(function(host) {
var existing = (host.get('instances')||[]).filterBy('id', id);
if ( existing.length )
{
curHostIds.push(host.get('id'));
}
});
// Remove from hosts the instance shouldn't be on
var remove = Util.arrayDiff(curHostIds, expectedHostIds);
remove.forEach(function(hostId) {
var host = self._findInCollection('hosts',hostId);
if ( host )
{
var instances = host.get('instances');
if ( !instances )
{
return;
}
instances.removeObjects(instances.filterBy('id', id));
}
});
// Add or update hosts the instance should be on
expectedHostIds.forEach(function(hostId) {
var host = self._findInCollection('hosts',hostId);
if ( host )
{
var instances = host.get('instances');
if ( !instances )
{
instances = [];
host.set('instances',instances);
}
var existing = instances.filterBy('id', id);
if ( existing.length === 0)
{
instances.pushObject(instance);
}
}
});
}
});

View File

@ -1,14 +1,10 @@
<div id="underlay" class="underlay hide"></div>
<div id="loading-underlay" class="underlay"></div>
<div id="loading-overlay" class="loading-overlay">
<div id="loading-underlay" class="underlay hide"></div>
<div id="loading-overlay" class="loading-overlay hide">
<div class="loading-box">
<h3><i class="fa fa-spinner fa-spin fa-fw"></i> Loading&hellip;</h3>
</div>
</div>
{{outlet "overlay"}}
{{outlet "error"}}
{{page-nav expand=navExpand}}
{{page-header pageName=pageName}}
<main>
{{outlet}}
</main>
{{outlet}}

View File

@ -1,8 +1,6 @@
import Ember from "ember";
export default Ember.View.extend({
classNameBindings: ['context.navExpand:nav-expand'],
didInsertElement: function() {
this.$().tooltip({
selector: '*[tooltip]',

View File

@ -0,0 +1,16 @@
import Ember from "ember";
export default Ember.Controller.extend({
navExpand: (parseInt($.cookie('navExpand'),10) !== 0),
error: null,
pageName: '',
navExpandChange: function() {
var inAYear = new Date();
inAYear.setYear(inAYear.getFullYear()+1);
$.cookie('navExpand', (this.get('navExpand') ? 1 : 0), {
expires: inAYear
});
}.observes('navExpand'),
});

View File

@ -0,0 +1,7 @@
import Ember from 'ember';
export default Ember.Route.extend({
beforeModel: function() {
this.transitionTo('hosts');
}
});

View File

@ -0,0 +1,282 @@
import Ember from 'ember';
import Socket from 'ui/utils/socket';
import Util from 'ui/utils/util';
import AuthenticatedRouteMixin from 'ui/mixins/authenticated-route';
export default Ember.Route.extend(AuthenticatedRouteMixin, {
socket: null,
model: function() {
return this.get('store').find('schema', null, {url: 'schemas'}).then(function() {
return Ember.RSVP.resolve();
});
},
actions: {
error: function(err,transition) {
// Unauthorized error, send back to login screen
if ( err.status === 401 )
{
this.send('logout',transition,true);
return false;
}
else
{
// Bubble up
return true;
}
},
setPageName: function(str) {
this.controller.set('pageName',str);
},
// Raw message from the WebSocket
wsMessage: function(/*data*/) {
//console.log('wsMessage',data);
},
// WebSocket connected
wsConnected: function(tries,msec) {
var msg = 'WebSocket connected';
if (tries > 0)
{
msg += ' (after '+ tries + ' ' + (tries === 1 ? 'try' : 'tries');
if (msec)
{
msg += ', ' + (msec/1000) + ' sec';
}
msg += ')';
}
console.log(msg);
},
// WebSocket disconnected
wsDisconnected: function() {
console.log('WebSocket disconnected');
},
wsPing: function() {
console.log('WebSocket ping');
},
/*
agentChanged: function(change) {
if (!change || !change.data || !change.data.resource)
{
return;
}
//console.log('Agent Changed:', change);
var agent = change.data.resource;
var id = agent.id;
delete agent.hosts;
var hosts = this.controllerFor('hosts');
hosts.forEach(function(host) {
if ( host.get('agent.id') === id )
{
host.get('agent').setProperties(agent);
}
});
},
*/
containerChanged: function(change) {
this._instanceChanged(change);
},
instanceChanged: function(change) {
this._instanceChanged(change);
},
mountChanged: function(change) {
var mount = change.data.resource;
var volume = this.get('store').getById('volume', mount.get('volumeId'));
if ( volume )
{
var mounts = volume.get('mounts');
if ( !Ember.isArray('mounts') )
{
mounts = [];
volume.set('mounts',mounts);
}
var existingMount = mounts.filterBy('id', mount.get('id')).get('firstObject');
if ( existingMount )
{
existingMount.setProperties(mount);
}
else
{
mounts.pushObject(mount);
}
}
}
},
enter: function() {
var self = this;
var store = this.get('store');
var boundTypeify = store._typeify.bind(store);
var url = "ws://"+window.location.host + this.get('app.wsEndpoint');
var token = this.get('session.token');
if ( token )
{
url += (url.indexOf('?') >= 0 ? '&' : '?') + 'token=' + encodeURIComponent(token);
}
var socket = Socket.create({
url: url
});
socket.on('message', function(event) {
var d = JSON.parse(event.data, boundTypeify);
self._trySend('wsMessage',d);
var str = d.name;
if ( d.resourceType )
{
str += ' ' + d.resourceType;
if ( d.resourceId )
{
str += ' ' + d.resourceId;
}
}
var action;
if ( d.name === 'resource.change' )
{
action = d.resourceType+'Changed';
}
else if ( d.name === 'ping' )
{
action = 'wsPing';
}
if ( action )
{
self._trySend(action,d);
}
});
socket.on('connected', function(tries, after) {
self._trySend('wsConnected', tries, after);
});
socket.on('disconnected', function() {
self._trySend('wsDisconnected', self.get('tries'));
});
this.set('socket', socket);
socket.connect();
},
exit: function() {
var socket = this.get('socket');
if ( socket )
{
socket.disconnect();
}
// Forget all the things
this.get('store').reset();
},
_trySend: function(/*arguments*/) {
try
{
this.send.apply(this,arguments);
}
catch (err)
{
if ( err instanceof Ember.Error && err.message.indexOf('Nothing handled the action') === 0 )
{
// Don't care
}
else
{
throw err;
}
}
},
_findInCollection: function(collectionName,id) {
var collection = this.controllerFor(collectionName);
var existing = collection.filterBy('id',id).get('firstObject');
return existing;
},
_instanceChanged: function(change) {
if (!change || !change.data || !change.data.resource)
{
return;
}
//console.log('Instance Changed:',change);
var self = this;
var instance = change.data.resource;
var id = instance.get('id');
// All the hosts
var allHosts = self.get('store').all('host');
// Host IDs the instance should be on
var expectedHostIds = [];
if ( instance.get('state') !== 'purged' )
{
expectedHostIds = (instance.get('hosts')||[]).map(function(host) {
return host.get('id');
});
}
// Host IDs it is currently on
var curHostIds = [];
allHosts.forEach(function(host) {
var existing = (host.get('instances')||[]).filterBy('id', id);
if ( existing.length )
{
curHostIds.push(host.get('id'));
}
});
// Remove from hosts the instance shouldn't be on
var remove = Util.arrayDiff(curHostIds, expectedHostIds);
remove.forEach(function(hostId) {
var host = self._findInCollection('hosts',hostId);
if ( host )
{
var instances = host.get('instances');
if ( !instances )
{
return;
}
instances.removeObjects(instances.filterBy('id', id));
}
});
// Add or update hosts the instance should be on
expectedHostIds.forEach(function(hostId) {
var host = self._findInCollection('hosts',hostId);
if ( host )
{
var instances = host.get('instances');
if ( !instances )
{
instances = [];
host.set('instances',instances);
}
var existing = instances.filterBy('id', id);
if ( existing.length === 0)
{
instances.pushObject(instance);
}
}
});
}
});

View File

@ -0,0 +1,5 @@
{{page-nav expand=navExpand}}
{{page-header pageName=pageName}}
<main>
{{outlet}}
</main>

View File

@ -0,0 +1,5 @@
import Ember from "ember";
export default Ember.View.extend({
classNameBindings: ['context.navExpand:nav-expand'],
});

View File

@ -0,0 +1,29 @@
import Ember from 'ember';
export default Ember.Component.extend({
type: 'user',
login: null,
classNames: ['gh-avatar'],
name: 'Loading...',
avatarUrl: null,
nameChanged: function() {
var self = this;
var url = 'https://api.github.com/' + this.get('type') + 's/' + this.get('login') + '?s=40';
Ember.$.ajax({url: url, dataType: 'json'}).then(function(body) {
self.set('name', body.name);
self.set('avatarUrl', body.avatar_url);
}, function() {
var type = self.get('type');
type = type.substr(0,1).toUpperCase() + type.substr(1);
self.set('name', 'Warning: ' + type + ' not found');
});
}.observes('login','type').on('init'),
url: function() {
return 'https://github.com/'+ encodeURIComponent(this.get('login'));
}.property('login'),
});

View File

@ -0,0 +1,16 @@
{{yield}}
<div class="gh-icon">
{{#if avatarUrl}}
<a {{bind-attr href=url}} target="_blank">
<img {{bind-attr src=avatarUrl}} width="40" height="40">
</a>
{{else}}
<div class="gh-placeholder"></div>
{{/if}}
</div>
<div class="gh-avatar-content">
<div class="clip">
<a {{bind-attr href=url}} target="_blank">{{login}}</a>
</div>
<div class="text-muted clip">{{name}}</div>
</div>

View File

@ -1,7 +1,33 @@
<div class="pull-right hide">
<span class="fa-stack fa-lg">
{{#if app.hasAuthentication}}
{{#if app.authenticationEnabled}}
<div class="pull-right">
<div class="dropdown">
<span class="fa-stack fa-lg hand" id="user-dropdown" data-toggle="dropdown" aria-expanded="true">
<i class="fa fa-circle-o fa-stack-2x"></i>
<i class="fa fa-user fa-stack-1x"></i>
</span>
</div>
<h2>{{pageName}}</h2>
<ul class="dropdown-menu dropdown-menu-right" role="menu" aria-labelledby="user-dropdown">
<li role="presentation" class="disabled">
<a role="menuitem" tabindex="-1">Settings</a>
</li>
<li role="presentation">
{{#link-to "settings.auth" role="menuitem" tabindex="-1"}}Access Control{{/link-to}}
</li>
<li role="presentation" class="divider"></li>
<li role="presentation">{{#link-to "logout" tabindex="-1"}}Log Out{{/link-to}}</li>
</ul>
</div>
</div>
{{else}}
<div class="pull-right">
<div class="alert alert-warning" role="alert" style="padding: 0 15px;">
<i class="fa fa-exclamation-triangle"></i>
Access Control is not configured&nbsp;&nbsp;
{{#link-to "settings.auth"}}Settings{{/link-to}}
</div>
</div>
{{/if}}
<h2>{{pageName}}</h2>
{{/if}}

View File

@ -17,4 +17,6 @@
</div>
{{/each}}
</div>
{{else}}
<div class="text-center text-muted">No hosts or containers yet.</div>
{{/each}}

View File

@ -5,11 +5,6 @@ export default Ember.Route.extend({
return this.get('store').findAll('host');
},
renderTemplate: function() {
this._super();
this.render('hosts', {into: 'application'});
},
actions: {
newContainer: function() {
this.transitionTo('newContainer');

View File

@ -0,0 +1,83 @@
import Ember from 'ember';
export default Ember.Controller.extend({
queryParams: ['timedOut'],
timedOut: false,
waiting: false,
errorMsg: null,
infoColor: function() {
if ( this.get('errorMsg') )
{
return 'alert-warning';
}
{
return 'alert-info';
}
}.property('errorMsg'),
infoMsg: function() {
if ( this.get('errorMsg') )
{
return this.get('errorMsg');
}
else if ( this.get('timedOut') )
{
return 'Your session has timed out. Log in again to continue.';
}
else
{
return '';
}
}.property('timedOut','waiting','errorMsg'),
actions: {
authenticate: function() {
var self = this;
var session = self.get('session');
var app = self.get('app');
self.set('timedOut', false);
self.set('waiting', true);
self.get('torii').open('github-oauth2').then(function(github){
return self.get('store').rawRequest({
url: 'token',
method: 'POST',
data: {
code: github.authorizationCode,
}
}).then(function(res) {
var auth = JSON.parse(res.xhr.responseText);
session.set('token', auth.jwt);
session.set('isLoggedIn',1);
var transition = app.get('afterLoginTransition');
if ( transition )
{
app.set('afterLoginTransition', null);
transition.retry();
}
else
{
self.transitionToRoute('index');
}
});
})
.catch(function(res) {
if ( res.xhr && res.xhr.responseText )
{
var body = JSON.parse(res.xhr.responseText);
self.set('errorMsg', body.message);
}
else
{
self.set('errorMsg', res.err);
}
}).finally(function() {
self.set('waiting', false);
});
}
}
});

10
app/pods/login/route.js Normal file
View File

@ -0,0 +1,10 @@
import Ember from 'ember';
export default Ember.Route.extend({
beforeModel: function() {
if ( !this.get('app.authenticationEnabled') )
{
this.transitionTo('index');
}
}
});

View File

@ -0,0 +1,25 @@
<div class="row">
<div class="col-sm-3"></div>
<div class="col-sm-6">
<div class="farm-box">
<section>
<h1>Welcome to Rancher</h1>
<br/>
{{#if infoMsg}}
<div {{bind-attr class=":alert infoColor"}} role="alert">{{infoMsg}}</div>
{{/if}}
<p>Rancher uses GitHub to manage acounts and teams. Click this comically large button to log in and give Rancher read-only access to your GitHub account.</p>
<br/>
<p class="text-center">
<button {{bind-attr disabled=waiting}} class="btn btn-lg btn-primary" {{action "authenticate"}}>
<i class="fa fa-github"></i> Authenticate with GitHub
</button>
</p>
</section>
</div>
</div>
<div class="col-sm-3"></div>
</div>

11
app/pods/login/view.js Normal file
View File

@ -0,0 +1,11 @@
import Ember from "ember";
export default Ember.View.extend({
didInsertElement: function() {
$('BODY').addClass('farm');
},
willDestroyElement: function() {
$('BODY').removeClass('farm');
},
});

9
app/pods/logout/route.js Normal file
View File

@ -0,0 +1,9 @@
import Ember from 'ember';
export default Ember.Route.extend({
beforeModel: function() {
var session = this.get('session');
session.clear();
this.send('logout');
}
});

View File

@ -0,0 +1,176 @@
import Ember from 'ember';
export default Ember.ObjectController.extend({
confirmDisable: false,
error: null,
createIncomplete: function() {
var id = (this.get('clientId')||'').trim();
var secret = (this.get('clientSecret')||'').trim();
return id.length < 20 ||secret.length < 40;
}.property('clientId','clientSecret'),
destinationUrl: function() {
return window.location.origin+'/';
}.property(),
actions: {
test: function() {
var self = this;
var model = this.get('model');
model.set('clientId', model.get('clientId').trim());
model.set('clientSecret', model.get('clientSecret').trim());
model.set('enabled',false); // It should already be, but just in case..
model.set('allowOrganizations',null);
model.set('allowUsers',null);
model.save().then(function() {
self.send('authenticate');
}).catch(function(err) {
self.send('gotError', err);
});
},
authenticate: function() {
var self = this;
self.get('torii').open('github-oauth2').then(function(github){
return self.get('store').rawRequest({
url: 'token',
method: 'POST',
data: {
code: github.authorizationCode,
}
}).then(function(res) {
var auth = JSON.parse(res.xhr.responseText);
self.send('authenticationSucceeded', auth);
}).catch(function(err) {
// Github auth succeeded but didn't get back a token
self.send('gotError', err);
});
})
.catch(function(err) {
// Github auth failed.. try again
self.send('gotError', err);
});
},
authenticationSucceeded: function(auth) {
console.log('Authentication succeeded');
var self = this;
var session = self.get('session');
session.set('token', auth.jwt);
session.set('isLoggedIn',1);
var model = this.get('model');
model.set('enabled',true);
model.set('allowOrganizations', auth.orgs||[]);
model.set('allowUsers', [auth.user]);
model.save().then(function() {
self.send('waitAndRefresh', true);
}).catch(function() {
// @TODO something
});
},
waitAndRefresh: function(expect,limit) {
console.log('Wait and refresh',expect,limit);
var self = this;
if ( limit === undefined )
{
limit = 5;
}
else if ( limit === 0 )
{
self.send('error', 'Timed out waiting for access control to turn ' + (expect ? 'on' : 'off'));
return;
}
setTimeout(function() {
self.get('store').rawRequest({
url: 'schemas',
headers: { 'authorization': undefined }
}).then(function() {
if ( expect === false )
{
window.location.href = window.location.href;
}
else
{
self.send('waitAndRefresh',expect,limit-1);
}
}).catch(function() {
if ( expect === true )
{
window.location.href = window.location.href;
}
else
{
self.send('waitAndRefresh',expect,limit-1);
}
});
}, 5000/limit);
},
addUser: function() {
var str = (this.get('addUser')||'').trim();
if ( str )
{
this.get('allowUsers').pushObject(str);
this.set('addUser','');
}
},
removeUser: function(user) {
this.get('allowUsers').removeObject(user);
},
addOrg: function() {
var str = (this.get('addOrg')||'').trim();
if ( str )
{
this.get('allowOrganizations').pushObject(str);
this.set('addOrg','');
}
},
removeOrg: function(org) {
this.get('allowOrganizations').removeObject(org);
},
saveAuthorization: function() {
var self = this;
var model = self.get('model');
model.save().then(function() {
self.send('waitAndRefresh', true);
}).catch(function(err) {
self.send('gotError', err);
});
},
promptDisable: function() {
this.set('confirmDisable',true);
},
gotError: function(err) {
this.set('error', err.message);
window.scrollY = 0;
},
disable: function() {
var self = this;
var model = this.get('model');
model.set('allowOrganizations',[]);
model.set('allowUsers',[]);
model.set('enabled',false);
model.save().then(function() {
self.send('waitAndRefresh', false);
}).catch(function(err) {
self.send('gotError', err);
});
},
},
});

View File

@ -0,0 +1,15 @@
import AuthenticatedRouteMixin from 'ui/mixins/authenticated-route';
import Ember from 'ember';
export default Ember.Route.extend(AuthenticatedRouteMixin,{
model: function() {
return this.get('store').find('githubconfig').then(function(collection) {
return collection.get('firstObject');
});
},
setupController: function(controller, model) {
this._super(controller,model);
controller.set('confirmDisable',false);
}
});

View File

@ -0,0 +1,170 @@
<div style="padding: 20px 20px 0 20px;">
<div class="well">
<h2>Access Control is {{#if app.authenticationEnabled}}<b>enabled</b>{{else}}<b class="text-warning">not configured</b>{{/if}}</h2>
<p>
{{#if app.authenticationEnabled}}
Rancher is configured to restrict access to the GitHub users and organization members below.
{{else}}
Rancher can be configured to restrict access to a set of GitHub users and organization members. This is not currently set up, so anybody that reach this page (or the API) has full control over the system.
{{/if}}
</p>
</div>
{{#if error}}
<div class="alert alert-danger">
<i style="float: left;" class="fa fa-exclamation-circle"></i>
<p style="margin-left: 50px">{{error}}</p>
</div>
{{/if}}
{{#if app.authenticationEnabled}}
<div class="well">
<h4>Configure Authorization</h4>
<hr/>
<p>Configure what users and organization members should be allowed to use Rancher.</p>
<div class="row">
<div class="col-sm-6">
<label>Users</label>
<form {{action "addUser"}}>
<div class="input-group">
{{input type="text" value=addUser placeholder="Add GitHub username" class="form-control"}}
<div class="input-group-btn">
<button class="btn btn-primary" {{action "addUser"}}><i class="fa fa-plus"></i></button>
</div>
</div>
</form>
<hr/>
<ul class="list-unstyled gh-avatar-list">
{{#each user in allowUsers}}
<li>
{{#github-avatar type="user" login=user}}
<button class="btn btn-danger btn-sm pull-right gh-action" {{action "removeUser" user}}>Remove</button>
{{/github-avatar}}
</li>
{{else}}
<span class="text-muted">No Users</span>
{{/each}}
</ul>
</div>
<div class="col-sm-6">
<label>Organizations</label>
<form {{action "addOrg"}}>
<div class="input-group">
{{input type="text" value=addOrg placeholder="Add GitHub organization name" class="form-control"}}
<div class="input-group-btn">
<button class="btn btn-primary" {{action "addOrg"}}><i class="fa fa-plus"></i></button>
</div>
</div>
</form>
<hr/>
<ul class="list-unstyled gh-avatar-list">
{{#each org in allowOrganizations}}
<li>
{{#github-avatar type="org" login=org}}
<button class="btn btn-danger btn-sm pull-right gh-action" {{action "removeOrg" org}}>Remove</button>
{{/github-avatar}}
</li>
{{else}}
<span class="text-muted">No Organizations</span>
{{/each}}
</ul>
</div>
</div>
<hr/>
<button class="btn btn-primary" {{action "saveAuthorization"}}>
Save Authorization Configuration
</button>
</div>
<div class="well">
<h4>Danger Zone&trade;</h4>
<hr/>
<p>
<b class="text-danger">Caution:</b> Disabling access control will give complete control over Rancher to anyone that can reach this page or the API.
</p>
{{#if confirmDisable}}
<button class="btn btn-danger" {{action "disable"}}>
<i class="fa fa-fire"></i> Are you sure? Click again to really disable Access Control
</button>
{{else}}
<button class="btn btn-danger" {{action "promptDisable"}}>
<i class="fa fa-fire"></i> Disable Access Control
</button>
{{/if}}
</div>
{{/if}}
{{#unless app.authenticationEnabled}}
<div class="well">
<h4>1. Setup a GitHub Application</h4>
<hr/>
<p>
<ul>
<li>
<a href="https://github.com/settings/applications" target="_blank">Click here</a> to go to your GitHub applications settings in a new window.
</li>
<li>
Click &quot;Register new application&quot; and fill out the form:
<ul>
<li><b>Application name:</b> <span class="text-muted">Anything you like, e.g. My Rancher</span></li>
<li>
<b>Homepage URL:&nbsp;</b> <span>{{destinationUrl}}</span>
</li>
<li><b>Application description:</b> <span class="text-muted">Anything you like, optional</span></li>
<li><b>Authorization callback URL</b> <span>{{destinationUrl}}</span></li>
</ul>
</li>
<li>Click &quot;Register application&quot;</li>
</ul>
</p>
</div>
<div class="well">
<h4>2. Configure Rancher to use your Application for Authentication</h4>
<hr/>
<p>Copy and paste the Client ID and Secret from the upper-right corner of your newly-created application.</p>
<div class="row">
<div class="col-md-6">
<div class="form-group">
<label for="client-id">Client ID</label>
{{input id="client-id" type="text" value=clientId classNames="form-control"}}
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label for="client-secret">Client Secret</label>
{{input id="client-secret" type="text" value=clientSecret classNames="form-control"}}
</div>
</div>
</div>
</div>
<div class="well">
<h4>3. Test and enable Authentication</h4>
<hr/>
<div class="row">
<div class="col-md-6">
<p>Check that your application is configured correctly by testing authentication with it:</p>
</div>
<div class="col-md-6">
<button {{bind-attr disabled=createIncomplete}} class="btn btn-primary" {{action "test"}}>
<i class="fa fa-github"></i> Authenticate with GitHub
</button>
</div>
</div>
</div>
{{/unless}}
</div>

View File

@ -0,0 +1,7 @@
import Ember from 'ember';
export default Ember.View.extend({
didInsertElement: function() {
$('#authEnabled').bootstrapSwitch();
},
});

View File

@ -0,0 +1,4 @@
import Ember from 'ember';
export default Ember.Route.extend({
});

View File

@ -0,0 +1 @@
{{outlet}}

View File

@ -4,9 +4,4 @@ export default Ember.Route.extend({
model: function() {
return this.get('store').findAll('volume');
},
renderTemplate: function() {
this.render('volumes', {into: 'application'});
},
});

View File

@ -6,7 +6,15 @@ var Router = Ember.Router.extend({
});
Router.map(function() {
this.route('failWhale', { path: '/failwhale' });
this.route('index');
this.route('failWhale', { path: '/fail' });
this.route('login');
this.route('logout');
this.route('authenticated', { path: '/'}, function() {
this.resource('settings', function() {
this.route('auth');
});
this.resource('hosts', { path: '/hosts'}, function() {
this.resource('host', { path: '/:host_id' }, function() {
@ -34,6 +42,8 @@ Router.map(function() {
this.route('delete');
});
});
});
});
export default Router;

View File

@ -1,6 +1,14 @@
import Ember from 'ember';
export default Ember.Route.extend({
enter: function() {
$('BODY').addClass('farm');
},
exit: function() {
$('BODY').removeClass('farm');
},
model: function() {
return this.controllerFor('application').get('error');
},
@ -8,7 +16,7 @@ export default Ember.Route.extend({
afterModel: function(model) {
if ( !model )
{
this.transitionTo('hosts');
// this.transitionTo('hosts');
}
}
});

View File

@ -3,5 +3,5 @@ import Ember from 'ember';
export default Ember.Route.extend({
enter: function() {
this.transitionTo('hosts');
}
},
});

View File

@ -6,7 +6,7 @@ export default Ember.Route.extend({
Ember.run(function() {
$('#loading-underlay').show().fadeIn({duration: 100, queue: false, easing: 'linear', complete: function() {
$('#loading-overlay').fadeIn({duration: 200, queue: false, easing: 'linear'});
$('#loading-overlay').show().fadeIn({duration: 200, queue: false, easing: 'linear'});
}});
});
},

View File

@ -10,3 +10,4 @@
@import "app/styles/overlay";
@import "app/styles/progress-bar";
@import "app/styles/host";
@import "app/styles/github-avatar";

View File

@ -0,0 +1,43 @@
.gh-avatar-list {
margin-bottom: 0;
LI {
margin-bottom: 5px;
&:last-of-type {
margin-bottom: 0;
}
}
}
.gh-avatar {
IMG {
border-radius: 3px;
}
.gh-icon {
float: left;
margin-right: 10px;
width: 40px;
height: 40px;
}
.gh-placeholder {
width: 40px;
height: 40px;
border: 1px solid #aaa;
border-radius: 3px;
}
.gh-avatar-content {
margin-left: 50px;
}
.gh-action {
display: none;
}
&:hover .gh-action {
display: block;
}
}

View File

@ -5,6 +5,13 @@ BODY {
background-color: #eceff4;
}
.farm {
background-color: #85a514;
background-image: url('images/background.jpg');
background-position: top center;
background-repeat: no-repeat;
}
/**********
* Navigation (left side bar)
**********/
@ -109,12 +116,13 @@ HEADER {
z-index: 2;
margin-left: $nav_width;
background-color: $header_bg;
padding: 0 20px;
padding: 0;
H2 {
font-weight: normal;
margin: 0;
line-height: $header_height;
padding-left: 20px;
}
}
@ -247,23 +255,26 @@ TABLE.graphs {
table-layout: fixed;
}
.fail-whale {
background-color: #85a514;
background-image: url('images/background.jpg');
background-position: top center;
background-repeat: no-repeat;
position: absolute;
.farm-box {
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 999;
SECTION {
width: 50%;
margin: 50px auto 0 auto;
min-width: 50%;
margin: 20px auto 0 auto;
background-color: white;
border-radius: 5px;
padding: 30px;
padding: 20px;
}
SECTION:first-of-type {
margin-top: 50px;
}
}
.hand {
cursor: pointer;
cursor: hand;
}

View File

@ -1,4 +1,7 @@
<div class="fail-whale">
<div class="row">
<div class="col-sm-3"></div>
<div class="col-sm-6">
<div class="farm-box">
<section>
<h1>Error</h1>
<div>{{code}} {{#if status}}({{status}}){{/if}}</div>
@ -7,4 +10,7 @@
<a href="#" onclick="window.location.href = window.location.href; return false;">Reload</a> to try again
</div>
</section>
</div>
</div>
<div class="col-sm-3"></div>
</div>

View File

@ -0,0 +1,35 @@
import Ember from "ember";
export default Ember.Object.extend({
unknownProperty: function(key) {
return localStorage[key];
},
setUnknownProperty: function(key, value) {
if( Ember.isNone(value) )
{
delete localStorage[key];
}
else
{
localStorage[key] = value;
}
this.notifyPropertyChange(key);
return value;
},
clear: function() {
var i;
this.beginPropertyChanges();
for ( i = 0 ; i < localStorage.length ; i++ )
{
this.set(localStorage.key(i));
}
localStorage.clear();
this.endPropertyChanges();
}
});

View File

@ -0,0 +1,35 @@
import Ember from "ember";
export default Ember.Object.extend({
unknownProperty: function(key) {
return sessionStorage[key];
},
setUnknownProperty: function(key, value) {
if( Ember.isNone(value) )
{
delete sessionStorage[key];
}
else
{
sessionStorage[key] = value;
}
this.notifyPropertyChange(key);
return value;
},
clear: function() {
var i;
this.beginPropertyChanges();
for ( i = 0 ; i < sessionStorage.length ; i++ )
{
this.set(sessionStorage.key(i));
}
sessionStorage.clear();
this.endPropertyChanges();
}
});

View File

@ -1,4 +1,4 @@
import AuthenticatedRouteMixin from 'simple-auth/mixins/authenticated-route-mixin';
import AuthenticatedRouteMixin from 'ui/mixins/authenticated-route';
import Ember from 'ember';
export default Ember.Route.extend(AuthenticatedRouteMixin, {

View File

@ -19,6 +19,7 @@
"c3": "~0.4.3",
"jquery.cookie": "~1.4.1",
"bootstrap-sass-official": "~3.3.1",
"bootstrap-multiselect": "~0.9.10"
"bootstrap-multiselect": "~0.9.10",
"bootstrap-switch": "~3.3.0"
}
}

View File

@ -16,12 +16,17 @@ module.exports = function(environment) {
}
},
torii: {
// This is configured at runtime, but torii complains
// on startup if there's no entry in the environment
},
contentSecurityPolicy: {
// Allow the occasional <elem style="blah">...
'style-src': "'self' cdn.rancher.io 'unsafe-inline'",
'font-src': "'self' cdn.rancher.io",
'script-src': "'self' cdn.rancher.io",
'img-src': "'self' cdn.rancher.io",
'img-src': "'self' cdn.rancher.io avatars.githubusercontent.com",
// Allow connect to anywhere, for console and event stream socket
'connect-src': '*'
@ -43,7 +48,7 @@ module.exports = function(environment) {
// ENV.APP.LOG_RESOLVER = true;
ENV.APP.LOG_ACTIVE_GENERATION = true;
ENV.APP.LOG_TRANSITIONS = true;
// ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
ENV.APP.LOG_TRANSITIONS_INTERNAL = true;
ENV.APP.LOG_VIEW_LOOKUPS = true;
ENV.contentSecurityPolicy['script-src'] = ENV.contentSecurityPolicy['script-src'] + " 'unsafe-eval'";
}

View File

@ -1,6 +1,6 @@
{
"name": "ui",
"version": "0.7.4",
"version": "0.8.0",
"private": true,
"directories": {
"doc": "doc",
@ -18,11 +18,12 @@
"author": "Rancher Labs",
"license": "Apache 2",
"devDependencies": {
"ember-api-store": "1.0.7",
"body-parser": "^1.2.0",
"broccoli-asset-rev": "^1.0.0",
"broccoli-sass": "^0.3.3",
"connect-restreamer": "^1.0.1",
"ember-api-store": "^1.0.6",
"ember-cli": "^0.1.5",
"ember-cli-content-security-policy": "0.3.0",
"ember-cli-dependency-checker": "0.0.7",
@ -37,6 +38,7 @@
"express": "^4.8.5",
"forever-agent": "^0.5.2",
"glob": "^4.0.5",
"http-proxy": "^1.6.2"
"http-proxy": "^1.6.2",
"torii": "^0.2.2"
}
}

View File

@ -26,7 +26,7 @@ module.exports = function(app, options) {
});
proxy.on('error', function onProxyError(err, req, res) {
console.log('Proxy Error:', err);
console.log('Proxy Error: on', req.method,'to', req.url,':', err);
var error = {
type: 'error',
status: 500,

View File

@ -0,0 +1,14 @@
import {
moduleFor,
test
} from 'ember-qunit';
moduleFor('route:authenticated', 'AuthenticatedRoute', {
// Specify the other units that are required for this test.
// needs: ['controller:foo']
});
test('it exists', function() {
var route = this.subject();
ok(route);
});

View File

@ -0,0 +1,14 @@
import {
moduleFor,
test
} from 'ember-qunit';
moduleFor('route:login', 'LoginRoute', {
// Specify the other units that are required for this test.
// needs: ['controller:foo']
});
test('it exists', function() {
var route = this.subject();
ok(route);
});

View File

@ -0,0 +1,14 @@
import {
moduleFor,
test
} from 'ember-qunit';
moduleFor('route:logout', 'LogoutRoute', {
// Specify the other units that are required for this test.
// needs: ['controller:foo']
});
test('it exists', function() {
var route = this.subject();
ok(route);
});

View File

@ -0,0 +1,14 @@
import {
moduleFor,
test
} from 'ember-qunit';
moduleFor('route:settings/auth', 'SettingsAuthRoute', {
// Specify the other units that are required for this test.
// needs: ['controller:foo']
});
test('it exists', function() {
var route = this.subject();
ok(route);
});

View File

@ -0,0 +1,14 @@
import {
moduleFor,
test
} from 'ember-qunit';
moduleFor('route:settings', 'SettingsRoute', {
// Specify the other units that are required for this test.
// needs: ['controller:foo']
});
test('it exists', function() {
var route = this.subject();
ok(route);
});