feat(secrets): add UAC (#1200)

pull/1203/head
Anthony Lapenna 2017-09-19 17:10:15 +02:00 committed by GitHub
parent a2b4cd8050
commit 61f652da04
11 changed files with 221 additions and 20 deletions

View File

@ -80,6 +80,8 @@ func (handler *ResourceHandler) handlePostResources(w http.ResponseWriter, r *ht
resourceControlType = portainer.VolumeResourceControl
case "network":
resourceControlType = portainer.NetworkResourceControl
case "secret":
resourceControlType = portainer.SecretResourceControl
default:
httperror.WriteErrorResponse(w, portainer.ErrInvalidResourceControlType, http.StatusBadRequest, handler.Logger)
return

View File

@ -106,6 +106,30 @@ func decorateNetworkList(networkData []interface{}, resourceControls []portainer
return decoratedNetworkData, nil
}
// decorateSecretList loops through all secrets and will decorate any secret with an existing resource control.
// Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList
func decorateSecretList(secretData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
decoratedSecretData := make([]interface{}, 0)
for _, secret := range secretData {
secretObject := secret.(map[string]interface{})
if secretObject[secretIdentifier] == nil {
return nil, ErrDockerSecretIdentifierNotFound
}
secretID := secretObject[secretIdentifier].(string)
resourceControl := getResourceControlByResourceID(secretID, resourceControls)
if resourceControl != nil {
secretObject = decorateObject(secretObject, resourceControl)
}
decoratedSecretData = append(decoratedSecretData, secretObject)
}
return decoratedSecretData, nil
}
func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} {
metadata := make(map[string]interface{})
metadata["ResourceControl"] = resourceControl

View File

@ -135,3 +135,28 @@ func filterNetworkList(networkData []interface{}, resourceControls []portainer.R
return filteredNetworkData, nil
}
// filterSecretList loops through all secrets, filters secrets without any resource control (public resources) or with
// any resource control giving access to the user (these secrets will be decorated).
// Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList
func filterSecretList(secretData []interface{}, resourceControls []portainer.ResourceControl, userID portainer.UserID, userTeamIDs []portainer.TeamID) ([]interface{}, error) {
filteredSecretData := make([]interface{}, 0)
for _, secret := range secretData {
secretObject := secret.(map[string]interface{})
if secretObject[secretIdentifier] == nil {
return nil, ErrDockerSecretIdentifierNotFound
}
secretID := secretObject[secretIdentifier].(string)
resourceControl := getResourceControlByResourceID(secretID, resourceControls)
if resourceControl == nil {
filteredSecretData = append(filteredSecretData, secretObject)
} else if resourceControl != nil && canUserAccessResource(userID, userTeamIDs, resourceControl) {
secretObject = decorateObject(secretObject, resourceControl)
filteredSecretData = append(filteredSecretData, secretObject)
}
}
return filteredSecretData, nil
}

67
api/http/proxy/secrets.go Normal file
View File

@ -0,0 +1,67 @@
package proxy
import (
"net/http"
"github.com/portainer/portainer"
)
const (
// ErrDockerSecretIdentifierNotFound defines an error raised when Portainer is unable to find a secret identifier
ErrDockerSecretIdentifierNotFound = portainer.Error("Docker secret identifier not found")
secretIdentifier = "ID"
)
// secretListOperation extracts the response as a JSON object, loop through the secrets array
// decorate and/or filter the secrets based on resource controls before rewriting the response
func secretListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
var err error
// SecretList response is a JSON array
// https://docs.docker.com/engine/api/v1.28/#operation/SecretList
responseArray, err := getResponseAsJSONArray(response)
if err != nil {
return err
}
if executor.operationContext.isAdmin {
responseArray, err = decorateSecretList(responseArray, executor.operationContext.resourceControls)
} else {
responseArray, err = filterSecretList(responseArray, executor.operationContext.resourceControls,
executor.operationContext.userID, executor.operationContext.userTeamIDs)
}
if err != nil {
return err
}
return rewriteResponse(response, responseArray, http.StatusOK)
}
// secretInspectOperation extracts the response as a JSON object, verify that the user
// has access to the secret based on resource control (check are done based on the secretID and optional Swarm service ID)
// and either rewrite an access denied response or a decorated secret.
func secretInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
// SecretInspect response is a JSON object
// https://docs.docker.com/engine/api/v1.28/#operation/SecretInspect
responseObject, err := getResponseAsJSONOBject(response)
if err != nil {
return err
}
if responseObject[secretIdentifier] == nil {
return ErrDockerSecretIdentifierNotFound
}
secretID := responseObject[secretIdentifier].(string)
resourceControl := getResourceControlByResourceID(secretID, executor.operationContext.resourceControls)
if resourceControl != nil {
if executor.operationContext.isAdmin || canUserAccessResource(executor.operationContext.userID,
executor.operationContext.userTeamIDs, resourceControl) {
responseObject = decorateObject(responseObject, resourceControl)
} else {
return rewriteAccessDeniedResponse(response)
}
}
return rewriteResponse(response, responseObject, http.StatusOK)
}

