Skip to content

Commit

Permalink
Merge pull request #131 from taskcluster/scope-inspector
Browse files Browse the repository at this point in the history
Added scope-inspector for exploring scopes, reverse lookup
  • Loading branch information
imbstack authored Aug 18, 2016
2 parents a8c7820 + e181248 commit b0683b4
Show file tree
Hide file tree
Showing 7 changed files with 364 additions and 0 deletions.
18 changes: 18 additions & 0 deletions auth/scopes/app.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
let React = require('react');
let ReactDOM = require('react-dom');
let ScopeInspector = require('./scopeinspector');
let $ = require('jquery');

var utils = require('../../lib/utils');

var hashManager = utils.createHashManager({
separator: '/'
});

// Render component
$(function() {
ReactDOM.render(
<ScopeInspector hashEntry={hashManager.root()}/>,
$('#container')[0]
);
});
4 changes: 4 additions & 0 deletions auth/scopes/app.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@

@import "../../lib/format.less";

@import "scopeinspector.less";
8 changes: 8 additions & 0 deletions auth/scopes/index.jade
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
extends ../../lib/layout.jade

block title
title Scope Inspector

block append head
link(rel="stylesheet", href="app.css")
script(src='app.bundle.js')
305 changes: 305 additions & 0 deletions auth/scopes/scopeinspector.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
var React = require('react');
var bs = require('react-bootstrap');
var utils = require('../../lib/utils');
var taskcluster = require('taskcluster-client');
var format = require('../../lib/format');
var _ = require('lodash');
var ReactDOM = require('react-dom');
var RoleEditor = require('../roles/roleeditor');
var ClientEditor = require('../clients/clienteditor');

