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:
parent
44c7c899c0
commit
c4fbf2c89b
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
};
|
||||
}
|
||||
);
|
|
@ -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> 
|
||||
<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> 
|
||||
<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> 
|
||||
{{ 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> 
|
||||
{{user.full_name}}
|
||||
<a class="close"
|
||||
ng-click="removeUser(user)"
|
||||
|
|
|
@ -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> 
|
||||
{{ 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> 
|
||||
{{user.full_name}}
|
||||
<a class="close"
|
||||
ng-click="removeUser(user)"
|
||||
|
|
|
@ -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> 
|
||||
<strong>This story is private</strong>.
|
||||
</p>
|
||||
<p ng-show="story.security">
|
||||
<i class="fa fa-fw fa-lock"></i> 
|
||||
<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> 
|
||||
{{ 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> 
|
||||
{{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"
|
||||
|
|
Loading…
Reference in New Issue