Allow marking stories as security-related

This commit adds a checkbox when creating or editing stories which
allows them to be explicitly marked as security-related. It also rewords
the label for the privacy checkbox to avoid any confusion.

When creating a security-related story, privacy is enforced and the
checkbox disabled and replaced with a message explaining that. Public
security-related stories can be created by editing a story after the
fact to make it public, since there is no limitation on privacy when
editing a story. This is intended to make it difficult to accidentally
file a security story publically.

In order for the checkbox to actually be useful, this commit also adds
code which automates the addition of relevant security teams to the
story's permissions as tasks are added (or the security checkbox is
toggled).

Story: 2000568
Task: 29892
Task: 29893
Change-Id: I8a2c6e47f926aedcbe77878f2bc5991cd84f815d
This commit is contained in:
Adam Coldrick 2019-03-08 16:39:48 +00:00
parent 44c7c899c0
commit c4fbf2c89b
8 changed files with 227 additions and 43 deletions

View File

@ -24,7 +24,7 @@ angular.module('sb.story').controller('StoryDetailController',
Story, Project, Branch, creator, tasks, Task, DSCacheFactory,
User, $q, storyboardApiBase, SessionModalService, moment,
$document, $anchorScroll, $timeout, $location, currentUser,
enableEditableComments, Tags, worklists, Team) {
enableEditableComments, Tags, worklists, Team, StoryHelper) {
'use strict';
var pageSize = Preference.get('story_detail_page_size');
@ -362,6 +362,16 @@ angular.module('sb.story').controller('StoryDetailController',
$scope.showEditForm = false;
};
$scope.privacyLocked = false;
/**
* Handle any change to whether or not the story is security-related
*/
$scope.updateSecurity = function(forcePrivate, update) {
$scope.privacyLocked = StoryHelper.updateSecurity(
forcePrivate, update, $scope.story, $scope.tasks);
};
/**
* Delete method.
*/
@ -637,6 +647,7 @@ angular.module('sb.story').controller('StoryDetailController',
branch.tasks.push(savedTask);
} else {
mapTaskToProject(savedTask);
$scope.updateSecurity(false, true);
}
$scope.loadEvents();
task.title = '';
@ -698,6 +709,7 @@ angular.module('sb.story').controller('StoryDetailController',
cleanBranchAndProject(projectName, branchName);
mapTaskToProject(updated);
$scope.updateSecurity(false, true);
}
});
}

View File