/** Define scope spector */
var ScopeInspector = React.createClass({
/** Initialize mixins */
mixins: [
utils.createTaskClusterMixin({
clients: {
auth: taskcluster.Auth
}
}),
// Serialize selectedScope and selectedEntity to location.hash as string
utils.createLocationHashMixin({
keys: ['selectedScope', 'selectedEntity'],
type: 'string'
})
],

/** Create an initial state */
getInitialState() {
return {
rolesLoaded: false,
rolesError: undefined,
roles: undefined,
clientsLoaded: false,
clientsError: undefined,
clients: undefined,
selectedScope: '',
selectedEntity: '',
scopeSearchTerm: '',
entitySearchMode: 'Has Scope',
};
},

/** Load state from auth (using TaskClusterMixin) */
load() {
// Creates state properties:
// - rolesLoaded
// - rolesError
// - roles
// - clientsLoaded
// - clientsError
// - clients
return {
roles: this.auth.listRoles(),
clients: this.auth.listClients(),
};
},

/** Render user-interface */
render() {
return (
this.renderWaitFor('roles') ||
this.renderWaitFor('clients') ||
this.renderSelectedEntity() ||
this.renderSelectedScope() ||
this.renderScopes()
);

},

renderSelectedEntity() {
if (this.state.selectedEntity === "") {
return undefined;
}
return (
<bs.Row>
<bs.Col md={12}>
<bs.Row>
<bs.Col md={1}>
<bs.Button onClick={this.clearSelectedEntity}>
<bs.Glyphicon glyph="chevron-left"/> Back
</bs.Button>
</bs.Col>
<bs.Col md={11}>
<div style={{fontSize: '26px'}}>
{this.state.selectedEntity.split(':')[0]}:&nbsp;
<code>{this.state.selectedEntity.split(':')[1]}</code>
</div>
</bs.Col>
</bs.Row>
{
_.startsWith(this.state.selectedEntity, 'role:') ? (
<RoleEditor
currentRoleId={this.state.selectedEntity.slice('role:'.length)}
reloadRoleId={this.reload}/>
) : (
<ClientEditor
currentClientId={this.state.selectedEntity.slice('client:'.length)}
reloadClientId={this.reload}/>
)
}
</bs.Col>
</bs.Row>
);
},

clearSelectedEntity() {
this.setState({selectedEntity: ""});
},

renderSelectedScope() {
if (this.state.selectedScope === "") {
return undefined;
}
let mode = this.state.entitySearchMode;
let match = () => true;
if (mode === 'Exact') {
match = scope => scope === this.state.selectedScope;
}
if (mode === 'Has Scope') {
match = scope => {
if (scope === this.state.selectedScope) {
return true;
}
if (/\*$/.test(scope)) {
return this.state.selectedScope.indexOf(scope.slice(0, -1)) === 0;
}
return false;
};
}
if (mode === 'Has Sub-Scope') {
let pattern = this.state.selectedScope;
if (!/\*$/.test(pattern)) {
pattern += "*"; // Otherwise this test doesn't make any sense
}
match = scope => {
if (scope === pattern) {
return true;
}
return scope.indexOf(pattern.slice(0, -1)) === 0;
};
}
let clients = this.state.clients.filter(client => _.some(client.expandedScopes, match));
let roles = this.state.roles.filter(role => _.some(role.expandedScopes, match));
roles = _.sortBy(roles, 'roleId');
clients = _.sortBy(clients, 'clientId');
return (
<bs.Row>
<bs.Col md={12}>
<bs.Row>
<bs.Col md={1}>
<bs.Button onClick={this.clearSelectedScope}>
<bs.Glyphicon glyph="chevron-left"/> Back
</bs.Button>
</bs.Col>
<bs.Col md={11}>
<bs.InputGroup>
<bs.InputGroup.Addon>Scope</bs.InputGroup.Addon>
<bs.FormControl type="text"
value={this.state.selectedScope}
onChange={this.selectedScopeChanged}
ref='selectedScope'
/>
<bs.DropdownButton componentClass={bs.InputGroup.Button} title={"Match: " + mode} pullRight id='match'>
<bs.MenuItem key="1" onClick={this.setEntitySearchMode.bind(this, 'Exact')}>
<bs.Glyphicon glyph="ok"
style={mode === 'Exact' ? {} : {visibility: 'hidden'}}/>
&nbsp; Exact
</bs.MenuItem>
<bs.MenuItem key="2" onClick={this.setEntitySearchMode.bind(this, 'Has Scope')}>
<bs.Glyphicon glyph="ok"
style={mode === 'Has Scope' ? {} : {visibility: 'hidden'}}/>
&nbsp; Has Scope
</bs.MenuItem>
<bs.MenuItem key="3" onClick={this.setEntitySearchMode.bind(this, 'Has Sub-Scope')}>
<bs.Glyphicon glyph="ok"
style={mode === 'Has Sub-Scope' ? {} : {visibility: 'hidden'}}/>
&nbsp; Has Sub-Scope
</bs.MenuItem>
</bs.DropdownButton>
</bs.InputGroup>
</bs.Col>
</bs.Row>
<bs.Table condensed hover className="scopes-inspector-scopes-table">
<thead>
<tr>
<th><format.Icon name='users' fixedWidth={true}/> Roles / <format.Icon name='user'/> Clients</th>
</tr>
</thead>
<tbody>
{roles.map((role, index) => {
return (
<tr key={index}
onClick={this.selectEntity.bind(this, 'role:' + role.roleId)}>
<td>
<format.Icon name='users' fixedWidth={true}/>&nbsp;
<code>{role.roleId}</code>
</td>
</tr>
);
})}
{clients.map((client, index) => {
return (
<tr key={index + roles.length}
onClick={this.selectEntity.bind(this, 'client:' + client.clientId)}>
<td>
<format.Icon name='user' fixedWidth={true}/>&nbsp;
<code>{client.clientId}</code>
</td>
</tr>
);
})}
</tbody>
</bs.Table>
</bs.Col>
</bs.Row>
);
},

selectEntity(value) {
this.setState({
selectedEntity: value,
});
},

selectedScopeChanged() {
this.setState({
selectedScope: ReactDOM.findDOMNode(this.refs.selectedScope).value,
});
},

setEntitySearchMode(mode) {
this.setState({entitySearchMode: mode});
},

clearSelectedScope() {
this.setState({selectedScope: ""});
},

renderScopes() {
let scopes = _.uniq(_.flatten(
this.state.roles.map(role => role.expandedScopes),
this.state.clients.map(client => client.expandedScopes),
));
scopes.sort()
scopes = scopes.filter(scope => _.includes(scope, this.state.scopeSearchTerm));
return (
<bs.Row>
<bs.Col md={12}>
<bs.InputGroup>
<bs.InputGroup.Addon><bs.Glyphicon glyph="search"/></bs.InputGroup.Addon>
<bs.FormControl
type="text" value={this.state.scopeSearchTerm}
ref='scopeSearchTerm' onChange={this.scopeSearchTermChanged}/>
<bs.InputGroup.Button>
<bs.Button onClick={this.clearScopeSearchTerm}>
<bs.Glyphicon glyph="remove"/> Clear
</bs.Button>
</bs.InputGroup.Button>
</bs.InputGroup>
<bs.Table condensed hover className="scopes-inspector-scopes-table">
<thead>
<tr>
<th>Scopes</th>
</tr>
</thead>
<tbody>
{scopes.map(this.renderScopeRow)}
</tbody>
</bs.Table>
</bs.Col>
</bs.Row>
)
},

scopeSearchTermChanged(e) {
this.setState({
scopeSearchTerm: ReactDOM.findDOMNode(this.refs.scopeSearchTerm).value,
});
},

clearScopeSearchTerm() {
this.setState({
scopeSearchTerm: '',
});
},

/** Render row with scope */
renderScopeRow(scope, index) {
var isSelected = (this.state.selectedScope === scope);
return (
<tr key={index}
className={isSelected ? 'info' : undefined}
onClick={this.selectScope.bind(this, scope)}>
<td><code>{scope}</code></td>
</tr>
);
},

selectScope(scope) {
this.setState({selectedScope: scope});
}
});

// Export ScopeInspector
module.exports = ScopeInspector;
13 changes: 13 additions & 0 deletions auth/scopes/scopeinspector.less
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
.scopes-inspector-scopes-table {
cursor: pointer;
}

.scopes-inspector-scopes-table code, .scopes-inspector-btn-group-text code{
word-break: break-all;
}

.scopes-inspector-btn-group-text {
margin-left: 10px;
float: left;
font-size: 22px;
}
5 changes: 5 additions & 0 deletions build-files.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ module.exports = [
'auth/roles/app.less',
'auth/roles/app.jsx',

// Scopes Manager
'auth/scopes/index.jade',
'auth/scopes/app.less',
'auth/scopes/app.jsx',

// Login
'login/index.jade',
'login/app.jsx',
Expand Down
11 changes: 11 additions & 0 deletions menu.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,17 @@ module.exports = [
"and explore indirect scopes."
].join('\n')
},
{
title: "Scope Inspector",
link: '/auth/scopes/',
icon: 'wifi',
display: true,
description: [
"Explore scopes on `auth.taskcluster.net`. This tool allows you to",
"find roles and clients with a given scope. This is effectively reverse",
"client and role lookup."
].join('\n')
},
{
title: "Pulse Inspector",
link: '/pulse-inspector/',
Expand Down

0 comments on commit b0683b4

Please sign in to comment.