View File

@ -62,6 +62,8 @@ func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Respon
return p.proxyVolumeRequest(request)
case strings.HasPrefix(path, "/networks"):
return p.proxyNetworkRequest(request)
case strings.HasPrefix(path, "/secrets"):
return p.proxySecretRequest(request)
case strings.HasPrefix(path, "/swarm"):
return p.proxySwarmRequest(request)
default:
@ -157,12 +159,30 @@ func (p *proxyTransport) proxyNetworkRequest(request *http.Request) (*http.Respo
return p.rewriteOperation(request, networkListOperation)
default:
// assume /networks/{name}
// assume /networks/{id}
if request.Method == http.MethodGet {
return p.rewriteOperation(request, networkInspectOperation)
}
volumeID := path.Base(requestPath)
return p.restrictedOperation(request, volumeID)
networkID := path.Base(requestPath)
return p.restrictedOperation(request, networkID)
}
}
func (p *proxyTransport) proxySecretRequest(request *http.Request) (*http.Response, error) {
switch requestPath := request.URL.Path; requestPath {
case "/secrets/create":
return p.executeDockerRequest(request)
case "/secrets":
return p.rewriteOperation(request, secretListOperation)
default:
// assume /secrets/{id}
if request.Method == http.MethodGet {
return p.rewriteOperation(request, secretInspectOperation)
}
secretID := path.Base(requestPath)
return p.restrictedOperation(request, secretID)
}
}

View File

@ -401,4 +401,6 @@ const (
VolumeResourceControl
// NetworkResourceControl represents a resource control associated to a Docker network
NetworkResourceControl
// SecretResourceControl represents a resource control associated to a Docker secret
SecretResourceControl
)

View File