@ -19,7 +19,7 @@
*/
angular.module('sb.story').controller('StoryModalController',
function ($scope, $modalInstance, params, Project, Story, Task, User,
Team, $q, CurrentUser) {
Team, $q, CurrentUser, StoryHelper) {
'use strict';
var currentUser = CurrentUser.resolve();
@ -38,6 +38,15 @@ angular.module('sb.story').controller('StoryModalController',
title: ''
})];
/**
* Handle any change to whether or not the story is security-related
*/
$scope.updateSecurity = function(forcePrivate, update) {
$scope.privacyLocked = StoryHelper.updateSecurity(
forcePrivate, update, $scope.story, $scope.tasks);
};
// Preload the project
if (params.projectId) {
Project.get({
@ -129,6 +138,7 @@ angular.module('sb.story').controller('StoryModalController',
});
}
$scope.tasks.push(current_task);
$scope.updateSecurity(true, false);
};
/**
@ -165,6 +175,7 @@ angular.module('sb.story').controller('StoryModalController',
*/
$scope.selectNewProject = function (model, task) {
task.project_id = model.id;
$scope.updateSecurity(true, false);
};
/**

View File

@ -28,6 +28,7 @@
* (i.e. private with the option to change privacy hidden).
* - private: If truthy, the story will begin set to private.
* Unlike force_private, this allows the user to change to public.
* - security: If truthy, the story will begin set as security-related.
* - tags: Tags to set on the story. Can be given multiple times.
* - team_id: A team ID to grant permissions for this story to. Can be
* given multiple times.
@ -36,13 +37,25 @@
*/
angular.module('sb.story').controller('StoryNewController',
function ($scope, $state, $stateParams, Story, Project, Branch, Tags,
Task, Team, User, $q, storyboardApiBase, currentUser) {
Task, Team, User, StoryHelper, $q, storyboardApiBase,
currentUser) {
'use strict';
/**
* Handle any change to whether or not the story is security-related
*/
$scope.updateSecurity = function(forcePrivate, update) {
$scope.privacyLocked = StoryHelper.updateSecurity(
forcePrivate, update, $scope.story, $scope.tasks);
};
var story = new Story({
title: $stateParams.title,
description: $stateParams.description,
private: !!$stateParams.private || !!$stateParams.force_private,
private: (!!$stateParams.private ||
!!$stateParams.force_private ||
!!$stateParams.security),
security: !!$stateParams.security,
users: [currentUser],
teams: []
});
@ -99,6 +112,7 @@ angular.module('sb.story').controller('StoryNewController',
$scope.projectNames = [];
$scope.projects = {};
$scope.tasks = [];
$scope.updateSecurity(true, false);
/**
* UI flag for when we're initially loading the view.
@ -248,7 +262,8 @@ angular.module('sb.story').controller('StoryNewController',
$scope.newTask = new Task({
project_id: $stateParams.project_id,
show: true,
status: 'todo'
status: 'todo',
title: $stateParams.title
});
function mapTaskToProject(task) {
@ -286,6 +301,10 @@ angular.module('sb.story').controller('StoryNewController',
});
}
$scope.validTask = function(task) {
return !isNaN(task.project_id) && !!task.title;
};
/**
* Adds a task.
*/
@ -305,6 +324,7 @@ angular.module('sb.story').controller('StoryNewController',
});
}
$scope.tasks.push(savedTask);
$scope.updateSecurity(true, false);
task.title = '';
};
@ -358,6 +378,7 @@ angular.module('sb.story').controller('StoryNewController',
cleanBranchAndProject(projectName, branchName);
mapTaskToProject(task);
$scope.updateSecurity(true, false);
}
};

View File

