Skip to content

Commit

Permalink
Add features for the read only user.
Browse files Browse the repository at this point in the history
Summary:
Read only users need to have the UI modified so that they cannot see the task triggering
options in the UI. Using the OSS model, we can blacklist a few components.

Test Plan:
Ran sbt run and verified that the read only user has a few options greyed out. We need to
probably flesh out more of the UI blacklists before landing this.

Example of Overview with cloud feature config
{F13204}

View of Backups w/o Create/Restore btn's
{F13205}

Reviewers: ram, andrew

Reviewed By: andrew

Subscribers: jenkins-bot, yugaware

Differential Revision: https://phabricator.dev.yugabyte.com/D7842
  • Loading branch information
Arnav15 committed Feb 7, 2020
1 parent 3ba3b4a commit 94747c4
Show file tree
Hide file tree
Showing 17 changed files with 157 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,12 @@ public Result index(UUID customerUUID) {
}
responseJson.put("callhomeLevel", CustomerConfig.getOrCreateCallhomeLevel(customerUUID).toString());

responseJson.put("features", customer.getFeatures());
Users user = (Users) ctx().args.get("user");
if (customer.getFeatures().size() == 0) {
responseJson.put("features", user.getFeatures());
} else {
responseJson.put("features", customer.getFeatures());
}