@ -1,11 +1,17 @@
angular.module('createSecret', [])
.controller('CreateSecretController', ['$scope', '$state', 'Notifications', 'SecretService', 'LabelHelper',
function ($scope, $state, Notifications, SecretService, LabelHelper) {
.controller('CreateSecretController', ['$scope', '$state', 'Notifications', 'SecretService', 'LabelHelper', 'Authentication', 'ResourceControlService', 'FormValidator',
function ($scope, $state, Notifications, SecretService, LabelHelper, Authentication, ResourceControlService, FormValidator) {
$scope.formValues = {
Name: '',
Data: '',
Labels: [],
encodeSecret: true
encodeSecret: true,
AccessControlData: new AccessControlFormData()
};
$scope.state = {
formValidationError: ''
};
$scope.addLabel = function() {
@ -36,10 +42,38 @@ function ($scope, $state, Notifications, SecretService, LabelHelper) {
return config;
}
function createSecret(config) {
$('#createSecretSpinner').show();
SecretService.create(config)
function validateForm(accessControlData, isAdmin) {
$scope.state.formValidationError = '';
var error = '';
error = FormValidator.validateAccessControl(accessControlData, isAdmin);
if (error) {
$scope.state.formValidationError = error;
return false;
}
return true;
}
$scope.create = function () {
$('#createResourceSpinner').show();
var accessControlData = $scope.formValues.AccessControlData;
var userDetails = Authentication.getUserDetails();
var isAdmin = userDetails.role === 1 ? true : false;
if (!validateForm(accessControlData, isAdmin)) {
$('#createResourceSpinner').hide();
return;
}
var secretConfiguration = prepareConfiguration();
SecretService.create(secretConfiguration)
.then(function success(data) {
var secretIdentifier = data.ID;
var userId = userDetails.ID;
return ResourceControlService.applyResourceControl('secret', secretIdentifier, userId, accessControlData, []);
})
.then(function success() {
Notifications.success('Secret successfully created');
$state.go('secrets', {}, {reload: true});
})
@ -47,12 +81,7 @@ function ($scope, $state, Notifications, SecretService, LabelHelper) {
Notifications.error('Failure', err, 'Unable to create secret');
})
.finally(function final() {
$('#createSecretSpinner').hide();
$('#createResourceSpinner').hide();
});
}
$scope.create = function () {
var config = prepareConfiguration();
createSecret(config);
};
}]);

View File

@ -66,6 +66,9 @@
<!-- !labels-input-list -->
</div>
<!-- !labels-->
<!-- access-control -->
<por-access-control-form form-data="formValues.AccessControlData" ng-if="applicationState.application.authentication"></por-access-control-form>
<!-- !access-control -->
<!-- actions -->
<div class="col-sm-12 form-section-title">
Actions
@ -74,7 +77,8 @@
<div class="col-sm-12">
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!formValues.Name || !formValues.Data" ng-click="create()">Create secret</button>
<a type="button" class="btn btn-default btn-sm" ui-sref="secrets">Cancel</a>
<i id="createSecretSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
<i id="createResourceSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
<span class="text-danger" ng-if="state.formValidationError" style="margin-left: 5px;">{{ state.formValidationError }}</span>
</div>
</div>
<!-- !actions -->

View File

@ -53,3 +53,12 @@
</rd-widget>
</div>
</div>
<!-- access-control-panel -->
<por-access-control-panel
ng-if="secret && applicationState.application.authentication"
resource-id="secret.Id"
resource-control="secret.ResourceControl"
resource-type="'secret'">
</por-access-control-panel>
<!-- !access-control-panel -->

View File

@ -30,31 +30,44 @@
<input type="checkbox" ng-model="allSelected" ng-change="selectItems(allSelected)" />
</th>
<th>
<a ui-sref="secrets" ng-click="order('Name')">
<a ng-click="order('Name')">
Name
<span ng-show="sortType == 'Name' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Name' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th>
<a ui-sref="secrets" ng-click="order('CreatedAt')">
<a ng-click="order('CreatedAt')">
Created at
<span ng-show="sortType == 'Image' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'Image' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
<th ng-if="applicationState.application.authentication">
<a ng-click="order('ResourceControl.Ownership')">
Ownership
<span ng-show="sortType == 'ResourceControl.Ownership' && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
<span ng-show="sortType == 'ResourceControl.Ownership' && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
</a>
</th>
</thead>
<tbody>
<tr dir-paginate="secret in (state.filteredSecrets = ( secrets | filter:state.filter | orderBy:sortType:sortReverse | itemsPerPage: pagination_count))">
<td><input type="checkbox" ng-model="secret.Checked" ng-change="selectItem(secret)"/></td>
<td><a ui-sref="secret({id: secret.Id})">{{ secret.Name }}</a></td>
<td>{{ secret.CreatedAt | getisodate }}</td>
<td ng-if="applicationState.application.authentication">
<span>
<i ng-class="secret.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
{{ secret.ResourceControl.Ownership ? secret.ResourceControl.Ownership : secret.ResourceControl.Ownership = 'public' }}
</span>
</td>
</tr>
<tr ng-if="!secrets">
<td colspan="3" class="text-center text-muted">Loading...</td>
<td colspan="4" class="text-center text-muted">Loading...</td>
</tr>
<tr ng-if="secrets.length == 0">
<td colspan="3" class="text-center text-muted">No secrets available.</td>
<td colspan="4" class="text-center text-muted">No secrets available.</td>
</tr>
</tbody>
</table>

View File

@ -5,4 +5,10 @@ function SecretViewModel(data) {
this.Version = data.Version.Index;
this.Name = data.Spec.Name;
this.Labels = data.Spec.Labels;
if (data.Portainer) {
if (data.Portainer.ResourceControl) {
this.ResourceControl = new ResourceControlViewModel(data.Portainer.ResourceControl);
}
}
}