@ -32,7 +32,7 @@ angular.module('sb.story', ['ui.router', 'sb.services', 'sb.util',
+ 'project_id&assignee_id';
var creationParams = 'title&description&project_id&'
+ 'private&force_private&tags&team_id&user_id';
+ 'private&force_private&security&tags&team_id&user_id';
// Set our page routes.
$stateProvider

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) 2019 Codethink Ltd.
*
* 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.
*/
angular.module('sb.story').factory('StoryHelper',
function(Story, Team) {
'use strict';
function updateSecurity(forcePrivate, update, story, tasks) {
var privacyLocked;
if (story.security) {
if (forcePrivate) {
story.private = true;
privacyLocked = true;
}
// Add security teams for affected projects
var projects = tasks.map(function(task) {
return task.project_id;
}).filter(function(value) {
// Remove any unset project_ids we've somehow got.
// Otherwise, the browse will return all teams, rather
// than only relevant teams.
return !isNaN(value);
});
angular.forEach(projects, function(project_id) {
Team.browse({project_id: project_id}, function(teams) {
var teamIds = story.teams.map(function(team) {
return team.id;
});
teams = teams.filter(function(team) {
return ((teamIds.indexOf(team.id) === -1)
&& team.security);
});
angular.forEach(teams, function(team) {
story.teams.push(team);
if (update) {
Story.TeamsController.create({
story_id: story.id,
team_id: team.id
});
}
});
});
});
} else {
if (forcePrivate) {
privacyLocked = false;
}
}
return privacyLocked;
}
return {
updateSecurity: updateSecurity
};
}
);

View File

@ -17,12 +17,19 @@
-->
<div class="container-fluid">
<div class="row">
<div class="row" ng-show="story.private && !showEditForm">
<div class="col-xs-12">
<div class="alert alert-danger" ng-show="story.private">
<i class="fa fa-eye-slash"></i>
<strong>This story is private</strong>.
Edit this story to change the privacy settings.
<div class="alert alert-danger">
<p>
<i class="fa fa-fw fa-eye-slash"></i>&emsp;
<strong>This story is private</strong>.
Edit this story to change the privacy settings.
</p>
<p ng-show="story.security">
<i class="fa fa-fw fa-lock"></i>&emsp;
<strong>This story is security-related</strong>.
Security Teams related to any affected projects will be automatically added to the story.
</p>
</div>
</div>
</div>
@ -183,6 +190,20 @@
type="checkbox"
class="modal-checkbox"
ng-model="story.private"
ng-disabled="isUpdating || privacyLocked"
/>
</div>
</div>
<div class="form-group">
<label for="security" class="col-sm-2 control-label">
Vulnerability or Security-related
</label>
<div class="col-sm-10 checkbox">
<input id="security"
type="checkbox"
class="modal-checkbox"
ng-model="story.security"
ng-change="updateSecurity(false)"
ng-disabled="isUpdating"
/>
</div>
@ -209,7 +230,7 @@
<tbody>
<tr ng-repeat="team in story.teams">
<td colspan="2">
<i class="fa fa-sb-team"></i>
<i class="fa fa-fw fa-sb-team text-muted"></i>&emsp;
{{ team.name }}
<a class="close"
ng-click="removeTeam(team)"
@ -220,7 +241,7 @@
</tr>
<tr ng-repeat="user in story.users">
<td colspan="2">
<i class="fa fa-sb-user"></i>
<i class="fa fa-fw fa-sb-user text-muted"></i>&emsp;
{{user.full_name}}
<a class="close"
ng-click="removeUser(user)"

View File

@ -49,20 +49,40 @@
</textarea>
</div>
</div>
<div class="form-group">
<label for="private"
class="col-sm-2 control-label">
Private or Security Vulnerability
</label>
<div class="col-sm-10 checkbox">
<input id="private"
type="checkbox"
class="modal-checkbox"
ng-model="story.private"
ng-disabled="isSaving"
/>
<div class="form-group" ng-if="!forcePrivate">
<label for="private" class="col-sm-2 control-label">
Private
</label>
<div class="col-sm-10">
<div class="checkbox" ng-show="!privacyLocked">
<input id="private"
type="checkbox"
class="modal-checkbox"
ng-model="story.private"
ng-disabled="isSaving || privacyLocked"
/>
</div>
<div class="checkbox help-block"
ng-show="privacyLocked">
Security-related stories are always private when created.
If privacy is really not required, the story can be edited
after creation to be made public.
</div>
</div>
</div>
<div class="form-group" ng-if="!forcePrivate">
<label for="security" class="col-sm-2 control-label">
Vulnerability or Security-related
</label>
<div class="col-sm-10 checkbox">
<input id="security"
type="checkbox"
class="modal-checkbox"
ng-model="story.security"
ng-change="updateSecurity(true, false)"
ng-disabled="isSaving"
/>
</div>
</div>
<div class="row">
<div class="col-md-6 col-md-offset-3"
@ -70,8 +90,7 @@
<table class="table table-striped">
<thead>
<tr>
<th>Teams and Users that can see this story. If it is a security vulnerability,
add the associated teams, e.g. vmt, $project coresec, etc.</th>
<th>Teams and Users that can see this story</th>
<th class="text-right">
<small>
<a href
@ -87,7 +106,7 @@
<tbody>
<tr ng-repeat="team in story.teams">
<td colspan="2">
<i class="fa fa-sb-team"></i>
<i class="fa fa-fw fa-sb-team text-muted"></i>&emsp;
{{ team.name }}
<a class="close"
ng-click="removeTeam(team)"
@ -98,7 +117,7 @@
</tr>
<tr ng-repeat="user in story.users">
<td colspan="2">
<i class="fa fa-sb-user"></i>
<i class="fa fa-fw fa-sb-user text-muted"></i>&emsp;
{{user.full_name}}
<a class="close"
ng-click="removeUser(user)"

View File

@ -20,8 +20,15 @@
<div class="row">
<div class="col-xs-12">
<div class="alert alert-danger" ng-show="story.private">
<i class="fa fa-eye-slash"></i>
<strong>This story is private</strong>.
<p>
<i class="fa fa-fw fa-eye-slash"></i>&emsp;
<strong>This story is private</strong>.
</p>
<p ng-show="story.security">
<i class="fa fa-fw fa-lock"></i>&emsp;
<strong>This story is security-related</strong>.
Security Teams related to any affected projects will be automatically added to the story.
</p>
</div>
</div>
</div>
@ -131,13 +138,35 @@
</div>
<div class="form-group" ng-if="!forcePrivate">
<label for="private" class="col-sm-2 control-label">
Private or Security Vulnerability
Private
</label>
<div class="col-sm-10">
<div class="checkbox" ng-show="!privacyLocked">
<input id="private"
type="checkbox"
class="modal-checkbox"
ng-model="story.private"
ng-disabled="isUpdating || privacyLocked"
/>
</div>
<div class="checkbox help-block"
ng-show="privacyLocked">
Security-related stories are always private when created.
If privacy is really not required, the story can be edited
after creation to be made public.
</div>
</div>
</div>
<div class="form-group" ng-if="!forcePrivate">
<label for="security" class="col-sm-2 control-label">
Vulnerability or Security-related
</label>
<div class="col-sm-10 checkbox">
<input id="private"
<input id="security"
type="checkbox"
class="modal-checkbox"
ng-model="story.private"
ng-model="story.security"
ng-change="updateSecurity(true, false)"
ng-disabled="isUpdating"
/>
</div>
@ -148,8 +177,7 @@
<table class="table table-striped">
<thead>
<tr>
<th>Teams and Users that can see this story. If it is a security vulnerability,
add the associated teams, e.g. vmt, $project coresec, etc.</th>
<th>Teams and Users that can see this story</th>
<th class="text-right">
<small>
<a href
@ -165,7 +193,7 @@
<tbody>
<tr ng-repeat="team in story.teams">
<td colspan="2">
<i class="fa fa-sb-team"></i>
<i class="fa fa-fw fa-sb-team text-muted"></i>&emsp;
{{ team.name }}
<a class="close"
ng-click="removeTeam(team)"
@ -176,7 +204,7 @@
</tr>
<tr ng-repeat="user in story.users">
<td colspan="2">
<i class="fa fa-sb-user"></i>
<i class="fa fa-fw fa-sb-user text-muted"></i>&emsp;
{{user.full_name}}
<a class="close"
ng-click="removeUser(user)"
@ -368,7 +396,7 @@
empty-prompt="Click to set a project"
empty-disabled-prompt="No project set."
as-inline="false"
placeholder="Enter project name">
placeholder="Type and select project">
</project-typeahead>
<button class="btn btn-xs btn-default"
ng-show="isLoggedIn"
@ -484,7 +512,8 @@
<div class="col-xs-2 text-right col-xs-offset-1">
<button ng-click="createTask(projects[name].branches[branchName].newTask,
projects[name].branches[branchName])"
class="btn btn-primary">
class="btn btn-primary"
ng-disabled="!validTask(projects[name].branches[branchName].newTask)">
Save
</button>
<button ng-click="projects[name].branches[branchName].showAddTask =
@ -521,7 +550,7 @@
empty-prompt="Click to set a project"
empty-disabled-prompt="No project set."
as-inline="false"
placeholder="Enter project name">
placeholder="Type and select project">
</project-typeahead>
</h4>
<div class="row">
@ -572,7 +601,8 @@
<!-- Review link should go here once implemented in the API -->
<div class="col-xs-2 text-right col-xs-offset-1">
<button ng-click="createTask(newTask)"
class="btn btn-primary">
class="btn btn-primary"
ng-disabled="!validTask(newTask)">
Add
</button>
<button ng-click="newTask.show = !newTask.show"