Compare commits
1 Commits
fix/CE/484
...
revert-396
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df59c29d12 |
@@ -6,6 +6,7 @@ env:
|
||||
|
||||
globals:
|
||||
angular: true
|
||||
__CONFIG_GA_ID: true
|
||||
|
||||
extends:
|
||||
- 'eslint:recommended'
|
||||
|
||||
11
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
11
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -1,10 +1,6 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a bug report
|
||||
title: ''
|
||||
labels: bug/need-confirmation, kind/bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
@@ -13,14 +9,14 @@ Thanks for reporting a bug for Portainer !
|
||||
|
||||
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
|
||||
|
||||
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
|
||||
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/.
|
||||
|
||||
Before opening a new issue, make sure that we do not have any duplicates
|
||||
already open. You can ensure this by searching the issue list for this
|
||||
repository. If there is a duplicate, please close your issue and add a comment
|
||||
to the existing issue instead.
|
||||
|
||||
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
|
||||
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
|
||||
-->
|
||||
|
||||
**Bug description**
|
||||
@@ -31,7 +27,7 @@ A clear and concise description of what you expected to happen.
|
||||
|
||||
**Portainer Logs**
|
||||
Provide the logs of your Portainer container or Service.
|
||||
You can see how [here](https://documentation.portainer.io/archive/1.23.2/faq/#how-do-i-get-the-logs-from-portainer)
|
||||
You can see how [here](https://portainer.readthedocs.io/en/stable/faq.html#how-do-i-get-the-logs-from-portainer)
|
||||
|
||||
**Steps to reproduce the issue:**
|
||||
|
||||
@@ -44,7 +40,6 @@ You can see how [here](https://documentation.portainer.io/archive/1.23.2/faq/#ho
|
||||
|
||||
- Portainer version:
|
||||
- Docker version (managed by Portainer):
|
||||
- Kubernetes version (managed by Portainer):
|
||||
- Platform (windows/linux):
|
||||
- Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`):
|
||||
- Browser:
|
||||
|
||||
37
.github/ISSUE_TEMPLATE/Custom.md
vendored
37
.github/ISSUE_TEMPLATE/Custom.md
vendored
@@ -1,20 +1,17 @@
|
||||
---
|
||||
name: Question
|
||||
about: Ask us a question about Portainer usage or deployment
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
|
||||
|
||||
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
|
||||
|
||||
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
|
||||
-->
|
||||
|
||||
**Question**:
|
||||
How can I deploy Portainer on... ?
|
||||
---
|
||||
name: Question
|
||||
about: Ask us a question about Portainer usage or deployment
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
|
||||
|
||||
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/
|
||||
|
||||
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
|
||||
-->
|
||||
|
||||
**Question**:
|
||||
How can I deploy Portainer on... ?
|
||||
|
||||
65
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
65
.github/ISSUE_TEMPLATE/Feature_request.md
vendored
@@ -1,34 +1,31 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a feature/enhancement that should be added in Portainer
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
Thanks for opening a feature request for Portainer !
|
||||
|
||||
Do you need help or have a question? Come chat with us on Slack http://portainer.slack.com/
|
||||
|
||||
Before opening a new issue, make sure that we do not have any duplicates
|
||||
already open. You can ensure this by searching the issue list for this
|
||||
repository. If there is a duplicate, please close your issue and add a comment
|
||||
to the existing issue instead.
|
||||
|
||||
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
|
||||
-->
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a feature/enhancement that should be added in Portainer
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
|
||||
Thanks for opening a feature request for Portainer !
|
||||
|
||||
Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/
|
||||
|
||||
Before opening a new issue, make sure that we do not have any duplicates
|
||||
already open. You can ensure this by searching the issue list for this
|
||||
repository. If there is a duplicate, please close your issue and add a comment
|
||||
to the existing issue instead.
|
||||
|
||||
Also, be sure to check our FAQ and documentation first: https://portainer.readthedocs.io
|
||||
-->
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
|
||||
13
.github/stale.yml
vendored
13
.github/stale.yml
vendored
@@ -12,14 +12,14 @@ issues:
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- kind/enhancement
|
||||
- kind/feature
|
||||
- kind/question
|
||||
- kind/style
|
||||
- kind/workaround
|
||||
- kind/refactor
|
||||
- bug/need-confirmation
|
||||
- bug/confirmed
|
||||
- status/discuss
|
||||
|
||||
|
||||
# Only issues with all of these labels are checked if stale. Defaults to `[]` (disabled)
|
||||
onlyLabels: []
|
||||
|
||||
@@ -35,9 +35,9 @@ issues:
|
||||
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been marked as stale as it has not had recent activity,
|
||||
This issue has been marked as stale as it has not had recent activity,
|
||||
it will be closed if no further activity occurs in the next 7 days.
|
||||
If you believe that it has been incorrectly labelled as stale,
|
||||
If you believe that it has been incorrectly labelled as stale,
|
||||
leave a comment and the label will be removed.
|
||||
|
||||
# Comment to post when removing the stale label.
|
||||
@@ -48,7 +48,8 @@ issues:
|
||||
closeComment: >
|
||||
Since no further activity has appeared on this issue it will be closed.
|
||||
If you believe that it has been incorrectly closed, leave a comment
|
||||
mentioning `ametdoohan`, `balasu` or `keverv` and one of our staff will then review the issue.
|
||||
|
||||
and mention @itsconquest. One of our staff will then review the issue.
|
||||
|
||||
Note - If it is an old bug report, make sure that it is reproduceable in the
|
||||
latest version of Portainer as it may have already been fixed.
|
||||
|
||||
@@ -74,23 +74,3 @@ Our contribution process is described below. Some of the steps can be visualized
|
||||
The feature request process is similar to the bug report process but has an extra functional validation before the technical validation as well as a documentation validation before the testing phase.
|
||||
|
||||

|
||||
|
||||
## Build Portainer locally
|
||||
|
||||
Ensure you have Docker, Node.js, yarn, and Golang installed in the correct versions.
|
||||
|
||||
Install dependencies with yarn:
|
||||
|
||||
```sh
|
||||
$ yarn
|
||||
```
|
||||
|
||||
Then build and run the project:
|
||||
|
||||
```sh
|
||||
$ yarn start
|
||||
```
|
||||
|
||||
Portainer can now be accessed at <http://localhost:9000>.
|
||||
|
||||
Find more detailed steps at <https://documentation.portainer.io/contributing/instructions/>.
|
||||
|
||||
23
README.md
23
README.md
@@ -10,7 +10,7 @@
|
||||
|
||||
**_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters).
|
||||
**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (can be deployed as Linux container or a Windows native container, supports other platforms too).
|
||||
**_Portainer_** allows you to manage all your Docker resources (containers, images, volumes, networks and more!) It is compatible with the _standalone Docker_ engine and with _Docker Swarm mode_.
|
||||
**_Portainer_** allows you to manage all your Docker resources (containers, images, volumes, networks and more) ! It is compatible with the _standalone Docker_ engine and with _Docker Swarm mode_.
|
||||
|
||||
## Demo
|
||||
|
||||
@@ -24,39 +24,32 @@ Alternatively, you can deploy a copy of the demo stack inside a [play-with-docke
|
||||
- Sign in with your [Docker ID](https://docs.docker.com/docker-id)
|
||||
- Follow [these](https://github.com/portainer/portainer-demo/blob/master/play-with-docker/docker-stack.yml#L5-L8) steps.
|
||||
|
||||
Unlike the public demo, the playground sessions are deleted after 4 hours. Apart from that, all the settings are the same, including default credentials.
|
||||
Unlike the public demo, the playground sessions are deleted after 4 hours. Apart from that, all the settings are same, including default credentials.
|
||||
|
||||
## Getting started
|
||||
|
||||
- [Deploy Portainer](https://documentation.portainer.io/quickstart/)
|
||||
- [Documentation](https://documentation.portainer.io)
|
||||
- [Building Portainer](https://documentation.portainer.io/contributing/instructions/)
|
||||
- [Deploy Portainer](https://www.portainer.io/installation/)
|
||||
- [Documentation](https://www.portainer.io/documentation/)
|
||||
|
||||
## Getting help
|
||||
|
||||
For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products/portainer-business
|
||||
For FORMAL Support, please purchase a support subscription from here: https://www.portainer.io/products-services/portainer-business-support/
|
||||
|
||||
For community support: You can find more information about Portainer's community support framework policy here: https://www.portainer.io/products/community-edition/customer-success
|
||||
For community support: You can find more information about Portainer's community support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/
|
||||
|
||||
- Issues: https://github.com/portainer/portainer/issues
|
||||
- FAQ: https://documentation.portainer.io
|
||||
- FAQ: https://www.portainer.io/documentation/faqs/
|
||||
- Slack (chat): https://portainer.io/slack/
|
||||
|
||||
## Reporting bugs and contributing
|
||||
|
||||
- Want to report a bug or request a feature? Please open [an issue](https://github.com/portainer/portainer/issues/new).
|
||||
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://documentation.portainer.io/contributing/instructions/) to build it locally and make a pull request. We need all the help we can get!
|
||||
- Want to help us build **_portainer_**? Follow our [contribution guidelines](https://www.portainer.io/documentation/how-to-contribute/) to build it locally and make a pull request. We need all the help we can get!
|
||||
|
||||
## Security
|
||||
|
||||
- Here at Portainer, we believe in [responsible disclosure](https://en.wikipedia.org/wiki/Responsible_disclosure) of security issues. If you have found a security issue, please report it to <security@portainer.io>.
|
||||
|
||||
## Privacy
|
||||
|
||||
**To make sure we focus our development effort in the right places we need to know which features get used most often. To give us this information we use [Matomo Analytics](https://matomo.org/), which is hosted in Germany and is fully GDPR compliant.**
|
||||
|
||||
When Portainer first starts, you are given the option to DISABLE analytics. If you **don't** choose to disable it, we collect anonymous usage as per [our privacy policy](https://www.portainer.io/documentation/in-app-analytics-and-privacy-policy/). **Please note**, there is no personally identifiable information sent or stored at any time and we only use the data to help us improve Portainer.
|
||||
|
||||
## Limitations
|
||||
|
||||
Portainer supports "Current - 2 docker versions only. Prior versions may operate, however these are not supported.
|
||||
|
||||
@@ -340,6 +340,11 @@ func (store *Store) EndpointRelation() portainer.EndpointRelationService {
|
||||
return store.EndpointRelationService
|
||||
}
|
||||
|
||||
// Extension gives access to the Extension data management layer
|
||||
func (store *Store) Extension() portainer.ExtensionService {
|
||||
return store.ExtensionService
|
||||
}
|
||||
|
||||
// Registry gives access to the Registry data management layer
|
||||
func (store *Store) Registry() portainer.RegistryService {
|
||||
return store.RegistryService
|
||||
|
||||
@@ -1,30 +1,14 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/gofrs/uuid"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
// Init creates the default data set.
|
||||
func (store *Store) Init() error {
|
||||
instanceID, err := store.VersionService.InstanceID()
|
||||
if err == errors.ErrObjectNotFound {
|
||||
uid, err := uuid.NewV4()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
instanceID = uid.String()
|
||||
err = store.VersionService.StoreInstanceID(instanceID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = store.SettingsService.Settings()
|
||||
_, err := store.SettingsService.Settings()
|
||||
if err == errors.ErrObjectNotFound {
|
||||
defaultSettings := &portainer.Settings{
|
||||
AuthenticationMethod: portainer.AuthenticationInternal,
|
||||
@@ -40,11 +24,14 @@ func (store *Store) Init() error {
|
||||
portainer.LDAPGroupSearchSettings{},
|
||||
},
|
||||
},
|
||||
OAuthSettings: portainer.OAuthSettings{},
|
||||
|
||||
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
|
||||
TemplatesURL: portainer.DefaultTemplatesURL,
|
||||
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
|
||||
OAuthSettings: portainer.OAuthSettings{},
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
EnableHostManagementFeatures: false,
|
||||
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
|
||||
TemplatesURL: portainer.DefaultTemplatesURL,
|
||||
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
|
||||
}
|
||||
|
||||
err = store.SettingsService.UpdateSettings(defaultSettings)
|
||||
@@ -92,5 +79,60 @@ func (store *Store) Init() error {
|
||||
}
|
||||
}
|
||||
|
||||
roles, err := store.RoleService.Roles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(roles) == 0 {
|
||||
environmentAdministratorRole := &portainer.Role{
|
||||
Name: "Endpoint administrator",
|
||||
Description: "Full control of all resources in an endpoint",
|
||||
Priority: 1,
|
||||
Authorizations: authorization.DefaultEndpointAuthorizationsForEndpointAdministratorRole(),
|
||||
}
|
||||
|
||||
err = store.RoleService.CreateRole(environmentAdministratorRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
environmentReadOnlyUserRole := &portainer.Role{
|
||||
Name: "Helpdesk",
|
||||
Description: "Read-only access of all resources in an endpoint",
|
||||
Priority: 2,
|
||||
Authorizations: authorization.DefaultEndpointAuthorizationsForHelpDeskRole(false),
|
||||
}
|
||||
|
||||
err = store.RoleService.CreateRole(environmentReadOnlyUserRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
standardUserRole := &portainer.Role{
|
||||
Name: "Standard user",
|
||||
Description: "Full control of assigned resources in an endpoint",
|
||||
Priority: 3,
|
||||
Authorizations: authorization.DefaultEndpointAuthorizationsForStandardUserRole(false),
|
||||
}
|
||||
|
||||
err = store.RoleService.CreateRole(standardUserRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
readOnlyUserRole := &portainer.Role{
|
||||
Name: "Read-only user",
|
||||
Description: "Read-only access of assigned resources in an endpoint",
|
||||
Priority: 4,
|
||||
Authorizations: authorization.DefaultEndpointAuthorizationsForReadOnlyUserRole(false),
|
||||
}
|
||||
|
||||
err = store.RoleService.CreateRole(readOnlyUserRole)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package migrator
|
||||
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateSettingsToDB24() error {
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
@@ -8,27 +10,11 @@ func (m *Migrator) updateSettingsToDB24() error {
|
||||
return err
|
||||
}
|
||||
|
||||
legacySettings.AllowHostNamespaceForRegularUsers = true
|
||||
legacySettings.AllowDeviceMappingForRegularUsers = true
|
||||
legacySettings.AllowStackManagementForRegularUsers = true
|
||||
if legacySettings.TemplatesURL == "" {
|
||||
legacySettings.TemplatesURL = portainer.DefaultTemplatesURL
|
||||
}
|
||||
|
||||
legacySettings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
|
||||
|
||||
return m.settingsService.UpdateSettings(legacySettings)
|
||||
}
|
||||
|
||||
func (m *Migrator) updateStacksToDB24() error {
|
||||
stacks, err := m.stackService.Stacks()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for idx := range stacks {
|
||||
stack := &stacks[idx]
|
||||
stack.Status = portainer.StackStatusActive
|
||||
err := m.stackService.UpdateStack(stack.ID, stack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateSettingsToDB25() error {
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if legacySettings.TemplatesURL == "" {
|
||||
legacySettings.TemplatesURL = portainer.DefaultTemplatesURL
|
||||
}
|
||||
|
||||
legacySettings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
|
||||
legacySettings.EnableTelemetry = true
|
||||
|
||||
legacySettings.AllowContainerCapabilitiesForRegularUsers = true
|
||||
|
||||
return m.settingsService.UpdateSettings(legacySettings)
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateEndpointSettingsToDB25() error {
|
||||
settings, err := m.settingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
endpoints, err := m.endpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range endpoints {
|
||||
endpoint := endpoints[i]
|
||||
|
||||
securitySettings := portainer.EndpointSecuritySettings{}
|
||||
|
||||
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment ||
|
||||
endpoint.Type == portainer.AgentOnDockerEnvironment ||
|
||||
endpoint.Type == portainer.DockerEnvironment {
|
||||
|
||||
securitySettings = portainer.EndpointSecuritySettings{
|
||||
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
|
||||
AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers,
|
||||
AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers,
|
||||
AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers,
|
||||
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
||||
AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers,
|
||||
}
|
||||
|
||||
if endpoint.Type == portainer.AgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
|
||||
securitySettings.AllowVolumeBrowserForRegularUsers = settings.AllowVolumeBrowserForRegularUsers
|
||||
securitySettings.EnableHostManagementFeatures = settings.EnableHostManagementFeatures
|
||||
}
|
||||
}
|
||||
|
||||
endpoint.SecuritySettings = securitySettings
|
||||
|
||||
err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -321,7 +321,7 @@ func (m *Migrator) Migrate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.24.1
|
||||
// Portainer 2.0
|
||||
if m.currentDBVersion < 24 {
|
||||
err := m.updateSettingsToDB24()
|
||||
if err != nil {
|
||||
@@ -329,26 +329,5 @@ func (m *Migrator) Migrate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.0.0
|
||||
if m.currentDBVersion < 25 {
|
||||
err := m.updateSettingsToDB25()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = m.updateStacksToDB24()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.1.0
|
||||
if m.currentDBVersion < 26 {
|
||||
err := m.updateEndpointSettingsToDB25()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/bolt/internal"
|
||||
"strings"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
@@ -48,8 +47,6 @@ func (service *Service) User(ID portainer.UserID) (*portainer.User, error) {
|
||||
func (service *Service) UserByUsername(username string) (*portainer.User, error) {
|
||||
var user *portainer.User
|
||||
|
||||
username = strings.ToLower(username)
|
||||
|
||||
err := service.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
cursor := bucket.Cursor()
|
||||
@@ -61,7 +58,7 @@ func (service *Service) UserByUsername(username string) (*portainer.User, error)
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.ToLower(u.Username) == username {
|
||||
if u.Username == username {
|
||||
user = &u
|
||||
break
|
||||
}
|
||||
@@ -126,7 +123,6 @@ func (service *Service) UsersByRole(role portainer.UserRole) ([]portainer.User,
|
||||
// UpdateUser saves a user.
|
||||
func (service *Service) UpdateUser(ID portainer.UserID, user *portainer.User) error {
|
||||
identifier := internal.Itob(int(ID))
|
||||
user.Username = strings.ToLower(user.Username)
|
||||
return internal.UpdateObject(service.db, BucketName, identifier, user)
|
||||
}
|
||||
|
||||
@@ -137,7 +133,6 @@ func (service *Service) CreateUser(user *portainer.User) error {
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
user.ID = portainer.UserID(id)
|
||||
user.Username = strings.ToLower(user.Username)
|
||||
|
||||
data, err := internal.MarshalObject(user)
|
||||
if err != nil {
|
||||
|
||||
@@ -10,9 +10,8 @@ import (
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "version"
|
||||
versionKey = "DB_VERSION"
|
||||
instanceKey = "INSTANCE_ID"
|
||||
BucketName = "version"
|
||||
versionKey = "DB_VERSION"
|
||||
)
|
||||
|
||||
// Service represents a service to manage stored versions.
|
||||
@@ -65,37 +64,3 @@ func (service *Service) StoreDBVersion(version int) error {
|
||||
return bucket.Put([]byte(versionKey), data)
|
||||
})
|
||||
}
|
||||
|
||||
// InstanceID retrieves the stored instance ID.
|
||||
func (service *Service) InstanceID() (string, error) {
|
||||
var data []byte
|
||||
|
||||
err := service.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
value := bucket.Get([]byte(instanceKey))
|
||||
if value == nil {
|
||||
return errors.ErrObjectNotFound
|
||||
}
|
||||
|
||||
data = make([]byte, len(value))
|
||||
copy(data, value)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// StoreInstanceID store the instance ID.
|
||||
func (service *Service) StoreInstanceID(ID string) error {
|
||||
return service.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(BucketName))
|
||||
|
||||
data := []byte(ID)
|
||||
return bucket.Put([]byte(instanceKey), data)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
@@ -36,7 +35,7 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
||||
EndpointURL: kingpin.Flag("host", "Endpoint URL").Short('H').String(),
|
||||
EnableEdgeComputeFeatures: kingpin.Flag("edge-compute", "Enable Edge Compute features").Bool(),
|
||||
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").Bool(),
|
||||
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(),
|
||||
TLS: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLS).Bool(),
|
||||
TLSSkipVerify: kingpin.Flag("tlsskipverify", "Disable TLS server verification").Default(defaultTLSSkipVerify).Bool(),
|
||||
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
|
||||
@@ -89,9 +88,7 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
||||
}
|
||||
|
||||
func displayDeprecationWarnings(flags *portainer.CLIFlags) {
|
||||
if *flags.NoAnalytics {
|
||||
log.Println("Warning: The --no-analytics flag has been kept to allow migration of instances running a previous version of Portainer with this flag enabled, to version 2.0 where enabling this flag will have no effect.")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func validateEndpointURL(endpointURL string) error {
|
||||
|
||||
@@ -8,6 +8,7 @@ const (
|
||||
defaultTunnelServerPort = "8000"
|
||||
defaultDataDirectory = "/data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "/certs/ca.pem"
|
||||
|
||||
@@ -6,6 +6,7 @@ const (
|
||||
defaultTunnelServerPort = "8000"
|
||||
defaultDataDirectory = "C:\\data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "C:\\certs\\ca.pem"
|
||||
|
||||
@@ -17,15 +17,13 @@ import (
|
||||
"github.com/portainer/portainer/api/git"
|
||||
"github.com/portainer/portainer/api/http"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
kubeproxy "github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"github.com/portainer/portainer/api/internal/snapshot"
|
||||
"github.com/portainer/portainer/api/jwt"
|
||||
"github.com/portainer/portainer/api/kubernetes"
|
||||
kubecli "github.com/portainer/portainer/api/kubernetes/cli"
|
||||
"github.com/portainer/portainer/api/ldap"
|
||||
"github.com/portainer/portainer/api/libcompose"
|
||||
"github.com/portainer/portainer/api/oauth"
|
||||
)
|
||||
|
||||
func initCLI() *portainer.CLIFlags {
|
||||
@@ -73,12 +71,7 @@ func initDataStore(dataStorePath string, fileService portainer.FileService) port
|
||||
return store
|
||||
}
|
||||
|
||||
func initComposeStackManager(assetsPath string, dataStorePath string, reverseTunnelService portainer.ReverseTunnelService, proxyManager *proxy.Manager) portainer.ComposeStackManager {
|
||||
composeWrapper := exec.NewComposeWrapper(assetsPath, proxyManager)
|
||||
if composeWrapper != nil {
|
||||
return composeWrapper
|
||||
}
|
||||
|
||||
func initComposeStackManager(dataStorePath string, reverseTunnelService portainer.ReverseTunnelService) portainer.ComposeStackManager {
|
||||
return libcompose.NewComposeStackManager(dataStorePath, reverseTunnelService)
|
||||
}
|
||||
|
||||
@@ -96,10 +89,6 @@ func initJWTService(dataStore portainer.DataStore) (portainer.JWTService, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if settings.UserSessionTimeout == "" {
|
||||
settings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
|
||||
dataStore.Settings().UpdateSettings(settings)
|
||||
}
|
||||
jwtService, err := jwt.NewService(settings.UserSessionTimeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -119,10 +108,6 @@ func initLDAPService() portainer.LDAPService {
|
||||
return &ldap.Service{}
|
||||
}
|
||||
|
||||
func initOAuthService() portainer.OAuthService {
|
||||
return oauth.NewService()
|
||||
}
|
||||
|
||||
func initGitService() portainer.GitService {
|
||||
return git.NewService()
|
||||
}
|
||||
@@ -131,8 +116,8 @@ func initDockerClientFactory(signatureService portainer.DigitalSignatureService,
|
||||
return docker.NewClientFactory(signatureService, reverseTunnelService)
|
||||
}
|
||||
|
||||
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string) *kubecli.ClientFactory {
|
||||
return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID)
|
||||
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *kubecli.ClientFactory {
|
||||
return kubecli.NewClientFactory(signatureService, reverseTunnelService)
|
||||
}
|
||||
|
||||
func initSnapshotService(snapshotInterval string, dataStore portainer.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory) (portainer.SnapshotService, error) {
|
||||
@@ -164,7 +149,8 @@ func loadEdgeJobsFromDatabase(dataStore portainer.DataStore, reverseTunnelServic
|
||||
|
||||
func initStatus(flags *portainer.CLIFlags) *portainer.Status {
|
||||
return &portainer.Status{
|
||||
Version: portainer.APIVersion,
|
||||
Analytics: !*flags.NoAnalytics,
|
||||
Version: portainer.APIVersion,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +163,6 @@ func updateSettingsFromFlags(dataStore portainer.DataStore, flags *portainer.CLI
|
||||
settings.LogoURL = *flags.Logo
|
||||
settings.SnapshotInterval = *flags.SnapshotInterval
|
||||
settings.EnableEdgeComputeFeatures = *flags.EnableEdgeComputeFeatures
|
||||
settings.EnableTelemetry = true
|
||||
|
||||
if *flags.Templates != "" {
|
||||
settings.TemplatesURL = *flags.Templates
|
||||
@@ -248,18 +233,6 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, dataStore portainer.Dat
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
|
||||
SecuritySettings: portainer.EndpointSecuritySettings{
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
EnableHostManagementFeatures: false,
|
||||
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
AllowContainerCapabilitiesForRegularUsers: true,
|
||||
AllowDeviceMappingForRegularUsers: true,
|
||||
AllowStackManagementForRegularUsers: true,
|
||||
},
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpoint.URL, "tcp://") {
|
||||
@@ -309,18 +282,6 @@ func createUnsecuredEndpoint(endpointURL string, dataStore portainer.DataStore,
|
||||
Status: portainer.EndpointStatusUp,
|
||||
Snapshots: []portainer.DockerSnapshot{},
|
||||
Kubernetes: portainer.KubernetesDefault(),
|
||||
|
||||
SecuritySettings: portainer.EndpointSecuritySettings{
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
EnableHostManagementFeatures: false,
|
||||
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
AllowContainerCapabilitiesForRegularUsers: true,
|
||||
AllowDeviceMappingForRegularUsers: true,
|
||||
AllowStackManagementForRegularUsers: true,
|
||||
},
|
||||
}
|
||||
|
||||
err := snapshotService.SnapshotEndpoint(endpoint)
|
||||
@@ -352,6 +313,17 @@ func initEndpoint(flags *portainer.CLIFlags, dataStore portainer.DataStore, snap
|
||||
return createUnsecuredEndpoint(*flags.EndpointURL, dataStore, snapshotService)
|
||||
}
|
||||
|
||||
func initExtensionManager(fileService portainer.FileService, dataStore portainer.DataStore) (portainer.ExtensionManager, error) {
|
||||
extensionManager := exec.NewExtensionManager(fileService, dataStore)
|
||||
|
||||
err := extensionManager.StartExtensions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return extensionManager, nil
|
||||
}
|
||||
|
||||
func terminateIfNoAdminCreated(dataStore portainer.DataStore) {
|
||||
timer1 := time.NewTimer(5 * time.Minute)
|
||||
<-timer1.C
|
||||
@@ -382,8 +354,6 @@ func main() {
|
||||
|
||||
ldapService := initLDAPService()
|
||||
|
||||
oauthService := initOAuthService()
|
||||
|
||||
gitService := initGitService()
|
||||
|
||||
cryptoService := initCryptoService()
|
||||
@@ -395,15 +365,15 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
reverseTunnelService := chisel.NewService(dataStore)
|
||||
|
||||
instanceID, err := dataStore.Version().InstanceID()
|
||||
extensionManager, err := initExtensionManager(fileService, dataStore)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
reverseTunnelService := chisel.NewService(dataStore)
|
||||
|
||||
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
|
||||
kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID)
|
||||
kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService)
|
||||
|
||||
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory)
|
||||
if err != nil {
|
||||
@@ -415,10 +385,8 @@ func main() {
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
kubernetesTokenCacheManager := kubeproxy.NewTokenCacheManager()
|
||||
proxyManager := proxy.NewManager(dataStore, digitalSignatureService, reverseTunnelService, dockerClientFactory, kubernetesClientFactory, kubernetesTokenCacheManager)
|
||||
|
||||
composeStackManager := initComposeStackManager(*flags.Assets, *flags.Data, reverseTunnelService, proxyManager)
|
||||
composeStackManager := initComposeStackManager(*flags.Data, reverseTunnelService)
|
||||
|
||||
kubernetesDeployer := initKubernetesDeployer(*flags.Assets)
|
||||
|
||||
@@ -464,9 +432,10 @@ func main() {
|
||||
if len(users) == 0 {
|
||||
log.Println("Created admin user with the given password.")
|
||||
user := &portainer.User{
|
||||
Username: "admin",
|
||||
Role: portainer.AdministratorRole,
|
||||
Password: adminPasswordHash,
|
||||
Username: "admin",
|
||||
Role: portainer.AdministratorRole,
|
||||
Password: adminPasswordHash,
|
||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
||||
}
|
||||
err := dataStore.User().CreateUser(user)
|
||||
if err != nil {
|
||||
@@ -485,29 +454,27 @@ func main() {
|
||||
}
|
||||
|
||||
var server portainer.Server = &http.Server{
|
||||
ReverseTunnelService: reverseTunnelService,
|
||||
Status: applicationStatus,
|
||||
BindAddress: *flags.Addr,
|
||||
AssetsPath: *flags.Assets,
|
||||
DataStore: dataStore,
|
||||
SwarmStackManager: swarmStackManager,
|
||||
ComposeStackManager: composeStackManager,
|
||||
KubernetesDeployer: kubernetesDeployer,
|
||||
CryptoService: cryptoService,
|
||||
JWTService: jwtService,
|
||||
FileService: fileService,
|
||||
LDAPService: ldapService,
|
||||
OAuthService: oauthService,
|
||||
GitService: gitService,
|
||||
ProxyManager: proxyManager,
|
||||
KubernetesTokenCacheManager: kubernetesTokenCacheManager,
|
||||
SignatureService: digitalSignatureService,
|
||||
SnapshotService: snapshotService,
|
||||
SSL: *flags.SSL,
|
||||
SSLCert: *flags.SSLCert,
|
||||
SSLKey: *flags.SSLKey,
|
||||
DockerClientFactory: dockerClientFactory,
|
||||
KubernetesClientFactory: kubernetesClientFactory,
|
||||
ReverseTunnelService: reverseTunnelService,
|
||||
Status: applicationStatus,
|
||||
BindAddress: *flags.Addr,
|
||||
AssetsPath: *flags.Assets,
|
||||
DataStore: dataStore,
|
||||
SwarmStackManager: swarmStackManager,
|
||||
ComposeStackManager: composeStackManager,
|
||||
KubernetesDeployer: kubernetesDeployer,
|
||||
ExtensionManager: extensionManager,
|
||||
CryptoService: cryptoService,
|
||||
JWTService: jwtService,
|
||||
FileService: fileService,
|
||||
LDAPService: ldapService,
|
||||
GitService: gitService,
|
||||
SignatureService: digitalSignatureService,
|
||||
SnapshotService: snapshotService,
|
||||
SSL: *flags.SSL,
|
||||
SSLCert: *flags.SSLCert,
|
||||
SSLKey: *flags.SSLKey,
|
||||
DockerClientFactory: dockerClientFactory,
|
||||
KubernetesClientFactory: kubernetesClientFactory,
|
||||
}
|
||||
|
||||
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)
|
||||
|
||||
@@ -6,24 +6,6 @@ import (
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
// CreateServerTLSConfiguration creates a basic tls.Config to be used by servers with recommended TLS settings
|
||||
func CreateServerTLSConfiguration() *tls.Config {
|
||||
return &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
CipherSuites: []uint16{
|
||||
tls.TLS_AES_128_GCM_SHA256,
|
||||
tls.TLS_AES_256_GCM_SHA384,
|
||||
tls.TLS_CHACHA20_POLY1305_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTLSConfigurationFromBytes initializes a tls.Config using a CA certificate, a certificate and a key
|
||||
// loaded from memory.
|
||||
func CreateTLSConfigurationFromBytes(caCert, cert, key []byte, skipClientVerification, skipServerVerification bool) (*tls.Config, error) {
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
)
|
||||
|
||||
// ComposeWrapper is a wrapper for docker-compose binary
|
||||
type ComposeWrapper struct {
|
||||
binaryPath string
|
||||
proxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// NewComposeWrapper returns a docker-compose wrapper if corresponding binary present, otherwise nil
|
||||
func NewComposeWrapper(binaryPath string, proxyManager *proxy.Manager) *ComposeWrapper {
|
||||
if !IsBinaryPresent(programPath(binaryPath, "docker-compose")) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &ComposeWrapper{
|
||||
binaryPath: binaryPath,
|
||||
proxyManager: proxyManager,
|
||||
}
|
||||
}
|
||||
|
||||
// ComposeSyntaxMaxVersion returns the maximum supported version of the docker compose syntax
|
||||
func (w *ComposeWrapper) ComposeSyntaxMaxVersion() string {
|
||||
return portainer.ComposeSyntaxMaxVersion
|
||||
}
|
||||
|
||||
// Up builds, (re)creates and starts containers in the background. Wraps `docker-compose up -d` command
|
||||
func (w *ComposeWrapper) Up(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
_, err := w.command([]string{"up", "-d"}, stack, endpoint)
|
||||
return err
|
||||
}
|
||||
|
||||
// Down stops and removes containers, networks, images, and volumes. Wraps `docker-compose down --remove-orphans` command
|
||||
func (w *ComposeWrapper) Down(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
_, err := w.command([]string{"down", "--remove-orphans"}, stack, endpoint)
|
||||
return err
|
||||
}
|
||||
|
||||
func (w *ComposeWrapper) command(command []string, stack *portainer.Stack, endpoint *portainer.Endpoint) ([]byte, error) {
|
||||
if endpoint == nil {
|
||||
return nil, errors.New("cannot call a compose command on an empty endpoint")
|
||||
}
|
||||
|
||||
program := programPath(w.binaryPath, "docker-compose")
|
||||
|
||||
options := setComposeFile(stack)
|
||||
|
||||
options = addProjectNameOption(options, stack)
|
||||
options, err := addEnvFileOption(options, stack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !(endpoint.URL == "" || strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://")) {
|
||||
|
||||
proxy, err := w.proxyManager.CreateComposeProxyServer(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer proxy.Close()
|
||||
|
||||
options = append(options, "-H", fmt.Sprintf("http://127.0.0.1:%d", proxy.Port))
|
||||
}
|
||||
|
||||
args := append(options, command...)
|
||||
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.Command(program, args...)
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
return out, errors.New(stderr.String())
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func setComposeFile(stack *portainer.Stack) []string {
|
||||
options := make([]string, 0)
|
||||
|
||||
if stack == nil || stack.EntryPoint == "" {
|
||||
return options
|
||||
}
|
||||
|
||||
composeFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
|
||||
options = append(options, "-f", composeFilePath)
|
||||
return options
|
||||
}
|
||||
|
||||
func addProjectNameOption(options []string, stack *portainer.Stack) []string {
|
||||
if stack == nil || stack.Name == "" {
|
||||
return options
|
||||
}
|
||||
|
||||
options = append(options, "-p", stack.Name)
|
||||
return options
|
||||
}
|
||||
|
||||
func addEnvFileOption(options []string, stack *portainer.Stack) ([]string, error) {
|
||||
if stack == nil || stack.Env == nil || len(stack.Env) == 0 {
|
||||
return options, nil
|
||||
}
|
||||
|
||||
envFilePath := path.Join(stack.ProjectPath, "stack.env")
|
||||
|
||||
envfile, err := os.OpenFile(envFilePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return options, err
|
||||
}
|
||||
|
||||
for _, v := range stack.Env {
|
||||
envfile.WriteString(fmt.Sprintf("%s=%s\n", v.Name, v.Value))
|
||||
}
|
||||
envfile.Close()
|
||||
|
||||
options = append(options, "--env-file", envFilePath)
|
||||
return options, nil
|
||||
}
|
||||
@@ -1,75 +0,0 @@
|
||||
// +build integration
|
||||
|
||||
package exec
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
const composeFile = `version: "3.9"
|
||||
services:
|
||||
busybox:
|
||||
image: "alpine:latest"
|
||||
container_name: "compose_wrapper_test"`
|
||||
const composedContainerName = "compose_wrapper_test"
|
||||
|
||||
func setup(t *testing.T) (*portainer.Stack, *portainer.Endpoint) {
|
||||
dir := t.TempDir()
|
||||
composeFileName := "compose_wrapper_test.yml"
|
||||
f, _ := os.Create(filepath.Join(dir, composeFileName))
|
||||
f.WriteString(composeFile)
|
||||
|
||||
stack := &portainer.Stack{
|
||||
ProjectPath: dir,
|
||||
EntryPoint: composeFileName,
|
||||
Name: "project-name",
|
||||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{}
|
||||
|
||||
return stack, endpoint
|
||||
}
|
||||
|
||||
func Test_UpAndDown(t *testing.T) {
|
||||
|
||||
stack, endpoint := setup(t)
|
||||
|
||||
w := NewComposeWrapper("", nil)
|
||||
|
||||
err := w.Up(stack, endpoint)
|
||||
if err != nil {
|
||||
t.Fatalf("Error calling docker-compose up: %s", err)
|
||||
}
|
||||
|
||||
if containerExists(composedContainerName) == false {
|
||||
t.Fatal("container should exist")
|
||||
}
|
||||
|
||||
err = w.Down(stack, endpoint)
|
||||
if err != nil {
|
||||
t.Fatalf("Error calling docker-compose down: %s", err)
|
||||
}
|
||||
|
||||
if containerExists(composedContainerName) {
|
||||
t.Fatal("container should be removed")
|
||||
}
|
||||
}
|
||||
|
||||
func containerExists(contaierName string) bool {
|
||||
cmd := exec.Command(osProgram("docker"), "ps", "-a", "-f", fmt.Sprintf("name=%s", contaierName))
|
||||
|
||||
out, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to list containers: %s", err)
|
||||
}
|
||||
|
||||
return strings.Contains(string(out), contaierName)
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func Test_setComposeFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
stack *portainer.Stack
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "should return empty result if stack is missing",
|
||||
stack: nil,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "should return empty result if stack don't have entrypoint",
|
||||
stack: &portainer.Stack{},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "should allow file name and dir",
|
||||
stack: &portainer.Stack{
|
||||
ProjectPath: "dir",
|
||||
EntryPoint: "file",
|
||||
},
|
||||
expected: []string{"-f", path.Join("dir", "file")},
|
||||
},
|
||||
{
|
||||
name: "should allow file name only",
|
||||
stack: &portainer.Stack{
|
||||
EntryPoint: "file",
|
||||
},
|
||||
expected: []string{"-f", "file"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := setComposeFile(tt.stack)
|
||||
assert.ElementsMatch(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_addProjectNameOption(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
stack *portainer.Stack
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "should not add project option if stack is missing",
|
||||
stack: nil,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "should not add project option if stack doesn't have name",
|
||||
stack: &portainer.Stack{},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "should add project name option if stack has a name",
|
||||
stack: &portainer.Stack{
|
||||
Name: "project-name",
|
||||
},
|
||||
expected: []string{"-p", "project-name"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
options := []string{"-a", "b"}
|
||||
result := addProjectNameOption(options, tt.stack)
|
||||
assert.ElementsMatch(t, append(options, tt.expected...), result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_addEnvFileOption(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
stack *portainer.Stack
|
||||
expected []string
|
||||
expectedContent string
|
||||
}{
|
||||
{
|
||||
name: "should not add env file option if stack is missing",
|
||||
stack: nil,
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "should not add env file option if stack doesn't have env variables",
|
||||
stack: &portainer.Stack{},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "should not add env file option if stack's env variables are empty",
|
||||
stack: &portainer.Stack{
|
||||
ProjectPath: dir,
|
||||
Env: []portainer.Pair{},
|
||||
},
|
||||
expected: []string{},
|
||||
},
|
||||
{
|
||||
name: "should add env file option if stack has env variables",
|
||||
stack: &portainer.Stack{
|
||||
ProjectPath: dir,
|
||||
Env: []portainer.Pair{
|
||||
{Name: "var1", Value: "value1"},
|
||||
{Name: "var2", Value: "value2"},
|
||||
},
|
||||
},
|
||||
expected: []string{"--env-file", path.Join(dir, "stack.env")},
|
||||
expectedContent: "var1=value1\nvar2=value2\n",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
options := []string{"-a", "b"}
|
||||
result, _ := addEnvFileOption(options, tt.stack)
|
||||
assert.ElementsMatch(t, append(options, tt.expected...), result)
|
||||
|
||||
if tt.expectedContent != "" {
|
||||
f, _ := os.Open(path.Join(dir, "stack.env"))
|
||||
content, _ := ioutil.ReadAll(f)
|
||||
|
||||
assert.Equal(t, tt.expectedContent, string(content))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
313
api/exec/extension.go
Normal file
313
api/exec/extension.go
Normal file
@@ -0,0 +1,313 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
|
||||
"github.com/orcaman/concurrent-map"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
)
|
||||
|
||||
var extensionDownloadBaseURL = portainer.AssetsServerURL + "/extensions/"
|
||||
var extensionVersionRegexp = regexp.MustCompile(`\d+(\.\d+)+`)
|
||||
|
||||
var extensionBinaryMap = map[portainer.ExtensionID]string{
|
||||
portainer.RegistryManagementExtension: "extension-registry-management",
|
||||
portainer.OAuthAuthenticationExtension: "extension-oauth-authentication",
|
||||
portainer.RBACExtension: "extension-rbac",
|
||||
}
|
||||
|
||||
// ExtensionManager represents a service used to
|
||||
// manage extension processes.
|
||||
type ExtensionManager struct {
|
||||
processes cmap.ConcurrentMap
|
||||
fileService portainer.FileService
|
||||
dataStore portainer.DataStore
|
||||
}
|
||||
|
||||
// NewExtensionManager returns a pointer to an ExtensionManager
|
||||
func NewExtensionManager(fileService portainer.FileService, dataStore portainer.DataStore) *ExtensionManager {
|
||||
return &ExtensionManager{
|
||||
processes: cmap.New(),
|
||||
fileService: fileService,
|
||||
dataStore: dataStore,
|
||||
}
|
||||
}
|
||||
|
||||
func processKey(ID portainer.ExtensionID) string {
|
||||
return strconv.Itoa(int(ID))
|
||||
}
|
||||
|
||||
func buildExtensionURL(extension *portainer.Extension) string {
|
||||
return fmt.Sprintf("%s%s-%s-%s-%s.zip", extensionDownloadBaseURL, extensionBinaryMap[extension.ID], runtime.GOOS, runtime.GOARCH, extension.Version)
|
||||
}
|
||||
|
||||
func buildExtensionPath(binaryPath string, extension *portainer.Extension) string {
|
||||
extensionFilename := fmt.Sprintf("%s-%s-%s-%s", extensionBinaryMap[extension.ID], runtime.GOOS, runtime.GOARCH, extension.Version)
|
||||
if runtime.GOOS == "windows" {
|
||||
extensionFilename += ".exe"
|
||||
}
|
||||
|
||||
extensionPath := path.Join(
|
||||
binaryPath,
|
||||
extensionFilename)
|
||||
|
||||
return extensionPath
|
||||
}
|
||||
|
||||
// FetchExtensionDefinitions will fetch the list of available
|
||||
// extension definitions from the official Portainer assets server.
|
||||
// If it cannot retrieve the data from the Internet it will fallback to the locally cached
|
||||
// manifest file.
|
||||
func (manager *ExtensionManager) FetchExtensionDefinitions() ([]portainer.Extension, error) {
|
||||
var extensionData []byte
|
||||
|
||||
extensionData, err := client.Get(portainer.ExtensionDefinitionsURL, 5)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [exec,extensions] [message: unable to retrieve extensions manifest via Internet. Extensions will be retrieved from local cache and might not be up to date] [err: %s]", err)
|
||||
|
||||
extensionData, err = manager.fileService.GetFileContent(portainer.LocalExtensionManifestFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var extensions []portainer.Extension
|
||||
err = json.Unmarshal(extensionData, &extensions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return extensions, nil
|
||||
}
|
||||
|
||||
// InstallExtension will install the extension from an archive. It will extract the extension version number from
|
||||
// the archive file name first and return an error if the file name is not valid (cannot find extension version).
|
||||
// It will then extract the archive and execute the EnableExtension function to enable the extension.
|
||||
// Since we're missing information about this extension (stored on Portainer.io server) we need to assume
|
||||
// default information based on the extension ID.
|
||||
func (manager *ExtensionManager) InstallExtension(extension *portainer.Extension, licenseKey string, archiveFileName string, extensionArchive []byte) error {
|
||||
extensionVersion := extensionVersionRegexp.FindString(archiveFileName)
|
||||
if extensionVersion == "" {
|
||||
return errors.New("invalid extension archive filename: unable to retrieve extension version")
|
||||
}
|
||||
|
||||
err := manager.fileService.ExtractExtensionArchive(extensionArchive)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch extension.ID {
|
||||
case portainer.RegistryManagementExtension:
|
||||
extension.Name = "Registry Manager"
|
||||
case portainer.OAuthAuthenticationExtension:
|
||||
extension.Name = "External Authentication"
|
||||
case portainer.RBACExtension:
|
||||
extension.Name = "Role-Based Access Control"
|
||||
}
|
||||
extension.ShortDescription = "Extension enabled offline"
|
||||
extension.Version = extensionVersion
|
||||
extension.Available = true
|
||||
|
||||
return manager.EnableExtension(extension, licenseKey)
|
||||
}
|
||||
|
||||
// EnableExtension will check for the existence of the extension binary on the filesystem
|
||||
// first. If it does not exist, it will download it from the official Portainer assets server.
|
||||
// After installing the binary on the filesystem, it will execute the binary in license check
|
||||
// mode to validate the extension license. If the license is valid, it will then start
|
||||
// the extension process and register it in the processes map.
|
||||
func (manager *ExtensionManager) EnableExtension(extension *portainer.Extension, licenseKey string) error {
|
||||
extensionBinaryPath := buildExtensionPath(manager.fileService.GetBinaryFolder(), extension)
|
||||
extensionBinaryExists, err := manager.fileService.FileExists(extensionBinaryPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !extensionBinaryExists {
|
||||
err := manager.downloadExtension(extension)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
licenseDetails, err := validateLicense(extensionBinaryPath, licenseKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
extension.License = portainer.LicenseInformation{
|
||||
LicenseKey: licenseKey,
|
||||
Company: licenseDetails[0],
|
||||
Expiration: licenseDetails[1],
|
||||
Valid: true,
|
||||
}
|
||||
extension.Version = licenseDetails[2]
|
||||
|
||||
return manager.startExtensionProcess(extension, extensionBinaryPath)
|
||||
}
|
||||
|
||||
// DisableExtension will retrieve the process associated to the extension
|
||||
// from the processes map and kill the process. It will then remove the process
|
||||
// from the processes map and remove the binary associated to the extension
|
||||
// from the filesystem
|
||||
func (manager *ExtensionManager) DisableExtension(extension *portainer.Extension) error {
|
||||
process, ok := manager.processes.Get(processKey(extension.ID))
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := process.(*exec.Cmd).Process.Kill()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
manager.processes.Remove(processKey(extension.ID))
|
||||
|
||||
extensionBinaryPath := buildExtensionPath(manager.fileService.GetBinaryFolder(), extension)
|
||||
return manager.fileService.RemoveDirectory(extensionBinaryPath)
|
||||
}
|
||||
|
||||
// StartExtensions will retrieve the extensions definitions from the Internet and check if a new version of each
|
||||
// extension is available. If so, it will automatically install the new version of the extension. If no update is
|
||||
// available it will simply start the extension.
|
||||
// The purpose of this function is to be ran at startup, as such most of the error handling won't block the program execution
|
||||
// and will log warning messages instead.
|
||||
func (manager *ExtensionManager) StartExtensions() error {
|
||||
extensions, err := manager.dataStore.Extension().Extensions()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
definitions, err := manager.FetchExtensionDefinitions()
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [exec,extensions] [message: unable to retrieve extension information from Internet. Skipping extensions update check.] [err: %s]", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return manager.updateAndStartExtensions(extensions, definitions)
|
||||
}
|
||||
|
||||
func (manager *ExtensionManager) updateAndStartExtensions(extensions []portainer.Extension, definitions []portainer.Extension) error {
|
||||
for _, definition := range definitions {
|
||||
for _, extension := range extensions {
|
||||
if extension.ID == definition.ID {
|
||||
definitionVersion := semver.New(definition.Version)
|
||||
extensionVersion := semver.New(extension.Version)
|
||||
|
||||
if extensionVersion.LessThan(*definitionVersion) {
|
||||
log.Printf("[INFO] [exec,extensions] [message: new version detected, updating extension] [extension: %s] [current_version: %s] [available_version: %s]", extension.Name, extension.Version, definition.Version)
|
||||
err := manager.UpdateExtension(&extension, definition.Version)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [exec,extensions] [message: unable to update extension automatically] [extension: %s] [current_version: %s] [available_version: %s] [err: %s]", extension.Name, extension.Version, definition.Version, err)
|
||||
}
|
||||
} else {
|
||||
err := manager.EnableExtension(&extension, extension.License.LicenseKey)
|
||||
if err != nil {
|
||||
log.Printf("[WARN] [exec,extensions] [message: unable to start extension] [extension: %s] [err: %s]", extension.Name, err)
|
||||
extension.Enabled = false
|
||||
extension.License.Valid = false
|
||||
}
|
||||
}
|
||||
|
||||
err := manager.dataStore.Extension().Persist(&extension)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateExtension will download the new extension binary from the official Portainer assets
|
||||
// server, disable the previous extension via DisableExtension, trigger a license check
|
||||
// and then start the extension process and add it to the processes map
|
||||
func (manager *ExtensionManager) UpdateExtension(extension *portainer.Extension, version string) error {
|
||||
oldVersion := extension.Version
|
||||
|
||||
extension.Version = version
|
||||
err := manager.downloadExtension(extension)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
extension.Version = oldVersion
|
||||
err = manager.DisableExtension(extension)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
extension.Version = version
|
||||
extensionBinaryPath := buildExtensionPath(manager.fileService.GetBinaryFolder(), extension)
|
||||
|
||||
licenseDetails, err := validateLicense(extensionBinaryPath, extension.License.LicenseKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
extension.Version = licenseDetails[2]
|
||||
|
||||
return manager.startExtensionProcess(extension, extensionBinaryPath)
|
||||
}
|
||||
|
||||
func (manager *ExtensionManager) downloadExtension(extension *portainer.Extension) error {
|
||||
extensionURL := buildExtensionURL(extension)
|
||||
|
||||
data, err := client.Get(extensionURL, 30)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return manager.fileService.ExtractExtensionArchive(data)
|
||||
}
|
||||
|
||||
func validateLicense(binaryPath, licenseKey string) ([]string, error) {
|
||||
licenseCheckProcess := exec.Command(binaryPath, "-license", licenseKey, "-check")
|
||||
cmdOutput := &bytes.Buffer{}
|
||||
licenseCheckProcess.Stdout = cmdOutput
|
||||
|
||||
err := licenseCheckProcess.Run()
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] [exec,extension] [message: unable to run extension process] [err: %s]", err)
|
||||
return nil, errors.New("invalid extension license key")
|
||||
}
|
||||
|
||||
output := string(cmdOutput.Bytes())
|
||||
|
||||
return strings.Split(output, "|"), nil
|
||||
}
|
||||
|
||||
func (manager *ExtensionManager) startExtensionProcess(extension *portainer.Extension, binaryPath string) error {
|
||||
extensionProcess := exec.Command(binaryPath, "-license", extension.License.LicenseKey)
|
||||
extensionProcess.Stdout = os.Stdout
|
||||
extensionProcess.Stderr = os.Stderr
|
||||
|
||||
err := extensionProcess.Start()
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] [exec,extension] [message: unable to start extension process] [err: %s]", err)
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
manager.processes.Set(processKey(extension.ID), extensionProcess)
|
||||
return nil
|
||||
}
|
||||
@@ -134,8 +134,6 @@ func (manager *SwarmStackManager) prepareDockerCommandAndArgs(binaryPath, dataPa
|
||||
|
||||
if !endpoint.TLSConfig.TLSSkipVerify {
|
||||
args = append(args, "--tlsverify", "--tlscacert", endpoint.TLSConfig.TLSCACertPath)
|
||||
} else {
|
||||
args = append(args, "--tlscacert", "''")
|
||||
}
|
||||
|
||||
if endpoint.TLSConfig.TLSCertPath != "" && endpoint.TLSConfig.TLSKeyPath != "" {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func osProgram(program string) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
program += ".exe"
|
||||
}
|
||||
return program
|
||||
}
|
||||
|
||||
func programPath(rootPath, program string) string {
|
||||
return filepath.Join(rootPath, osProgram(program))
|
||||
}
|
||||
|
||||
// IsBinaryPresent returns true if corresponding program exists on PATH
|
||||
func IsBinaryPresent(program string) bool {
|
||||
_, err := exec.LookPath(program)
|
||||
return err == nil
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_isBinaryPresent(t *testing.T) {
|
||||
|
||||
if !IsBinaryPresent("docker") {
|
||||
t.Error("expect docker binary to exist on the path")
|
||||
}
|
||||
|
||||
if IsBinaryPresent("executable-with-this-name-should-not-exist") {
|
||||
t.Error("expect binary with a random name to be missing on the path")
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/archive"
|
||||
|
||||
"io"
|
||||
"os"
|
||||
@@ -95,6 +96,12 @@ func (service *Service) GetBinaryFolder() string {
|
||||
return path.Join(service.fileStorePath, BinaryStorePath)
|
||||
}
|
||||
|
||||
// ExtractExtensionArchive extracts the content of an extension archive
|
||||
// specified as raw data into the binary store on the filesystem
|
||||
func (service *Service) ExtractExtensionArchive(data []byte) error {
|
||||
return archive.UnzipArchive(data, path.Join(service.fileStorePath, BinaryStorePath))
|
||||
}
|
||||
|
||||
// RemoveDirectory removes a directory on the filesystem.
|
||||
func (service *Service) RemoveDirectory(directoryPath string) error {
|
||||
return os.RemoveAll(directoryPath)
|
||||
|
||||
@@ -28,10 +28,8 @@ require (
|
||||
github.com/portainer/libcompose v0.5.3
|
||||
github.com/portainer/libcrypto v0.0.0-20190723020515-23ebe86ab2c2
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
|
||||
github.com/stretchr/testify v1.6.1 // indirect
|
||||
golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1
|
||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1
|
||||
k8s.io/api v0.17.2
|
||||
|
||||
@@ -262,15 +262,12 @@ github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4=
|
||||
github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce h1:fb190+cK2Xz/dvi9Hv8eCYJYvIGUTN2/KLq1pT6CjEc=
|
||||
github.com/tomasen/realip v0.0.0-20180522021738-f0c99a92ddce/go.mod h1:o8v6yHRoik09Xen7gje4m9ERNah1d1PPsVq1VEx9vE4=
|
||||
github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ=
|
||||
@@ -395,8 +392,6 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
|
||||
gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
type authenticatePayload struct {
|
||||
@@ -78,6 +79,11 @@ func (handler *Handler) authenticateLDAP(w http.ResponseWriter, user *portainer.
|
||||
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
}
|
||||
|
||||
@@ -97,8 +103,9 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
|
||||
}
|
||||
|
||||
user := &portainer.User{
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
||||
}
|
||||
|
||||
err = handler.DataStore.User().CreateUser(user)
|
||||
@@ -111,6 +118,11 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
|
||||
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
@@ -11,6 +13,7 @@ import (
|
||||
"github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
type oauthPayload struct {
|
||||
@@ -24,21 +27,52 @@ func (payload *oauthPayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (string, error) {
|
||||
if code == "" {
|
||||
return "", errors.New("Invalid OAuth authorization code")
|
||||
func (handler *Handler) authenticateThroughExtension(code, licenseKey string, settings *portainer.OAuthSettings) (string, error) {
|
||||
extensionURL := handler.ProxyManager.GetExtensionURL(portainer.OAuthAuthenticationExtension)
|
||||
|
||||
encodedConfiguration, err := json.Marshal(settings)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if settings == nil {
|
||||
return "", errors.New("Invalid OAuth configuration")
|
||||
}
|
||||
|
||||
username, err := handler.OAuthService.Authenticate(code, settings)
|
||||
req, err := http.NewRequest("GET", extensionURL+"/validate", nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return username, nil
|
||||
client := &http.Client{}
|
||||
req.Header.Set("X-OAuth-Config", string(encodedConfiguration))
|
||||
req.Header.Set("X-OAuth-Code", code)
|
||||
req.Header.Set("X-PortainerExtension-License", licenseKey)
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
type extensionResponse struct {
|
||||
Username string `json:"Username,omitempty"`
|
||||
Err string `json:"err,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
var extResp extensionResponse
|
||||
err = json.Unmarshal(body, &extResp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", errors.New(extResp.Err + ":" + extResp.Details)
|
||||
}
|
||||
|
||||
return extResp.Username, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
@@ -57,7 +91,14 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
|
||||
return &httperror.HandlerError{http.StatusForbidden, "OAuth authentication is not enabled", errors.New("OAuth authentication is not enabled")}
|
||||
}
|
||||
|
||||
username, err := handler.authenticateOAuth(payload.Code, &settings.OAuthSettings)
|
||||
extension, err := handler.DataStore.Extension().Extension(portainer.OAuthAuthenticationExtension)
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Oauth authentication extension is not enabled", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
username, err := handler.authenticateThroughExtension(payload.Code, extension.License.LicenseKey, &settings.OAuthSettings)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] - OAuth authentication error: %s", err)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to authenticate through OAuth", httperrors.ErrUnauthorized}
|
||||
@@ -74,8 +115,9 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
|
||||
|
||||
if user == nil {
|
||||
user = &portainer.User{
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
||||
}
|
||||
|
||||
err = handler.DataStore.User().CreateUser(user)
|
||||
@@ -96,6 +138,10 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
}
|
||||
|
||||
return handler.writeToken(w, user)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle authentication operations.
|
||||
@@ -18,8 +19,8 @@ type Handler struct {
|
||||
CryptoService portainer.CryptoService
|
||||
JWTService portainer.JWTService
|
||||
LDAPService portainer.LDAPService
|
||||
OAuthService portainer.OAuthService
|
||||
ProxyManager *proxy.Manager
|
||||
AuthorizationService *authorization.Service
|
||||
KubernetesTokenCacheManager *kubernetes.TokenCacheManager
|
||||
}
|
||||
|
||||
|
||||
@@ -57,17 +57,6 @@ func (handler *Handler) customTemplateUpdate(w http.ResponseWriter, r *http.Requ
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
customTemplates, err := handler.DataStore.CustomTemplate().CustomTemplates()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve custom templates from the database", err}
|
||||
}
|
||||
|
||||
for _, existingTemplate := range customTemplates {
|
||||
if existingTemplate.ID != portainer.CustomTemplateID(customTemplateID) && existingTemplate.Title == payload.Title {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Template name must be unique", errors.New("Template name must be unique")}
|
||||
}
|
||||
}
|
||||
|
||||
customTemplate, err := handler.DataStore.CustomTemplate().CustomTemplate(portainer.CustomTemplateID(customTemplateID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a custom template with the specified identifier inside the database", err}
|
||||
|
||||
@@ -39,8 +39,10 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err}
|
||||
}
|
||||
|
||||
updateAuthorizations := false
|
||||
for _, endpoint := range endpoints {
|
||||
if endpoint.GroupID == portainer.EndpointGroupID(endpointGroupID) {
|
||||
updateAuthorizations = true
|
||||
endpoint.GroupID = portainer.EndpointGroupID(1)
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
@@ -54,6 +56,13 @@ func (handler *Handler) endpointGroupDelete(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
}
|
||||
|
||||
if updateAuthorizations {
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
}
|
||||
|
||||
for _, tagID := range endpointGroup.TagIDs {
|
||||
tag, err := handler.DataStore.Tag().Tag(tagID)
|
||||
if err != nil {
|
||||
|
||||
@@ -92,12 +92,15 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
|
||||
}
|
||||
}
|
||||
|
||||
updateAuthorizations := false
|
||||
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpointGroup.UserAccessPolicies) {
|
||||
endpointGroup.UserAccessPolicies = payload.UserAccessPolicies
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpointGroup.TeamAccessPolicies) {
|
||||
endpointGroup.TeamAccessPolicies = payload.TeamAccessPolicies
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
err = handler.DataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, endpointGroup)
|
||||
@@ -105,6 +108,13 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err}
|
||||
}
|
||||
|
||||
if updateAuthorizations {
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
}
|
||||
|
||||
if tagsChanged {
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
|
||||
@@ -7,12 +7,14 @@ import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle endpoint group operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
DataStore portainer.DataStore
|
||||
AuthorizationService *authorization.Service
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint group operations.
|
||||
|
||||
@@ -24,7 +24,7 @@ func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.R
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ func (handler *Handler) proxyRequestsToKubernetesAPI(w http.ResponseWriter, r *h
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func (handler *Handler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *htt
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,17 @@ package endpoints
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
)
|
||||
@@ -23,7 +20,7 @@ import (
|
||||
type endpointCreatePayload struct {
|
||||
Name string
|
||||
URL string
|
||||
EndpointCreationType endpointCreationEnum
|
||||
EndpointType int
|
||||
PublicURL string
|
||||
GroupID int
|
||||
TLS bool
|
||||
@@ -39,17 +36,6 @@ type endpointCreatePayload struct {
|
||||
EdgeCheckinInterval int
|
||||
}
|
||||
|
||||
type endpointCreationEnum int
|
||||
|
||||
const (
|
||||
_ endpointCreationEnum = iota
|
||||
localDockerEnvironment
|
||||
agentEnvironment
|
||||
azureEnvironment
|
||||
edgeAgentEnvironment
|
||||
localKubernetesEnvironment
|
||||
)
|
||||
|
||||
func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
name, err := request.RetrieveMultiPartFormValue(r, "Name", false)
|
||||
if err != nil {
|
||||
@@ -57,11 +43,11 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
}
|
||||
payload.Name = name
|
||||
|
||||
endpointCreationType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointCreationType", false)
|
||||
if err != nil || endpointCreationType == 0 {
|
||||
return errors.New("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment), 4 (Edge Agent environment) or 5 (Local Kubernetes environment)")
|
||||
endpointType, err := request.RetrieveNumericMultiPartFormValue(r, "EndpointType", false)
|
||||
if err != nil || endpointType == 0 {
|
||||
return errors.New("Invalid endpoint type value. Value must be one of: 1 (Docker environment), 2 (Agent environment), 3 (Azure environment) or 4 (Edge Agent environment)")
|
||||
}
|
||||
payload.EndpointCreationType = endpointCreationEnum(endpointCreationType)
|
||||
payload.EndpointType = endpointType
|
||||
|
||||
groupID, _ := request.RetrieveNumericMultiPartFormValue(r, "GroupID", true)
|
||||
if groupID == 0 {
|
||||
@@ -111,8 +97,8 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
}
|
||||
}
|
||||
|
||||
switch payload.EndpointCreationType {
|
||||
case azureEnvironment:
|
||||
switch portainer.EndpointType(payload.EndpointType) {
|
||||
case portainer.AzureEnvironment:
|
||||
azureApplicationID, err := request.RetrieveMultiPartFormValue(r, "AzureApplicationID", false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid Azure application ID")
|
||||
@@ -196,34 +182,22 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
|
||||
func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
switch payload.EndpointCreationType {
|
||||
case azureEnvironment:
|
||||
switch portainer.EndpointType(payload.EndpointType) {
|
||||
case portainer.AzureEnvironment:
|
||||
return handler.createAzureEndpoint(payload)
|
||||
|
||||
case edgeAgentEnvironment:
|
||||
return handler.createEdgeAgentEndpoint(payload)
|
||||
case portainer.EdgeAgentOnDockerEnvironment:
|
||||
return handler.createEdgeAgentEndpoint(payload, portainer.EdgeAgentOnDockerEnvironment)
|
||||
|
||||
case localKubernetesEnvironment:
|
||||
case portainer.KubernetesLocalEnvironment:
|
||||
return handler.createKubernetesEndpoint(payload)
|
||||
}
|
||||
|
||||
endpointType := portainer.DockerEnvironment
|
||||
if payload.EndpointCreationType == agentEnvironment {
|
||||
agentPlatform, err := handler.pingAndCheckPlatform(payload)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to get endpoint type", err}
|
||||
}
|
||||
|
||||
if agentPlatform == portainer.AgentPlatformDocker {
|
||||
endpointType = portainer.AgentOnDockerEnvironment
|
||||
} else if agentPlatform == portainer.AgentPlatformKubernetes {
|
||||
endpointType = portainer.AgentOnKubernetesEnvironment
|
||||
payload.URL = strings.TrimPrefix(payload.URL, "tcp://")
|
||||
}
|
||||
case portainer.EdgeAgentOnKubernetesEnvironment:
|
||||
return handler.createEdgeAgentEndpoint(payload, portainer.EdgeAgentOnKubernetesEnvironment)
|
||||
}
|
||||
|
||||
if payload.TLS {
|
||||
return handler.createTLSSecuredEndpoint(payload, endpointType)
|
||||
return handler.createTLSSecuredEndpoint(payload, portainer.EndpointType(payload.EndpointType))
|
||||
}
|
||||
return handler.createUnsecuredEndpoint(payload)
|
||||
}
|
||||
@@ -267,7 +241,7 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload, endpointType portainer.EndpointType) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
endpointID := handler.DataStore.Endpoint().GetNextIdentifier()
|
||||
|
||||
portainerURL, err := url.Parse(payload.URL)
|
||||
@@ -290,7 +264,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload)
|
||||
ID: portainer.EndpointID(endpointID),
|
||||
Name: payload.Name,
|
||||
URL: portainerHost,
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
Type: endpointType,
|
||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
@@ -440,23 +414,20 @@ func (handler *Handler) snapshotAndPersistEndpoint(endpoint *portainer.Endpoint)
|
||||
}
|
||||
|
||||
func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer.Endpoint) error {
|
||||
endpoint.SecuritySettings = portainer.EndpointSecuritySettings{
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
EnableHostManagementFeatures: false,
|
||||
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
AllowContainerCapabilitiesForRegularUsers: true,
|
||||
AllowDeviceMappingForRegularUsers: true,
|
||||
AllowStackManagementForRegularUsers: true,
|
||||
}
|
||||
|
||||
err := handler.DataStore.Endpoint().CreateEndpoint(endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
group, err := handler.DataStore.EndpointGroup().EndpointGroup(endpoint.GroupID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(group.UserAccessPolicies) > 0 || len(group.TeamAccessPolicies) > 0 {
|
||||
return handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
}
|
||||
|
||||
for _, tagID := range endpoint.TagIDs {
|
||||
tag, err := handler.DataStore.Tag().Tag(tagID)
|
||||
if err != nil {
|
||||
@@ -501,58 +472,3 @@ func (handler *Handler) storeTLSFiles(endpoint *portainer.Endpoint, payload *end
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) pingAndCheckPlatform(payload *endpointCreatePayload) (portainer.AgentPlatform, error) {
|
||||
httpCli := &http.Client{
|
||||
Timeout: 3 * time.Second,
|
||||
}
|
||||
|
||||
if payload.TLS {
|
||||
tlsConfig, err := crypto.CreateTLSConfigurationFromBytes(payload.TLSCACertFile, payload.TLSCertFile, payload.TLSKeyFile, payload.TLSSkipVerify, payload.TLSSkipClientVerify)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
httpCli.Transport = &http.Transport{
|
||||
TLSClientConfig: tlsConfig,
|
||||
}
|
||||
}
|
||||
|
||||
url, err := url.Parse(fmt.Sprintf("%s/ping", payload.URL))
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
url.Scheme = "https"
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, url.String(), nil)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
resp, err := httpCli.Do(req)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
return 0, fmt.Errorf("Failed request with status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
agentPlatformHeader := resp.Header.Get(portainer.HTTPResponseAgentPlatform)
|
||||
if agentPlatformHeader == "" {
|
||||
return 0, errors.New("Agent Platform Header is missing")
|
||||
}
|
||||
|
||||
agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
if agentPlatformNumber == 0 {
|
||||
return 0, errors.New("Agent platform is invalid")
|
||||
}
|
||||
|
||||
return portainer.AgentPlatform(agentPlatformNumber), nil
|
||||
}
|
||||
|
||||
@@ -40,6 +40,13 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
||||
|
||||
handler.ProxyManager.DeleteEndpointProxy(endpoint)
|
||||
|
||||
if len(endpoint.UserAccessPolicies) > 0 || len(endpoint.TeamAccessPolicies) > 0 {
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.DataStore.EndpointRelation().DeleteEndpointRelation(endpoint.ID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint relation from the database", err}
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
)
|
||||
|
||||
@@ -24,13 +24,12 @@ func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
hideFields(endpoint)
|
||||
endpoint.ComposeSyntaxMaxVersion = handler.ComposeStackManager.ComposeSyntaxMaxVersion()
|
||||
|
||||
return response.JSON(w, endpoint)
|
||||
}
|
||||
|
||||
@@ -5,11 +5,12 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
|
||||
"github.com/portainer/libhttp/request"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
@@ -88,7 +89,6 @@ func (handler *Handler) endpointList(w http.ResponseWriter, r *http.Request) *ht
|
||||
|
||||
for idx := range paginatedEndpoints {
|
||||
hideFields(&paginatedEndpoints[idx])
|
||||
paginatedEndpoints[idx].ComposeSyntaxMaxVersion = handler.ComposeStackManager.ComposeSyntaxMaxVersion()
|
||||
}
|
||||
|
||||
w.Header().Set("X-Total-Count", strconv.Itoa(filteredEndpointCount))
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
package endpoints
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
)
|
||||
|
||||
type endpointSettingsUpdatePayload struct {
|
||||
AllowBindMountsForRegularUsers *bool `json:"allowBindMountsForRegularUsers"`
|
||||
AllowPrivilegedModeForRegularUsers *bool `json:"allowPrivilegedModeForRegularUsers"`
|
||||
AllowVolumeBrowserForRegularUsers *bool `json:"allowVolumeBrowserForRegularUsers"`
|
||||
AllowHostNamespaceForRegularUsers *bool `json:"allowHostNamespaceForRegularUsers"`
|
||||
AllowDeviceMappingForRegularUsers *bool `json:"allowDeviceMappingForRegularUsers"`
|
||||
AllowStackManagementForRegularUsers *bool `json:"allowStackManagementForRegularUsers"`
|
||||
AllowContainerCapabilitiesForRegularUsers *bool `json:"allowContainerCapabilitiesForRegularUsers"`
|
||||
EnableHostManagementFeatures *bool `json:"enableHostManagementFeatures"`
|
||||
}
|
||||
|
||||
func (payload *endpointSettingsUpdatePayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// PUT request on /api/endpoints/:id/settings
|
||||
func (handler *Handler) endpointSettingsUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err}
|
||||
}
|
||||
|
||||
var payload endpointSettingsUpdatePayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
securitySettings := endpoint.SecuritySettings
|
||||
|
||||
if payload.AllowBindMountsForRegularUsers != nil {
|
||||
securitySettings.AllowBindMountsForRegularUsers = *payload.AllowBindMountsForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowContainerCapabilitiesForRegularUsers != nil {
|
||||
securitySettings.AllowContainerCapabilitiesForRegularUsers = *payload.AllowContainerCapabilitiesForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowDeviceMappingForRegularUsers != nil {
|
||||
securitySettings.AllowDeviceMappingForRegularUsers = *payload.AllowDeviceMappingForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowHostNamespaceForRegularUsers != nil {
|
||||
securitySettings.AllowHostNamespaceForRegularUsers = *payload.AllowHostNamespaceForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowPrivilegedModeForRegularUsers != nil {
|
||||
securitySettings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowStackManagementForRegularUsers != nil {
|
||||
securitySettings.AllowStackManagementForRegularUsers = *payload.AllowStackManagementForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowVolumeBrowserForRegularUsers != nil {
|
||||
securitySettings.AllowVolumeBrowserForRegularUsers = *payload.AllowVolumeBrowserForRegularUsers
|
||||
}
|
||||
|
||||
if payload.EnableHostManagementFeatures != nil {
|
||||
securitySettings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures
|
||||
}
|
||||
|
||||
endpoint.SecuritySettings = securitySettings
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(portainer.EndpointID(endpointID), endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Failed persisting endpoint in database", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, endpoint)
|
||||
}
|
||||
@@ -2,15 +2,13 @@ package endpoints
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
)
|
||||
|
||||
type stackStatusResponse struct {
|
||||
@@ -43,7 +41,7 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
@@ -56,27 +54,10 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req
|
||||
|
||||
if endpoint.EdgeID == "" {
|
||||
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
|
||||
|
||||
endpoint.EdgeID = edgeIdentifier
|
||||
|
||||
agentPlatformHeader := r.Header.Get(portainer.HTTPResponseAgentPlatform)
|
||||
if agentPlatformHeader == "" {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Agent Platform Header is missing", errors.New("Agent Platform Header is missing")}
|
||||
}
|
||||
|
||||
agentPlatformNumber, err := strconv.Atoi(agentPlatformHeader)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to parse agent platform header", err}
|
||||
}
|
||||
|
||||
agentPlatform := portainer.AgentPlatform(agentPlatformNumber)
|
||||
|
||||
if agentPlatform == portainer.AgentPlatformDocker {
|
||||
endpoint.Type = portainer.EdgeAgentOnDockerEnvironment
|
||||
} else if agentPlatform == portainer.AgentPlatformKubernetes {
|
||||
endpoint.Type = portainer.EdgeAgentOnKubernetesEnvironment
|
||||
}
|
||||
|
||||
err = handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
err := handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist endpoint changes inside the database", err}
|
||||
}
|
||||
|
||||
@@ -126,12 +126,15 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
endpoint.Kubernetes = *payload.Kubernetes
|
||||
}
|
||||
|
||||
updateAuthorizations := false
|
||||
if payload.UserAccessPolicies != nil && !reflect.DeepEqual(payload.UserAccessPolicies, endpoint.UserAccessPolicies) {
|
||||
endpoint.UserAccessPolicies = payload.UserAccessPolicies
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
if payload.TeamAccessPolicies != nil && !reflect.DeepEqual(payload.TeamAccessPolicies, endpoint.TeamAccessPolicies) {
|
||||
endpoint.TeamAccessPolicies = payload.TeamAccessPolicies
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
if payload.Status != nil {
|
||||
@@ -223,6 +226,13 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err}
|
||||
}
|
||||
|
||||
if updateAuthorizations {
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
}
|
||||
|
||||
if (endpoint.Type == portainer.EdgeAgentOnDockerEnvironment || endpoint.Type == portainer.EdgeAgentOnKubernetesEnvironment) && (groupIDChanged || tagsChanged) {
|
||||
relation, err := handler.DataStore.EndpointRelation().EndpointRelation(endpoint.ID)
|
||||
if err != nil {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
|
||||
"net/http"
|
||||
|
||||
@@ -23,11 +24,11 @@ type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer *security.RequestBouncer
|
||||
DataStore portainer.DataStore
|
||||
AuthorizationService *authorization.Service
|
||||
FileService portainer.FileService
|
||||
ProxyManager *proxy.Manager
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
SnapshotService portainer.SnapshotService
|
||||
ComposeStackManager portainer.ComposeStackManager
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage endpoint operations.
|
||||
@@ -39,8 +40,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
|
||||
h.Handle("/endpoints",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints/{id}/settings",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSettingsUpdate))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoints/snapshot",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints",
|
||||
|
||||
117
api/http/handler/extensions/data.go
Normal file
117
api/http/handler/extensions/data.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
func updateUserAccessPolicyToReadOnlyRole(policies portainer.UserAccessPolicies, key portainer.UserID) {
|
||||
tmp := policies[key]
|
||||
tmp.RoleID = 4
|
||||
policies[key] = tmp
|
||||
}
|
||||
|
||||
func updateTeamAccessPolicyToReadOnlyRole(policies portainer.TeamAccessPolicies, key portainer.TeamID) {
|
||||
tmp := policies[key]
|
||||
tmp.RoleID = 4
|
||||
policies[key] = tmp
|
||||
}
|
||||
|
||||
func (handler *Handler) upgradeRBACData() error {
|
||||
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpointGroup := range endpointGroups {
|
||||
for key := range endpointGroup.UserAccessPolicies {
|
||||
updateUserAccessPolicyToReadOnlyRole(endpointGroup.UserAccessPolicies, key)
|
||||
}
|
||||
|
||||
for key := range endpointGroup.TeamAccessPolicies {
|
||||
updateTeamAccessPolicyToReadOnlyRole(endpointGroup.TeamAccessPolicies, key)
|
||||
}
|
||||
|
||||
err := handler.DataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
for key := range endpoint.UserAccessPolicies {
|
||||
updateUserAccessPolicyToReadOnlyRole(endpoint.UserAccessPolicies, key)
|
||||
}
|
||||
|
||||
for key := range endpoint.TeamAccessPolicies {
|
||||
updateTeamAccessPolicyToReadOnlyRole(endpoint.TeamAccessPolicies, key)
|
||||
}
|
||||
|
||||
err := handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
}
|
||||
|
||||
func updateUserAccessPolicyToNoRole(policies portainer.UserAccessPolicies, key portainer.UserID) {
|
||||
tmp := policies[key]
|
||||
tmp.RoleID = 0
|
||||
policies[key] = tmp
|
||||
}
|
||||
|
||||
func updateTeamAccessPolicyToNoRole(policies portainer.TeamAccessPolicies, key portainer.TeamID) {
|
||||
tmp := policies[key]
|
||||
tmp.RoleID = 0
|
||||
policies[key] = tmp
|
||||
}
|
||||
|
||||
func (handler *Handler) downgradeRBACData() error {
|
||||
endpointGroups, err := handler.DataStore.EndpointGroup().EndpointGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpointGroup := range endpointGroups {
|
||||
for key := range endpointGroup.UserAccessPolicies {
|
||||
updateUserAccessPolicyToNoRole(endpointGroup.UserAccessPolicies, key)
|
||||
}
|
||||
|
||||
for key := range endpointGroup.TeamAccessPolicies {
|
||||
updateTeamAccessPolicyToNoRole(endpointGroup.TeamAccessPolicies, key)
|
||||
}
|
||||
|
||||
err := handler.DataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
endpoints, err := handler.DataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
for key := range endpoint.UserAccessPolicies {
|
||||
updateUserAccessPolicyToNoRole(endpoint.UserAccessPolicies, key)
|
||||
}
|
||||
|
||||
for key := range endpoint.TeamAccessPolicies {
|
||||
updateTeamAccessPolicyToNoRole(endpoint.TeamAccessPolicies, key)
|
||||
}
|
||||
|
||||
err := handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
}
|
||||
87
api/http/handler/extensions/extension_create.go
Normal file
87
api/http/handler/extensions/extension_create.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type extensionCreatePayload struct {
|
||||
License string
|
||||
}
|
||||
|
||||
func (payload *extensionCreatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.License) {
|
||||
return errors.New("Invalid license")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) extensionCreate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
var payload extensionCreatePayload
|
||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
extensionIdentifier, err := strconv.Atoi(string(payload.License[0]))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid license format", err}
|
||||
}
|
||||
extensionID := portainer.ExtensionID(extensionIdentifier)
|
||||
|
||||
extensions, err := handler.DataStore.Extension().Extensions()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions status from the database", err}
|
||||
}
|
||||
|
||||
for _, existingExtension := range extensions {
|
||||
if existingExtension.ID == extensionID && existingExtension.Enabled {
|
||||
return &httperror.HandlerError{http.StatusConflict, "Unable to enable extension", errors.New("This extension is already enabled")}
|
||||
}
|
||||
}
|
||||
|
||||
extension := &portainer.Extension{
|
||||
ID: extensionID,
|
||||
}
|
||||
|
||||
extensionDefinitions, err := handler.ExtensionManager.FetchExtensionDefinitions()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extension definitions", err}
|
||||
}
|
||||
|
||||
for _, def := range extensionDefinitions {
|
||||
if def.ID == extension.ID {
|
||||
extension.Version = def.Version
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.ExtensionManager.EnableExtension(extension, payload.License)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to enable extension", err}
|
||||
}
|
||||
|
||||
extension.Enabled = true
|
||||
|
||||
if extension.ID == portainer.RBACExtension {
|
||||
err = handler.upgradeRBACData()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured during database update", err}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.DataStore.Extension().Persist(extension)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
46
api/http/handler/extensions/extension_delete.go
Normal file
46
api/http/handler/extensions/extension_delete.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
)
|
||||
|
||||
// DELETE request on /api/extensions/:id
|
||||
func (handler *Handler) extensionDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
extensionIdentifier, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid extension identifier route variable", err}
|
||||
}
|
||||
extensionID := portainer.ExtensionID(extensionIdentifier)
|
||||
|
||||
extension, err := handler.DataStore.Extension().Extension(extensionID)
|
||||
if err == errors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a extension with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.ExtensionManager.DisableExtension(extension)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete extension", err}
|
||||
}
|
||||
|
||||
if extensionID == portainer.RBACExtension {
|
||||
err = handler.downgradeRBACData()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured during database update", err}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.DataStore.Extension().DeleteExtension(extensionID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete the extension from the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
55
api/http/handler/extensions/extension_inspect.go
Normal file
55
api/http/handler/extensions/extension_inspect.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
)
|
||||
|
||||
// GET request on /api/extensions/:id
|
||||
func (handler *Handler) extensionInspect(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
extensionIdentifier, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid extension identifier route variable", err}
|
||||
}
|
||||
|
||||
extensionID := portainer.ExtensionID(extensionIdentifier)
|
||||
|
||||
definitions, err := handler.ExtensionManager.FetchExtensionDefinitions()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions informations", err}
|
||||
}
|
||||
|
||||
localExtension, err := handler.DataStore.Extension().Extension(extensionID)
|
||||
if err != nil && err != errors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extension information from the database", err}
|
||||
}
|
||||
|
||||
var extension portainer.Extension
|
||||
var extensionDefinition portainer.Extension
|
||||
|
||||
for _, definition := range definitions {
|
||||
if definition.ID == extensionID {
|
||||
extensionDefinition = definition
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if localExtension == nil {
|
||||
extension = extensionDefinition
|
||||
} else {
|
||||
extension = *localExtension
|
||||
}
|
||||
|
||||
mergeExtensionAndDefinition(&extension, &extensionDefinition)
|
||||
|
||||
description, _ := client.Get(extension.DescriptionURL, 5)
|
||||
extension.Description = string(description)
|
||||
|
||||
return response.JSON(w, extension)
|
||||
}
|
||||
30
api/http/handler/extensions/extension_list.go
Normal file
30
api/http/handler/extensions/extension_list.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
// GET request on /api/extensions?store=<store>
|
||||
func (handler *Handler) extensionList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
fetchManifestInformation, _ := request.RetrieveBooleanQueryParameter(r, "store", true)
|
||||
|
||||
extensions, err := handler.DataStore.Extension().Extensions()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions from the database", err}
|
||||
}
|
||||
|
||||
if fetchManifestInformation {
|
||||
definitions, err := handler.ExtensionManager.FetchExtensionDefinitions()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions informations", err}
|
||||
}
|
||||
|
||||
extensions = mergeExtensionsAndDefinitions(extensions, definitions)
|
||||
}
|
||||
|
||||
return response.JSON(w, extensions)
|
||||
}
|
||||
58
api/http/handler/extensions/extension_update.go
Normal file
58
api/http/handler/extensions/extension_update.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
)
|
||||
|
||||
type extensionUpdatePayload struct {
|
||||
Version string
|
||||
}
|
||||
|
||||
func (payload *extensionUpdatePayload) Validate(r *http.Request) error {
|
||||
if govalidator.IsNull(payload.Version) {
|
||||
return errors.New("Invalid extension version")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) extensionUpdate(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
extensionIdentifier, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid extension identifier route variable", err}
|
||||
}
|
||||
extensionID := portainer.ExtensionID(extensionIdentifier)
|
||||
|
||||
var payload extensionUpdatePayload
|
||||
err = request.DecodeAndValidateJSONPayload(r, &payload)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
extension, err := handler.DataStore.Extension().Extension(extensionID)
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a extension with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.ExtensionManager.UpdateExtension(extension, payload.Version)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update extension", err}
|
||||
}
|
||||
|
||||
err = handler.DataStore.Extension().Persist(extension)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
76
api/http/handler/extensions/extension_upload.go
Normal file
76
api/http/handler/extensions/extension_upload.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type extensionUploadPayload struct {
|
||||
License string
|
||||
ExtensionArchive []byte
|
||||
ArchiveFileName string
|
||||
}
|
||||
|
||||
func (payload *extensionUploadPayload) Validate(r *http.Request) error {
|
||||
license, err := request.RetrieveMultiPartFormValue(r, "License", false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid license")
|
||||
}
|
||||
payload.License = license
|
||||
|
||||
fileData, fileName, err := request.RetrieveMultiPartFormFile(r, "file")
|
||||
if err != nil {
|
||||
return errors.New("Invalid extension archive file. Ensure that the file is uploaded correctly")
|
||||
}
|
||||
payload.ExtensionArchive = fileData
|
||||
payload.ArchiveFileName = fileName
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) extensionUpload(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
payload := &extensionUploadPayload{}
|
||||
err := payload.Validate(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||
}
|
||||
|
||||
extensionIdentifier, err := strconv.Atoi(string(payload.License[0]))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid license format", err}
|
||||
}
|
||||
extensionID := portainer.ExtensionID(extensionIdentifier)
|
||||
|
||||
extension := &portainer.Extension{
|
||||
ID: extensionID,
|
||||
}
|
||||
|
||||
_ = handler.ExtensionManager.DisableExtension(extension)
|
||||
|
||||
err = handler.ExtensionManager.InstallExtension(extension, payload.License, payload.ArchiveFileName, payload.ExtensionArchive)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to install extension", err}
|
||||
}
|
||||
|
||||
extension.Enabled = true
|
||||
|
||||
if extension.ID == portainer.RBACExtension {
|
||||
err = handler.upgradeRBACData()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "An error occured during database update", err}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.DataStore.Extension().Persist(extension)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
84
api/http/handler/extensions/handler.go
Normal file
84
api/http/handler/extensions/handler.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package extensions
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/coreos/go-semver/semver"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle extension operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
ExtensionManager portainer.ExtensionManager
|
||||
AuthorizationService *authorization.Service
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage extension operations.
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
|
||||
h.Handle("/extensions",
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet)
|
||||
h.Handle("/extensions",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.extensionCreate))).Methods(http.MethodPost)
|
||||
h.Handle("/extensions/upload",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.extensionUpload))).Methods(http.MethodPost)
|
||||
h.Handle("/extensions/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.extensionInspect))).Methods(http.MethodGet)
|
||||
h.Handle("/extensions/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.extensionDelete))).Methods(http.MethodDelete)
|
||||
h.Handle("/extensions/{id}/update",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.extensionUpdate))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func mergeExtensionsAndDefinitions(extensions, definitions []portainer.Extension) []portainer.Extension {
|
||||
for _, definition := range definitions {
|
||||
foundInDB := false
|
||||
|
||||
for idx, extension := range extensions {
|
||||
if extension.ID == definition.ID {
|
||||
foundInDB = true
|
||||
mergeExtensionAndDefinition(&extensions[idx], &definition)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundInDB {
|
||||
extensions = append(extensions, definition)
|
||||
}
|
||||
}
|
||||
|
||||
return extensions
|
||||
}
|
||||
|
||||
func mergeExtensionAndDefinition(extension, definition *portainer.Extension) {
|
||||
extension.Name = definition.Name
|
||||
extension.ShortDescription = definition.ShortDescription
|
||||
extension.Deal = definition.Deal
|
||||
extension.Available = definition.Available
|
||||
extension.DescriptionURL = definition.DescriptionURL
|
||||
extension.Images = definition.Images
|
||||
extension.Logo = definition.Logo
|
||||
extension.Price = definition.Price
|
||||
extension.PriceDescription = definition.PriceDescription
|
||||
extension.ShopURL = definition.ShopURL
|
||||
|
||||
definitionVersion := semver.New(definition.Version)
|
||||
extensionVersion := semver.New(extension.Version)
|
||||
if extensionVersion.LessThan(*definitionVersion) {
|
||||
extension.UpdateAvailable = true
|
||||
}
|
||||
|
||||
extension.Version = definition.Version
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/portainer/portainer/api/http/handler/endpointgroups"
|
||||
"github.com/portainer/portainer/api/http/handler/endpointproxy"
|
||||
"github.com/portainer/portainer/api/http/handler/endpoints"
|
||||
"github.com/portainer/portainer/api/http/handler/extensions"
|
||||
"github.com/portainer/portainer/api/http/handler/file"
|
||||
"github.com/portainer/portainer/api/http/handler/motd"
|
||||
"github.com/portainer/portainer/api/http/handler/registries"
|
||||
@@ -23,6 +24,7 @@ import (
|
||||
"github.com/portainer/portainer/api/http/handler/settings"
|
||||
"github.com/portainer/portainer/api/http/handler/stacks"
|
||||
"github.com/portainer/portainer/api/http/handler/status"
|
||||
"github.com/portainer/portainer/api/http/handler/support"
|
||||
"github.com/portainer/portainer/api/http/handler/tags"
|
||||
"github.com/portainer/portainer/api/http/handler/teammemberships"
|
||||
"github.com/portainer/portainer/api/http/handler/teams"
|
||||
@@ -48,12 +50,14 @@ type Handler struct {
|
||||
EndpointProxyHandler *endpointproxy.Handler
|
||||
FileHandler *file.Handler
|
||||
MOTDHandler *motd.Handler
|
||||
ExtensionHandler *extensions.Handler
|
||||
RegistryHandler *registries.Handler
|
||||
ResourceControlHandler *resourcecontrols.Handler
|
||||
RoleHandler *roles.Handler
|
||||
SettingsHandler *settings.Handler
|
||||
StackHandler *stacks.Handler
|
||||
StatusHandler *status.Handler
|
||||
SupportHandler *support.Handler
|
||||
TagHandler *tags.Handler
|
||||
TeamMembershipHandler *teammemberships.Handler
|
||||
TeamHandler *teams.Handler
|
||||
@@ -100,6 +104,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
default:
|
||||
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
||||
}
|
||||
case strings.HasPrefix(r.URL.Path, "/api/extensions"):
|
||||
http.StripPrefix("/api", h.ExtensionHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/motd"):
|
||||
http.StripPrefix("/api", h.MOTDHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/registries"):
|
||||
@@ -114,6 +120,8 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
http.StripPrefix("/api", h.StackHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/status"):
|
||||
http.StripPrefix("/api", h.StatusHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/support"):
|
||||
http.StripPrefix("/api", h.SupportHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/tags"):
|
||||
http.StripPrefix("/api", h.TagHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/templates"):
|
||||
|
||||
@@ -43,6 +43,10 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.registryConfigure))).Methods(http.MethodPost)
|
||||
h.Handle("/registries/{id}",
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete)
|
||||
h.PathPrefix("/registries/{id}/v2").Handler(
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI)))
|
||||
h.PathPrefix("/registries/{id}/proxies/gitlab").Handler(
|
||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToGitlabAPIWithRegistry)))
|
||||
h.PathPrefix("/registries/proxies/gitlab").Handler(
|
||||
bouncer.AdminAccess(httperror.LoggerHandler(h.proxyRequestsToGitlabAPIWithoutRegistry)))
|
||||
return h
|
||||
|
||||
85
api/http/handler/registries/proxy.go
Normal file
85
api/http/handler/registries/proxy.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package registries
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/errors"
|
||||
)
|
||||
|
||||
// request on /api/registries/:id/v2
|
||||
func (handler *Handler) proxyRequestsToRegistryAPI(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
|
||||
}
|
||||
|
||||
registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.RegistryAccess(r, registry)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", errors.ErrEndpointAccessDenied}
|
||||
}
|
||||
|
||||
extension, err := handler.DataStore.Extension().Extension(portainer.RegistryManagementExtension)
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
var proxy http.Handler
|
||||
proxy = handler.ProxyManager.GetExtensionProxy(portainer.RegistryManagementExtension)
|
||||
if proxy == nil {
|
||||
proxy, err = handler.ProxyManager.CreateExtensionProxy(portainer.RegistryManagementExtension)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create extension proxy for registry manager", err}
|
||||
}
|
||||
}
|
||||
|
||||
managementConfiguration := registry.ManagementConfiguration
|
||||
if managementConfiguration == nil {
|
||||
managementConfiguration = createDefaultManagementConfiguration(registry)
|
||||
}
|
||||
|
||||
encodedConfiguration, err := json.Marshal(managementConfiguration)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to encode management configuration", err}
|
||||
}
|
||||
|
||||
id := strconv.Itoa(int(registryID))
|
||||
r.Header.Set("X-RegistryManagement-Key", id)
|
||||
r.Header.Set("X-RegistryManagement-URI", registry.URL)
|
||||
r.Header.Set("X-RegistryManagement-Config", string(encodedConfiguration))
|
||||
r.Header.Set("X-PortainerExtension-License", extension.License.LicenseKey)
|
||||
|
||||
http.StripPrefix("/registries/"+id, proxy).ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
|
||||
func createDefaultManagementConfiguration(registry *portainer.Registry) *portainer.RegistryManagementConfiguration {
|
||||
config := &portainer.RegistryManagementConfiguration{
|
||||
Type: registry.Type,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
},
|
||||
}
|
||||
|
||||
if registry.Authentication {
|
||||
config.Authentication = true
|
||||
config.Username = registry.Username
|
||||
config.Password = registry.Password
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
68
api/http/handler/registries/proxy_management_gitlab.go
Normal file
68
api/http/handler/registries/proxy_management_gitlab.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package registries
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/errors"
|
||||
)
|
||||
|
||||
// request on /api/registries/{id}/proxies/gitlab
|
||||
func (handler *Handler) proxyRequestsToGitlabAPIWithRegistry(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
registryID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid registry identifier route variable", err}
|
||||
}
|
||||
|
||||
registry, err := handler.DataStore.Registry().Registry(portainer.RegistryID(registryID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a registry with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a registry with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.RegistryAccess(r, registry)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access registry", errors.ErrEndpointAccessDenied}
|
||||
}
|
||||
|
||||
extension, err := handler.DataStore.Extension().Extension(portainer.RegistryManagementExtension)
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Registry management extension is not enabled", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
var proxy http.Handler
|
||||
proxy = handler.ProxyManager.GetExtensionProxy(portainer.RegistryManagementExtension)
|
||||
if proxy == nil {
|
||||
proxy, err = handler.ProxyManager.CreateExtensionProxy(portainer.RegistryManagementExtension)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create extension proxy for registry manager", err}
|
||||
}
|
||||
}
|
||||
|
||||
config := &portainer.RegistryManagementConfiguration{
|
||||
Type: portainer.GitlabRegistry,
|
||||
Password: registry.Password,
|
||||
}
|
||||
|
||||
encodedConfiguration, err := json.Marshal(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to encode management configuration", err}
|
||||
}
|
||||
|
||||
id := strconv.Itoa(int(registryID))
|
||||
r.Header.Set("X-RegistryManagement-Key", id+"-gitlab")
|
||||
r.Header.Set("X-RegistryManagement-URI", registry.Gitlab.InstanceURL)
|
||||
r.Header.Set("X-RegistryManagement-Config", string(encodedConfiguration))
|
||||
r.Header.Set("X-PortainerExtension-License", extension.License.LicenseKey)
|
||||
|
||||
http.StripPrefix("/registries/"+id+"/proxies/gitlab", proxy).ServeHTTP(w, r)
|
||||
return nil
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
)
|
||||
|
||||
@@ -71,7 +71,7 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
registry.Username = *payload.Username
|
||||
}
|
||||
|
||||
if payload.Password != nil && *payload.Password != "" {
|
||||
if payload.Password != nil {
|
||||
registry.Password = *payload.Password
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ package settings
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api"
|
||||
@@ -17,11 +19,12 @@ func hideFields(settings *portainer.Settings) {
|
||||
// Handler is the HTTP handler used to handle settings operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
FileService portainer.FileService
|
||||
JWTService portainer.JWTService
|
||||
LDAPService portainer.LDAPService
|
||||
SnapshotService portainer.SnapshotService
|
||||
AuthorizationService *authorization.Service
|
||||
DataStore portainer.DataStore
|
||||
FileService portainer.FileService
|
||||
JWTService portainer.JWTService
|
||||
LDAPService portainer.LDAPService
|
||||
SnapshotService portainer.SnapshotService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage settings operations.
|
||||
|
||||
@@ -6,15 +6,18 @@ import (
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type publicSettingsResponse struct {
|
||||
LogoURL string `json:"LogoURL"`
|
||||
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
|
||||
OAuthLoginURI string `json:"OAuthLoginURI"`
|
||||
EnableTelemetry bool `json:"EnableTelemetry"`
|
||||
LogoURL string `json:"LogoURL"`
|
||||
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
|
||||
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
||||
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
|
||||
OAuthLoginURI string `json:"OAuthLoginURI"`
|
||||
}
|
||||
|
||||
// GET request on /api/settings/public
|
||||
@@ -25,10 +28,13 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
|
||||
publicSettings := &publicSettingsResponse{
|
||||
LogoURL: settings.LogoURL,
|
||||
AuthenticationMethod: settings.AuthenticationMethod,
|
||||
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
|
||||
EnableTelemetry: settings.EnableTelemetry,
|
||||
LogoURL: settings.LogoURL,
|
||||
AuthenticationMethod: settings.AuthenticationMethod,
|
||||
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
|
||||
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
||||
AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers,
|
||||
EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
|
||||
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
|
||||
OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login",
|
||||
settings.OAuthSettings.AuthorizationURI,
|
||||
settings.OAuthSettings.ClientID,
|
||||
|
||||
@@ -9,26 +9,30 @@ import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
)
|
||||
|
||||
type settingsUpdatePayload struct {
|
||||
LogoURL *string
|
||||
BlackListedLabels []portainer.Pair
|
||||
AuthenticationMethod *int
|
||||
LDAPSettings *portainer.LDAPSettings
|
||||
OAuthSettings *portainer.OAuthSettings
|
||||
SnapshotInterval *string
|
||||
TemplatesURL *string
|
||||
EdgeAgentCheckinInterval *int
|
||||
EnableEdgeComputeFeatures *bool
|
||||
UserSessionTimeout *string
|
||||
EnableTelemetry *bool
|
||||
LogoURL *string
|
||||
BlackListedLabels []portainer.Pair
|
||||
AuthenticationMethod *int
|
||||
LDAPSettings *portainer.LDAPSettings
|
||||
OAuthSettings *portainer.OAuthSettings
|
||||
AllowBindMountsForRegularUsers *bool
|
||||
AllowPrivilegedModeForRegularUsers *bool
|
||||
AllowVolumeBrowserForRegularUsers *bool
|
||||
EnableHostManagementFeatures *bool
|
||||
SnapshotInterval *string
|
||||
TemplatesURL *string
|
||||
EdgeAgentCheckinInterval *int
|
||||
EnableEdgeComputeFeatures *bool
|
||||
UserSessionTimeout *string
|
||||
}
|
||||
|
||||
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||
if payload.AuthenticationMethod != nil && *payload.AuthenticationMethod != 1 && *payload.AuthenticationMethod != 2 && *payload.AuthenticationMethod != 3 {
|
||||
if *payload.AuthenticationMethod != 1 && *payload.AuthenticationMethod != 2 && *payload.AuthenticationMethod != 3 {
|
||||
return errors.New("Invalid authentication method value. Value must be one of: 1 (internal), 2 (LDAP/AD) or 3 (OAuth)")
|
||||
}
|
||||
if payload.LogoURL != nil && *payload.LogoURL != "" && !govalidator.IsURL(*payload.LogoURL) {
|
||||
@@ -99,6 +103,24 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
settings.OAuthSettings.ClientSecret = clientSecret
|
||||
}
|
||||
|
||||
if payload.AllowBindMountsForRegularUsers != nil {
|
||||
settings.AllowBindMountsForRegularUsers = *payload.AllowBindMountsForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowPrivilegedModeForRegularUsers != nil {
|
||||
settings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers
|
||||
}
|
||||
|
||||
updateAuthorizations := false
|
||||
if payload.AllowVolumeBrowserForRegularUsers != nil {
|
||||
settings.AllowVolumeBrowserForRegularUsers = *payload.AllowVolumeBrowserForRegularUsers
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
if payload.EnableHostManagementFeatures != nil {
|
||||
settings.EnableHostManagementFeatures = *payload.EnableHostManagementFeatures
|
||||
}
|
||||
|
||||
if payload.EnableEdgeComputeFeatures != nil {
|
||||
settings.EnableEdgeComputeFeatures = *payload.EnableEdgeComputeFeatures
|
||||
}
|
||||
@@ -122,10 +144,6 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
handler.JWTService.SetUserSessionDuration(userSessionDuration)
|
||||
}
|
||||
|
||||
if payload.EnableTelemetry != nil {
|
||||
settings.EnableTelemetry = *payload.EnableTelemetry
|
||||
}
|
||||
|
||||
tlsError := handler.updateTLS(settings)
|
||||
if tlsError != nil {
|
||||
return tlsError
|
||||
@@ -136,9 +154,37 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist settings changes inside the database", err}
|
||||
}
|
||||
|
||||
if updateAuthorizations {
|
||||
err := handler.updateVolumeBrowserSetting(settings)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update RBAC authorizations", err}
|
||||
}
|
||||
}
|
||||
|
||||
return response.JSON(w, settings)
|
||||
}
|
||||
|
||||
func (handler *Handler) updateVolumeBrowserSetting(settings *portainer.Settings) error {
|
||||
err := handler.AuthorizationService.UpdateVolumeBrowsingAuthorizations(settings.AllowVolumeBrowserForRegularUsers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
extension, err := handler.DataStore.Extension().Extension(portainer.RBACExtension)
|
||||
if err != nil && err != bolterrors.ErrObjectNotFound {
|
||||
return err
|
||||
}
|
||||
|
||||
if extension != nil {
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) updateSnapshotInterval(settings *portainer.Settings, snapshotInterval string) error {
|
||||
settings.SnapshotInterval = snapshotInterval
|
||||
|
||||
|
||||
@@ -7,12 +7,11 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
@@ -61,14 +60,12 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
@@ -91,8 +88,6 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
stack.CreatedBy = config.user.Username
|
||||
|
||||
err = handler.DataStore.Stack().CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
@@ -150,14 +145,12 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
Env: payload.Env,
|
||||
}
|
||||
|
||||
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
|
||||
@@ -190,8 +183,6 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
stack.CreatedBy = config.user.Username
|
||||
|
||||
err = handler.DataStore.Stack().CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
@@ -249,14 +240,12 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerComposeStack,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
@@ -279,8 +268,6 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
stack.CreatedBy = config.user.Username
|
||||
|
||||
err = handler.DataStore.Stack().CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
@@ -296,7 +283,6 @@ type composeStackDeploymentConfig struct {
|
||||
dockerhub *portainer.DockerHub
|
||||
registries []portainer.Registry
|
||||
isAdmin bool
|
||||
user *portainer.User
|
||||
}
|
||||
|
||||
func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint) (*composeStackDeploymentConfig, *httperror.HandlerError) {
|
||||
@@ -316,18 +302,12 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai
|
||||
}
|
||||
filteredRegistries := security.FilterRegistries(registries, securityContext)
|
||||
|
||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
|
||||
}
|
||||
|
||||
config := &composeStackDeploymentConfig{
|
||||
stack: stack,
|
||||
endpoint: endpoint,
|
||||
dockerhub: dockerhub,
|
||||
registries: filteredRegistries,
|
||||
isAdmin: securityContext.IsAdmin,
|
||||
user: user,
|
||||
}
|
||||
|
||||
return config, nil
|
||||
@@ -339,30 +319,26 @@ func (handler *Handler) createComposeDeployConfig(r *http.Request, stack *portai
|
||||
// clean it. Hence the use of the mutex.
|
||||
// We should contribute to libcompose to support authentication without using the config.json file.
|
||||
func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig) error {
|
||||
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
securitySettings := &config.endpoint.SecuritySettings
|
||||
|
||||
if (!securitySettings.AllowBindMountsForRegularUsers ||
|
||||
!securitySettings.AllowPrivilegedModeForRegularUsers ||
|
||||
!securitySettings.AllowHostNamespaceForRegularUsers ||
|
||||
!securitySettings.AllowDeviceMappingForRegularUsers ||
|
||||
!securitySettings.AllowContainerCapabilitiesForRegularUsers) &&
|
||||
!isAdminOrEndpointAdmin {
|
||||
|
||||
if !settings.AllowBindMountsForRegularUsers && !config.isAdmin {
|
||||
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
|
||||
|
||||
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = handler.isValidStackFile(stackContent, securitySettings)
|
||||
valid, err := handler.isValidStackFile(stackContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !valid {
|
||||
return errors.New("bind-mount disabled for non administrator users")
|
||||
}
|
||||
}
|
||||
|
||||
handler.stackCreationMutex.Lock()
|
||||
|
||||
@@ -6,12 +6,11 @@ import (
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
@@ -56,15 +55,13 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
@@ -87,8 +84,6 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
stack.CreatedBy = config.user.Username
|
||||
|
||||
err = handler.DataStore.Stack().CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
@@ -149,15 +144,13 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
Env: payload.Env,
|
||||
}
|
||||
|
||||
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
|
||||
@@ -190,8 +183,6 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
stack.CreatedBy = config.user.Username
|
||||
|
||||
err = handler.DataStore.Stack().CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
@@ -256,15 +247,13 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
|
||||
|
||||
stackID := handler.DataStore.Stack().GetNextIdentifier()
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
CreationDate: time.Now().Unix(),
|
||||
ID: portainer.StackID(stackID),
|
||||
Name: payload.Name,
|
||||
Type: portainer.DockerSwarmStack,
|
||||
SwarmID: payload.SwarmID,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
@@ -287,8 +276,6 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
}
|
||||
|
||||
stack.CreatedBy = config.user.Username
|
||||
|
||||
err = handler.DataStore.Stack().CreateStack(stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist the stack inside the database", err}
|
||||
@@ -305,7 +292,6 @@ type swarmStackDeploymentConfig struct {
|
||||
registries []portainer.Registry
|
||||
prune bool
|
||||
isAdmin bool
|
||||
user *portainer.User
|
||||
}
|
||||
|
||||
func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portainer.Stack, endpoint *portainer.Endpoint, prune bool) (*swarmStackDeploymentConfig, *httperror.HandlerError) {
|
||||
@@ -325,11 +311,6 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
|
||||
}
|
||||
filteredRegistries := security.FilterRegistries(registries, securityContext)
|
||||
|
||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return nil, &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
|
||||
}
|
||||
|
||||
config := &swarmStackDeploymentConfig{
|
||||
stack: stack,
|
||||
endpoint: endpoint,
|
||||
@@ -337,21 +318,18 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
|
||||
registries: filteredRegistries,
|
||||
prune: prune,
|
||||
isAdmin: securityContext.IsAdmin,
|
||||
user: user,
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) error {
|
||||
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings := &config.endpoint.SecuritySettings
|
||||
|
||||
if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin {
|
||||
if !settings.AllowBindMountsForRegularUsers && !config.isAdmin {
|
||||
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
|
||||
|
||||
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
|
||||
@@ -359,10 +337,13 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err
|
||||
return err
|
||||
}
|
||||
|
||||
err = handler.isValidStackFile(stackContent, settings)
|
||||
valid, err := handler.isValidStackFile(stackContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !valid {
|
||||
return errors.New("bind-mount disabled for non administrator users")
|
||||
}
|
||||
}
|
||||
|
||||
handler.stackCreationMutex.Lock()
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
@@ -53,17 +54,12 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackFile))).Methods(http.MethodGet)
|
||||
h.Handle("/stacks/{id}/migrate",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackMigrate))).Methods(http.MethodPost)
|
||||
h.Handle("/stacks/{id}/start",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackStart))).Methods(http.MethodPost)
|
||||
h.Handle("/stacks/{id}/stop",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackStop))).Methods(http.MethodPost)
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedRequestContext, endpointID portainer.EndpointID, resourceControl *portainer.ResourceControl) (bool, error) {
|
||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
if securityContext.IsAdmin {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
userTeamIDs := make([]portainer.TeamID, 0)
|
||||
@@ -75,31 +71,21 @@ func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedR
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return handler.userIsAdminOrEndpointAdmin(user, endpointID)
|
||||
}
|
||||
|
||||
func (handler *Handler) userIsAdmin(userID portainer.UserID) (bool, error) {
|
||||
user, err := handler.DataStore.User().User(userID)
|
||||
if err != nil {
|
||||
_, err := handler.DataStore.Extension().Extension(portainer.RBACExtension)
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return false, nil
|
||||
} else if err != nil && err != bolterrors.ErrObjectNotFound {
|
||||
return false, err
|
||||
}
|
||||
|
||||
isAdmin := user.Role == portainer.AdministratorRole
|
||||
|
||||
return isAdmin, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) userIsAdminOrEndpointAdmin(user *portainer.User, endpointID portainer.EndpointID) (bool, error) {
|
||||
isAdmin := user.Role == portainer.AdministratorRole
|
||||
|
||||
return isAdmin, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) userCanCreateStack(securityContext *security.RestrictedRequestContext, endpointID portainer.EndpointID) (bool, error) {
|
||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return handler.userIsAdminOrEndpointAdmin(user, endpointID)
|
||||
_, ok := user.EndpointAuthorizations[endpointID][portainer.EndpointResourcesAccess]
|
||||
if ok {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
@@ -53,25 +53,7 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
if !endpoint.SecuritySettings.AllowStackManagementForRegularUsers {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
|
||||
}
|
||||
|
||||
canCreate, err := handler.userCanCreateStack(securityContext, portainer.EndpointID(endpointID))
|
||||
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack creation", err}
|
||||
}
|
||||
|
||||
if !canCreate {
|
||||
errMsg := "Stack creation is disabled for non-admin users"
|
||||
return &httperror.HandlerError{http.StatusForbidden, errMsg, errors.New(errMsg)}
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
@@ -124,10 +106,10 @@ func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request,
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)}
|
||||
}
|
||||
|
||||
func (handler *Handler) isValidStackFile(stackFileContent []byte, securitySettings *portainer.EndpointSecuritySettings) error {
|
||||
func (handler *Handler) isValidStackFile(stackFileContent []byte) (bool, error) {
|
||||
composeConfigYAML, err := loader.ParseYAML(stackFileContent)
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
composeConfigFile := types.ConfigFile{
|
||||
@@ -144,54 +126,25 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte, securitySettin
|
||||
options.SkipInterpolation = true
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
|
||||
for key := range composeConfig.Services {
|
||||
service := composeConfig.Services[key]
|
||||
if !securitySettings.AllowBindMountsForRegularUsers {
|
||||
for _, volume := range service.Volumes {
|
||||
if volume.Type == "bind" {
|
||||
return errors.New("bind-mount disabled for non administrator users")
|
||||
}
|
||||
for _, volume := range service.Volumes {
|
||||
if volume.Type == "bind" {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
if !securitySettings.AllowPrivilegedModeForRegularUsers && service.Privileged == true {
|
||||
return errors.New("privileged mode disabled for non administrator users")
|
||||
}
|
||||
|
||||
if !securitySettings.AllowHostNamespaceForRegularUsers && service.Pid == "host" {
|
||||
return errors.New("pid host disabled for non administrator users")
|
||||
}
|
||||
|
||||
if !securitySettings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 {
|
||||
return errors.New("device mapping disabled for non administrator users")
|
||||
}
|
||||
|
||||
if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) {
|
||||
return errors.New("container capabilities disabled for non administrator users")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *portainer.Stack, userID portainer.UserID) *httperror.HandlerError {
|
||||
var resourceControl *portainer.ResourceControl
|
||||
resourceControl := authorization.NewPrivateResourceControl(stack.Name, portainer.StackResourceControl, userID)
|
||||
|
||||
isAdmin, err := handler.userIsAdmin(userID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
|
||||
}
|
||||
|
||||
if isAdmin {
|
||||
resourceControl = authorization.NewAdministratorsOnlyResourceControl(stack.Name, portainer.StackResourceControl)
|
||||
} else {
|
||||
resourceControl = authorization.NewPrivateResourceControl(stack.Name, portainer.StackResourceControl, userID)
|
||||
}
|
||||
|
||||
err = handler.DataStore.ResourceControl().CreateResourceControl(resourceControl)
|
||||
err := handler.DataStore.ResourceControl().CreateResourceControl(resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist resource control inside the database", err}
|
||||
}
|
||||
|
||||
@@ -65,7 +65,7 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
@@ -114,8 +114,30 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin {
|
||||
return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", httperrors.ErrUnauthorized}
|
||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to load user information from the database", err}
|
||||
}
|
||||
|
||||
rbacExtension, err := handler.DataStore.Extension().Extension(portainer.RBACExtension)
|
||||
if err != nil && err != bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify if RBAC extension is loaded", err}
|
||||
}
|
||||
|
||||
endpointResourceAccess := false
|
||||
_, ok := user.EndpointAuthorizations[portainer.EndpointID(endpointID)][portainer.EndpointResourcesAccess]
|
||||
if ok {
|
||||
endpointResourceAccess = true
|
||||
}
|
||||
|
||||
if rbacExtension != nil {
|
||||
if !securityContext.IsAdmin && !endpointResourceAccess {
|
||||
return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", httperrors.ErrUnauthorized}
|
||||
}
|
||||
} else {
|
||||
if !securityContext.IsAdmin {
|
||||
return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", httperrors.ErrUnauthorized}
|
||||
}
|
||||
}
|
||||
|
||||
stack, err := handler.DataStore.Stack().StackByName(stackName)
|
||||
@@ -133,7 +155,7 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
@@ -155,6 +177,5 @@ func (handler *Handler) deleteStack(stack *portainer.Stack, endpoint *portainer.
|
||||
if stack.Type == portainer.DockerSwarmStack {
|
||||
return handler.SwarmStackManager.Remove(stack, endpoint)
|
||||
}
|
||||
|
||||
return handler.ComposeStackManager.Down(stack, endpoint)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
@@ -43,6 +44,14 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe
|
||||
stacks = authorization.DecorateStacks(stacks, resourceControls)
|
||||
|
||||
if !securityContext.IsAdmin {
|
||||
rbacExtensionEnabled := true
|
||||
_, err := handler.DataStore.Extension().Extension(portainer.RBACExtension)
|
||||
if err == errors.ErrObjectNotFound {
|
||||
rbacExtensionEnabled = false
|
||||
} else if err != nil && err != errors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check if RBAC extension is enabled", err}
|
||||
}
|
||||
|
||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user information from the database", err}
|
||||
@@ -53,7 +62,7 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe
|
||||
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
||||
}
|
||||
|
||||
stacks = authorization.FilterAuthorizedStacks(stacks, user, userTeamIDs)
|
||||
stacks = authorization.FilterAuthorizedStacks(stacks, user, userTeamIDs, rbacExtensionEnabled)
|
||||
}
|
||||
|
||||
return response.JSON(w, stacks)
|
||||
|
||||
@@ -53,7 +53,7 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
)
|
||||
|
||||
// POST request on /api/stacks/:id/start
|
||||
func (handler *Handler) stackStart(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
if stack.Status == portainer.StackStatusActive {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Stack is already active", errors.New("Stack is already active")}
|
||||
}
|
||||
|
||||
err = handler.startStack(stack, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to stop stack", err}
|
||||
}
|
||||
|
||||
stack.Status = portainer.StackStatusActive
|
||||
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update stack status", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
|
||||
func (handler *Handler) startStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
switch stack.Type {
|
||||
case portainer.DockerComposeStack:
|
||||
return handler.ComposeStackManager.Up(stack, endpoint)
|
||||
case portainer.DockerSwarmStack:
|
||||
return handler.SwarmStackManager.Deploy(stack, true, endpoint)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// POST request on /api/stacks/:id/stop
|
||||
func (handler *Handler) stackStop(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
stackID, err := request.RetrieveNumericRouteVariableValue(r, "id")
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid stack identifier route variable", err}
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||
}
|
||||
|
||||
stack, err := handler.DataStore.Stack().Stack(portainer.StackID(stackID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(stack.EndpointID)
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
} else if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
resourceControl, err := handler.DataStore.ResourceControl().ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||
}
|
||||
|
||||
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||
}
|
||||
if !access {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", httperrors.ErrResourceAccessDenied}
|
||||
}
|
||||
|
||||
if stack.Status == portainer.StackStatusInactive {
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Stack is already inactive", errors.New("Stack is already inactive")}
|
||||
}
|
||||
|
||||
err = handler.stopStack(stack, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to stop stack", err}
|
||||
}
|
||||
|
||||
stack.Status = portainer.StackStatusInactive
|
||||
err = handler.DataStore.Stack().UpdateStack(stack.ID, stack)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update stack status", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, stack)
|
||||
}
|
||||
|
||||
func (handler *Handler) stopStack(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
switch stack.Type {
|
||||
case portainer.DockerComposeStack:
|
||||
return handler.ComposeStackManager.Down(stack, endpoint)
|
||||
case portainer.DockerSwarmStack:
|
||||
return handler.SwarmStackManager.Remove(stack, endpoint)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -4,13 +4,12 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
@@ -73,7 +72,7 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
@@ -136,9 +135,6 @@ func (handler *Handler) updateComposeStack(r *http.Request, stack *portainer.Sta
|
||||
return configErr
|
||||
}
|
||||
|
||||
stack.UpdateDate = time.Now().Unix()
|
||||
stack.UpdatedBy = config.user.Username
|
||||
|
||||
err = handler.deployComposeStack(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
@@ -167,9 +163,6 @@ func (handler *Handler) updateSwarmStack(r *http.Request, stack *portainer.Stack
|
||||
return configErr
|
||||
}
|
||||
|
||||
stack.UpdateDate = time.Now().Unix()
|
||||
stack.UpdatedBy = config.user.Username
|
||||
|
||||
err = handler.deploySwarmStack(config)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err}
|
||||
|
||||
26
api/http/handler/support/handler.go
Normal file
26
api/http/handler/support/handler.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package support
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
)
|
||||
|
||||
// Handler is the HTTP handler used to handle support operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
}
|
||||
|
||||
// NewHandler returns a new Handler
|
||||
func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||
h := &Handler{
|
||||
Router: mux.NewRouter(),
|
||||
}
|
||||
h.Handle("/support",
|
||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.supportList))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
39
api/http/handler/support/support_list.go
Normal file
39
api/http/handler/support/support_list.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package support
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
|
||||
"github.com/portainer/libhttp/response"
|
||||
)
|
||||
|
||||
type supportProduct struct {
|
||||
ID int `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
ShortDescription string `json:"ShortDescription"`
|
||||
Price string `json:"Price"`
|
||||
PriceDescription string `json:"PriceDescription"`
|
||||
Description string `json:"Description"`
|
||||
ProductID string `json:"ProductId"`
|
||||
}
|
||||
|
||||
func (handler *Handler) supportList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
supportData, err := client.Get(portainer.SupportProductsURL, 30)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to fetch support options", err}
|
||||
}
|
||||
|
||||
var supportProducts []supportProduct
|
||||
err = json.Unmarshal(supportData, &supportProducts)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to fetch support options", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, supportProducts)
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
|
||||
"net/http"
|
||||
|
||||
@@ -13,7 +14,8 @@ import (
|
||||
// Handler is the HTTP handler used to handle team membership operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
DataStore portainer.DataStore
|
||||
AuthorizationService *authorization.Service
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage team membership operations.
|
||||
|
||||
@@ -72,5 +72,10 @@ func (handler *Handler) teamMembershipCreate(w http.ResponseWriter, r *http.Requ
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team memberships inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
|
||||
return response.JSON(w, membership)
|
||||
}
|
||||
|
||||
@@ -40,5 +40,10 @@ func (handler *Handler) teamMembershipDelete(w http.ResponseWriter, r *http.Requ
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the team membership from the database", err}
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package teams
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -12,7 +13,8 @@ import (
|
||||
// Handler is the HTTP handler used to handle team operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
DataStore portainer.DataStore
|
||||
AuthorizationService *authorization.Service
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage team operations.
|
||||
|
||||
@@ -34,5 +34,10 @@ func (handler *Handler) teamDelete(w http.ResponseWriter, r *http.Request) *http
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete associated team memberships from the database", err}
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.RemoveTeamAccessPolicies(portainer.TeamID(teamID))
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clean-up team access policies", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
type adminInitPayload struct {
|
||||
@@ -44,8 +45,9 @@ func (handler *Handler) adminInit(w http.ResponseWriter, r *http.Request) *httpe
|
||||
}
|
||||
|
||||
user := &portainer.User{
|
||||
Username: payload.Username,
|
||||
Role: portainer.AdministratorRole,
|
||||
Username: payload.Username,
|
||||
Role: portainer.AdministratorRole,
|
||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
||||
}
|
||||
|
||||
user.Password, err = handler.CryptoService.Hash(payload.Password)
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
|
||||
"net/http"
|
||||
|
||||
@@ -27,8 +28,9 @@ func hideFields(user *portainer.User) {
|
||||
// Handler is the HTTP handler used to handle user operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
CryptoService portainer.CryptoService
|
||||
DataStore portainer.DataStore
|
||||
CryptoService portainer.CryptoService
|
||||
AuthorizationService *authorization.Service
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage user operations.
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
httperrors "github.com/portainer/portainer/api/http/errors"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
type userCreatePayload struct {
|
||||
@@ -61,8 +62,9 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http
|
||||
}
|
||||
|
||||
user = &portainer.User{
|
||||
Username: payload.Username,
|
||||
Role: portainer.UserRole(payload.Role),
|
||||
Username: payload.Username,
|
||||
Role: portainer.UserRole(payload.Role),
|
||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
|
||||
@@ -81,5 +81,10 @@ func (handler *Handler) deleteUser(w http.ResponseWriter, user *portainer.User)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove user memberships from the database", err}
|
||||
}
|
||||
|
||||
err = handler.AuthorizationService.RemoveUserAccessPolicies(user.ID)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to clean-up user access policies", err}
|
||||
}
|
||||
|
||||
return response.Empty(w)
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ func (handler *Handler) websocketAttach(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func (handler *Handler) websocketExec(w http.ResponseWriter, r *http.Request) *h
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func (handler *Handler) websocketPodExec(w http.ResponseWriter, r *http.Request)
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
@@ -33,12 +33,9 @@ func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r
|
||||
}
|
||||
|
||||
func (handler *Handler) proxyAgentWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
|
||||
endpointURL := params.endpoint.URL
|
||||
if params.endpoint.Type == portainer.AgentOnKubernetesEnvironment {
|
||||
endpointURL = fmt.Sprintf("http://%s", params.endpoint.URL)
|
||||
}
|
||||
|
||||
agentURL, err := url.Parse(endpointURL)
|
||||
// TODO: k8s merge - make sure this is still working with Docker agent
|
||||
//agentURL, err := url.Parse(params.endpoint.URL)
|
||||
agentURL, err := url.Parse(fmt.Sprintf("http://%s", params.endpoint.URL))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -155,11 +155,11 @@ func (transport *Transport) applyAccessControlOnResource(parameters *resourceOpe
|
||||
return err
|
||||
}
|
||||
|
||||
if resourceControl == nil && (executor.operationContext.isAdmin) {
|
||||
if resourceControl == nil && (executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess) {
|
||||
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
|
||||
if executor.operationContext.isAdmin || (resourceControl != nil && authorization.UserCanAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl)) {
|
||||
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess || (resourceControl != nil && authorization.UserCanAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl)) {
|
||||
responseObject = decorateObject(responseObject, resourceControl)
|
||||
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
@@ -168,7 +168,7 @@ func (transport *Transport) applyAccessControlOnResource(parameters *resourceOpe
|
||||
}
|
||||
|
||||
func (transport *Transport) applyAccessControlOnResourceList(parameters *resourceOperationParameters, resourceData []interface{}, executor *operationExecutor) ([]interface{}, error) {
|
||||
if executor.operationContext.isAdmin {
|
||||
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
|
||||
return transport.decorateResourceList(parameters, resourceData, executor.operationContext.resourceControls)
|
||||
}
|
||||
|
||||
@@ -241,13 +241,13 @@ func (transport *Transport) filterResourceList(parameters *resourceOperationPara
|
||||
}
|
||||
|
||||
if resourceControl == nil {
|
||||
if context.isAdmin {
|
||||
if context.isAdmin || context.endpointResourceAccess {
|
||||
filteredResourceData = append(filteredResourceData, resourceObject)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if context.isAdmin || authorization.UserCanAccessResource(context.userID, context.userTeamIDs, resourceControl) {
|
||||
if context.isAdmin || context.endpointResourceAccess || authorization.UserCanAccessResource(context.userID, context.userTeamIDs, resourceControl) {
|
||||
resourceObject = decorateObject(resourceObject, resourceControl)
|
||||
filteredResourceData = append(filteredResourceData, resourceObject)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
@@ -153,81 +148,3 @@ func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelB
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (transport *Transport) decorateContainerCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) {
|
||||
type PartialContainer struct {
|
||||
HostConfig struct {
|
||||
Privileged bool `json:"Privileged"`
|
||||
PidMode string `json:"PidMode"`
|
||||
Devices []interface{} `json:"Devices"`
|
||||
CapAdd []string `json:"CapAdd"`
|
||||
CapDrop []string `json:"CapDrop"`
|
||||
Binds []string `json:"Binds"`
|
||||
} `json:"HostConfig"`
|
||||
}
|
||||
|
||||
forbiddenResponse := &http.Response{
|
||||
StatusCode: http.StatusForbidden,
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isAdminOrEndpointAdmin, err := transport.isAdminOrEndpointAdmin(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isAdminOrEndpointAdmin {
|
||||
securitySettings, err := transport.fetchEndpointSecuritySettings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
partialContainer := &PartialContainer{}
|
||||
err = json.Unmarshal(body, partialContainer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !securitySettings.AllowPrivilegedModeForRegularUsers && partialContainer.HostConfig.Privileged {
|
||||
return forbiddenResponse, errors.New("forbidden to use privileged mode")
|
||||
}
|
||||
|
||||
if !securitySettings.AllowHostNamespaceForRegularUsers && partialContainer.HostConfig.PidMode == "host" {
|
||||
return forbiddenResponse, errors.New("forbidden to use pid host namespace")
|
||||
}
|
||||
|
||||
if !securitySettings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 {
|
||||
return forbiddenResponse, errors.New("forbidden to use device mapping")
|
||||
}
|
||||
|
||||
if !securitySettings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) {
|
||||
return nil, errors.New("forbidden to use container capabilities")
|
||||
}
|
||||
|
||||
if !securitySettings.AllowBindMountsForRegularUsers && (len(partialContainer.HostConfig.Binds) > 0) {
|
||||
return forbiddenResponse, errors.New("forbidden to use bind mounts")
|
||||
}
|
||||
|
||||
request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
}
|
||||
|
||||
response, err := transport.executeDockerRequest(request)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
if response.StatusCode == http.StatusCreated {
|
||||
err = transport.decorateGenericResourceCreationResponse(response, resourceIdentifierAttribute, resourceType, tokenData.ID)
|
||||
}
|
||||
|
||||
return response, err
|
||||
}
|
||||
|
||||
@@ -1,17 +1,13 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/client"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
@@ -89,54 +85,3 @@ func selectorServiceLabels(responseObject map[string]interface{}) map[string]int
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (transport *Transport) decorateServiceCreationOperation(request *http.Request) (*http.Response, error) {
|
||||
type PartialService struct {
|
||||
TaskTemplate struct {
|
||||
ContainerSpec struct {
|
||||
Mounts []struct {
|
||||
Type string
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
forbiddenResponse := &http.Response{
|
||||
StatusCode: http.StatusForbidden,
|
||||
}
|
||||
|
||||
isAdminOrEndpointAdmin, err := transport.isAdminOrEndpointAdmin(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !isAdminOrEndpointAdmin {
|
||||
securitySettings, err := transport.fetchEndpointSecuritySettings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(request.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
partialService := &PartialService{}
|
||||
err = json.Unmarshal(body, partialService)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !securitySettings.AllowBindMountsForRegularUsers && (len(partialService.TaskTemplate.ContainerSpec.Mounts) > 0) {
|
||||
for _, mount := range partialService.TaskTemplate.ContainerSpec.Mounts {
|
||||
if mount.Type == "bind" {
|
||||
return forbiddenResponse, errors.New("forbidden to use bind mounts")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
request.Body = ioutil.NopCloser(bytes.NewBuffer(body))
|
||||
}
|
||||
|
||||
return transport.replaceRegistryAuthenticationHeader(request)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
@@ -43,10 +44,11 @@ type (
|
||||
}
|
||||
|
||||
restrictedDockerOperationContext struct {
|
||||
isAdmin bool
|
||||
userID portainer.UserID
|
||||
userTeamIDs []portainer.TeamID
|
||||
resourceControls []portainer.ResourceControl
|
||||
isAdmin bool
|
||||
endpointResourceAccess bool
|
||||
userID portainer.UserID
|
||||
userTeamIDs []portainer.TeamID
|
||||
resourceControls []portainer.ResourceControl
|
||||
}
|
||||
|
||||
operationExecutor struct {
|
||||
@@ -155,14 +157,8 @@ func (transport *Transport) proxyAgentRequest(r *http.Request) (*http.Response,
|
||||
return transport.administratorOperation(r)
|
||||
}
|
||||
|
||||
agentTargetHeader := r.Header.Get(portainer.PortainerAgentTargetHeader)
|
||||
resourceID, err := transport.getVolumeResourceID(agentTargetHeader, volumeIDParameter[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// volume browser request
|
||||
return transport.restrictedResourceOperation(r, resourceID, portainer.VolumeResourceControl, true)
|
||||
return transport.restrictedResourceOperation(r, volumeIDParameter[0], portainer.VolumeResourceControl, true)
|
||||
}
|
||||
|
||||
return transport.executeDockerRequest(r)
|
||||
@@ -193,7 +189,7 @@ func (transport *Transport) proxyConfigRequest(request *http.Request) (*http.Res
|
||||
func (transport *Transport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/containers/create":
|
||||
return transport.decorateContainerCreationOperation(request, containerObjectIdentifier, portainer.ContainerResourceControl)
|
||||
return transport.decorateGenericResourceCreationOperation(request, containerObjectIdentifier, portainer.ContainerResourceControl)
|
||||
|
||||
case "/containers/prune":
|
||||
return transport.administratorOperation(request)
|
||||
@@ -229,7 +225,7 @@ func (transport *Transport) proxyContainerRequest(request *http.Request) (*http.
|
||||
func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/services/create":
|
||||
return transport.decorateServiceCreationOperation(request)
|
||||
return transport.replaceRegistryAuthenticationHeader(request)
|
||||
|
||||
case "/services":
|
||||
return transport.rewriteOperation(request, transport.serviceListOperation)
|
||||
@@ -406,17 +402,45 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
rbacExtension, err := transport.dataStore.Extension().Extension(portainer.RBACExtension)
|
||||
if err != nil && err != bolterrors.ErrObjectNotFound {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := transport.dataStore.User().User(tokenData.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if volumeBrowseRestrictionCheck {
|
||||
securitySettings, err := transport.fetchEndpointSecuritySettings()
|
||||
settings, err := transport.dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !securitySettings.AllowVolumeBrowserForRegularUsers {
|
||||
return responseutils.WriteAccessDeniedResponse()
|
||||
if !settings.AllowVolumeBrowserForRegularUsers {
|
||||
if rbacExtension == nil {
|
||||
return responseutils.WriteAccessDeniedResponse()
|
||||
}
|
||||
|
||||
// Return access denied for all roles except endpoint-administrator
|
||||
_, userCanBrowse := user.EndpointAuthorizations[transport.endpoint.ID][portainer.OperationDockerAgentBrowseList]
|
||||
if !userCanBrowse {
|
||||
return responseutils.WriteAccessDeniedResponse()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endpointResourceAccess := false
|
||||
_, ok := user.EndpointAuthorizations[transport.endpoint.ID][portainer.EndpointResourcesAccess]
|
||||
if ok {
|
||||
endpointResourceAccess = true
|
||||
}
|
||||
|
||||
if rbacExtension != nil && endpointResourceAccess {
|
||||
return transport.executeDockerRequest(request)
|
||||
}
|
||||
|
||||
teamMemberships, err := transport.dataStore.TeamMembership().TeamMembershipsByUserID(tokenData.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -559,18 +583,16 @@ func (transport *Transport) executeGenericResourceDeletionOperation(request *htt
|
||||
return response, err
|
||||
}
|
||||
|
||||
if response.StatusCode == http.StatusNoContent || response.StatusCode == http.StatusOK {
|
||||
resourceControl, err := transport.dataStore.ResourceControl().ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType)
|
||||
resourceControl, err := transport.dataStore.ResourceControl().ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
if resourceControl != nil {
|
||||
err = transport.dataStore.ResourceControl().DeleteResourceControl(resourceControl.ID)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
if resourceControl != nil {
|
||||
err = transport.dataStore.ResourceControl().DeleteResourceControl(resourceControl.ID)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response, err
|
||||
@@ -651,14 +673,25 @@ func (transport *Transport) createOperationContext(request *http.Request) (*rest
|
||||
}
|
||||
|
||||
operationContext := &restrictedDockerOperationContext{
|
||||
isAdmin: true,
|
||||
userID: tokenData.ID,
|
||||
resourceControls: resourceControls,
|
||||
isAdmin: true,
|
||||
userID: tokenData.ID,
|
||||
resourceControls: resourceControls,
|
||||
endpointResourceAccess: false,
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
operationContext.isAdmin = false
|
||||
|
||||
user, err := transport.dataStore.User().User(operationContext.userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
_, ok := user.EndpointAuthorizations[transport.endpoint.ID][portainer.EndpointResourcesAccess]
|
||||
if ok {
|
||||
operationContext.endpointResourceAccess = true
|
||||
}
|
||||
|
||||
teamMemberships, err := transport.dataStore.TeamMembership().TeamMembershipsByUserID(tokenData.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -673,21 +706,3 @@ func (transport *Transport) createOperationContext(request *http.Request) (*rest
|
||||
|
||||
return operationContext, nil
|
||||
}
|
||||
|
||||
func (transport *Transport) isAdminOrEndpointAdmin(request *http.Request) (bool, error) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return tokenData.Role == portainer.AdministratorRole, nil
|
||||
}
|
||||
|
||||
func (transport *Transport) fetchEndpointSecuritySettings() (*portainer.EndpointSecuritySettings, error) {
|
||||
endpoint, err := transport.dataStore.Endpoint().Endpoint(portainer.EndpointID(transport.endpoint.ID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &endpoint.SecuritySettings, nil
|
||||
}
|
||||
|
||||
@@ -168,30 +168,16 @@ func (transport *Transport) restrictedVolumeOperation(requestPath string, reques
|
||||
return transport.rewriteOperation(request, transport.volumeInspectOperation)
|
||||
}
|
||||
|
||||
agentTargetHeader := request.Header.Get(portainer.PortainerAgentTargetHeader)
|
||||
|
||||
resourceID, err := transport.getVolumeResourceID(agentTargetHeader, path.Base(requestPath))
|
||||
cli := transport.dockerClient
|
||||
volume, err := cli.VolumeInspect(context.Background(), path.Base(requestPath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
volumeID := volume.Name + volume.CreatedAt
|
||||
|
||||
if request.Method == http.MethodDelete {
|
||||
return transport.executeGenericResourceDeletionOperation(request, resourceID, portainer.VolumeResourceControl)
|
||||
return transport.executeGenericResourceDeletionOperation(request, volumeID, portainer.VolumeResourceControl)
|
||||
}
|
||||
return transport.restrictedResourceOperation(request, resourceID, portainer.VolumeResourceControl, false)
|
||||
}
|
||||
|
||||
func (transport *Transport) getVolumeResourceID(nodename, volumeID string) (string, error) {
|
||||
cli, err := transport.dockerClientFactory.CreateClient(transport.endpoint, nodename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
volume, err := cli.VolumeInspect(context.Background(), volumeID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return volume.Name + volume.CreatedAt, nil
|
||||
return transport.restrictedResourceOperation(request, volumeID, portainer.VolumeResourceControl, false)
|
||||
}
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/dockercompose"
|
||||
)
|
||||
|
||||
// ProxyServer provide an extedned proxy with a local server to forward requests
|
||||
type ProxyServer struct {
|
||||
server *http.Server
|
||||
Port int
|
||||
}
|
||||
|
||||
func (factory *ProxyFactory) NewDockerComposeAgentProxy(endpoint *portainer.Endpoint) (*ProxyServer, error) {
|
||||
|
||||
if endpoint.Type == portainer.EdgeAgentOnDockerEnvironment {
|
||||
return &ProxyServer{
|
||||
Port: factory.reverseTunnelService.GetTunnelDetails(endpoint.ID).Port,
|
||||
}, nil
|
||||
}
|
||||
|
||||
endpointURL, err := url.Parse(endpoint.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
endpointURL.Scheme = "http"
|
||||
httpTransport := &http.Transport{}
|
||||
|
||||
if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify {
|
||||
config, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpTransport.TLSClientConfig = config
|
||||
endpointURL.Scheme = "https"
|
||||
}
|
||||
|
||||
proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
|
||||
|
||||
proxy.Transport = dockercompose.NewAgentTransport(factory.signatureService, httpTransport)
|
||||
|
||||
proxyServer := &ProxyServer{
|
||||
&http.Server{
|
||||
Handler: proxy,
|
||||
},
|
||||
0,
|
||||
}
|
||||
|
||||
return proxyServer, proxyServer.start()
|
||||
}
|
||||
|
||||
func (proxy *ProxyServer) start() error {
|
||||
listener, err := net.Listen("tcp", ":0")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
proxy.Port = listener.Addr().(*net.TCPAddr).Port
|
||||
go func() {
|
||||
proxyHost := fmt.Sprintf("127.0.0.1:%d", proxy.Port)
|
||||
log.Printf("Starting Proxy server on %s...\n", proxyHost)
|
||||
|
||||
err := proxy.server.Serve(listener)
|
||||
log.Printf("Exiting Proxy server %s\n", proxyHost)
|
||||
|
||||
if err != http.ErrServerClosed {
|
||||
log.Printf("Proxy server %s exited with an error: %s\n", proxyHost, err)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close shuts down the server
|
||||
func (proxy *ProxyServer) Close() {
|
||||
if proxy.server != nil {
|
||||
proxy.server.Close()
|
||||
}
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package dockercompose
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type (
|
||||
// AgentTransport is an http.Transport wrapper that adds custom http headers to communicate to an Agent
|
||||
AgentTransport struct {
|
||||
httpTransport *http.Transport
|
||||
signatureService portainer.DigitalSignatureService
|
||||
endpointIdentifier portainer.EndpointID
|
||||
}
|
||||
)
|
||||
|
||||
// NewAgentTransport returns a new transport that can be used to send signed requests to a Portainer agent
|
||||
func NewAgentTransport(signatureService portainer.DigitalSignatureService, httpTransport *http.Transport) *AgentTransport {
|
||||
transport := &AgentTransport{
|
||||
httpTransport: httpTransport,
|
||||
signatureService: signatureService,
|
||||
}
|
||||
|
||||
return transport
|
||||
}
|
||||
|
||||
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||
func (transport *AgentTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
|
||||
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey())
|
||||
request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
|
||||
|
||||
return transport.httpTransport.RoundTrip(request)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
@@ -15,8 +16,14 @@ import (
|
||||
|
||||
const azureAPIBaseURL = "https://management.azure.com"
|
||||
|
||||
var extensionPorts = map[portainer.ExtensionID]string{
|
||||
portainer.RegistryManagementExtension: "7001",
|
||||
portainer.OAuthAuthenticationExtension: "7002",
|
||||
portainer.RBACExtension: "7003",
|
||||
}
|
||||
|
||||
type (
|
||||
// ProxyFactory is a factory to create reverse proxies
|
||||
// ProxyFactory is a factory to create reverse proxies to Docker endpoints and extensions
|
||||
ProxyFactory struct {
|
||||
dataStore portainer.DataStore
|
||||
signatureService portainer.DigitalSignatureService
|
||||
@@ -39,6 +46,25 @@ func NewProxyFactory(dataStore portainer.DataStore, signatureService portainer.D
|
||||
}
|
||||
}
|
||||
|
||||
// BuildExtensionURL returns the URL to an extension server
|
||||
func BuildExtensionURL(extensionID portainer.ExtensionID) string {
|
||||
return fmt.Sprintf("http://%s:%s", portainer.ExtensionServer, extensionPorts[extensionID])
|
||||
}
|
||||
|
||||
// NewExtensionProxy returns a new HTTP proxy to an extension server
|
||||
func (factory *ProxyFactory) NewExtensionProxy(extensionID portainer.ExtensionID) (http.Handler, error) {
|
||||
address := "http://" + portainer.ExtensionServer + ":" + extensionPorts[extensionID]
|
||||
|
||||
extensionURL, err := url.Parse(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
extensionURL.Scheme = "http"
|
||||
proxy := httputil.NewSingleHostReverseProxy(extensionURL)
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
// NewLegacyExtensionProxy returns a new HTTP proxy to a legacy extension server (Storidge)
|
||||
func (factory *ProxyFactory) NewLegacyExtensionProxy(extensionAPIURL string) (http.Handler, error) {
|
||||
extensionURL, err := url.Parse(extensionAPIURL)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user