Compare commits
159 Commits
revert-396
...
fix/GH/449
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce74e4d5c8 | ||
|
|
3d9c10adf1 | ||
|
|
0d20988bef | ||
|
|
1545a42f08 | ||
|
|
a12f2ee893 | ||
|
|
174e28b850 | ||
|
|
3da9751c82 | ||
|
|
ccea7cca3d | ||
|
|
43891703c2 | ||
|
|
74429d6d46 | ||
|
|
bb5c2c2875 | ||
|
|
3e82d01894 | ||
|
|
9e80037e72 | ||
|
|
da29c2b6a5 | ||
|
|
0ed4d443ee | ||
|
|
a4fa44f831 | ||
|
|
e479e41aee | ||
|
|
d4c4c4e895 | ||
|
|
466bd24648 | ||
|
|
2fc60f14e1 | ||
|
|
9300603777 | ||
|
|
8dac2df7bf | ||
|
|
90fd5af4b9 | ||
|
|
3ec05accbc | ||
|
|
1bc0c1baa9 | ||
|
|
ce8e245759 | ||
|
|
b91895d618 | ||
|
|
0019b22be5 | ||
|
|
eb0278d230 | ||
|
|
787cf41ee3 | ||
|
|
0ebf0ab199 | ||
|
|
6fa450a981 | ||
|
|
b4f97efb85 | ||
|
|
45cada05d5 | ||
|
|
d5d7b17dc4 | ||
|
|
859d26aef6 | ||
|
|
fc248c31c7 | ||
|
|
383e19077f | ||
|
|
a3b54e1981 | ||
|
|
403dbb1245 | ||
|
|
c48d05449c | ||
|
|
9fd38a0543 | ||
|
|
f8be9bb57a | ||
|
|
7329ea91ca | ||
|
|
d850e18ff0 | ||
|
|
68851aada4 | ||
|
|
aeb3bf535f | ||
|
|
7b77a92a2d | ||
|
|
35fa9d6981 | ||
|
|
b3b706d88d | ||
|
|
297eea5da6 | ||
|
|
b6fc434291 | ||
|
|
5c6147c9b9 | ||
|
|
8c3160d061 | ||
|
|
1ef78c0fdf | ||
|
|
9733d32551 | ||
|
|
bd0d1c25fa | ||
|
|
b77e39c065 | ||
|
|
8d6f6e306a | ||
|
|
36bf9c24b9 | ||
|
|
e10cf3e59b | ||
|
|
46762f3e67 | ||
|
|
7ad06b3be5 | ||
|
|
877e2baf59 | ||
|
|
9f0ff5181b | ||
|
|
56cda7f260 | ||
|
|
449b7888d3 | ||
|
|
83c3f9ed06 | ||
|
|
52bdcf2e2b | ||
|
|
32bac9ffcc | ||
|
|
00389a7da9 | ||
|
|
fe4a80c7bd | ||
|
|
6615e354c4 | ||
|
|
69e9e566c5 | ||
|
|
f91d3f1ca3 | ||
|
|
201c3ac143 | ||
|
|
2c15dcd1f2 | ||
|
|
1bf97426bf | ||
|
|
1f614ee95a | ||
|
|
b4c2e5d235 | ||
|
|
9d18d47194 | ||
|
|
8629738e34 | ||
|
|
a3925c3371 | ||
|
|
6720c31aa9 | ||
|
|
01d414b578 | ||
|
|
6d069cc8d6 | ||
|
|
a1e3ed7f78 | ||
|
|
baaa96f34f | ||
|
|
56524ca7d5 | ||
|
|
c439bc56ff | ||
|
|
134f2f1532 | ||
|
|
b4aca3822d | ||
|
|
59cc02137d | ||
|
|
8408484f8b | ||
|
|
c5731e237e | ||
|
|
cb1a1e7be5 | ||
|
|
e7a33347c6 | ||
|
|
26ee78e1e7 | ||
|
|
61f97469ab | ||
|
|
b9c2bf487b | ||
|
|
1b88ca2285 | ||
|
|
747fdae269 | ||
|
|
b8f8c75380 | ||
|
|
d85708f6ea | ||
|
|
e4ca58a042 | ||
|
|
2158cc5157 | ||
|
|
7aaf9d0eb7 | ||
|
|
82064152ec | ||
|
|
7e90bf11b7 | ||
|
|
ff250a202a | ||
|
|
00f4fe0039 | ||
|
|
148ccd1bc4 | ||
|
|
6756b04b67 | ||
|
|
909e1ef02c | ||
|
|
bd7d7dcef5 | ||
|
|
490b7ad26f | ||
|
|
4d5836138b | ||
|
|
da143a7a22 | ||
|
|
4431d748c2 | ||
|
|
63bf654d8d | ||
|
|
93d8c179f1 | ||
|
|
7539f09f98 | ||
|
|
1a3f77137a | ||
|
|
fec85c77d6 | ||
|
|
1ff5708183 | ||
|
|
1edf981330 | ||
|
|
fa9eeaf3b1 | ||
|
|
07efd4bdda | ||
|
|
2bc6b2dff7 | ||
|
|
0cebe6588a | ||
|
|
990f3cad88 | ||
|
|
7e7a8e521b | ||
|
|
43bbc14c58 | ||
|
|
adf33385ce | ||
|
|
e78aaec558 | ||
|
|
3953acf110 | ||
|
|
99db41f96e | ||
|
|
822c4e117c | ||
|
|
f761e65167 | ||
|
|
1ef7347f19 | ||
|
|
a473d738be | ||
|
|
7eb8d5449a | ||
|
|
435f15ec6a | ||
|
|
5abd35d4c1 | ||
|
|
b50497301d | ||
|
|
4534ccb499 | ||
|
|
6f6bc24efd | ||
|
|
4346bf95a7 | ||
|
|
c9dd6e3851 | ||
|
|
3a33365133 | ||
|
|
67069547b8 | ||
|
|
6fc923b05b | ||
|
|
8e7aaa23d5 | ||
|
|
227fbeb1b7 | ||
|
|
53cddeb283 | ||
|
|
4b97cf738e | ||
|
|
66a3104805 | ||
|
|
5a4a10859d | ||
|
|
94676df329 |
@@ -6,7 +6,6 @@ env:
|
||||
|
||||
globals:
|
||||
angular: true
|
||||
__CONFIG_GA_ID: true
|
||||
|
||||
extends:
|
||||
- 'eslint:recommended'
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
4
.github/ISSUE_TEMPLATE/Bug_report.md
vendored
@@ -16,7 +16,7 @@ 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
|
||||
Also, be sure to check our FAQ and documentation first: https://documentation.portainer.io/
|
||||
-->
|
||||
|
||||
**Bug description**
|
||||
@@ -27,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://portainer.readthedocs.io/en/stable/faq.html#how-do-i-get-the-logs-from-portainer)
|
||||
You can see how [here](https://documentation.portainer.io/archive/1.23.2/faq/#how-do-i-get-the-logs-from-portainer)
|
||||
|
||||
**Steps to reproduce the issue:**
|
||||
|
||||
|
||||
12
.github/stale.yml
vendored
12
.github/stale.yml
vendored
@@ -12,14 +12,13 @@ issues:
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- kind/enhancement
|
||||
- kind/feature
|
||||
- kind/question
|
||||
- kind/style
|
||||
- kind/workaround
|
||||
- bug/need-confirmation
|
||||
- bug/confirmed
|
||||
- status/discuss
|
||||
|
||||
|
||||
# Only issues with all of these labels are checked if stale. Defaults to `[]` (disabled)
|
||||
onlyLabels: []
|
||||
|
||||
@@ -35,9 +34,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,8 +47,7 @@ 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
|
||||
and mention @itsconquest. One of our staff will then review the issue.
|
||||
|
||||
mentioning `ametdoohan`, `balasu` or `keverv` and 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.
|
||||
|
||||
14
README.md
14
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,12 +24,12 @@ 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 same, including default credentials.
|
||||
Unlike the public demo, the playground sessions are deleted after 4 hours. Apart from that, all the settings are the same, including default credentials.
|
||||
|
||||
## Getting started
|
||||
|
||||
- [Deploy Portainer](https://www.portainer.io/installation/)
|
||||
- [Documentation](https://www.portainer.io/documentation/)
|
||||
- [Documentation](https://documentation.portainer.io)
|
||||
|
||||
## Getting help
|
||||
|
||||
@@ -38,7 +38,7 @@ For FORMAL Support, please purchase a support subscription from here: https://ww
|
||||
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://www.portainer.io/documentation/faqs/
|
||||
- FAQ: https://documentation.portainer.io
|
||||
- Slack (chat): https://portainer.io/slack/
|
||||
|
||||
## Reporting bugs and contributing
|
||||
@@ -50,6 +50,12 @@ For community support: You can find more information about Portainer's community
|
||||
|
||||
- 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,11 +340,6 @@ 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,14 +1,30 @@
|
||||
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 {
|
||||
_, err := store.SettingsService.Settings()
|
||||
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()
|
||||
if err == errors.ErrObjectNotFound {
|
||||
defaultSettings := &portainer.Settings{
|
||||
AuthenticationMethod: portainer.AuthenticationInternal,
|
||||
@@ -24,14 +40,18 @@ func (store *Store) Init() error {
|
||||
portainer.LDAPGroupSearchSettings{},
|
||||
},
|
||||
},
|
||||
OAuthSettings: portainer.OAuthSettings{},
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
EnableHostManagementFeatures: false,
|
||||
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
|
||||
TemplatesURL: portainer.DefaultTemplatesURL,
|
||||
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
|
||||
OAuthSettings: portainer.OAuthSettings{},
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
AllowVolumeBrowserForRegularUsers: false,
|
||||
AllowHostNamespaceForRegularUsers: true,
|
||||
AllowDeviceMappingForRegularUsers: true,
|
||||
AllowStackManagementForRegularUsers: true,
|
||||
AllowContainerCapabilitiesForRegularUsers: true,
|
||||
EnableHostManagementFeatures: false,
|
||||
EdgeAgentCheckinInterval: portainer.DefaultEdgeAgentCheckinIntervalInSeconds,
|
||||
TemplatesURL: portainer.DefaultTemplatesURL,
|
||||
UserSessionTimeout: portainer.DefaultUserSessionTimeout,
|
||||
}
|
||||
|
||||
err = store.SettingsService.UpdateSettings(defaultSettings)
|
||||
@@ -79,60 +99,5 @@ 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,8 +1,6 @@
|
||||
package migrator
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
import portainer "github.com/portainer/portainer/api"
|
||||
|
||||
func (m *Migrator) updateSettingsToDB24() error {
|
||||
legacySettings, err := m.settingsService.Settings()
|
||||
@@ -10,11 +8,27 @@ func (m *Migrator) updateSettingsToDB24() error {
|
||||
return err
|
||||
}
|
||||
|
||||
if legacySettings.TemplatesURL == "" {
|
||||
legacySettings.TemplatesURL = portainer.DefaultTemplatesURL
|
||||
}
|
||||
|
||||
legacySettings.UserSessionTimeout = portainer.DefaultUserSessionTimeout
|
||||
legacySettings.AllowHostNamespaceForRegularUsers = true
|
||||
legacySettings.AllowDeviceMappingForRegularUsers = true
|
||||
legacySettings.AllowStackManagementForRegularUsers = true
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
23
api/bolt/migrator/migrate_dbversion24.go
Normal file
23
api/bolt/migrator/migrate_dbversion24.go
Normal file
@@ -0,0 +1,23 @@
|
||||
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)
|
||||
}
|
||||
@@ -321,7 +321,7 @@ func (m *Migrator) Migrate() error {
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 2.0
|
||||
// Portainer 1.24.1
|
||||
if m.currentDBVersion < 24 {
|
||||
err := m.updateSettingsToDB24()
|
||||
if err != nil {
|
||||
@@ -329,5 +329,18 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||
}
|
||||
|
||||
@@ -10,8 +10,9 @@ import (
|
||||
|
||||
const (
|
||||
// BucketName represents the name of the bucket where this service stores data.
|
||||
BucketName = "version"
|
||||
versionKey = "DB_VERSION"
|
||||
BucketName = "version"
|
||||
versionKey = "DB_VERSION"
|
||||
instanceKey = "INSTANCE_ID"
|
||||
)
|
||||
|
||||
// Service represents a service to manage stored versions.
|
||||
@@ -64,3 +65,37 @@ 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,6 +2,7 @@ package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
@@ -35,7 +36,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").Default(defaultNoAnalytics).Bool(),
|
||||
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app (deprecated)").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(),
|
||||
@@ -88,7 +89,9 @@ 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,7 +8,6 @@ const (
|
||||
defaultTunnelServerPort = "8000"
|
||||
defaultDataDirectory = "/data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "/certs/ca.pem"
|
||||
|
||||
@@ -6,7 +6,6 @@ const (
|
||||
defaultTunnelServerPort = "8000"
|
||||
defaultDataDirectory = "C:\\data"
|
||||
defaultAssetsDirectory = "./"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLS = "false"
|
||||
defaultTLSSkipVerify = "false"
|
||||
defaultTLSCACertPath = "C:\\certs\\ca.pem"
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/bolt"
|
||||
"github.com/portainer/portainer/api/chisel"
|
||||
"github.com/portainer/portainer/api/cli"
|
||||
@@ -17,13 +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/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 {
|
||||
@@ -108,6 +108,10 @@ func initLDAPService() portainer.LDAPService {
|
||||
return &ldap.Service{}
|
||||
}
|
||||
|
||||
func initOAuthService() portainer.OAuthService {
|
||||
return oauth.NewService()
|
||||
}
|
||||
|
||||
func initGitService() portainer.GitService {
|
||||
return git.NewService()
|
||||
}
|
||||
@@ -116,8 +120,8 @@ func initDockerClientFactory(signatureService portainer.DigitalSignatureService,
|
||||
return docker.NewClientFactory(signatureService, reverseTunnelService)
|
||||
}
|
||||
|
||||
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *kubecli.ClientFactory {
|
||||
return kubecli.NewClientFactory(signatureService, reverseTunnelService)
|
||||
func initKubernetesClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string) *kubecli.ClientFactory {
|
||||
return kubecli.NewClientFactory(signatureService, reverseTunnelService, instanceID)
|
||||
}
|
||||
|
||||
func initSnapshotService(snapshotInterval string, dataStore portainer.DataStore, dockerClientFactory *docker.ClientFactory, kubernetesClientFactory *kubecli.ClientFactory) (portainer.SnapshotService, error) {
|
||||
@@ -149,8 +153,7 @@ func loadEdgeJobsFromDatabase(dataStore portainer.DataStore, reverseTunnelServic
|
||||
|
||||
func initStatus(flags *portainer.CLIFlags) *portainer.Status {
|
||||
return &portainer.Status{
|
||||
Analytics: !*flags.NoAnalytics,
|
||||
Version: portainer.APIVersion,
|
||||
Version: portainer.APIVersion,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,6 +166,7 @@ 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
|
||||
@@ -313,17 +317,6 @@ 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
|
||||
@@ -354,6 +347,8 @@ func main() {
|
||||
|
||||
ldapService := initLDAPService()
|
||||
|
||||
oauthService := initOAuthService()
|
||||
|
||||
gitService := initGitService()
|
||||
|
||||
cryptoService := initCryptoService()
|
||||
@@ -365,15 +360,15 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
extensionManager, err := initExtensionManager(fileService, dataStore)
|
||||
reverseTunnelService := chisel.NewService(dataStore)
|
||||
|
||||
instanceID, err := dataStore.Version().InstanceID()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
reverseTunnelService := chisel.NewService(dataStore)
|
||||
|
||||
dockerClientFactory := initDockerClientFactory(digitalSignatureService, reverseTunnelService)
|
||||
kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService)
|
||||
kubernetesClientFactory := initKubernetesClientFactory(digitalSignatureService, reverseTunnelService, instanceID)
|
||||
|
||||
snapshotService, err := initSnapshotService(*flags.SnapshotInterval, dataStore, dockerClientFactory, kubernetesClientFactory)
|
||||
if err != nil {
|
||||
@@ -432,10 +427,9 @@ 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,
|
||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
||||
Username: "admin",
|
||||
Role: portainer.AdministratorRole,
|
||||
Password: adminPasswordHash,
|
||||
}
|
||||
err := dataStore.User().CreateUser(user)
|
||||
if err != nil {
|
||||
@@ -462,11 +456,11 @@ func main() {
|
||||
SwarmStackManager: swarmStackManager,
|
||||
ComposeStackManager: composeStackManager,
|
||||
KubernetesDeployer: kubernetesDeployer,
|
||||
ExtensionManager: extensionManager,
|
||||
CryptoService: cryptoService,
|
||||
JWTService: jwtService,
|
||||
FileService: fileService,
|
||||
LDAPService: ldapService,
|
||||
OAuthService: oauthService,
|
||||
GitService: gitService,
|
||||
SignatureService: digitalSignatureService,
|
||||
SnapshotService: snapshotService,
|
||||
|
||||
@@ -6,6 +6,24 @@ 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,313 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/archive"
|
||||
|
||||
"io"
|
||||
"os"
|
||||
@@ -96,12 +95,6 @@ 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)
|
||||
|
||||
@@ -30,6 +30,7 @@ require (
|
||||
github.com/portainer/libhttp v0.0.0-20190806161843-ba068f58be33
|
||||
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
|
||||
|
||||
@@ -13,7 +13,6 @@ 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 {
|
||||
@@ -79,11 +78,6 @@ 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)
|
||||
}
|
||||
|
||||
@@ -103,9 +97,8 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
|
||||
}
|
||||
|
||||
user := &portainer.User{
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
}
|
||||
|
||||
err = handler.DataStore.User().CreateUser(user)
|
||||
@@ -118,11 +111,6 @@ 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,9 +1,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
@@ -13,7 +11,6 @@ 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 {
|
||||
@@ -27,52 +24,21 @@ func (payload *oauthPayload) Validate(r *http.Request) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
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
|
||||
func (handler *Handler) authenticateOAuth(code string, settings *portainer.OAuthSettings) (string, error) {
|
||||
if code == "" {
|
||||
return "", errors.New("Invalid OAuth authorization code")
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", extensionURL+"/validate", nil)
|
||||
if settings == nil {
|
||||
return "", errors.New("Invalid OAuth configuration")
|
||||
}
|
||||
|
||||
username, err := handler.OAuthService.Authenticate(code, settings)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
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
|
||||
return username, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||
@@ -91,14 +57,7 @@ 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")}
|
||||
}
|
||||
|
||||
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)
|
||||
username, err := handler.authenticateOAuth(payload.Code, &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}
|
||||
@@ -115,9 +74,8 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
|
||||
|
||||
if user == nil {
|
||||
user = &portainer.User{
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
||||
Username: username,
|
||||
Role: portainer.StandardUserRole,
|
||||
}
|
||||
|
||||
err = handler.DataStore.User().CreateUser(user)
|
||||
@@ -138,10 +96,6 @@ 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,7 +9,6 @@ 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.
|
||||
@@ -19,8 +18,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,6 +57,17 @@ 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,10 +39,8 @@ 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 {
|
||||
@@ -56,13 +54,6 @@ 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,15 +92,12 @@ 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)
|
||||
@@ -108,13 +105,6 @@ 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,14 +7,12 @@ 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
|
||||
AuthorizationService *authorization.Service
|
||||
DataStore portainer.DataStore
|
||||
}
|
||||
|
||||
// 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, false)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
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, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
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, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
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, false)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
@@ -2,17 +2,20 @@ 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"
|
||||
"github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/http/client"
|
||||
"github.com/portainer/portainer/api/internal/edge"
|
||||
)
|
||||
@@ -20,7 +23,7 @@ import (
|
||||
type endpointCreatePayload struct {
|
||||
Name string
|
||||
URL string
|
||||
EndpointType int
|
||||
EndpointCreationType endpointCreationEnum
|
||||
PublicURL string
|
||||
GroupID int
|
||||
TLS bool
|
||||
@@ -36,6 +39,17 @@ 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 {
|
||||
@@ -43,11 +57,11 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
}
|
||||
payload.Name = name
|
||||
|
||||
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)")
|
||||
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)")
|
||||
}
|
||||
payload.EndpointType = endpointType
|
||||
payload.EndpointCreationType = endpointCreationEnum(endpointCreationType)
|
||||
|
||||
groupID, _ := request.RetrieveNumericMultiPartFormValue(r, "GroupID", true)
|
||||
if groupID == 0 {
|
||||
@@ -97,8 +111,8 @@ func (payload *endpointCreatePayload) Validate(r *http.Request) error {
|
||||
}
|
||||
}
|
||||
|
||||
switch portainer.EndpointType(payload.EndpointType) {
|
||||
case portainer.AzureEnvironment:
|
||||
switch payload.EndpointCreationType {
|
||||
case azureEnvironment:
|
||||
azureApplicationID, err := request.RetrieveMultiPartFormValue(r, "AzureApplicationID", false)
|
||||
if err != nil {
|
||||
return errors.New("Invalid Azure application ID")
|
||||
@@ -182,22 +196,34 @@ func (handler *Handler) endpointCreate(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
|
||||
func (handler *Handler) createEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
switch portainer.EndpointType(payload.EndpointType) {
|
||||
case portainer.AzureEnvironment:
|
||||
switch payload.EndpointCreationType {
|
||||
case azureEnvironment:
|
||||
return handler.createAzureEndpoint(payload)
|
||||
|
||||
case portainer.EdgeAgentOnDockerEnvironment:
|
||||
return handler.createEdgeAgentEndpoint(payload, portainer.EdgeAgentOnDockerEnvironment)
|
||||
case edgeAgentEnvironment:
|
||||
return handler.createEdgeAgentEndpoint(payload)
|
||||
|
||||
case portainer.KubernetesLocalEnvironment:
|
||||
case localKubernetesEnvironment:
|
||||
return handler.createKubernetesEndpoint(payload)
|
||||
}
|
||||
|
||||
case portainer.EdgeAgentOnKubernetesEnvironment:
|
||||
return handler.createEdgeAgentEndpoint(payload, portainer.EdgeAgentOnKubernetesEnvironment)
|
||||
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://")
|
||||
}
|
||||
}
|
||||
|
||||
if payload.TLS {
|
||||
return handler.createTLSSecuredEndpoint(payload, portainer.EndpointType(payload.EndpointType))
|
||||
return handler.createTLSSecuredEndpoint(payload, endpointType)
|
||||
}
|
||||
return handler.createUnsecuredEndpoint(payload)
|
||||
}
|
||||
@@ -241,7 +267,7 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po
|
||||
return endpoint, nil
|
||||
}
|
||||
|
||||
func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload, endpointType portainer.EndpointType) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload) (*portainer.Endpoint, *httperror.HandlerError) {
|
||||
endpointID := handler.DataStore.Endpoint().GetNextIdentifier()
|
||||
|
||||
portainerURL, err := url.Parse(payload.URL)
|
||||
@@ -264,7 +290,7 @@ func (handler *Handler) createEdgeAgentEndpoint(payload *endpointCreatePayload,
|
||||
ID: portainer.EndpointID(endpointID),
|
||||
Name: payload.Name,
|
||||
URL: portainerHost,
|
||||
Type: endpointType,
|
||||
Type: portainer.EdgeAgentOnDockerEnvironment,
|
||||
GroupID: portainer.EndpointGroupID(payload.GroupID),
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: false,
|
||||
@@ -419,15 +445,6 @@ func (handler *Handler) saveEndpointAndUpdateAuthorizations(endpoint *portainer.
|
||||
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 {
|
||||
@@ -472,3 +489,58 @@ 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,13 +40,6 @@ 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}
|
||||
|
||||
@@ -24,7 +24,7 @@ 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, false)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@ 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"
|
||||
"github.com/portainer/portainer/api/bolt/errors"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
)
|
||||
|
||||
type stackStatusResponse struct {
|
||||
@@ -41,7 +43,7 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == errors.ErrObjectNotFound {
|
||||
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}
|
||||
@@ -54,10 +56,27 @@ func (handler *Handler) endpointStatusInspect(w http.ResponseWriter, r *http.Req
|
||||
|
||||
if endpoint.EdgeID == "" {
|
||||
edgeIdentifier := r.Header.Get(portainer.PortainerAgentEdgeIDHeader)
|
||||
|
||||
endpoint.EdgeID = edgeIdentifier
|
||||
|
||||
err := handler.DataStore.Endpoint().UpdateEndpoint(endpoint.ID, endpoint)
|
||||
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)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to Unable to persist endpoint changes inside the database", err}
|
||||
}
|
||||
|
||||
@@ -126,15 +126,12 @@ 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 {
|
||||
@@ -226,13 +223,6 @@ 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,7 +5,6 @@ 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"
|
||||
|
||||
@@ -24,7 +23,6 @@ type Handler struct {
|
||||
*mux.Router
|
||||
requestBouncer *security.RequestBouncer
|
||||
DataStore portainer.DataStore
|
||||
AuthorizationService *authorization.Service
|
||||
FileService portainer.FileService
|
||||
ProxyManager *proxy.Manager
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
|
||||
@@ -1,117 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
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,7 +15,6 @@ 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"
|
||||
@@ -24,7 +23,6 @@ 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"
|
||||
@@ -50,14 +48,12 @@ 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
|
||||
@@ -104,8 +100,6 @@ 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"):
|
||||
@@ -120,8 +114,6 @@ 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,10 +43,6 @@ 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
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -3,8 +3,6 @@ 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"
|
||||
@@ -19,12 +17,11 @@ func hideFields(settings *portainer.Settings) {
|
||||
// Handler is the HTTP handler used to handle settings operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
AuthorizationService *authorization.Service
|
||||
DataStore portainer.DataStore
|
||||
FileService portainer.FileService
|
||||
JWTService portainer.JWTService
|
||||
LDAPService portainer.LDAPService
|
||||
SnapshotService portainer.SnapshotService
|
||||
DataStore portainer.DataStore
|
||||
FileService portainer.FileService
|
||||
JWTService portainer.JWTService
|
||||
LDAPService portainer.LDAPService
|
||||
SnapshotService portainer.SnapshotService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage settings operations.
|
||||
|
||||
@@ -6,18 +6,23 @@ import (
|
||||
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
type publicSettingsResponse struct {
|
||||
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"`
|
||||
LogoURL string `json:"LogoURL"`
|
||||
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
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"`
|
||||
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
|
||||
OAuthLoginURI string `json:"OAuthLoginURI"`
|
||||
EnableTelemetry bool `json:"EnableTelemetry"`
|
||||
}
|
||||
|
||||
// GET request on /api/settings/public
|
||||
@@ -28,13 +33,18 @@ func (handler *Handler) settingsPublic(w http.ResponseWriter, r *http.Request) *
|
||||
}
|
||||
|
||||
publicSettings := &publicSettingsResponse{
|
||||
LogoURL: settings.LogoURL,
|
||||
AuthenticationMethod: settings.AuthenticationMethod,
|
||||
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
|
||||
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
||||
AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers,
|
||||
EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
|
||||
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
|
||||
LogoURL: settings.LogoURL,
|
||||
AuthenticationMethod: settings.AuthenticationMethod,
|
||||
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
|
||||
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
||||
AllowVolumeBrowserForRegularUsers: settings.AllowVolumeBrowserForRegularUsers,
|
||||
AllowHostNamespaceForRegularUsers: settings.AllowHostNamespaceForRegularUsers,
|
||||
AllowDeviceMappingForRegularUsers: settings.AllowDeviceMappingForRegularUsers,
|
||||
AllowStackManagementForRegularUsers: settings.AllowStackManagementForRegularUsers,
|
||||
AllowContainerCapabilitiesForRegularUsers: settings.AllowContainerCapabilitiesForRegularUsers,
|
||||
EnableHostManagementFeatures: settings.EnableHostManagementFeatures,
|
||||
EnableEdgeComputeFeatures: settings.EnableEdgeComputeFeatures,
|
||||
EnableTelemetry: settings.EnableTelemetry,
|
||||
OAuthLoginURI: fmt.Sprintf("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&prompt=login",
|
||||
settings.OAuthSettings.AuthorizationURI,
|
||||
settings.OAuthSettings.ClientID,
|
||||
|
||||
@@ -9,30 +9,34 @@ import (
|
||||
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"
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/filesystem"
|
||||
)
|
||||
|
||||
type settingsUpdatePayload struct {
|
||||
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
|
||||
LogoURL *string
|
||||
BlackListedLabels []portainer.Pair
|
||||
AuthenticationMethod *int
|
||||
LDAPSettings *portainer.LDAPSettings
|
||||
OAuthSettings *portainer.OAuthSettings
|
||||
AllowBindMountsForRegularUsers *bool
|
||||
AllowPrivilegedModeForRegularUsers *bool
|
||||
AllowHostNamespaceForRegularUsers *bool
|
||||
AllowVolumeBrowserForRegularUsers *bool
|
||||
AllowDeviceMappingForRegularUsers *bool
|
||||
AllowStackManagementForRegularUsers *bool
|
||||
AllowContainerCapabilitiesForRegularUsers *bool
|
||||
EnableHostManagementFeatures *bool
|
||||
SnapshotInterval *string
|
||||
TemplatesURL *string
|
||||
EdgeAgentCheckinInterval *int
|
||||
EnableEdgeComputeFeatures *bool
|
||||
UserSessionTimeout *string
|
||||
EnableTelemetry *bool
|
||||
}
|
||||
|
||||
func (payload *settingsUpdatePayload) Validate(r *http.Request) error {
|
||||
if *payload.AuthenticationMethod != 1 && *payload.AuthenticationMethod != 2 && *payload.AuthenticationMethod != 3 {
|
||||
if payload.AuthenticationMethod != nil && *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) {
|
||||
@@ -111,10 +115,8 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
settings.AllowPrivilegedModeForRegularUsers = *payload.AllowPrivilegedModeForRegularUsers
|
||||
}
|
||||
|
||||
updateAuthorizations := false
|
||||
if payload.AllowVolumeBrowserForRegularUsers != nil {
|
||||
settings.AllowVolumeBrowserForRegularUsers = *payload.AllowVolumeBrowserForRegularUsers
|
||||
updateAuthorizations = true
|
||||
}
|
||||
|
||||
if payload.EnableHostManagementFeatures != nil {
|
||||
@@ -125,6 +127,18 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
settings.EnableEdgeComputeFeatures = *payload.EnableEdgeComputeFeatures
|
||||
}
|
||||
|
||||
if payload.AllowHostNamespaceForRegularUsers != nil {
|
||||
settings.AllowHostNamespaceForRegularUsers = *payload.AllowHostNamespaceForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowStackManagementForRegularUsers != nil {
|
||||
settings.AllowStackManagementForRegularUsers = *payload.AllowStackManagementForRegularUsers
|
||||
}
|
||||
|
||||
if payload.AllowContainerCapabilitiesForRegularUsers != nil {
|
||||
settings.AllowContainerCapabilitiesForRegularUsers = *payload.AllowContainerCapabilitiesForRegularUsers
|
||||
}
|
||||
|
||||
if payload.SnapshotInterval != nil && *payload.SnapshotInterval != settings.SnapshotInterval {
|
||||
err := handler.updateSnapshotInterval(settings, *payload.SnapshotInterval)
|
||||
if err != nil {
|
||||
@@ -144,6 +158,14 @@ func (handler *Handler) settingsUpdate(w http.ResponseWriter, r *http.Request) *
|
||||
handler.JWTService.SetUserSessionDuration(userSessionDuration)
|
||||
}
|
||||
|
||||
if payload.AllowDeviceMappingForRegularUsers != nil {
|
||||
settings.AllowDeviceMappingForRegularUsers = *payload.AllowDeviceMappingForRegularUsers
|
||||
}
|
||||
|
||||
if payload.EnableTelemetry != nil {
|
||||
settings.EnableTelemetry = *payload.EnableTelemetry
|
||||
}
|
||||
|
||||
tlsError := handler.updateTLS(settings)
|
||||
if tlsError != nil {
|
||||
return tlsError
|
||||
@@ -154,37 +176,9 @@ 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
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
@@ -151,6 +152,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
}
|
||||
|
||||
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
|
||||
@@ -246,6 +248,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
@@ -283,6 +286,7 @@ 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) {
|
||||
@@ -302,12 +306,18 @@ 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
|
||||
@@ -324,7 +334,18 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
|
||||
return err
|
||||
}
|
||||
|
||||
if !settings.AllowBindMountsForRegularUsers && !config.isAdmin {
|
||||
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if (!settings.AllowBindMountsForRegularUsers ||
|
||||
!settings.AllowPrivilegedModeForRegularUsers ||
|
||||
!settings.AllowHostNamespaceForRegularUsers ||
|
||||
!settings.AllowDeviceMappingForRegularUsers ||
|
||||
!settings.AllowContainerCapabilitiesForRegularUsers) &&
|
||||
!isAdminOrEndpointAdmin {
|
||||
|
||||
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
|
||||
|
||||
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
|
||||
@@ -332,13 +353,10 @@ func (handler *Handler) deployComposeStack(config *composeStackDeploymentConfig)
|
||||
return err
|
||||
}
|
||||
|
||||
valid, err := handler.isValidStackFile(stackContent)
|
||||
err = handler.isValidStackFile(stackContent, settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !valid {
|
||||
return errors.New("bind-mount disabled for non administrator users")
|
||||
}
|
||||
}
|
||||
|
||||
handler.stackCreationMutex.Lock()
|
||||
|
||||
@@ -62,6 +62,7 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
@@ -151,6 +152,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: payload.ComposeFilePathInRepository,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
}
|
||||
|
||||
projectPath := handler.FileService.GetStackProjectPath(strconv.Itoa(int(stack.ID)))
|
||||
@@ -254,6 +256,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
|
||||
EndpointID: endpoint.ID,
|
||||
EntryPoint: filesystem.ComposeFileDefaultName,
|
||||
Env: payload.Env,
|
||||
Status: portainer.StackStatusActive,
|
||||
}
|
||||
|
||||
stackFolder := strconv.Itoa(int(stack.ID))
|
||||
@@ -292,6 +295,7 @@ 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) {
|
||||
@@ -311,6 +315,11 @@ 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,
|
||||
@@ -318,6 +327,7 @@ func (handler *Handler) createSwarmDeployConfig(r *http.Request, stack *portaine
|
||||
registries: filteredRegistries,
|
||||
prune: prune,
|
||||
isAdmin: securityContext.IsAdmin,
|
||||
user: user,
|
||||
}
|
||||
|
||||
return config, nil
|
||||
@@ -329,7 +339,12 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err
|
||||
return err
|
||||
}
|
||||
|
||||
if !settings.AllowBindMountsForRegularUsers && !config.isAdmin {
|
||||
isAdminOrEndpointAdmin, err := handler.userIsAdminOrEndpointAdmin(config.user, config.endpoint.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !settings.AllowBindMountsForRegularUsers && !isAdminOrEndpointAdmin {
|
||||
composeFilePath := path.Join(config.stack.ProjectPath, config.stack.EntryPoint)
|
||||
|
||||
stackContent, err := handler.FileService.GetFileContent(composeFilePath)
|
||||
@@ -337,13 +352,10 @@ func (handler *Handler) deploySwarmStack(config *swarmStackDeploymentConfig) err
|
||||
return err
|
||||
}
|
||||
|
||||
valid, err := handler.isValidStackFile(stackContent)
|
||||
err = handler.isValidStackFile(stackContent, settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !valid {
|
||||
return errors.New("bind-mount disabled for non administrator users")
|
||||
}
|
||||
}
|
||||
|
||||
handler.stackCreationMutex.Lock()
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"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"
|
||||
)
|
||||
@@ -54,12 +53,17 @@ 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) {
|
||||
if securityContext.IsAdmin {
|
||||
return true, nil
|
||||
user, err := handler.DataStore.User().User(securityContext.UserID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
userTeamIDs := make([]portainer.TeamID, 0)
|
||||
@@ -71,21 +75,20 @@ func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedR
|
||||
return true, 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
|
||||
}
|
||||
return handler.userIsAdminOrEndpointAdmin(user, endpointID)
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
_, ok := user.EndpointAuthorizations[endpointID][portainer.EndpointResourcesAccess]
|
||||
if ok {
|
||||
return true, nil
|
||||
}
|
||||
return false, nil
|
||||
return handler.userIsAdminOrEndpointAdmin(user, endpointID)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
httperror "github.com/portainer/libhttp/error"
|
||||
"github.com/portainer/libhttp/request"
|
||||
"github.com/portainer/libhttp/response"
|
||||
"github.com/portainer/portainer/api"
|
||||
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"
|
||||
@@ -46,6 +46,29 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve settings from the database", err}
|
||||
}
|
||||
|
||||
if !settings.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)}
|
||||
}
|
||||
}
|
||||
|
||||
endpoint, err := handler.DataStore.Endpoint().Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err}
|
||||
@@ -53,7 +76,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}
|
||||
}
|
||||
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
@@ -106,10 +129,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) (bool, error) {
|
||||
func (handler *Handler) isValidStackFile(stackFileContent []byte, settings *portainer.Settings) error {
|
||||
composeConfigYAML, err := loader.ParseYAML(stackFileContent)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return err
|
||||
}
|
||||
|
||||
composeConfigFile := types.ConfigFile{
|
||||
@@ -126,19 +149,37 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte) (bool, error)
|
||||
options.SkipInterpolation = true
|
||||
})
|
||||
if err != nil {
|
||||
return false, err
|
||||
return err
|
||||
}
|
||||
|
||||
for key := range composeConfig.Services {
|
||||
service := composeConfig.Services[key]
|
||||
for _, volume := range service.Volumes {
|
||||
if volume.Type == "bind" {
|
||||
return false, nil
|
||||
if !settings.AllowBindMountsForRegularUsers {
|
||||
for _, volume := range service.Volumes {
|
||||
if volume.Type == "bind" {
|
||||
return errors.New("bind-mount disabled for non administrator users")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !settings.AllowPrivilegedModeForRegularUsers && service.Privileged == true {
|
||||
return errors.New("privileged mode disabled for non administrator users")
|
||||
}
|
||||
|
||||
if !settings.AllowHostNamespaceForRegularUsers && service.Pid == "host" {
|
||||
return errors.New("pid host disabled for non administrator users")
|
||||
}
|
||||
|
||||
if !settings.AllowDeviceMappingForRegularUsers && service.Devices != nil && len(service.Devices) > 0 {
|
||||
return errors.New("device mapping disabled for non administrator users")
|
||||
}
|
||||
|
||||
if !settings.AllowContainerCapabilitiesForRegularUsers && (len(service.CapAdd) > 0 || len(service.CapDrop) > 0) {
|
||||
return errors.New("container capabilities disabled for non administrator users")
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *portainer.Stack, userID portainer.UserID) *httperror.HandlerError {
|
||||
|
||||
@@ -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, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
@@ -114,30 +114,8 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit
|
||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err}
|
||||
}
|
||||
|
||||
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}
|
||||
}
|
||||
if !securityContext.IsAdmin {
|
||||
return &httperror.HandlerError{http.StatusUnauthorized, "Permission denied to delete the stack", httperrors.ErrUnauthorized}
|
||||
}
|
||||
|
||||
stack, err := handler.DataStore.Stack().StackByName(stackName)
|
||||
@@ -155,7 +133,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, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
@@ -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, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
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, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ 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"
|
||||
)
|
||||
@@ -44,14 +43,6 @@ 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}
|
||||
@@ -62,7 +53,7 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe
|
||||
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
||||
}
|
||||
|
||||
stacks = authorization.FilterAuthorizedStacks(stacks, user, userTeamIDs, rbacExtensionEnabled)
|
||||
stacks = authorization.FilterAuthorizedStacks(stacks, user, userTeamIDs)
|
||||
}
|
||||
|
||||
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, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
87
api/http/handler/stacks/stack_start.go
Normal file
87
api/http/handler/stacks/stack_start.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
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"
|
||||
"github.com/portainer/portainer/api"
|
||||
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
|
||||
}
|
||||
88
api/http/handler/stacks/stack_stop.go
Normal file
88
api/http/handler/stacks/stack_stop.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package stacks
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
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"
|
||||
"github.com/portainer/portainer/api"
|
||||
bolterrors "github.com/portainer/portainer/api/bolt/errors"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
@@ -72,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, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
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,7 +4,6 @@ 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"
|
||||
|
||||
@@ -14,8 +13,7 @@ import (
|
||||
// Handler is the HTTP handler used to handle team membership operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
AuthorizationService *authorization.Service
|
||||
DataStore portainer.DataStore
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage team membership operations.
|
||||
|
||||
@@ -72,10 +72,5 @@ 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,10 +40,5 @@ 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,7 +1,6 @@
|
||||
package teams
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
@@ -13,8 +12,7 @@ import (
|
||||
// Handler is the HTTP handler used to handle team operations.
|
||||
type Handler struct {
|
||||
*mux.Router
|
||||
DataStore portainer.DataStore
|
||||
AuthorizationService *authorization.Service
|
||||
DataStore portainer.DataStore
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage team operations.
|
||||
|
||||
@@ -34,10 +34,5 @@ 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,7 +9,6 @@ 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 {
|
||||
@@ -45,9 +44,8 @@ func (handler *Handler) adminInit(w http.ResponseWriter, r *http.Request) *httpe
|
||||
}
|
||||
|
||||
user := &portainer.User{
|
||||
Username: payload.Username,
|
||||
Role: portainer.AdministratorRole,
|
||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
||||
Username: payload.Username,
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
|
||||
user.Password, err = handler.CryptoService.Hash(payload.Password)
|
||||
|
||||
@@ -6,7 +6,6 @@ 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"
|
||||
|
||||
@@ -28,9 +27,8 @@ 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
|
||||
AuthorizationService *authorization.Service
|
||||
DataStore portainer.DataStore
|
||||
CryptoService portainer.CryptoService
|
||||
}
|
||||
|
||||
// NewHandler creates a handler to manage user operations.
|
||||
|
||||
@@ -12,7 +12,6 @@ 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 {
|
||||
@@ -62,9 +61,8 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http
|
||||
}
|
||||
|
||||
user = &portainer.User{
|
||||
Username: payload.Username,
|
||||
Role: portainer.UserRole(payload.Role),
|
||||
PortainerAuthorizations: authorization.DefaultPortainerAuthorizations(),
|
||||
Username: payload.Username,
|
||||
Role: portainer.UserRole(payload.Role),
|
||||
}
|
||||
|
||||
settings, err := handler.DataStore.Settings().Settings()
|
||||
|
||||
@@ -81,10 +81,5 @@ 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, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
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, true)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
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, false)
|
||||
err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint)
|
||||
if err != nil {
|
||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||
}
|
||||
|
||||
@@ -33,9 +33,12 @@ func (handler *Handler) proxyEdgeAgentWebsocketRequest(w http.ResponseWriter, r
|
||||
}
|
||||
|
||||
func (handler *Handler) proxyAgentWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error {
|
||||
// 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))
|
||||
endpointURL := params.endpoint.URL
|
||||
if params.endpoint.Type == portainer.AgentOnKubernetesEnvironment {
|
||||
endpointURL = fmt.Sprintf("http://%s", params.endpoint.URL)
|
||||
}
|
||||
|
||||
agentURL, err := url.Parse(endpointURL)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -155,11 +155,11 @@ func (transport *Transport) applyAccessControlOnResource(parameters *resourceOpe
|
||||
return err
|
||||
}
|
||||
|
||||
if resourceControl == nil && (executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess) {
|
||||
if resourceControl == nil && (executor.operationContext.isAdmin) {
|
||||
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
|
||||
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess || (resourceControl != nil && authorization.UserCanAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl)) {
|
||||
if executor.operationContext.isAdmin || (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 || executor.operationContext.endpointResourceAccess {
|
||||
if executor.operationContext.isAdmin {
|
||||
return transport.decorateResourceList(parameters, resourceData, executor.operationContext.resourceControls)
|
||||
}
|
||||
|
||||
@@ -241,13 +241,13 @@ func (transport *Transport) filterResourceList(parameters *resourceOperationPara
|
||||
}
|
||||
|
||||
if resourceControl == nil {
|
||||
if context.isAdmin || context.endpointResourceAccess {
|
||||
if context.isAdmin {
|
||||
filteredResourceData = append(filteredResourceData, resourceObject)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if context.isAdmin || context.endpointResourceAccess || authorization.UserCanAccessResource(context.userID, context.userTeamIDs, resourceControl) {
|
||||
if context.isAdmin || authorization.UserCanAccessResource(context.userID, context.userTeamIDs, resourceControl) {
|
||||
resourceObject = decorateObject(resourceObject, resourceControl)
|
||||
filteredResourceData = append(filteredResourceData, resourceObject)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
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"
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||
"github.com/portainer/portainer/api/http/security"
|
||||
"github.com/portainer/portainer/api/internal/authorization"
|
||||
)
|
||||
|
||||
@@ -148,3 +153,81 @@ 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 {
|
||||
settings, err := transport.dataStore.Settings().Settings()
|
||||
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 !settings.AllowPrivilegedModeForRegularUsers && partialContainer.HostConfig.Privileged {
|
||||
return forbiddenResponse, errors.New("forbidden to use privileged mode")
|
||||
}
|
||||
|
||||
if !settings.AllowHostNamespaceForRegularUsers && partialContainer.HostConfig.PidMode == "host" {
|
||||
return forbiddenResponse, errors.New("forbidden to use pid host namespace")
|
||||
}
|
||||
|
||||
if !settings.AllowDeviceMappingForRegularUsers && len(partialContainer.HostConfig.Devices) > 0 {
|
||||
return forbiddenResponse, errors.New("forbidden to use device mapping")
|
||||
}
|
||||
|
||||
if !settings.AllowContainerCapabilitiesForRegularUsers && (len(partialContainer.HostConfig.CapAdd) > 0 || len(partialContainer.HostConfig.CapDrop) > 0) {
|
||||
return nil, errors.New("forbidden to use container capabilities")
|
||||
}
|
||||
|
||||
if !settings.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,7 +1,11 @@
|
||||
package docker
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
@@ -85,3 +89,54 @@ 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 {
|
||||
settings, err := transport.dataStore.Settings().Settings()
|
||||
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 !settings.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)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
|
||||
"github.com/docker/docker/client"
|
||||
"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"
|
||||
@@ -44,11 +43,10 @@ type (
|
||||
}
|
||||
|
||||
restrictedDockerOperationContext struct {
|
||||
isAdmin bool
|
||||
endpointResourceAccess bool
|
||||
userID portainer.UserID
|
||||
userTeamIDs []portainer.TeamID
|
||||
resourceControls []portainer.ResourceControl
|
||||
isAdmin bool
|
||||
userID portainer.UserID
|
||||
userTeamIDs []portainer.TeamID
|
||||
resourceControls []portainer.ResourceControl
|
||||
}
|
||||
|
||||
operationExecutor struct {
|
||||
@@ -157,8 +155,14 @@ 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, volumeIDParameter[0], portainer.VolumeResourceControl, true)
|
||||
return transport.restrictedResourceOperation(r, resourceID, portainer.VolumeResourceControl, true)
|
||||
}
|
||||
|
||||
return transport.executeDockerRequest(r)
|
||||
@@ -189,7 +193,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.decorateGenericResourceCreationOperation(request, containerObjectIdentifier, portainer.ContainerResourceControl)
|
||||
return transport.decorateContainerCreationOperation(request, containerObjectIdentifier, portainer.ContainerResourceControl)
|
||||
|
||||
case "/containers/prune":
|
||||
return transport.administratorOperation(request)
|
||||
@@ -225,7 +229,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.replaceRegistryAuthenticationHeader(request)
|
||||
return transport.decorateServiceCreationOperation(request)
|
||||
|
||||
case "/services":
|
||||
return transport.rewriteOperation(request, transport.serviceListOperation)
|
||||
@@ -402,16 +406,6 @@ 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 {
|
||||
settings, err := transport.dataStore.Settings().Settings()
|
||||
if err != nil {
|
||||
@@ -419,28 +413,10 @@ func (transport *Transport) restrictedResourceOperation(request *http.Request, r
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
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
|
||||
@@ -673,25 +649,14 @@ func (transport *Transport) createOperationContext(request *http.Request) (*rest
|
||||
}
|
||||
|
||||
operationContext := &restrictedDockerOperationContext{
|
||||
isAdmin: true,
|
||||
userID: tokenData.ID,
|
||||
resourceControls: resourceControls,
|
||||
endpointResourceAccess: false,
|
||||
isAdmin: true,
|
||||
userID: tokenData.ID,
|
||||
resourceControls: resourceControls,
|
||||
}
|
||||
|
||||
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
|
||||
@@ -706,3 +671,12 @@ 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
|
||||
}
|
||||
|
||||
@@ -168,16 +168,30 @@ func (transport *Transport) restrictedVolumeOperation(requestPath string, reques
|
||||
return transport.rewriteOperation(request, transport.volumeInspectOperation)
|
||||
}
|
||||
|
||||
cli := transport.dockerClient
|
||||
volume, err := cli.VolumeInspect(context.Background(), path.Base(requestPath))
|
||||
agentTargetHeader := request.Header.Get(portainer.PortainerAgentTargetHeader)
|
||||
|
||||
resourceID, err := transport.getVolumeResourceID(agentTargetHeader, path.Base(requestPath))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
volumeID := volume.Name + volume.CreatedAt
|
||||
|
||||
if request.Method == http.MethodDelete {
|
||||
return transport.executeGenericResourceDeletionOperation(request, volumeID, portainer.VolumeResourceControl)
|
||||
return transport.executeGenericResourceDeletionOperation(request, resourceID, portainer.VolumeResourceControl)
|
||||
}
|
||||
return transport.restrictedResourceOperation(request, volumeID, portainer.VolumeResourceControl, false)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
@@ -16,14 +15,8 @@ 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 to Docker endpoints and extensions
|
||||
// ProxyFactory is a factory to create reverse proxies
|
||||
ProxyFactory struct {
|
||||
dataStore portainer.DataStore
|
||||
signatureService portainer.DigitalSignatureService
|
||||
@@ -46,25 +39,6 @@ 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)
|
||||
|
||||
@@ -45,7 +45,7 @@ func (manager *tokenManager) getAdminServiceAccountToken() string {
|
||||
return manager.adminToken
|
||||
}
|
||||
|
||||
func (manager *tokenManager) getUserServiceAccountToken(userID int, username string) (string, error) {
|
||||
func (manager *tokenManager) getUserServiceAccountToken(userID int) (string, error) {
|
||||
manager.mutex.Lock()
|
||||
defer manager.mutex.Unlock()
|
||||
|
||||
@@ -61,12 +61,12 @@ func (manager *tokenManager) getUserServiceAccountToken(userID int, username str
|
||||
teamIds = append(teamIds, int(membership.TeamID))
|
||||
}
|
||||
|
||||
err = manager.kubecli.SetupUserServiceAccount(userID, username, teamIds)
|
||||
err = manager.kubecli.SetupUserServiceAccount(userID, teamIds)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
serviceAccountToken, err := manager.kubecli.GetServiceAccountBearerToken(userID, username)
|
||||
serviceAccountToken, err := manager.kubecli.GetServiceAccountBearerToken(userID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func (transport *localTransport) RoundTrip(request *http.Request) (*http.Respons
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
token = transport.tokenManager.getAdminServiceAccountToken()
|
||||
} else {
|
||||
token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID), tokenData.Username)
|
||||
token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -94,7 +94,7 @@ func (transport *agentTransport) RoundTrip(request *http.Request) (*http.Respons
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
token = transport.tokenManager.getAdminServiceAccountToken()
|
||||
} else {
|
||||
token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID), tokenData.Username)
|
||||
token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -136,7 +136,7 @@ func (transport *edgeTransport) RoundTrip(request *http.Request) (*http.Response
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
token = transport.tokenManager.getAdminServiceAccountToken()
|
||||
} else {
|
||||
token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID), tokenData.Username)
|
||||
token, err = transport.tokenManager.getUserServiceAccountToken(int(tokenData.ID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer/api/http/proxy/factory/kubernetes"
|
||||
|
||||
@@ -21,7 +20,6 @@ type (
|
||||
Manager struct {
|
||||
proxyFactory *factory.ProxyFactory
|
||||
endpointProxies cmap.ConcurrentMap
|
||||
extensionProxies cmap.ConcurrentMap
|
||||
legacyExtensionProxies cmap.ConcurrentMap
|
||||
}
|
||||
)
|
||||
@@ -30,7 +28,6 @@ type (
|
||||
func NewManager(dataStore portainer.DataStore, signatureService portainer.DigitalSignatureService, tunnelService portainer.ReverseTunnelService, clientFactory *docker.ClientFactory, kubernetesClientFactory *cli.ClientFactory, kubernetesTokenCacheManager *kubernetes.TokenCacheManager) *Manager {
|
||||
return &Manager{
|
||||
endpointProxies: cmap.New(),
|
||||
extensionProxies: cmap.New(),
|
||||
legacyExtensionProxies: cmap.New(),
|
||||
proxyFactory: factory.NewProxyFactory(dataStore, signatureService, tunnelService, clientFactory, kubernetesClientFactory, kubernetesTokenCacheManager),
|
||||
}
|
||||
@@ -63,38 +60,6 @@ func (manager *Manager) DeleteEndpointProxy(endpoint *portainer.Endpoint) {
|
||||
manager.endpointProxies.Remove(string(endpoint.ID))
|
||||
}
|
||||
|
||||
// CreateExtensionProxy creates a new HTTP reverse proxy for an extension and
|
||||
// registers it in the extension map associated to the specified extension identifier
|
||||
func (manager *Manager) CreateExtensionProxy(extensionID portainer.ExtensionID) (http.Handler, error) {
|
||||
proxy, err := manager.proxyFactory.NewExtensionProxy(extensionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
manager.extensionProxies.Set(strconv.Itoa(int(extensionID)), proxy)
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
// GetExtensionProxy returns an extension proxy associated to an extension identifier
|
||||
func (manager *Manager) GetExtensionProxy(extensionID portainer.ExtensionID) http.Handler {
|
||||
proxy, ok := manager.extensionProxies.Get(strconv.Itoa(int(extensionID)))
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
return proxy.(http.Handler)
|
||||
}
|
||||
|
||||
// GetExtensionURL retrieves the URL of an extension running locally based on the extension port table
|
||||
func (manager *Manager) GetExtensionURL(extensionID portainer.ExtensionID) string {
|
||||
return factory.BuildExtensionURL(extensionID)
|
||||
}
|
||||
|
||||
// DeleteExtensionProxy deletes the extension proxy associated to an extension identifier
|
||||
func (manager *Manager) DeleteExtensionProxy(extensionID portainer.ExtensionID) {
|
||||
manager.extensionProxies.Remove(strconv.Itoa(int(extensionID)))
|
||||
}
|
||||
|
||||
// CreateLegacyExtensionProxy creates a new HTTP reverse proxy for a legacy extension and adds it to the registered proxies
|
||||
func (manager *Manager) CreateLegacyExtensionProxy(key, extensionAPIURL string) (http.Handler, error) {
|
||||
proxy, err := manager.proxyFactory.NewLegacyExtensionProxy(extensionAPIURL)
|
||||
|
||||
@@ -14,9 +14,8 @@ import (
|
||||
type (
|
||||
// RequestBouncer represents an entity that manages API request accesses
|
||||
RequestBouncer struct {
|
||||
dataStore portainer.DataStore
|
||||
jwtService portainer.JWTService
|
||||
rbacExtensionClient *rbacExtensionClient
|
||||
dataStore portainer.DataStore
|
||||
jwtService portainer.JWTService
|
||||
}
|
||||
|
||||
// RestrictedRequestContext is a data structure containing information
|
||||
@@ -30,11 +29,10 @@ type (
|
||||
)
|
||||
|
||||
// NewRequestBouncer initializes a new RequestBouncer
|
||||
func NewRequestBouncer(dataStore portainer.DataStore, jwtService portainer.JWTService, rbacExtensionURL string) *RequestBouncer {
|
||||
func NewRequestBouncer(dataStore portainer.DataStore, jwtService portainer.JWTService) *RequestBouncer {
|
||||
return &RequestBouncer{
|
||||
dataStore: dataStore,
|
||||
jwtService: jwtService,
|
||||
rbacExtensionClient: newRBACExtensionClient(rbacExtensionURL),
|
||||
dataStore: dataStore,
|
||||
jwtService: jwtService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,8 +45,7 @@ func (bouncer *RequestBouncer) PublicAccess(h http.Handler) http.Handler {
|
||||
|
||||
// AdminAccess defines a security check for API endpoints that require an authorization check.
|
||||
// Authentication is required to access these endpoints.
|
||||
// If the RBAC extension is enabled, authorizations are required to use these endpoints.
|
||||
// If the RBAC extension is not enabled, the administrator role is required to use these endpoints.
|
||||
// The administrator role is required to use these endpoints.
|
||||
// The request context will be enhanced with a RestrictedRequestContext object
|
||||
// that might be used later to inside the API operation for extra authorization validation
|
||||
// and resource filtering.
|
||||
@@ -61,8 +58,6 @@ func (bouncer *RequestBouncer) AdminAccess(h http.Handler) http.Handler {
|
||||
|
||||
// RestrictedAccess defines a security check for restricted API endpoints.
|
||||
// Authentication is required to access these endpoints.
|
||||
// If the RBAC extension is enabled, authorizations are required to use these endpoints.
|
||||
// If the RBAC extension is not enabled, access is granted to any authenticated user.
|
||||
// The request context will be enhanced with a RestrictedRequestContext object
|
||||
// that might be used later to inside the API operation for extra authorization validation
|
||||
// and resource filtering.
|
||||
@@ -86,11 +81,9 @@ func (bouncer *RequestBouncer) AuthenticatedAccess(h http.Handler) http.Handler
|
||||
|
||||
// AuthorizedEndpointOperation retrieves the JWT token from the request context and verifies
|
||||
// that the user can access the specified endpoint.
|
||||
// If the RBAC extension is enabled and the authorizationCheck flag is set,
|
||||
// it will also validate that the user can execute the specified operation.
|
||||
// An error is returned when access to the endpoint is denied or if the user do not have the required
|
||||
// authorization to execute the operation.
|
||||
func (bouncer *RequestBouncer) AuthorizedEndpointOperation(r *http.Request, endpoint *portainer.Endpoint, authorizationCheck bool) error {
|
||||
func (bouncer *RequestBouncer) AuthorizedEndpointOperation(r *http.Request, endpoint *portainer.Endpoint) error {
|
||||
tokenData, err := RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -114,13 +107,6 @@ func (bouncer *RequestBouncer) AuthorizedEndpointOperation(r *http.Request, endp
|
||||
return httperrors.ErrEndpointAccessDenied
|
||||
}
|
||||
|
||||
if authorizationCheck {
|
||||
err = bouncer.checkEndpointOperationAuthorization(r, endpoint)
|
||||
if err != nil {
|
||||
return ErrAuthorizationRequired
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -142,38 +128,6 @@ func (bouncer *RequestBouncer) AuthorizedEdgeEndpointOperation(r *http.Request,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (bouncer *RequestBouncer) checkEndpointOperationAuthorization(r *http.Request, endpoint *portainer.Endpoint) error {
|
||||
tokenData, err := RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
return nil
|
||||
}
|
||||
|
||||
extension, err := bouncer.dataStore.Extension().Extension(portainer.RBACExtension)
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
return nil
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := bouncer.dataStore.User().User(tokenData.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
apiOperation := &portainer.APIOperationAuthorizationRequest{
|
||||
Path: r.URL.String(),
|
||||
Method: r.Method,
|
||||
Authorizations: user.EndpointAuthorizations[endpoint.ID],
|
||||
}
|
||||
|
||||
bouncer.rbacExtensionClient.setLicenseKey(extension.License.LicenseKey)
|
||||
return bouncer.rbacExtensionClient.checkAuthorization(apiOperation)
|
||||
}
|
||||
|
||||
// RegistryAccess retrieves the JWT token from the request context and verifies
|
||||
// that the user can access the specified registry.
|
||||
// An error is returned when access is denied.
|
||||
@@ -206,9 +160,8 @@ func (bouncer *RequestBouncer) mwAuthenticatedUser(h http.Handler) http.Handler
|
||||
}
|
||||
|
||||
// mwCheckPortainerAuthorizations will verify that the user has the required authorization to access
|
||||
// a specific API endpoint. It will leverage the RBAC extension authorization validation if the extension
|
||||
// is enabled.
|
||||
// If the administratorOnly flag is specified and the RBAC extension is not enabled, this will prevent non-admin
|
||||
// a specific API endpoint.
|
||||
// If the administratorOnly flag is specified, this will prevent non-admin
|
||||
// users from accessing the endpoint.
|
||||
func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler, administratorOnly bool) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -223,21 +176,12 @@ func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler,
|
||||
return
|
||||
}
|
||||
|
||||
extension, err := bouncer.dataStore.Extension().Extension(portainer.RBACExtension)
|
||||
if err == bolterrors.ErrObjectNotFound {
|
||||
if administratorOnly {
|
||||
httperror.WriteError(w, http.StatusForbidden, "Access denied", httperrors.ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteError(w, http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err)
|
||||
if administratorOnly {
|
||||
httperror.WriteError(w, http.StatusForbidden, "Access denied", httperrors.ErrUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := bouncer.dataStore.User().User(tokenData.ID)
|
||||
_, err = bouncer.dataStore.User().User(tokenData.ID)
|
||||
if err != nil && err == bolterrors.ErrObjectNotFound {
|
||||
httperror.WriteError(w, http.StatusUnauthorized, "Unauthorized", httperrors.ErrUnauthorized)
|
||||
return
|
||||
@@ -246,19 +190,6 @@ func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler,
|
||||
return
|
||||
}
|
||||
|
||||
apiOperation := &portainer.APIOperationAuthorizationRequest{
|
||||
Path: r.URL.String(),
|
||||
Method: r.Method,
|
||||
Authorizations: user.PortainerAuthorizations,
|
||||
}
|
||||
|
||||
bouncer.rbacExtensionClient.setLicenseKey(extension.License.LicenseKey)
|
||||
err = bouncer.rbacExtensionClient.checkAuthorization(apiOperation)
|
||||
if err != nil {
|
||||
httperror.WriteError(w, http.StatusForbidden, "Access denied", ErrAuthorizationRequired)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultHTTPTimeout = 5
|
||||
)
|
||||
|
||||
type rbacExtensionClient struct {
|
||||
httpClient *http.Client
|
||||
extensionURL string
|
||||
licenseKey string
|
||||
}
|
||||
|
||||
func newRBACExtensionClient(extensionURL string) *rbacExtensionClient {
|
||||
return &rbacExtensionClient{
|
||||
extensionURL: extensionURL,
|
||||
httpClient: &http.Client{
|
||||
Timeout: time.Second * time.Duration(defaultHTTPTimeout),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (client *rbacExtensionClient) setLicenseKey(licenseKey string) {
|
||||
client.licenseKey = licenseKey
|
||||
}
|
||||
|
||||
func (client *rbacExtensionClient) checkAuthorization(authRequest *portainer.APIOperationAuthorizationRequest) error {
|
||||
encodedAuthRequest, err := json.Marshal(authRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", client.extensionURL+"/authorized_operation", nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req.Header.Set("X-RBAC-AuthorizationRequest", string(encodedAuthRequest))
|
||||
req.Header.Set("X-PortainerExtension-License", client.licenseKey)
|
||||
|
||||
resp, err := client.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusNoContent {
|
||||
return ErrAuthorizationRequired
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
portainer "github.com/portainer/portainer/api"
|
||||
"github.com/portainer/portainer/api/crypto"
|
||||
"github.com/portainer/portainer/api/docker"
|
||||
"github.com/portainer/portainer/api/http/handler"
|
||||
"github.com/portainer/portainer/api/http/handler/auth"
|
||||
@@ -19,7 +20,6 @@ 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"
|
||||
@@ -28,7 +28,6 @@ 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"
|
||||
@@ -40,7 +39,6 @@ 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"
|
||||
"github.com/portainer/portainer/api/kubernetes/cli"
|
||||
)
|
||||
|
||||
@@ -50,7 +48,6 @@ type Server struct {
|
||||
AssetsPath string
|
||||
Status *portainer.Status
|
||||
ReverseTunnelService portainer.ReverseTunnelService
|
||||
ExtensionManager portainer.ExtensionManager
|
||||
ComposeStackManager portainer.ComposeStackManager
|
||||
CryptoService portainer.CryptoService
|
||||
SignatureService portainer.DigitalSignatureService
|
||||
@@ -60,6 +57,7 @@ type Server struct {
|
||||
GitService portainer.GitService
|
||||
JWTService portainer.JWTService
|
||||
LDAPService portainer.LDAPService
|
||||
OAuthService portainer.OAuthService
|
||||
SwarmStackManager portainer.SwarmStackManager
|
||||
Handler *handler.Handler
|
||||
SSL bool
|
||||
@@ -72,12 +70,10 @@ type Server struct {
|
||||
|
||||
// Start starts the HTTP server
|
||||
func (server *Server) Start() error {
|
||||
authorizationService := authorization.NewService(server.DataStore)
|
||||
kubernetesTokenCacheManager := kubernetes.NewTokenCacheManager()
|
||||
proxyManager := proxy.NewManager(server.DataStore, server.SignatureService, server.ReverseTunnelService, server.DockerClientFactory, server.KubernetesClientFactory, kubernetesTokenCacheManager)
|
||||
|
||||
rbacExtensionURL := proxyManager.GetExtensionURL(portainer.RBACExtension)
|
||||
requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService, rbacExtensionURL)
|
||||
requestBouncer := security.NewRequestBouncer(server.DataStore, server.JWTService)
|
||||
|
||||
rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour)
|
||||
|
||||
@@ -87,8 +83,8 @@ func (server *Server) Start() error {
|
||||
authHandler.JWTService = server.JWTService
|
||||
authHandler.LDAPService = server.LDAPService
|
||||
authHandler.ProxyManager = proxyManager
|
||||
authHandler.AuthorizationService = authorizationService
|
||||
authHandler.KubernetesTokenCacheManager = kubernetesTokenCacheManager
|
||||
authHandler.OAuthService = server.OAuthService
|
||||
|
||||
var roleHandler = roles.NewHandler(requestBouncer)
|
||||
roleHandler.DataStore = server.DataStore
|
||||
@@ -119,7 +115,6 @@ func (server *Server) Start() error {
|
||||
|
||||
var endpointHandler = endpoints.NewHandler(requestBouncer)
|
||||
endpointHandler.DataStore = server.DataStore
|
||||
endpointHandler.AuthorizationService = authorizationService
|
||||
endpointHandler.FileService = server.FileService
|
||||
endpointHandler.ProxyManager = proxyManager
|
||||
endpointHandler.SnapshotService = server.SnapshotService
|
||||
@@ -133,7 +128,6 @@ func (server *Server) Start() error {
|
||||
|
||||
var endpointGroupHandler = endpointgroups.NewHandler(requestBouncer)
|
||||
endpointGroupHandler.DataStore = server.DataStore
|
||||
endpointGroupHandler.AuthorizationService = authorizationService
|
||||
|
||||
var endpointProxyHandler = endpointproxy.NewHandler(requestBouncer)
|
||||
endpointProxyHandler.DataStore = server.DataStore
|
||||
@@ -144,11 +138,6 @@ func (server *Server) Start() error {
|
||||
|
||||
var motdHandler = motd.NewHandler(requestBouncer)
|
||||
|
||||
var extensionHandler = extensions.NewHandler(requestBouncer)
|
||||
extensionHandler.DataStore = server.DataStore
|
||||
extensionHandler.ExtensionManager = server.ExtensionManager
|
||||
extensionHandler.AuthorizationService = authorizationService
|
||||
|
||||
var registryHandler = registries.NewHandler(requestBouncer)
|
||||
registryHandler.DataStore = server.DataStore
|
||||
registryHandler.FileService = server.FileService
|
||||
@@ -158,7 +147,6 @@ func (server *Server) Start() error {
|
||||
resourceControlHandler.DataStore = server.DataStore
|
||||
|
||||
var settingsHandler = settings.NewHandler(requestBouncer)
|
||||
settingsHandler.AuthorizationService = authorizationService
|
||||
settingsHandler.DataStore = server.DataStore
|
||||
settingsHandler.FileService = server.FileService
|
||||
settingsHandler.JWTService = server.JWTService
|
||||
@@ -178,16 +166,12 @@ func (server *Server) Start() error {
|
||||
|
||||
var teamHandler = teams.NewHandler(requestBouncer)
|
||||
teamHandler.DataStore = server.DataStore
|
||||
teamHandler.AuthorizationService = authorizationService
|
||||
|
||||
var teamMembershipHandler = teammemberships.NewHandler(requestBouncer)
|
||||
teamMembershipHandler.DataStore = server.DataStore
|
||||
teamMembershipHandler.AuthorizationService = authorizationService
|
||||
|
||||
var statusHandler = status.NewHandler(requestBouncer, server.Status)
|
||||
|
||||
var supportHandler = support.NewHandler(requestBouncer)
|
||||
|
||||
var templatesHandler = templates.NewHandler(requestBouncer)
|
||||
templatesHandler.DataStore = server.DataStore
|
||||
templatesHandler.FileService = server.FileService
|
||||
@@ -199,7 +183,6 @@ func (server *Server) Start() error {
|
||||
var userHandler = users.NewHandler(requestBouncer, rateLimiter)
|
||||
userHandler.DataStore = server.DataStore
|
||||
userHandler.CryptoService = server.CryptoService
|
||||
userHandler.AuthorizationService = authorizationService
|
||||
|
||||
var websocketHandler = websocket.NewHandler(requestBouncer)
|
||||
websocketHandler.DataStore = server.DataStore
|
||||
@@ -226,13 +209,11 @@ func (server *Server) Start() error {
|
||||
EndpointProxyHandler: endpointProxyHandler,
|
||||
FileHandler: fileHandler,
|
||||
MOTDHandler: motdHandler,
|
||||
ExtensionHandler: extensionHandler,
|
||||
RegistryHandler: registryHandler,
|
||||
ResourceControlHandler: resourceControlHandler,
|
||||
SettingsHandler: settingsHandler,
|
||||
StatusHandler: statusHandler,
|
||||
StackHandler: stackHandler,
|
||||
SupportHandler: supportHandler,
|
||||
TagHandler: tagHandler,
|
||||
TeamHandler: teamHandler,
|
||||
TeamMembershipHandler: teamMembershipHandler,
|
||||
@@ -243,8 +224,14 @@ func (server *Server) Start() error {
|
||||
WebhookHandler: webhookHandler,
|
||||
}
|
||||
|
||||
if server.SSL {
|
||||
return http.ListenAndServeTLS(server.BindAddress, server.SSLCert, server.SSLKey, server.Handler)
|
||||
httpServer := &http.Server{
|
||||
Addr: server.BindAddress,
|
||||
Handler: server.Handler,
|
||||
}
|
||||
return http.ListenAndServe(server.BindAddress, server.Handler)
|
||||
|
||||
if server.SSL {
|
||||
httpServer.TLSConfig = crypto.CreateServerTLSConfiguration()
|
||||
return httpServer.ListenAndServeTLS(server.SSLCert, server.SSLKey)
|
||||
}
|
||||
return httpServer.ListenAndServe()
|
||||
}
|
||||
|
||||
@@ -119,16 +119,10 @@ func DecorateCustomTemplates(templates []portainer.CustomTemplate, resourceContr
|
||||
}
|
||||
|
||||
// FilterAuthorizedStacks returns a list of decorated stacks filtered through resource control access checks.
|
||||
func FilterAuthorizedStacks(stacks []portainer.Stack, user *portainer.User, userTeamIDs []portainer.TeamID, rbacEnabled bool) []portainer.Stack {
|
||||
func FilterAuthorizedStacks(stacks []portainer.Stack, user *portainer.User, userTeamIDs []portainer.TeamID) []portainer.Stack {
|
||||
authorizedStacks := make([]portainer.Stack, 0)
|
||||
|
||||
for _, stack := range stacks {
|
||||
_, ok := user.EndpointAuthorizations[stack.EndpointID][portainer.EndpointResourcesAccess]
|
||||
if rbacEnabled && ok {
|
||||
authorizedStacks = append(authorizedStacks, stack)
|
||||
continue
|
||||
}
|
||||
|
||||
if stack.ResourceControl != nil && UserCanAccessResource(user.ID, userTeamIDs, stack.ResourceControl) {
|
||||
authorizedStacks = append(authorizedStacks, stack)
|
||||
}
|
||||
|
||||
@@ -412,7 +412,6 @@ func DefaultPortainerAuthorizations() portainer.Authorizations {
|
||||
portainer.OperationPortainerEndpointInspect: true,
|
||||
portainer.OperationPortainerEndpointExtensionAdd: true,
|
||||
portainer.OperationPortainerEndpointExtensionRemove: true,
|
||||
portainer.OperationPortainerExtensionList: true,
|
||||
portainer.OperationPortainerMOTD: true,
|
||||
portainer.OperationPortainerRegistryList: true,
|
||||
portainer.OperationPortainerRegistryInspect: true,
|
||||
@@ -425,182 +424,6 @@ func DefaultPortainerAuthorizations() portainer.Authorizations {
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateVolumeBrowsingAuthorizations will update all the volume browsing authorizations for each role (except endpoint administrator)
|
||||
// based on the specified removeAuthorizations parameter. If removeAuthorizations is set to true, all
|
||||
// the authorizations will be dropped for the each role. If removeAuthorizations is set to false, the authorizations
|
||||
// will be reset based for each role.
|
||||
func (service Service) UpdateVolumeBrowsingAuthorizations(remove bool) error {
|
||||
roles, err := service.dataStore.Role().Roles()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, role := range roles {
|
||||
// all roles except endpoint administrator
|
||||
if role.ID != portainer.RoleID(1) {
|
||||
updateRoleVolumeBrowsingAuthorizations(&role, remove)
|
||||
|
||||
err := service.dataStore.Role().UpdateRole(role.ID, &role)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateRoleVolumeBrowsingAuthorizations(role *portainer.Role, removeAuthorizations bool) {
|
||||
if !removeAuthorizations {
|
||||
delete(role.Authorizations, portainer.OperationDockerAgentBrowseDelete)
|
||||
delete(role.Authorizations, portainer.OperationDockerAgentBrowseGet)
|
||||
delete(role.Authorizations, portainer.OperationDockerAgentBrowseList)
|
||||
delete(role.Authorizations, portainer.OperationDockerAgentBrowsePut)
|
||||
delete(role.Authorizations, portainer.OperationDockerAgentBrowseRename)
|
||||
return
|
||||
}
|
||||
|
||||
role.Authorizations[portainer.OperationDockerAgentBrowseGet] = true
|
||||
role.Authorizations[portainer.OperationDockerAgentBrowseList] = true
|
||||
|
||||
// Standard-user
|
||||
if role.ID == portainer.RoleID(3) {
|
||||
role.Authorizations[portainer.OperationDockerAgentBrowseDelete] = true
|
||||
role.Authorizations[portainer.OperationDockerAgentBrowsePut] = true
|
||||
role.Authorizations[portainer.OperationDockerAgentBrowseRename] = true
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveTeamAccessPolicies will remove all existing access policies associated to the specified team
|
||||
func (service *Service) RemoveTeamAccessPolicies(teamID portainer.TeamID) error {
|
||||
endpoints, err := service.dataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
for policyTeamID := range endpoint.TeamAccessPolicies {
|
||||
if policyTeamID == teamID {
|
||||
delete(endpoint.TeamAccessPolicies, policyTeamID)
|
||||
|
||||
err := service.dataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endpointGroups, err := service.dataStore.EndpointGroup().EndpointGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpointGroup := range endpointGroups {
|
||||
for policyTeamID := range endpointGroup.TeamAccessPolicies {
|
||||
if policyTeamID == teamID {
|
||||
delete(endpointGroup.TeamAccessPolicies, policyTeamID)
|
||||
|
||||
err := service.dataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registries, err := service.dataStore.Registry().Registries()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, registry := range registries {
|
||||
for policyTeamID := range registry.TeamAccessPolicies {
|
||||
if policyTeamID == teamID {
|
||||
delete(registry.TeamAccessPolicies, policyTeamID)
|
||||
|
||||
err := service.dataStore.Registry().UpdateRegistry(registry.ID, ®istry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return service.UpdateUsersAuthorizations()
|
||||
}
|
||||
|
||||
// RemoveUserAccessPolicies will remove all existing access policies associated to the specified user
|
||||
func (service *Service) RemoveUserAccessPolicies(userID portainer.UserID) error {
|
||||
endpoints, err := service.dataStore.Endpoint().Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
for policyUserID := range endpoint.UserAccessPolicies {
|
||||
if policyUserID == userID {
|
||||
delete(endpoint.UserAccessPolicies, policyUserID)
|
||||
|
||||
err := service.dataStore.Endpoint().UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endpointGroups, err := service.dataStore.EndpointGroup().EndpointGroups()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpointGroup := range endpointGroups {
|
||||
for policyUserID := range endpointGroup.UserAccessPolicies {
|
||||
if policyUserID == userID {
|
||||
delete(endpointGroup.UserAccessPolicies, policyUserID)
|
||||
|
||||
err := service.dataStore.EndpointGroup().UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registries, err := service.dataStore.Registry().Registries()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, registry := range registries {
|
||||
for policyUserID := range registry.UserAccessPolicies {
|
||||
if policyUserID == userID {
|
||||
delete(registry.UserAccessPolicies, policyUserID)
|
||||
|
||||
err := service.dataStore.Registry().UpdateRegistry(registry.ID, ®istry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateUsersAuthorizations will trigger an update of the authorizations for all the users.
|
||||
func (service *Service) UpdateUsersAuthorizations() error {
|
||||
users, err := service.dataStore.User().Users()
|
||||
|
||||
@@ -3,8 +3,10 @@ package portainer
|
||||
func KubernetesDefault() KubernetesData {
|
||||
return KubernetesData{
|
||||
Configuration: KubernetesConfiguration{
|
||||
UseLoadBalancer: false,
|
||||
UseLoadBalancer: false,
|
||||
UseServerMetrics: false,
|
||||
StorageClasses: []KubernetesStorageClassConfig{},
|
||||
IngressClasses: []KubernetesIngressClassConfig{},
|
||||
},
|
||||
Snapshots: []KubernetesSnapshot{},
|
||||
}
|
||||
|
||||
@@ -19,20 +19,23 @@ type (
|
||||
ClientFactory struct {
|
||||
reverseTunnelService portainer.ReverseTunnelService
|
||||
signatureService portainer.DigitalSignatureService
|
||||
instanceID string
|
||||
endpointClients cmap.ConcurrentMap
|
||||
}
|
||||
|
||||
// KubeClient represent a service used to execute Kubernetes operations
|
||||
KubeClient struct {
|
||||
cli *kubernetes.Clientset
|
||||
cli *kubernetes.Clientset
|
||||
instanceID string
|
||||
}
|
||||
)
|
||||
|
||||
// NewClientFactory returns a new instance of a ClientFactory
|
||||
func NewClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService) *ClientFactory {
|
||||
func NewClientFactory(signatureService portainer.DigitalSignatureService, reverseTunnelService portainer.ReverseTunnelService, instanceID string) *ClientFactory {
|
||||
return &ClientFactory{
|
||||
signatureService: signatureService,
|
||||
reverseTunnelService: reverseTunnelService,
|
||||
instanceID: instanceID,
|
||||
endpointClients: cmap.New(),
|
||||
}
|
||||
}
|
||||
@@ -62,7 +65,8 @@ func (factory *ClientFactory) createKubeClient(endpoint *portainer.Endpoint) (po
|
||||
}
|
||||
|
||||
kubecli := &KubeClient{
|
||||
cli: cli,
|
||||
cli: cli,
|
||||
instanceID: factory.instanceID,
|
||||
}
|
||||
|
||||
return kubecli, nil
|
||||
|
||||
@@ -13,14 +13,14 @@ const (
|
||||
portainerConfigMapAccessPoliciesKey = "NamespaceAccessPolicies"
|
||||
)
|
||||
|
||||
func userServiceAccountName(userID int, username string) string {
|
||||
return fmt.Sprintf("%s-%d-%s", portainerUserServiceAccountPrefix, userID, username)
|
||||
func userServiceAccountName(userID int, instanceID string) string {
|
||||
return fmt.Sprintf("%s-%s-%d", portainerUserServiceAccountPrefix, instanceID, userID)
|
||||
}
|
||||
|
||||
func userServiceAccountTokenSecretName(serviceAccountName string) string {
|
||||
return fmt.Sprintf("%s-secret", serviceAccountName)
|
||||
func userServiceAccountTokenSecretName(serviceAccountName string, instanceID string) string {
|
||||
return fmt.Sprintf("%s-%s-secret", instanceID, serviceAccountName)
|
||||
}
|
||||
|
||||
func namespaceClusterRoleBindingName(namespace string) string {
|
||||
return fmt.Sprintf("%s-%s", portainerRBPrefix, namespace)
|
||||
func namespaceClusterRoleBindingName(namespace string, instanceID string) string {
|
||||
return fmt.Sprintf("%s-%s-%s", portainerRBPrefix, instanceID, namespace)
|
||||
}
|
||||
|
||||
@@ -13,6 +13,16 @@ func getPortainerUserDefaultPolicies() []rbacv1.PolicyRule {
|
||||
Resources: []string{"namespaces", "nodes"},
|
||||
APIGroups: []string{""},
|
||||
},
|
||||
{
|
||||
Verbs: []string{"list"},
|
||||
Resources: []string{"storageclasses"},
|
||||
APIGroups: []string{"storage.k8s.io"},
|
||||
},
|
||||
{
|
||||
Verbs: []string{"list"},
|
||||
Resources: []string{"ingresses"},
|
||||
APIGroups: []string{"networking.k8s.io"},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
)
|
||||
|
||||
func (kcl *KubeClient) createServiceAccountToken(serviceAccountName string) error {
|
||||
serviceAccountSecretName := userServiceAccountTokenSecretName(serviceAccountName)
|
||||
serviceAccountSecretName := userServiceAccountTokenSecretName(serviceAccountName, kcl.instanceID)
|
||||
|
||||
serviceAccountSecret := &v1.Secret{
|
||||
TypeMeta: metav1.TypeMeta{},
|
||||
@@ -33,7 +33,7 @@ func (kcl *KubeClient) createServiceAccountToken(serviceAccountName string) erro
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) getServiceAccountToken(serviceAccountName string) (string, error) {
|
||||
serviceAccountSecretName := userServiceAccountTokenSecretName(serviceAccountName)
|
||||
serviceAccountSecretName := userServiceAccountTokenSecretName(serviceAccountName, kcl.instanceID)
|
||||
|
||||
secret, err := kcl.cli.CoreV1().Secrets(portainerNamespace).Get(serviceAccountSecretName, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
|
||||
@@ -8,8 +8,8 @@ import (
|
||||
)
|
||||
|
||||
// GetServiceAccountBearerToken returns the ServiceAccountToken associated to the specified user.
|
||||
func (kcl *KubeClient) GetServiceAccountBearerToken(userID int, username string) (string, error) {
|
||||
serviceAccountName := userServiceAccountName(userID, username)
|
||||
func (kcl *KubeClient) GetServiceAccountBearerToken(userID int) (string, error) {
|
||||
serviceAccountName := userServiceAccountName(userID, kcl.instanceID)
|
||||
|
||||
return kcl.getServiceAccountToken(serviceAccountName)
|
||||
}
|
||||
@@ -17,8 +17,8 @@ func (kcl *KubeClient) GetServiceAccountBearerToken(userID int, username string)
|
||||
// SetupUserServiceAccount will make sure that all the required resources are created inside the Kubernetes
|
||||
// cluster before creating a ServiceAccount and a ServiceAccountToken for the specified Portainer user.
|
||||
//It will also create required default RoleBinding and ClusterRoleBinding rules.
|
||||
func (kcl *KubeClient) SetupUserServiceAccount(userID int, username string, teamIDs []int) error {
|
||||
serviceAccountName := userServiceAccountName(userID, username)
|
||||
func (kcl *KubeClient) SetupUserServiceAccount(userID int, teamIDs []int) error {
|
||||
serviceAccountName := userServiceAccountName(userID, kcl.instanceID)
|
||||
|
||||
err := kcl.ensureRequiredResourcesExist()
|
||||
if err != nil {
|
||||
@@ -114,7 +114,7 @@ func (kcl *KubeClient) ensureServiceAccountHasPortainerUserClusterRole(serviceAc
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) removeNamespaceAccessForServiceAccount(serviceAccountName, namespace string) error {
|
||||
roleBindingName := namespaceClusterRoleBindingName(namespace)
|
||||
roleBindingName := namespaceClusterRoleBindingName(namespace, kcl.instanceID)
|
||||
|
||||
roleBinding, err := kcl.cli.RbacV1().RoleBindings(namespace).Get(roleBindingName, metav1.GetOptions{})
|
||||
if k8serrors.IsNotFound(err) {
|
||||
@@ -138,7 +138,7 @@ func (kcl *KubeClient) removeNamespaceAccessForServiceAccount(serviceAccountName
|
||||
}
|
||||
|
||||
func (kcl *KubeClient) ensureNamespaceAccessForServiceAccount(serviceAccountName, namespace string) error {
|
||||
roleBindingName := namespaceClusterRoleBindingName(namespace)
|
||||
roleBindingName := namespaceClusterRoleBindingName(namespace, kcl.instanceID)
|
||||
|
||||
roleBinding, err := kcl.cli.RbacV1().RoleBindings(namespace).Get(roleBindingName, metav1.GetOptions{})
|
||||
if k8serrors.IsNotFound(err) {
|
||||
|
||||
137
api/oauth/oauth.go
Normal file
137
api/oauth/oauth.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"golang.org/x/oauth2"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/portainer/portainer/api"
|
||||
)
|
||||
|
||||
// Service represents a service used to authenticate users against an authorization server
|
||||
type Service struct{}
|
||||
|
||||
// NewService returns a pointer to a new instance of this service
|
||||
func NewService() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
|
||||
// Authenticate takes an access code and exchanges it for an access token from portainer OAuthSettings token endpoint.
|
||||
// On success, it will then return the username associated to authenticated user by fetching this information
|
||||
// from the resource server and matching it with the user identifier setting.
|
||||
func (*Service) Authenticate(code string, configuration *portainer.OAuthSettings) (string, error) {
|
||||
token, err := getAccessToken(code, configuration)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] - Failed retrieving access token: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
return getUsername(token, configuration)
|
||||
}
|
||||
|
||||
func getAccessToken(code string, configuration *portainer.OAuthSettings) (string, error) {
|
||||
unescapedCode, err := url.QueryUnescape(code)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
config := buildConfig(configuration)
|
||||
token, err := config.Exchange(context.Background(), unescapedCode)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return token.AccessToken, nil
|
||||
}
|
||||
|
||||
func getUsername(token string, configuration *portainer.OAuthSettings) (string, error) {
|
||||
req, err := http.NewRequest("GET", configuration.ResourceURI, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
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
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", &oauth2.RetrieveError{
|
||||
Response: resp,
|
||||
Body: body,
|
||||
}
|
||||
}
|
||||
|
||||
content, _, err := mime.ParseMediaType(resp.Header.Get("Content-Type"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if content == "application/x-www-form-urlencoded" || content == "text/plain" {
|
||||
values, err := url.ParseQuery(string(body))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
username := values.Get(configuration.UserIdentifier)
|
||||
if username == "" {
|
||||
return username, &oauth2.RetrieveError{
|
||||
Response: resp,
|
||||
Body: body,
|
||||
}
|
||||
}
|
||||
|
||||
return username, nil
|
||||
}
|
||||
|
||||
var datamap map[string]interface{}
|
||||
if err = json.Unmarshal(body, &datamap); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
username, ok := datamap[configuration.UserIdentifier].(string)
|
||||
if ok && username != "" {
|
||||
return username, nil
|
||||
}
|
||||
|
||||
if !ok {
|
||||
username, ok := datamap[configuration.UserIdentifier].(float64)
|
||||
if ok && username != 0 {
|
||||
return fmt.Sprint(int(username)), nil
|
||||
}
|
||||
}
|
||||
|
||||
return "", &oauth2.RetrieveError{
|
||||
Response: resp,
|
||||
Body: body,
|
||||
}
|
||||
}
|
||||
|
||||
func buildConfig(configuration *portainer.OAuthSettings) *oauth2.Config {
|
||||
endpoint := oauth2.Endpoint{
|
||||
AuthURL: configuration.AuthorizationURI,
|
||||
TokenURL: configuration.AccessTokenURI,
|
||||
}
|
||||
|
||||
return &oauth2.Config{
|
||||
ClientID: configuration.ClientID,
|
||||
ClientSecret: configuration.ClientSecret,
|
||||
Endpoint: endpoint,
|
||||
RedirectURL: configuration.RedirectURI,
|
||||
Scopes: []string{configuration.Scopes},
|
||||
}
|
||||
}
|
||||
150
api/portainer.go
150
api/portainer.go
@@ -11,12 +11,8 @@ type (
|
||||
RoleID RoleID `json:"RoleId"`
|
||||
}
|
||||
|
||||
// APIOperationAuthorizationRequest represent an request for the authorization to execute an API operation
|
||||
APIOperationAuthorizationRequest struct {
|
||||
Path string
|
||||
Method string
|
||||
Authorizations Authorizations
|
||||
}
|
||||
// AgentPlatform represents a platform type for an Agent
|
||||
AgentPlatform int
|
||||
|
||||
// AuthenticationMethod represents the authentication method used to authenticate a user
|
||||
AuthenticationMethod int
|
||||
@@ -284,7 +280,7 @@ type (
|
||||
EdgeStacks map[EdgeStackID]bool
|
||||
}
|
||||
|
||||
// Extension represents a Portainer extension
|
||||
// Extension represents a deprecated Portainer extension
|
||||
Extension struct {
|
||||
ID ExtensionID `json:"Id"`
|
||||
Enabled bool `json:"Enabled"`
|
||||
@@ -334,15 +330,24 @@ type (
|
||||
|
||||
// KubernetesConfiguration represents the configuration of a Kubernetes endpoint
|
||||
KubernetesConfiguration struct {
|
||||
UseLoadBalancer bool `json:"UseLoadBalancer"`
|
||||
StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"`
|
||||
UseLoadBalancer bool `json:"UseLoadBalancer"`
|
||||
UseServerMetrics bool `json:"UseServerMetrics"`
|
||||
StorageClasses []KubernetesStorageClassConfig `json:"StorageClasses"`
|
||||
IngressClasses []KubernetesIngressClassConfig `json:"IngressClasses"`
|
||||
}
|
||||
|
||||
// KubernetesStorageClassConfig represents a Kubernetes Storage Class configuration
|
||||
KubernetesStorageClassConfig struct {
|
||||
Name string `json:"Name"`
|
||||
AccessModes []string `json:"AccessModes"`
|
||||
Provisioner string `json:"Provisioner"`
|
||||
Name string `json:"Name"`
|
||||
AccessModes []string `json:"AccessModes"`
|
||||
Provisioner string `json:"Provisioner"`
|
||||
AllowVolumeExpansion bool `json:"AllowVolumeExpansion"`
|
||||
}
|
||||
|
||||
// KubernetesIngressClassConfig represents a Kubernetes Ingress Class configuration
|
||||
KubernetesIngressClassConfig struct {
|
||||
Name string `json:"Name"`
|
||||
Type string `json:"Type"`
|
||||
}
|
||||
|
||||
// LDAPGroupSearchSettings represents settings used to search for groups in a LDAP server
|
||||
@@ -510,20 +515,25 @@ type (
|
||||
|
||||
// Settings represents the application settings
|
||||
Settings struct {
|
||||
LogoURL string `json:"LogoURL"`
|
||||
BlackListedLabels []Pair `json:"BlackListedLabels"`
|
||||
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
LDAPSettings LDAPSettings `json:"LDAPSettings"`
|
||||
OAuthSettings OAuthSettings `json:"OAuthSettings"`
|
||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||
AllowVolumeBrowserForRegularUsers bool `json:"AllowVolumeBrowserForRegularUsers"`
|
||||
SnapshotInterval string `json:"SnapshotInterval"`
|
||||
TemplatesURL string `json:"TemplatesURL"`
|
||||
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
||||
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"`
|
||||
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
|
||||
UserSessionTimeout string `json:"UserSessionTimeout"`
|
||||
LogoURL string `json:"LogoURL"`
|
||||
BlackListedLabels []Pair `json:"BlackListedLabels"`
|
||||
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
LDAPSettings LDAPSettings `json:"LDAPSettings"`
|
||||
OAuthSettings OAuthSettings `json:"OAuthSettings"`
|
||||
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"`
|
||||
SnapshotInterval string `json:"SnapshotInterval"`
|
||||
TemplatesURL string `json:"TemplatesURL"`
|
||||
EnableHostManagementFeatures bool `json:"EnableHostManagementFeatures"`
|
||||
EdgeAgentCheckinInterval int `json:"EdgeAgentCheckinInterval"`
|
||||
EnableEdgeComputeFeatures bool `json:"EnableEdgeComputeFeatures"`
|
||||
UserSessionTimeout string `json:"UserSessionTimeout"`
|
||||
EnableTelemetry bool `json:"EnableTelemetry"`
|
||||
|
||||
// Deprecated fields
|
||||
DisplayDonationHeader bool
|
||||
@@ -543,19 +553,22 @@ type (
|
||||
EntryPoint string `json:"EntryPoint"`
|
||||
Env []Pair `json:"Env"`
|
||||
ResourceControl *ResourceControl `json:"ResourceControl"`
|
||||
Status StackStatus `json:"Status"`
|
||||
ProjectPath string
|
||||
}
|
||||
|
||||
// StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier)
|
||||
StackID int
|
||||
|
||||
// StackStatus represent a status for a stack
|
||||
StackStatus int
|
||||
|
||||
// StackType represents the type of the stack (compose v2, stack deploy v3)
|
||||
StackType int
|
||||
|
||||
// Status represents the application status
|
||||
Status struct {
|
||||
Analytics bool `json:"Analytics"`
|
||||
Version string `json:"Version"`
|
||||
Version string `json:"Version"`
|
||||
}
|
||||
|
||||
// Tag represents a tag that can be associated to a resource
|
||||
@@ -711,10 +724,13 @@ type (
|
||||
|
||||
// User represents a user account
|
||||
User struct {
|
||||
ID UserID `json:"Id"`
|
||||
Username string `json:"Username"`
|
||||
Password string `json:"Password,omitempty"`
|
||||
Role UserRole `json:"Role"`
|
||||
ID UserID `json:"Id"`
|
||||
Username string `json:"Username"`
|
||||
Password string `json:"Password,omitempty"`
|
||||
Role UserRole `json:"Role"`
|
||||
|
||||
// Deprecated fields
|
||||
// Deprecated in DBVersion == 25
|
||||
PortainerAuthorizations Authorizations `json:"PortainerAuthorizations"`
|
||||
EndpointAuthorizations EndpointAuthorizations `json:"EndpointAuthorizations"`
|
||||
}
|
||||
@@ -794,7 +810,6 @@ type (
|
||||
Endpoint() EndpointService
|
||||
EndpointGroup() EndpointGroupService
|
||||
EndpointRelation() EndpointRelationService
|
||||
Extension() ExtensionService
|
||||
Registry() RegistryService
|
||||
ResourceControl() ResourceControlService
|
||||
Role() RoleService
|
||||
@@ -886,24 +901,6 @@ type (
|
||||
DeleteEndpointRelation(EndpointID EndpointID) error
|
||||
}
|
||||
|
||||
// ExtensionManager represents a service used to manage extensions
|
||||
ExtensionManager interface {
|
||||
FetchExtensionDefinitions() ([]Extension, error)
|
||||
InstallExtension(extension *Extension, licenseKey string, archiveFileName string, extensionArchive []byte) error
|
||||
EnableExtension(extension *Extension, licenseKey string) error
|
||||
DisableExtension(extension *Extension) error
|
||||
UpdateExtension(extension *Extension, version string) error
|
||||
StartExtensions() error
|
||||
}
|
||||
|
||||
// ExtensionService represents a service for managing extension data
|
||||
ExtensionService interface {
|
||||
Extension(ID ExtensionID) (*Extension, error)
|
||||
Extensions() ([]Extension, error)
|
||||
Persist(extension *Extension) error
|
||||
DeleteExtension(ID ExtensionID) error
|
||||
}
|
||||
|
||||
// FileService represents a service for managing files
|
||||
FileService interface {
|
||||
GetFileContent(filePath string) ([]byte, error)
|
||||
@@ -928,7 +925,6 @@ type (
|
||||
ClearEdgeJobTaskLogs(edgeJobID, taskID string) error
|
||||
GetEdgeJobTaskLogFileContent(edgeJobID, taskID string) (string, error)
|
||||
StoreEdgeJobTaskLogFileFromBytes(edgeJobID, taskID string, data []byte) error
|
||||
ExtractExtensionArchive(data []byte) error
|
||||
GetBinaryFolder() string
|
||||
StoreCustomTemplateFileFromBytes(identifier, fileName string, data []byte) (string, error)
|
||||
GetCustomTemplateProjectPath(identifier string) string
|
||||
@@ -950,8 +946,8 @@ type (
|
||||
|
||||
// KubeClient represents a service used to query a Kubernetes environment
|
||||
KubeClient interface {
|
||||
SetupUserServiceAccount(userID int, username string, teamIDs []int) error
|
||||
GetServiceAccountBearerToken(userID int, username string) (string, error)
|
||||
SetupUserServiceAccount(userID int, teamIDs []int) error
|
||||
GetServiceAccountBearerToken(userID int) (string, error)
|
||||
StartExecProcess(namespace, podName, containerName string, command []string, stdin io.Reader, stdout io.Writer) error
|
||||
}
|
||||
|
||||
@@ -972,6 +968,11 @@ type (
|
||||
GetUserGroups(username string, settings *LDAPSettings) ([]string, error)
|
||||
}
|
||||
|
||||
// OAuthService represents a service used to authenticate users using OAuth
|
||||
OAuthService interface {
|
||||
Authenticate(code string, configuration *OAuthSettings) (string, error)
|
||||
}
|
||||
|
||||
// RegistryService represents a service for managing registry data
|
||||
RegistryService interface {
|
||||
Registry(ID RegistryID) (*Registry, error)
|
||||
@@ -1101,6 +1102,8 @@ type (
|
||||
VersionService interface {
|
||||
DBVersion() (int, error)
|
||||
StoreDBVersion(version int) error
|
||||
InstanceID() (string, error)
|
||||
StoreInstanceID(ID string) error
|
||||
}
|
||||
|
||||
// WebhookService represents a service for managing webhook data.
|
||||
@@ -1116,23 +1119,21 @@ type (
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of the Portainer API
|
||||
APIVersion = "2.0.0-dev"
|
||||
APIVersion = "2.0.0"
|
||||
// DBVersion is the version number of the Portainer database
|
||||
DBVersion = 24
|
||||
DBVersion = 25
|
||||
// AssetsServerURL represents the URL of the Portainer asset server
|
||||
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
||||
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
||||
MessageOfTheDayURL = AssetsServerURL + "/motd.json"
|
||||
// VersionCheckURL represents the URL used to retrieve the latest version of Portainer
|
||||
VersionCheckURL = "https://api.github.com/repos/portainer/portainer/releases/latest"
|
||||
// ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved
|
||||
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-" + APIVersion + ".json"
|
||||
// SupportProductsURL represents the URL where Portainer support products can be retrieved
|
||||
SupportProductsURL = AssetsServerURL + "/support.json"
|
||||
// PortainerAgentHeader represents the name of the header available in any agent response
|
||||
PortainerAgentHeader = "Portainer-Agent"
|
||||
// PortainerAgentEdgeIDHeader represent the name of the header containing the Edge ID associated to an agent/agent cluster
|
||||
PortainerAgentEdgeIDHeader = "X-PortainerAgent-EdgeID"
|
||||
// HTTPResponseAgentPlatform represents the name of the header containing the Agent platform
|
||||
HTTPResponseAgentPlatform = "Portainer-Agent-Platform"
|
||||
// PortainerAgentTargetHeader represent the name of the header containing the target node name
|
||||
PortainerAgentTargetHeader = "X-PortainerAgent-Target"
|
||||
// PortainerAgentSignatureHeader represent the name of the header containing the digital signature
|
||||
@@ -1144,12 +1145,8 @@ const (
|
||||
// PortainerAgentSignatureMessage represents the message used to create a digital signature
|
||||
// to be used when communicating with an agent
|
||||
PortainerAgentSignatureMessage = "Portainer-App"
|
||||
// ExtensionServer represents the server used by Portainer to communicate with extensions
|
||||
ExtensionServer = "127.0.0.1"
|
||||
// DefaultEdgeAgentCheckinIntervalInSeconds represents the default interval (in seconds) used by Edge agents to checkin with the Portainer instance
|
||||
DefaultEdgeAgentCheckinIntervalInSeconds = 5
|
||||
// LocalExtensionManifestFile represents the name of the local manifest file for extensions
|
||||
LocalExtensionManifestFile = "/extensions.json"
|
||||
// DefaultTemplatesURL represents the URL to the official templates supported by Portainer
|
||||
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates-2.0.json"
|
||||
// DefaultUserSessionTimeout represents the default timeout after which the user session is cleared
|
||||
@@ -1166,6 +1163,14 @@ const (
|
||||
AuthenticationOAuth
|
||||
)
|
||||
|
||||
const (
|
||||
_ AgentPlatform = iota
|
||||
// AgentPlatformDocker represent the Docker platform (Standalone/Swarm)
|
||||
AgentPlatformDocker
|
||||
// AgentPlatformKubernetes represent the Kubernetes platform
|
||||
AgentPlatformKubernetes
|
||||
)
|
||||
|
||||
const (
|
||||
_ EdgeJobLogsStatus = iota
|
||||
// EdgeJobLogsStatusIdle represents an idle log collection job
|
||||
@@ -1226,16 +1231,6 @@ const (
|
||||
EdgeAgentOnKubernetesEnvironment
|
||||
)
|
||||
|
||||
const (
|
||||
_ ExtensionID = iota
|
||||
// RegistryManagementExtension represents the registry management extension
|
||||
RegistryManagementExtension
|
||||
// OAuthAuthenticationExtension represents the OAuth authentication extension
|
||||
OAuthAuthenticationExtension
|
||||
// RBACExtension represents the RBAC extension
|
||||
RBACExtension
|
||||
)
|
||||
|
||||
const (
|
||||
_ JobType = iota
|
||||
// SnapshotJobType is a system job used to create endpoint snapshots
|
||||
@@ -1298,6 +1293,13 @@ const (
|
||||
KubernetesStack
|
||||
)
|
||||
|
||||
// StackStatus represents a status for a stack
|
||||
const (
|
||||
_ StackStatus = iota
|
||||
StackStatusActive
|
||||
StackStatusInactive
|
||||
)
|
||||
|
||||
const (
|
||||
_ TemplateType = iota
|
||||
// ContainerTemplate represents a container template
|
||||
|
||||
5086
api/swagger.yaml
5086
api/swagger.yaml
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user