return ok(responseJson);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,9 @@ public class SessionController extends Controller {
@Inject
ConfigHelper configHelper;


@Inject
Environment environment;


public static final String AUTH_TOKEN = "authToken";
public static final String API_TOKEN = "apiToken";
public static final String CUSTOMER_UUID = "customerUUID";
Expand Down Expand Up @@ -147,7 +145,7 @@ public Result set_security(UUID customerUUID) {
}

try {
InputStream featureStream = environment.resourceAsStream("sampleFeatureConfig.json");
InputStream featureStream = environment.resourceAsStream("ossFeatureConfig.json");
ObjectMapper mapper = new ObjectMapper();
JsonNode features = mapper.readTree(featureStream);
Customer.get(customerUUID).upsertFeatures(features);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

package com.yugabyte.yw.controllers;

import java.io.InputStream;
import java.io.IOException;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
Expand All @@ -12,7 +15,9 @@
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.inject.Inject;
import com.yugabyte.yw.models.Audit;
import com.yugabyte.yw.models.Customer;
Expand All @@ -24,6 +29,8 @@
import play.libs.Json;
import play.mvc.Result;

import play.Environment;

import static com.yugabyte.yw.models.Users.Role;

public class UsersController extends AuthenticatedController {
Expand All @@ -33,6 +40,9 @@ public class UsersController extends AuthenticatedController {
@Inject
FormFactory formFactory;

@Inject
Environment environment;

/**
* GET endpoint for listing the provider User.
* @return JSON response with user.
Expand Down Expand Up @@ -88,6 +98,9 @@ public Result create(UUID customerUUID) {
try {
user = Users.create(formData.get().email, formData.get().password,
formData.get().role, customerUUID);
if (formData.get().role == Role.ReadOnly) {
updateFeatures(user);
}
} catch (Exception e) {
return ApiResponse.error(INTERNAL_SERVER_ERROR, "Could not create user");
}
Expand Down Expand Up @@ -156,6 +169,11 @@ public Result changeRole(UUID customerUUID, UUID userUUID) {
try {
user.setRole(Role.valueOf(role));
user.save();
if (user.getRole() == Role.ReadOnly) {
updateFeatures(user);
} else {
user.setFeatures(Json.newObject());
}
} catch (Exception e) {
return ApiResponse.error(BAD_REQUEST, "Incorrect Role Specified");
}
Expand Down Expand Up @@ -197,5 +215,20 @@ public Result changePassword(UUID customerUUID, UUID userUUID) {
}
}
return ApiResponse.error(BAD_REQUEST, "Invalid User Credentials.");

private void updateFeatures(Users user) {
try {
Customer customer = Customer.get(user.customerUUID);
String configFile = "readOnlyFeatureConfig.json";
if (customer.code.equals("cloud")) {
configFile = "cloudFeatureConfig.json";
}
InputStream featureStream = environment.resourceAsStream(configFile);
ObjectMapper mapper = new ObjectMapper();
JsonNode features = mapper.readTree(featureStream);
user.upsertFeatures(features);
} catch (IOException e) {
LOG.error("Failed to parse sample feature config file for OSS mode.");
}
}
}
7 changes: 7 additions & 0 deletions managed/src/main/java/com/yugabyte/yw/models/Users.java
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,13 @@ public JsonNode getFeatures() {
return features == null ? Json.newObject() : features;
}

/**
* Set features for this User.
*/
public void setFeatures(JsonNode input) {
this.features = input;
}

/**
* Upserts features for this Users. If updating a feature, only specified features will
* be updated.
Expand Down
31 changes: 31 additions & 0 deletions managed/src/main/resources/cloudFeatureConfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"universe": {
"import": "disabled",
"create": "disabled",
"backup": "hidden"
},
"config": {
"infra": "disabled",
"backup": "disabled"
},
"main": {
"dropdown": "hidden"
},
"menu": {
"config": "disabled",
"sidebar": "hidden"
},
"universes": {
"details": {
"overview": {
"upgradeSoftware": "hidden",
"editGFlags": "hidden",
"readReplica": "hidden",
"editUniverse": "hidden",
"manageEncryption": "hidden",
"deleteUniverse": "hidden"
}
},
"tableActions": "disabled"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"readReplica": "disabled",
"manageEncryption": "disabled",
"metricsInterval": 15000,
"editUniverse": "hidden",
"deleteUniverse": "disabled"
}
},
Expand Down
26 changes: 26 additions & 0 deletions managed/src/main/resources/readOnlyFeatureConfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"universe": {
"import": "disabled",
"create": "disabled"
},
"config": {
"infra": "disabled",
"backup": "disabled"
},
"menu": {
"config": "disabled"
},
"universes": {
"details": {
"overview": {
"upgradeSoftware": "hidden",
"editGFlags": "hidden",
"editUniverse": "hidden",
"readReplica": "hidden",
"manageEncryption": "hidden",
"deleteUniverse": "hidden"
}
},
"tableActions": "disabled"
}
}
10 changes: 10 additions & 0 deletions managed/ui/src/app/stylesheets/layout.scss
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@
}
}

.sidebar-hidden .container-body {
padding: 60px 0 45px 0;

& > *:first-child {
&:before {
left: 0;
}
}
}

.container-fluid {
padding: 0 !important;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import React, { Component } from 'react';
import { withRouter } from 'react-router';
import { isNonEmptyArray } from 'utils/ObjectUtils';
import { getPromiseState } from 'utils/PromiseUtils';
import { isHidden } from 'utils/LayoutUtils';
const PropTypes = require('prop-types');

class AuthenticatedComponent extends Component {
Expand Down Expand Up @@ -81,8 +83,10 @@ class AuthenticatedComponent extends Component {
};

render() {
const { currentCustomer } = this.props;
const sidebarHidden = getPromiseState(currentCustomer).isSuccess() && isHidden(currentCustomer.data.features, "menu.sidebar");
return (
<div className="full-height-container">
<div className={sidebarHidden ? 'full-height-container sidebar-hidden' : 'full-height-container'}>
{this.props.children}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ const mapDispatchToProps = (dispatch) => {
const mapStateToProps = (state) => {
return {
cloud: state.cloud,
customer: state.customer,
currentCustomer: state.customer.currentCustomer,
universe: state.universe,
tasks: state.tasks,
fetchMetadata: state.cloud.fetchMetadata,
Expand Down
9 changes: 8 additions & 1 deletion managed/ui/src/components/common/nav_bar/SideNavBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { NavDropdown } from 'react-bootstrap';
import slackIcon from './images/slack-monochrome-black.svg';
import './stylesheets/SideNavBar.scss';
import { getPromiseState } from 'utils/PromiseUtils';
import { isNotHidden, getFeatureState } from 'utils/LayoutUtils';
import { isHidden, isNotHidden, getFeatureState } from 'utils/LayoutUtils';

class NavLink extends Component {
render () {
Expand Down Expand Up @@ -38,6 +38,13 @@ export default class SideNavBar extends Component {

render() {
const { customer: { currentCustomer } } = this.props;

// Add check for initial state of `currentCustomer` to avoid first load showing the sidebar
// Just in case we are on cloud and don't want to cause visual flicker
if (getPromiseState(currentCustomer).isInit() || isHidden(currentCustomer.data.features, "menu.sidebar")) {
return null;
}

return (
<div className="side-nav-container">
<div className="left_col" >
Expand Down
13 changes: 7 additions & 6 deletions managed/ui/src/components/common/nav_bar/TopNavBar.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,13 @@ export default class TopNavBar extends Component {
// TODO(bogdan): icon for logs...
return (
<Navbar fixedTop>
<Navbar.Header>
<Link to="/" className="left_col text-center">
<YBLogo />
</Link>
</Navbar.Header>

{getPromiseState(currentCustomer).isSuccess() && isNotHidden(currentCustomer.data.features, "menu.sidebar") &&
<Navbar.Header>
<Link to="/" className="left_col text-center">
<YBLogo />
</Link>
</Navbar.Header>
}
<div className="flex-grow"></div>
{getPromiseState(currentCustomer).isSuccess() && isNotHidden(currentCustomer.data.features, "main.dropdown") &&
<Nav pullRight>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
}

& > *:first-child {
flex: 0 0 90px;
flex: 1 0 90px;
}

& > *:not(:first-child):not(:last-child) {
Expand Down
19 changes: 11 additions & 8 deletions managed/ui/src/components/tables/ListBackups/ListBackups.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { DropdownButton } from 'react-bootstrap';
import { BootstrapTable, TableHeaderColumn } from 'react-bootstrap-table';
import { YBPanelItem } from '../../panels';
import { getPromiseState } from 'utils/PromiseUtils';
import { isNotHidden } from 'utils/LayoutUtils';
import { timeFormatter, successStringFormatter } from 'utils/TableFormatters';
import { YBLoadingCircleIcon } from '../../common/indicators';
import { TableAction } from '../../tables';
Expand All @@ -32,7 +33,7 @@ export default class ListBackups extends Component {
}

render() {
const { universeBackupList, universeTableTypes, title } = this.props;
const { currentCustomer, universeBackupList, universeTableTypes, title } = this.props;
if (getPromiseState(universeBackupList).isLoading() ||
getPromiseState(universeBackupList).isInit()) {
return <YBLoadingCircleIcon size="medium" />;
Expand All @@ -54,7 +55,7 @@ export default class ListBackups extends Component {
}).filter(Boolean);

const formatActionButtons = function(item, row) {
if (row.showActions) {
if (row.showActions && isNotHidden(currentCustomer.data.features, "universe.backup")) {
return (
<DropdownButton className="btn btn-default" title="Actions" id="bg-nested-dropdown" pullRight>
<TableAction currentRow={row} actionType="restore-backup" />
Expand All @@ -71,12 +72,14 @@ export default class ListBackups extends Component {
<h2 className="task-list-header content-title pull-left">{title}</h2>
</div>
<div className="pull-right">
<div className="backup-action-btn-group">
<TableAction className="table-action" btnClass={"btn-orange"}
actionType="create-backup" isMenuItem={false} />
<TableAction className="table-action" btnClass={"btn-default"}
actionType="restore-backup" isMenuItem={false} />
</div>
{isNotHidden(currentCustomer.data.features, "universe.backup") &&
<div className="backup-action-btn-group">
<TableAction className="table-action" btnClass={"btn-orange"}
actionType="create-backup" isMenuItem={false} />
<TableAction className="table-action" btnClass={"btn-default"}
actionType="restore-backup" isMenuItem={false} />
</div>
}
</div>
</div>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ function mapStateToProps(state, ownProps) {
return {
universeBackupList: state.universe.universeBackupList,
universeTableTypes: tableTypes,
currentCustomer: state.customer.currentCustomer
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,8 +359,8 @@ class UniverseDetail extends Component {
</YBLabelWithIcon>
{ this.showUpgradeMarker() ? <span className="badge badge-pill badge-red pull-right">{updateAvailable}</span> : ""}
</YBMenuItem>
{!isReadOnlyUniverse && isNotHidden(currentCustomer.data.features, "universes.details.metrics") &&
<YBMenuItem eventKey="2" to={`/universes/${uuid}/edit/primary`} availability={getFeatureState(currentCustomer.data.features, "universes.details.metrics")}>
{!isReadOnlyUniverse && isNotHidden(currentCustomer.data.features, "universes.details.overview.editUniverse") &&
<YBMenuItem eventKey="2" to={`/universes/${uuid}/edit/primary`} availability={getFeatureState(currentCustomer.data.features, "universes.details.overview.editUniverse")}>
<YBLabelWithIcon icon="fa fa-pencil">
Edit Universe
</YBLabelWithIcon>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@
}
}
}
.sidebar-hidden .universe-detail-status-container {
left: 30px;
}
.universe-detail {
.content-title {
position: static;
Expand Down Expand Up @@ -261,6 +264,9 @@
margin-left: 15px;
}
}
.sidebar-hidden .content-title {
left: 30px;
}

h2 {
a {
Expand Down

0 comments on commit 94747c4

Please sign in to comment.