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) } 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) responseObject, access := applyResourceAccessControl(responseObject, secretID, executor.operationContext) if !access { return rewriteAccessDeniedResponse(response) } return rewriteResponse(response, responseObject, http.StatusOK) } // decorateSecretList loops through all secrets and decorates any secret with an existing resource control. // Resource controls checks are based on: resource identifier. // 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) secretObject = decorateResourceWithAccessControl(secretObject, secretID, resourceControls) decoratedSecretData = append(decoratedSecretData, secretObject) } return decoratedSecretData, nil } // filterSecretList loops through all secrets and filters public secrets (no associated resource control) // as well as authorized secrets (access granted to the user based on existing resource control). // Authorized secrets are decorated during the process. // Resource controls checks are based on: resource identifier. // Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList func filterSecretList(secretData []interface{}, context *restrictedOperationContext) ([]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) secretObject, access := applyResourceAccessControl(secretObject, secretID, context) if access { filteredSecretData = append(filteredSecretData, secretObject) } } return filteredSecretData, nil }