mirror of
https://github.com/portainer/portainer.git
synced 2025-08-04 05:15:25 +02:00
Merge branch 'release/1.23.0'
This commit is contained in:
commit
b7eca7ce17
257 changed files with 7961 additions and 5721 deletions
2
.babelrc
2
.babelrc
|
@ -5,7 +5,7 @@
|
||||||
"@babel/preset-env",
|
"@babel/preset-env",
|
||||||
{
|
{
|
||||||
"modules": false,
|
"modules": false,
|
||||||
"useBuiltIns": "usage"
|
"useBuiltIns": "entry"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
|
@ -24,7 +24,7 @@ rules:
|
||||||
# no-cond-assign: error
|
# no-cond-assign: error
|
||||||
# no-console: off
|
# no-console: off
|
||||||
# no-constant-condition: error
|
# no-constant-condition: error
|
||||||
# no-control-regex: error
|
no-control-regex: off
|
||||||
# no-debugger: error
|
# no-debugger: error
|
||||||
# no-dupe-args: error
|
# no-dupe-args: error
|
||||||
# no-dupe-keys: error
|
# no-dupe-keys: error
|
||||||
|
|
154
api/access_control.go
Normal file
154
api/access_control.go
Normal file
|
@ -0,0 +1,154 @@
|
||||||
|
package portainer
|
||||||
|
|
||||||
|
// NewPrivateResourceControl will create a new private resource control associated to the resource specified by the
|
||||||
|
// identifier and type parameters. It automatically assigns it to the user specified by the userID parameter.
|
||||||
|
func NewPrivateResourceControl(resourceIdentifier string, resourceType ResourceControlType, userID UserID) *ResourceControl {
|
||||||
|
return &ResourceControl{
|
||||||
|
Type: resourceType,
|
||||||
|
ResourceID: resourceIdentifier,
|
||||||
|
SubResourceIDs: []string{},
|
||||||
|
UserAccesses: []UserResourceAccess{
|
||||||
|
{
|
||||||
|
UserID: userID,
|
||||||
|
AccessLevel: ReadWriteAccessLevel,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TeamAccesses: []TeamResourceAccess{},
|
||||||
|
AdministratorsOnly: false,
|
||||||
|
Public: false,
|
||||||
|
System: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewSystemResourceControl will create a new public resource control with the System flag set to true.
|
||||||
|
// These kind of resource control are not persisted and are created on the fly by the Portainer API.
|
||||||
|
func NewSystemResourceControl(resourceIdentifier string, resourceType ResourceControlType) *ResourceControl {
|
||||||
|
return &ResourceControl{
|
||||||
|
Type: resourceType,
|
||||||
|
ResourceID: resourceIdentifier,
|
||||||
|
SubResourceIDs: []string{},
|
||||||
|
UserAccesses: []UserResourceAccess{},
|
||||||
|
TeamAccesses: []TeamResourceAccess{},
|
||||||
|
AdministratorsOnly: false,
|
||||||
|
Public: true,
|
||||||
|
System: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewPublicResourceControl will create a new public resource control.
|
||||||
|
func NewPublicResourceControl(resourceIdentifier string, resourceType ResourceControlType) *ResourceControl {
|
||||||
|
return &ResourceControl{
|
||||||
|
Type: resourceType,
|
||||||
|
ResourceID: resourceIdentifier,
|
||||||
|
SubResourceIDs: []string{},
|
||||||
|
UserAccesses: []UserResourceAccess{},
|
||||||
|
TeamAccesses: []TeamResourceAccess{},
|
||||||
|
AdministratorsOnly: false,
|
||||||
|
Public: true,
|
||||||
|
System: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRestrictedResourceControl will create a new resource control with user and team accesses restrictions.
|
||||||
|
func NewRestrictedResourceControl(resourceIdentifier string, resourceType ResourceControlType, userIDs []UserID, teamIDs []TeamID) *ResourceControl {
|
||||||
|
userAccesses := make([]UserResourceAccess, 0)
|
||||||
|
teamAccesses := make([]TeamResourceAccess, 0)
|
||||||
|
|
||||||
|
for _, id := range userIDs {
|
||||||
|
access := UserResourceAccess{
|
||||||
|
UserID: id,
|
||||||
|
AccessLevel: ReadWriteAccessLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
userAccesses = append(userAccesses, access)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, id := range teamIDs {
|
||||||
|
access := TeamResourceAccess{
|
||||||
|
TeamID: id,
|
||||||
|
AccessLevel: ReadWriteAccessLevel,
|
||||||
|
}
|
||||||
|
|
||||||
|
teamAccesses = append(teamAccesses, access)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ResourceControl{
|
||||||
|
Type: resourceType,
|
||||||
|
ResourceID: resourceIdentifier,
|
||||||
|
SubResourceIDs: []string{},
|
||||||
|
UserAccesses: userAccesses,
|
||||||
|
TeamAccesses: teamAccesses,
|
||||||
|
AdministratorsOnly: false,
|
||||||
|
Public: false,
|
||||||
|
System: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecorateStacks will iterate through a list of stacks, check for an associated resource control for each
|
||||||
|
// stack and decorate the stack element if a resource control is found.
|
||||||
|
func DecorateStacks(stacks []Stack, resourceControls []ResourceControl) []Stack {
|
||||||
|
for idx, stack := range stacks {
|
||||||
|
|
||||||
|
resourceControl := GetResourceControlByResourceIDAndType(stack.Name, StackResourceControl, resourceControls)
|
||||||
|
if resourceControl != nil {
|
||||||
|
stacks[idx].ResourceControl = resourceControl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stacks
|
||||||
|
}
|
||||||
|
|
||||||
|
// FilterAuthorizedStacks returns a list of decorated stacks filtered through resource control access checks.
|
||||||
|
func FilterAuthorizedStacks(stacks []Stack, user *User, userTeamIDs []TeamID, rbacEnabled bool) []Stack {
|
||||||
|
authorizedStacks := make([]Stack, 0)
|
||||||
|
|
||||||
|
for _, stack := range stacks {
|
||||||
|
_, ok := user.EndpointAuthorizations[stack.EndpointID][EndpointResourcesAccess]
|
||||||
|
if rbacEnabled && ok {
|
||||||
|
authorizedStacks = append(authorizedStacks, stack)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if stack.ResourceControl != nil && UserCanAccessResource(user.ID, userTeamIDs, stack.ResourceControl) {
|
||||||
|
authorizedStacks = append(authorizedStacks, stack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorizedStacks
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserCanAccessResource will valide that a user has permissions defined in the specified resource control
|
||||||
|
// based on its identifier and the team(s) he is part of.
|
||||||
|
func UserCanAccessResource(userID UserID, userTeamIDs []TeamID, resourceControl *ResourceControl) bool {
|
||||||
|
for _, authorizedUserAccess := range resourceControl.UserAccesses {
|
||||||
|
if userID == authorizedUserAccess.UserID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, authorizedTeamAccess := range resourceControl.TeamAccesses {
|
||||||
|
for _, userTeamID := range userTeamIDs {
|
||||||
|
if userTeamID == authorizedTeamAccess.TeamID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourceControl.Public
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResourceControlByResourceIDAndType retrieves the first matching resource control in a set of resource controls
|
||||||
|
// based on the specified id and resource type parameters.
|
||||||
|
func GetResourceControlByResourceIDAndType(resourceID string, resourceType ResourceControlType, resourceControls []ResourceControl) *ResourceControl {
|
||||||
|
for _, resourceControl := range resourceControls {
|
||||||
|
if resourceID == resourceControl.ResourceID && resourceType == resourceControl.Type {
|
||||||
|
return &resourceControl
|
||||||
|
}
|
||||||
|
for _, subResourceID := range resourceControl.SubResourceIDs {
|
||||||
|
if resourceID == subResourceID {
|
||||||
|
return &resourceControl
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -17,32 +17,38 @@ func UnzipArchive(archiveData []byte, dest string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, zipFile := range zipReader.File {
|
for _, zipFile := range zipReader.File {
|
||||||
|
err := extractFileFromArchive(zipFile, dest)
|
||||||
f, err := zipFile.Open()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
data, err := ioutil.ReadAll(f)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
fpath := filepath.Join(dest, zipFile.Name)
|
|
||||||
|
|
||||||
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, zipFile.Mode())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = io.Copy(outFile, bytes.NewReader(data))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
outFile.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func extractFileFromArchive(file *zip.File, dest string) error {
|
||||||
|
f, err := file.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
data, err := ioutil.ReadAll(f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fpath := filepath.Join(dest, file.Name)
|
||||||
|
|
||||||
|
outFile, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(outFile, bytes.NewReader(data))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return outFile.Close()
|
||||||
|
}
|
||||||
|
|
|
@ -34,6 +34,394 @@ func NewAuthorizationService(parameters *AuthorizationServiceParameters) *Author
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultEndpointAuthorizationsForEndpointAdministratorRole returns the default endpoint authorizations
|
||||||
|
// associated to the endpoint administrator role.
|
||||||
|
func DefaultEndpointAuthorizationsForEndpointAdministratorRole() Authorizations {
|
||||||
|
return map[Authorization]bool{
|
||||||
|
OperationDockerContainerArchiveInfo: true,
|
||||||
|
OperationDockerContainerList: true,
|
||||||
|
OperationDockerContainerExport: true,
|
||||||
|
OperationDockerContainerChanges: true,
|
||||||
|
OperationDockerContainerInspect: true,
|
||||||
|
OperationDockerContainerTop: true,
|
||||||
|
OperationDockerContainerLogs: true,
|
||||||
|
OperationDockerContainerStats: true,
|
||||||
|
OperationDockerContainerAttachWebsocket: true,
|
||||||
|
OperationDockerContainerArchive: true,
|
||||||
|
OperationDockerContainerCreate: true,
|
||||||
|
OperationDockerContainerPrune: true,
|
||||||
|
OperationDockerContainerKill: true,
|
||||||
|
OperationDockerContainerPause: true,
|
||||||
|
OperationDockerContainerUnpause: true,
|
||||||
|
OperationDockerContainerRestart: true,
|
||||||
|
OperationDockerContainerStart: true,
|
||||||
|
OperationDockerContainerStop: true,
|
||||||
|
OperationDockerContainerWait: true,
|
||||||
|
OperationDockerContainerResize: true,
|
||||||
|
OperationDockerContainerAttach: true,
|
||||||
|
OperationDockerContainerExec: true,
|
||||||
|
OperationDockerContainerRename: true,
|
||||||
|
OperationDockerContainerUpdate: true,
|
||||||
|
OperationDockerContainerPutContainerArchive: true,
|
||||||
|
OperationDockerContainerDelete: true,
|
||||||
|
OperationDockerImageList: true,
|
||||||
|
OperationDockerImageSearch: true,
|
||||||
|
OperationDockerImageGetAll: true,
|
||||||
|
OperationDockerImageGet: true,
|
||||||
|
OperationDockerImageHistory: true,
|
||||||
|
OperationDockerImageInspect: true,
|
||||||
|
OperationDockerImageLoad: true,
|
||||||
|
OperationDockerImageCreate: true,
|
||||||
|
OperationDockerImagePrune: true,
|
||||||
|
OperationDockerImagePush: true,
|
||||||
|
OperationDockerImageTag: true,
|
||||||
|
OperationDockerImageDelete: true,
|
||||||
|
OperationDockerImageCommit: true,
|
||||||
|
OperationDockerImageBuild: true,
|
||||||
|
OperationDockerNetworkList: true,
|
||||||
|
OperationDockerNetworkInspect: true,
|
||||||
|
OperationDockerNetworkCreate: true,
|
||||||
|
OperationDockerNetworkConnect: true,
|
||||||
|
OperationDockerNetworkDisconnect: true,
|
||||||
|
OperationDockerNetworkPrune: true,
|
||||||
|
OperationDockerNetworkDelete: true,
|
||||||
|
OperationDockerVolumeList: true,
|
||||||
|
OperationDockerVolumeInspect: true,
|
||||||
|
OperationDockerVolumeCreate: true,
|
||||||
|
OperationDockerVolumePrune: true,
|
||||||
|
OperationDockerVolumeDelete: true,
|
||||||
|
OperationDockerExecInspect: true,
|
||||||
|
OperationDockerExecStart: true,
|
||||||
|
OperationDockerExecResize: true,
|
||||||
|
OperationDockerSwarmInspect: true,
|
||||||
|
OperationDockerSwarmUnlockKey: true,
|
||||||
|
OperationDockerSwarmInit: true,
|
||||||
|
OperationDockerSwarmJoin: true,
|
||||||
|
OperationDockerSwarmLeave: true,
|
||||||
|
OperationDockerSwarmUpdate: true,
|
||||||
|
OperationDockerSwarmUnlock: true,
|
||||||
|
OperationDockerNodeList: true,
|
||||||
|
OperationDockerNodeInspect: true,
|
||||||
|
OperationDockerNodeUpdate: true,
|
||||||
|
OperationDockerNodeDelete: true,
|
||||||
|
OperationDockerServiceList: true,
|
||||||
|
OperationDockerServiceInspect: true,
|
||||||
|
OperationDockerServiceLogs: true,
|
||||||
|
OperationDockerServiceCreate: true,
|
||||||
|
OperationDockerServiceUpdate: true,
|
||||||
|
OperationDockerServiceDelete: true,
|
||||||
|
OperationDockerSecretList: true,
|
||||||
|
OperationDockerSecretInspect: true,
|
||||||
|
OperationDockerSecretCreate: true,
|
||||||
|
OperationDockerSecretUpdate: true,
|
||||||
|
OperationDockerSecretDelete: true,
|
||||||
|
OperationDockerConfigList: true,
|
||||||
|
OperationDockerConfigInspect: true,
|
||||||
|
OperationDockerConfigCreate: true,
|
||||||
|
OperationDockerConfigUpdate: true,
|
||||||
|
OperationDockerConfigDelete: true,
|
||||||
|
OperationDockerTaskList: true,
|
||||||
|
OperationDockerTaskInspect: true,
|
||||||
|
OperationDockerTaskLogs: true,
|
||||||
|
OperationDockerPluginList: true,
|
||||||
|
OperationDockerPluginPrivileges: true,
|
||||||
|
OperationDockerPluginInspect: true,
|
||||||
|
OperationDockerPluginPull: true,
|
||||||
|
OperationDockerPluginCreate: true,
|
||||||
|
OperationDockerPluginEnable: true,
|
||||||
|
OperationDockerPluginDisable: true,
|
||||||
|
OperationDockerPluginPush: true,
|
||||||
|
OperationDockerPluginUpgrade: true,
|
||||||
|
OperationDockerPluginSet: true,
|
||||||
|
OperationDockerPluginDelete: true,
|
||||||
|
OperationDockerSessionStart: true,
|
||||||
|
OperationDockerDistributionInspect: true,
|
||||||
|
OperationDockerBuildPrune: true,
|
||||||
|
OperationDockerBuildCancel: true,
|
||||||
|
OperationDockerPing: true,
|
||||||
|
OperationDockerInfo: true,
|
||||||
|
OperationDockerVersion: true,
|
||||||
|
OperationDockerEvents: true,
|
||||||
|
OperationDockerSystem: true,
|
||||||
|
OperationDockerUndefined: true,
|
||||||
|
OperationDockerAgentPing: true,
|
||||||
|
OperationDockerAgentList: true,
|
||||||
|
OperationDockerAgentHostInfo: true,
|
||||||
|
OperationDockerAgentBrowseDelete: true,
|
||||||
|
OperationDockerAgentBrowseGet: true,
|
||||||
|
OperationDockerAgentBrowseList: true,
|
||||||
|
OperationDockerAgentBrowsePut: true,
|
||||||
|
OperationDockerAgentBrowseRename: true,
|
||||||
|
OperationDockerAgentUndefined: true,
|
||||||
|
OperationPortainerResourceControlCreate: true,
|
||||||
|
OperationPortainerResourceControlUpdate: true,
|
||||||
|
OperationPortainerStackList: true,
|
||||||
|
OperationPortainerStackInspect: true,
|
||||||
|
OperationPortainerStackFile: true,
|
||||||
|
OperationPortainerStackCreate: true,
|
||||||
|
OperationPortainerStackMigrate: true,
|
||||||
|
OperationPortainerStackUpdate: true,
|
||||||
|
OperationPortainerStackDelete: true,
|
||||||
|
OperationPortainerWebsocketExec: true,
|
||||||
|
OperationPortainerWebhookList: true,
|
||||||
|
OperationPortainerWebhookCreate: true,
|
||||||
|
OperationPortainerWebhookDelete: true,
|
||||||
|
OperationIntegrationStoridgeAdmin: true,
|
||||||
|
EndpointResourcesAccess: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultEndpointAuthorizationsForHelpDeskRole returns the default endpoint authorizations
|
||||||
|
// associated to the helpdesk role.
|
||||||
|
func DefaultEndpointAuthorizationsForHelpDeskRole(volumeBrowsingAuthorizations bool) Authorizations {
|
||||||
|
authorizations := map[Authorization]bool{
|
||||||
|
OperationDockerContainerArchiveInfo: true,
|
||||||
|
OperationDockerContainerList: true,
|
||||||
|
OperationDockerContainerChanges: true,
|
||||||
|
OperationDockerContainerInspect: true,
|
||||||
|
OperationDockerContainerTop: true,
|
||||||
|
OperationDockerContainerLogs: true,
|
||||||
|
OperationDockerContainerStats: true,
|
||||||
|
OperationDockerImageList: true,
|
||||||
|
OperationDockerImageSearch: true,
|
||||||
|
OperationDockerImageGetAll: true,
|
||||||
|
OperationDockerImageGet: true,
|
||||||
|
OperationDockerImageHistory: true,
|
||||||
|
OperationDockerImageInspect: true,
|
||||||
|
OperationDockerNetworkList: true,
|
||||||
|
OperationDockerNetworkInspect: true,
|
||||||
|
OperationDockerVolumeList: true,
|
||||||
|
OperationDockerVolumeInspect: true,
|
||||||
|
OperationDockerSwarmInspect: true,
|
||||||
|
OperationDockerNodeList: true,
|
||||||
|
OperationDockerNodeInspect: true,
|
||||||
|
OperationDockerServiceList: true,
|
||||||
|
OperationDockerServiceInspect: true,
|
||||||
|
OperationDockerServiceLogs: true,
|
||||||
|
OperationDockerSecretList: true,
|
||||||
|
OperationDockerSecretInspect: true,
|
||||||
|
OperationDockerConfigList: true,
|
||||||
|
OperationDockerConfigInspect: true,
|
||||||
|
OperationDockerTaskList: true,
|
||||||
|
OperationDockerTaskInspect: true,
|
||||||
|
OperationDockerTaskLogs: true,
|
||||||
|
OperationDockerPluginList: true,
|
||||||
|
OperationDockerDistributionInspect: true,
|
||||||
|
OperationDockerPing: true,
|
||||||
|
OperationDockerInfo: true,
|
||||||
|
OperationDockerVersion: true,
|
||||||
|
OperationDockerEvents: true,
|
||||||
|
OperationDockerSystem: true,
|
||||||
|
OperationDockerAgentPing: true,
|
||||||
|
OperationDockerAgentList: true,
|
||||||
|
OperationDockerAgentHostInfo: true,
|
||||||
|
OperationPortainerStackList: true,
|
||||||
|
OperationPortainerStackInspect: true,
|
||||||
|
OperationPortainerStackFile: true,
|
||||||
|
OperationPortainerWebhookList: true,
|
||||||
|
EndpointResourcesAccess: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if volumeBrowsingAuthorizations {
|
||||||
|
authorizations[OperationDockerAgentBrowseGet] = true
|
||||||
|
authorizations[OperationDockerAgentBrowseList] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorizations
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultEndpointAuthorizationsForStandardUserRole returns the default endpoint authorizations
|
||||||
|
// associated to the standard user role.
|
||||||
|
func DefaultEndpointAuthorizationsForStandardUserRole(volumeBrowsingAuthorizations bool) Authorizations {
|
||||||
|
authorizations := map[Authorization]bool{
|
||||||
|
OperationDockerContainerArchiveInfo: true,
|
||||||
|
OperationDockerContainerList: true,
|
||||||
|
OperationDockerContainerExport: true,
|
||||||
|
OperationDockerContainerChanges: true,
|
||||||
|
OperationDockerContainerInspect: true,
|
||||||
|
OperationDockerContainerTop: true,
|
||||||
|
OperationDockerContainerLogs: true,
|
||||||
|
OperationDockerContainerStats: true,
|
||||||
|
OperationDockerContainerAttachWebsocket: true,
|
||||||
|
OperationDockerContainerArchive: true,
|
||||||
|
OperationDockerContainerCreate: true,
|
||||||
|
OperationDockerContainerKill: true,
|
||||||
|
OperationDockerContainerPause: true,
|
||||||
|
OperationDockerContainerUnpause: true,
|
||||||
|
OperationDockerContainerRestart: true,
|
||||||
|
OperationDockerContainerStart: true,
|
||||||
|
OperationDockerContainerStop: true,
|
||||||
|
OperationDockerContainerWait: true,
|
||||||
|
OperationDockerContainerResize: true,
|
||||||
|
OperationDockerContainerAttach: true,
|
||||||
|
OperationDockerContainerExec: true,
|
||||||
|
OperationDockerContainerRename: true,
|
||||||
|
OperationDockerContainerUpdate: true,
|
||||||
|
OperationDockerContainerPutContainerArchive: true,
|
||||||
|
OperationDockerContainerDelete: true,
|
||||||
|
OperationDockerImageList: true,
|
||||||
|
OperationDockerImageSearch: true,
|
||||||
|
OperationDockerImageGetAll: true,
|
||||||
|
OperationDockerImageGet: true,
|
||||||
|
OperationDockerImageHistory: true,
|
||||||
|
OperationDockerImageInspect: true,
|
||||||
|
OperationDockerImageLoad: true,
|
||||||
|
OperationDockerImageCreate: true,
|
||||||
|
OperationDockerImagePush: true,
|
||||||
|
OperationDockerImageTag: true,
|
||||||
|
OperationDockerImageDelete: true,
|
||||||
|
OperationDockerImageCommit: true,
|
||||||
|
OperationDockerImageBuild: true,
|
||||||
|
OperationDockerNetworkList: true,
|
||||||
|
OperationDockerNetworkInspect: true,
|
||||||
|
OperationDockerNetworkCreate: true,
|
||||||
|
OperationDockerNetworkConnect: true,
|
||||||
|
OperationDockerNetworkDisconnect: true,
|
||||||
|
OperationDockerNetworkDelete: true,
|
||||||
|
OperationDockerVolumeList: true,
|
||||||
|
OperationDockerVolumeInspect: true,
|
||||||
|
OperationDockerVolumeCreate: true,
|
||||||
|
OperationDockerVolumeDelete: true,
|
||||||
|
OperationDockerExecInspect: true,
|
||||||
|
OperationDockerExecStart: true,
|
||||||
|
OperationDockerExecResize: true,
|
||||||
|
OperationDockerSwarmInspect: true,
|
||||||
|
OperationDockerSwarmUnlockKey: true,
|
||||||
|
OperationDockerSwarmInit: true,
|
||||||
|
OperationDockerSwarmJoin: true,
|
||||||
|
OperationDockerSwarmLeave: true,
|
||||||
|
OperationDockerSwarmUpdate: true,
|
||||||
|
OperationDockerSwarmUnlock: true,
|
||||||
|
OperationDockerNodeList: true,
|
||||||
|
OperationDockerNodeInspect: true,
|
||||||
|
OperationDockerNodeUpdate: true,
|
||||||
|
OperationDockerNodeDelete: true,
|
||||||
|
OperationDockerServiceList: true,
|
||||||
|
OperationDockerServiceInspect: true,
|
||||||
|
OperationDockerServiceLogs: true,
|
||||||
|
OperationDockerServiceCreate: true,
|
||||||
|
OperationDockerServiceUpdate: true,
|
||||||
|
OperationDockerServiceDelete: true,
|
||||||
|
OperationDockerSecretList: true,
|
||||||
|
OperationDockerSecretInspect: true,
|
||||||
|
OperationDockerSecretCreate: true,
|
||||||
|
OperationDockerSecretUpdate: true,
|
||||||
|
OperationDockerSecretDelete: true,
|
||||||
|
OperationDockerConfigList: true,
|
||||||
|
OperationDockerConfigInspect: true,
|
||||||
|
OperationDockerConfigCreate: true,
|
||||||
|
OperationDockerConfigUpdate: true,
|
||||||
|
OperationDockerConfigDelete: true,
|
||||||
|
OperationDockerTaskList: true,
|
||||||
|
OperationDockerTaskInspect: true,
|
||||||
|
OperationDockerTaskLogs: true,
|
||||||
|
OperationDockerPluginList: true,
|
||||||
|
OperationDockerPluginPrivileges: true,
|
||||||
|
OperationDockerPluginInspect: true,
|
||||||
|
OperationDockerPluginPull: true,
|
||||||
|
OperationDockerPluginCreate: true,
|
||||||
|
OperationDockerPluginEnable: true,
|
||||||
|
OperationDockerPluginDisable: true,
|
||||||
|
OperationDockerPluginPush: true,
|
||||||
|
OperationDockerPluginUpgrade: true,
|
||||||
|
OperationDockerPluginSet: true,
|
||||||
|
OperationDockerPluginDelete: true,
|
||||||
|
OperationDockerSessionStart: true,
|
||||||
|
OperationDockerDistributionInspect: true,
|
||||||
|
OperationDockerBuildPrune: true,
|
||||||
|
OperationDockerBuildCancel: true,
|
||||||
|
OperationDockerPing: true,
|
||||||
|
OperationDockerInfo: true,
|
||||||
|
OperationDockerVersion: true,
|
||||||
|
OperationDockerEvents: true,
|
||||||
|
OperationDockerSystem: true,
|
||||||
|
OperationDockerUndefined: true,
|
||||||
|
OperationDockerAgentPing: true,
|
||||||
|
OperationDockerAgentList: true,
|
||||||
|
OperationDockerAgentHostInfo: true,
|
||||||
|
OperationDockerAgentUndefined: true,
|
||||||
|
OperationPortainerResourceControlUpdate: true,
|
||||||
|
OperationPortainerStackList: true,
|
||||||
|
OperationPortainerStackInspect: true,
|
||||||
|
OperationPortainerStackFile: true,
|
||||||
|
OperationPortainerStackCreate: true,
|
||||||
|
OperationPortainerStackMigrate: true,
|
||||||
|
OperationPortainerStackUpdate: true,
|
||||||
|
OperationPortainerStackDelete: true,
|
||||||
|
OperationPortainerWebsocketExec: true,
|
||||||
|
OperationPortainerWebhookList: true,
|
||||||
|
OperationPortainerWebhookCreate: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if volumeBrowsingAuthorizations {
|
||||||
|
authorizations[OperationDockerAgentBrowseGet] = true
|
||||||
|
authorizations[OperationDockerAgentBrowseList] = true
|
||||||
|
authorizations[OperationDockerAgentBrowseDelete] = true
|
||||||
|
authorizations[OperationDockerAgentBrowsePut] = true
|
||||||
|
authorizations[OperationDockerAgentBrowseRename] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorizations
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultEndpointAuthorizationsForReadOnlyUserRole returns the default endpoint authorizations
|
||||||
|
// associated to the readonly user role.
|
||||||
|
func DefaultEndpointAuthorizationsForReadOnlyUserRole(volumeBrowsingAuthorizations bool) Authorizations {
|
||||||
|
authorizations := map[Authorization]bool{
|
||||||
|
OperationDockerContainerArchiveInfo: true,
|
||||||
|
OperationDockerContainerList: true,
|
||||||
|
OperationDockerContainerChanges: true,
|
||||||
|
OperationDockerContainerInspect: true,
|
||||||
|
OperationDockerContainerTop: true,
|
||||||
|
OperationDockerContainerLogs: true,
|
||||||
|
OperationDockerContainerStats: true,
|
||||||
|
OperationDockerImageList: true,
|
||||||
|
OperationDockerImageSearch: true,
|
||||||
|
OperationDockerImageGetAll: true,
|
||||||
|
OperationDockerImageGet: true,
|
||||||
|
OperationDockerImageHistory: true,
|
||||||
|
OperationDockerImageInspect: true,
|
||||||
|
OperationDockerNetworkList: true,
|
||||||
|
OperationDockerNetworkInspect: true,
|
||||||
|
OperationDockerVolumeList: true,
|
||||||
|
OperationDockerVolumeInspect: true,
|
||||||
|
OperationDockerSwarmInspect: true,
|
||||||
|
OperationDockerNodeList: true,
|
||||||
|
OperationDockerNodeInspect: true,
|
||||||
|
OperationDockerServiceList: true,
|
||||||
|
OperationDockerServiceInspect: true,
|
||||||
|
OperationDockerServiceLogs: true,
|
||||||
|
OperationDockerSecretList: true,
|
||||||
|
OperationDockerSecretInspect: true,
|
||||||
|
OperationDockerConfigList: true,
|
||||||
|
OperationDockerConfigInspect: true,
|
||||||
|
OperationDockerTaskList: true,
|
||||||
|
OperationDockerTaskInspect: true,
|
||||||
|
OperationDockerTaskLogs: true,
|
||||||
|
OperationDockerPluginList: true,
|
||||||
|
OperationDockerDistributionInspect: true,
|
||||||
|
OperationDockerPing: true,
|
||||||
|
OperationDockerInfo: true,
|
||||||
|
OperationDockerVersion: true,
|
||||||
|
OperationDockerEvents: true,
|
||||||
|
OperationDockerSystem: true,
|
||||||
|
OperationDockerAgentPing: true,
|
||||||
|
OperationDockerAgentList: true,
|
||||||
|
OperationDockerAgentHostInfo: true,
|
||||||
|
OperationPortainerStackList: true,
|
||||||
|
OperationPortainerStackInspect: true,
|
||||||
|
OperationPortainerStackFile: true,
|
||||||
|
OperationPortainerWebhookList: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
if volumeBrowsingAuthorizations {
|
||||||
|
authorizations[OperationDockerAgentBrowseGet] = true
|
||||||
|
authorizations[OperationDockerAgentBrowseList] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return authorizations
|
||||||
|
}
|
||||||
|
|
||||||
// DefaultPortainerAuthorizations returns the default Portainer authorizations used by non-admin users.
|
// DefaultPortainerAuthorizations returns the default Portainer authorizations used by non-admin users.
|
||||||
func DefaultPortainerAuthorizations() Authorizations {
|
func DefaultPortainerAuthorizations() Authorizations {
|
||||||
return map[Authorization]bool{
|
return map[Authorization]bool{
|
||||||
|
@ -164,7 +552,7 @@ func (service *AuthorizationService) RemoveTeamAccessPolicies(teamID TeamID) err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return service.UpdateUsersAuthorizations()
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveUserAccessPolicies will remove all existing access policies associated to the specified user
|
// RemoveUserAccessPolicies will remove all existing access policies associated to the specified user
|
||||||
|
@ -383,37 +771,25 @@ func getAuthorizationsFromTeamEndpointGroupPolicies(memberships []TeamMembership
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAuthorizationsFromRoles(roleIdentifiers []RoleID, roles []Role) Authorizations {
|
func getAuthorizationsFromRoles(roleIdentifiers []RoleID, roles []Role) Authorizations {
|
||||||
var roleAuthorizations []Authorizations
|
var associatedRoles []Role
|
||||||
|
|
||||||
for _, id := range roleIdentifiers {
|
for _, id := range roleIdentifiers {
|
||||||
for _, role := range roles {
|
for _, role := range roles {
|
||||||
if role.ID == id {
|
if role.ID == id {
|
||||||
roleAuthorizations = append(roleAuthorizations, role.Authorizations)
|
associatedRoles = append(associatedRoles, role)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
processedAuthorizations := make(Authorizations)
|
var authorizations Authorizations
|
||||||
if len(roleAuthorizations) > 0 {
|
highestPriority := 0
|
||||||
processedAuthorizations = roleAuthorizations[0]
|
for _, role := range associatedRoles {
|
||||||
for idx, authorizations := range roleAuthorizations {
|
if role.Priority > highestPriority {
|
||||||
if idx == 0 {
|
highestPriority = role.Priority
|
||||||
continue
|
authorizations = role.Authorizations
|
||||||
}
|
|
||||||
processedAuthorizations = mergeAuthorizations(processedAuthorizations, authorizations)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return processedAuthorizations
|
return authorizations
|
||||||
}
|
|
||||||
|
|
||||||
func mergeAuthorizations(a, b Authorizations) Authorizations {
|
|
||||||
c := make(map[Authorization]bool)
|
|
||||||
|
|
||||||
for k := range b {
|
|
||||||
if _, ok := a[k]; ok {
|
|
||||||
c[k] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
}
|
||||||
|
|
371
api/bolt/init.go
371
api/bolt/init.go
|
@ -32,141 +32,10 @@ func (store *Store) Init() error {
|
||||||
|
|
||||||
if len(roles) == 0 {
|
if len(roles) == 0 {
|
||||||
environmentAdministratorRole := &portainer.Role{
|
environmentAdministratorRole := &portainer.Role{
|
||||||
Name: "Endpoint administrator",
|
Name: "Endpoint administrator",
|
||||||
Description: "Full control of all resources in an endpoint",
|
Description: "Full control of all resources in an endpoint",
|
||||||
Authorizations: map[portainer.Authorization]bool{
|
Priority: 1,
|
||||||
portainer.OperationDockerContainerArchiveInfo: true,
|
Authorizations: portainer.DefaultEndpointAuthorizationsForEndpointAdministratorRole(),
|
||||||
portainer.OperationDockerContainerList: true,
|
|
||||||
portainer.OperationDockerContainerExport: true,
|
|
||||||
portainer.OperationDockerContainerChanges: true,
|
|
||||||
portainer.OperationDockerContainerInspect: true,
|
|
||||||
portainer.OperationDockerContainerTop: true,
|
|
||||||
portainer.OperationDockerContainerLogs: true,
|
|
||||||
portainer.OperationDockerContainerStats: true,
|
|
||||||
portainer.OperationDockerContainerAttachWebsocket: true,
|
|
||||||
portainer.OperationDockerContainerArchive: true,
|
|
||||||
portainer.OperationDockerContainerCreate: true,
|
|
||||||
portainer.OperationDockerContainerPrune: true,
|
|
||||||
portainer.OperationDockerContainerKill: true,
|
|
||||||
portainer.OperationDockerContainerPause: true,
|
|
||||||
portainer.OperationDockerContainerUnpause: true,
|
|
||||||
portainer.OperationDockerContainerRestart: true,
|
|
||||||
portainer.OperationDockerContainerStart: true,
|
|
||||||
portainer.OperationDockerContainerStop: true,
|
|
||||||
portainer.OperationDockerContainerWait: true,
|
|
||||||
portainer.OperationDockerContainerResize: true,
|
|
||||||
portainer.OperationDockerContainerAttach: true,
|
|
||||||
portainer.OperationDockerContainerExec: true,
|
|
||||||
portainer.OperationDockerContainerRename: true,
|
|
||||||
portainer.OperationDockerContainerUpdate: true,
|
|
||||||
portainer.OperationDockerContainerPutContainerArchive: true,
|
|
||||||
portainer.OperationDockerContainerDelete: true,
|
|
||||||
portainer.OperationDockerImageList: true,
|
|
||||||
portainer.OperationDockerImageSearch: true,
|
|
||||||
portainer.OperationDockerImageGetAll: true,
|
|
||||||
portainer.OperationDockerImageGet: true,
|
|
||||||
portainer.OperationDockerImageHistory: true,
|
|
||||||
portainer.OperationDockerImageInspect: true,
|
|
||||||
portainer.OperationDockerImageLoad: true,
|
|
||||||
portainer.OperationDockerImageCreate: true,
|
|
||||||
portainer.OperationDockerImagePrune: true,
|
|
||||||
portainer.OperationDockerImagePush: true,
|
|
||||||
portainer.OperationDockerImageTag: true,
|
|
||||||
portainer.OperationDockerImageDelete: true,
|
|
||||||
portainer.OperationDockerImageCommit: true,
|
|
||||||
portainer.OperationDockerImageBuild: true,
|
|
||||||
portainer.OperationDockerNetworkList: true,
|
|
||||||
portainer.OperationDockerNetworkInspect: true,
|
|
||||||
portainer.OperationDockerNetworkCreate: true,
|
|
||||||
portainer.OperationDockerNetworkConnect: true,
|
|
||||||
portainer.OperationDockerNetworkDisconnect: true,
|
|
||||||
portainer.OperationDockerNetworkPrune: true,
|
|
||||||
portainer.OperationDockerNetworkDelete: true,
|
|
||||||
portainer.OperationDockerVolumeList: true,
|
|
||||||
portainer.OperationDockerVolumeInspect: true,
|
|
||||||
portainer.OperationDockerVolumeCreate: true,
|
|
||||||
portainer.OperationDockerVolumePrune: true,
|
|
||||||
portainer.OperationDockerVolumeDelete: true,
|
|
||||||
portainer.OperationDockerExecInspect: true,
|
|
||||||
portainer.OperationDockerExecStart: true,
|
|
||||||
portainer.OperationDockerExecResize: true,
|
|
||||||
portainer.OperationDockerSwarmInspect: true,
|
|
||||||
portainer.OperationDockerSwarmUnlockKey: true,
|
|
||||||
portainer.OperationDockerSwarmInit: true,
|
|
||||||
portainer.OperationDockerSwarmJoin: true,
|
|
||||||
portainer.OperationDockerSwarmLeave: true,
|
|
||||||
portainer.OperationDockerSwarmUpdate: true,
|
|
||||||
portainer.OperationDockerSwarmUnlock: true,
|
|
||||||
portainer.OperationDockerNodeList: true,
|
|
||||||
portainer.OperationDockerNodeInspect: true,
|
|
||||||
portainer.OperationDockerNodeUpdate: true,
|
|
||||||
portainer.OperationDockerNodeDelete: true,
|
|
||||||
portainer.OperationDockerServiceList: true,
|
|
||||||
portainer.OperationDockerServiceInspect: true,
|
|
||||||
portainer.OperationDockerServiceLogs: true,
|
|
||||||
portainer.OperationDockerServiceCreate: true,
|
|
||||||
portainer.OperationDockerServiceUpdate: true,
|
|
||||||
portainer.OperationDockerServiceDelete: true,
|
|
||||||
portainer.OperationDockerSecretList: true,
|
|
||||||
portainer.OperationDockerSecretInspect: true,
|
|
||||||
portainer.OperationDockerSecretCreate: true,
|
|
||||||
portainer.OperationDockerSecretUpdate: true,
|
|
||||||
portainer.OperationDockerSecretDelete: true,
|
|
||||||
portainer.OperationDockerConfigList: true,
|
|
||||||
portainer.OperationDockerConfigInspect: true,
|
|
||||||
portainer.OperationDockerConfigCreate: true,
|
|
||||||
portainer.OperationDockerConfigUpdate: true,
|
|
||||||
portainer.OperationDockerConfigDelete: true,
|
|
||||||
portainer.OperationDockerTaskList: true,
|
|
||||||
portainer.OperationDockerTaskInspect: true,
|
|
||||||
portainer.OperationDockerTaskLogs: true,
|
|
||||||
portainer.OperationDockerPluginList: true,
|
|
||||||
portainer.OperationDockerPluginPrivileges: true,
|
|
||||||
portainer.OperationDockerPluginInspect: true,
|
|
||||||
portainer.OperationDockerPluginPull: true,
|
|
||||||
portainer.OperationDockerPluginCreate: true,
|
|
||||||
portainer.OperationDockerPluginEnable: true,
|
|
||||||
portainer.OperationDockerPluginDisable: true,
|
|
||||||
portainer.OperationDockerPluginPush: true,
|
|
||||||
portainer.OperationDockerPluginUpgrade: true,
|
|
||||||
portainer.OperationDockerPluginSet: true,
|
|
||||||
portainer.OperationDockerPluginDelete: true,
|
|
||||||
portainer.OperationDockerSessionStart: true,
|
|
||||||
portainer.OperationDockerDistributionInspect: true,
|
|
||||||
portainer.OperationDockerBuildPrune: true,
|
|
||||||
portainer.OperationDockerBuildCancel: true,
|
|
||||||
portainer.OperationDockerPing: true,
|
|
||||||
portainer.OperationDockerInfo: true,
|
|
||||||
portainer.OperationDockerVersion: true,
|
|
||||||
portainer.OperationDockerEvents: true,
|
|
||||||
portainer.OperationDockerSystem: true,
|
|
||||||
portainer.OperationDockerUndefined: true,
|
|
||||||
portainer.OperationDockerAgentPing: true,
|
|
||||||
portainer.OperationDockerAgentList: true,
|
|
||||||
portainer.OperationDockerAgentHostInfo: true,
|
|
||||||
portainer.OperationDockerAgentBrowseDelete: true,
|
|
||||||
portainer.OperationDockerAgentBrowseGet: true,
|
|
||||||
portainer.OperationDockerAgentBrowseList: true,
|
|
||||||
portainer.OperationDockerAgentBrowsePut: true,
|
|
||||||
portainer.OperationDockerAgentBrowseRename: true,
|
|
||||||
portainer.OperationDockerAgentUndefined: true,
|
|
||||||
portainer.OperationPortainerResourceControlCreate: true,
|
|
||||||
portainer.OperationPortainerResourceControlUpdate: true,
|
|
||||||
portainer.OperationPortainerResourceControlDelete: true,
|
|
||||||
portainer.OperationPortainerStackList: true,
|
|
||||||
portainer.OperationPortainerStackInspect: true,
|
|
||||||
portainer.OperationPortainerStackFile: true,
|
|
||||||
portainer.OperationPortainerStackCreate: true,
|
|
||||||
portainer.OperationPortainerStackMigrate: true,
|
|
||||||
portainer.OperationPortainerStackUpdate: true,
|
|
||||||
portainer.OperationPortainerStackDelete: true,
|
|
||||||
portainer.OperationPortainerWebsocketExec: true,
|
|
||||||
portainer.OperationPortainerWebhookList: true,
|
|
||||||
portainer.OperationPortainerWebhookCreate: true,
|
|
||||||
portainer.OperationPortainerWebhookDelete: true,
|
|
||||||
portainer.OperationIntegrationStoridgeAdmin: true,
|
|
||||||
portainer.EndpointResourcesAccess: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = store.RoleService.CreateRole(environmentAdministratorRole)
|
err = store.RoleService.CreateRole(environmentAdministratorRole)
|
||||||
|
@ -175,55 +44,10 @@ func (store *Store) Init() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
environmentReadOnlyUserRole := &portainer.Role{
|
environmentReadOnlyUserRole := &portainer.Role{
|
||||||
Name: "Helpdesk",
|
Name: "Helpdesk",
|
||||||
Description: "Read-only access of all resources in an endpoint",
|
Description: "Read-only access of all resources in an endpoint",
|
||||||
Authorizations: map[portainer.Authorization]bool{
|
Priority: 2,
|
||||||
portainer.OperationDockerContainerArchiveInfo: true,
|
Authorizations: portainer.DefaultEndpointAuthorizationsForHelpDeskRole(false),
|
||||||
portainer.OperationDockerContainerList: true,
|
|
||||||
portainer.OperationDockerContainerChanges: true,
|
|
||||||
portainer.OperationDockerContainerInspect: true,
|
|
||||||
portainer.OperationDockerContainerTop: true,
|
|
||||||
portainer.OperationDockerContainerLogs: true,
|
|
||||||
portainer.OperationDockerContainerStats: true,
|
|
||||||
portainer.OperationDockerImageList: true,
|
|
||||||
portainer.OperationDockerImageSearch: true,
|
|
||||||
portainer.OperationDockerImageGetAll: true,
|
|
||||||
portainer.OperationDockerImageGet: true,
|
|
||||||
portainer.OperationDockerImageHistory: true,
|
|
||||||
portainer.OperationDockerImageInspect: true,
|
|
||||||
portainer.OperationDockerNetworkList: true,
|
|
||||||
portainer.OperationDockerNetworkInspect: true,
|
|
||||||
portainer.OperationDockerVolumeList: true,
|
|
||||||
portainer.OperationDockerVolumeInspect: true,
|
|
||||||
portainer.OperationDockerSwarmInspect: true,
|
|
||||||
portainer.OperationDockerNodeList: true,
|
|
||||||
portainer.OperationDockerNodeInspect: true,
|
|
||||||
portainer.OperationDockerServiceList: true,
|
|
||||||
portainer.OperationDockerServiceInspect: true,
|
|
||||||
portainer.OperationDockerServiceLogs: true,
|
|
||||||
portainer.OperationDockerSecretList: true,
|
|
||||||
portainer.OperationDockerSecretInspect: true,
|
|
||||||
portainer.OperationDockerConfigList: true,
|
|
||||||
portainer.OperationDockerConfigInspect: true,
|
|
||||||
portainer.OperationDockerTaskList: true,
|
|
||||||
portainer.OperationDockerTaskInspect: true,
|
|
||||||
portainer.OperationDockerTaskLogs: true,
|
|
||||||
portainer.OperationDockerPluginList: true,
|
|
||||||
portainer.OperationDockerDistributionInspect: true,
|
|
||||||
portainer.OperationDockerPing: true,
|
|
||||||
portainer.OperationDockerInfo: true,
|
|
||||||
portainer.OperationDockerVersion: true,
|
|
||||||
portainer.OperationDockerEvents: true,
|
|
||||||
portainer.OperationDockerSystem: true,
|
|
||||||
portainer.OperationDockerAgentPing: true,
|
|
||||||
portainer.OperationDockerAgentList: true,
|
|
||||||
portainer.OperationDockerAgentHostInfo: true,
|
|
||||||
portainer.OperationPortainerStackList: true,
|
|
||||||
portainer.OperationPortainerStackInspect: true,
|
|
||||||
portainer.OperationPortainerStackFile: true,
|
|
||||||
portainer.OperationPortainerWebhookList: true,
|
|
||||||
portainer.EndpointResourcesAccess: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = store.RoleService.CreateRole(environmentReadOnlyUserRole)
|
err = store.RoleService.CreateRole(environmentReadOnlyUserRole)
|
||||||
|
@ -232,129 +56,10 @@ func (store *Store) Init() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
standardUserRole := &portainer.Role{
|
standardUserRole := &portainer.Role{
|
||||||
Name: "Standard user",
|
Name: "Standard user",
|
||||||
Description: "Full control of assigned resources in an endpoint",
|
Description: "Full control of assigned resources in an endpoint",
|
||||||
Authorizations: map[portainer.Authorization]bool{
|
Priority: 3,
|
||||||
portainer.OperationDockerContainerArchiveInfo: true,
|
Authorizations: portainer.DefaultEndpointAuthorizationsForStandardUserRole(false),
|
||||||
portainer.OperationDockerContainerList: true,
|
|
||||||
portainer.OperationDockerContainerExport: true,
|
|
||||||
portainer.OperationDockerContainerChanges: true,
|
|
||||||
portainer.OperationDockerContainerInspect: true,
|
|
||||||
portainer.OperationDockerContainerTop: true,
|
|
||||||
portainer.OperationDockerContainerLogs: true,
|
|
||||||
portainer.OperationDockerContainerStats: true,
|
|
||||||
portainer.OperationDockerContainerAttachWebsocket: true,
|
|
||||||
portainer.OperationDockerContainerArchive: true,
|
|
||||||
portainer.OperationDockerContainerCreate: true,
|
|
||||||
portainer.OperationDockerContainerKill: true,
|
|
||||||
portainer.OperationDockerContainerPause: true,
|
|
||||||
portainer.OperationDockerContainerUnpause: true,
|
|
||||||
portainer.OperationDockerContainerRestart: true,
|
|
||||||
portainer.OperationDockerContainerStart: true,
|
|
||||||
portainer.OperationDockerContainerStop: true,
|
|
||||||
portainer.OperationDockerContainerWait: true,
|
|
||||||
portainer.OperationDockerContainerResize: true,
|
|
||||||
portainer.OperationDockerContainerAttach: true,
|
|
||||||
portainer.OperationDockerContainerExec: true,
|
|
||||||
portainer.OperationDockerContainerRename: true,
|
|
||||||
portainer.OperationDockerContainerUpdate: true,
|
|
||||||
portainer.OperationDockerContainerPutContainerArchive: true,
|
|
||||||
portainer.OperationDockerContainerDelete: true,
|
|
||||||
portainer.OperationDockerImageList: true,
|
|
||||||
portainer.OperationDockerImageSearch: true,
|
|
||||||
portainer.OperationDockerImageGetAll: true,
|
|
||||||
portainer.OperationDockerImageGet: true,
|
|
||||||
portainer.OperationDockerImageHistory: true,
|
|
||||||
portainer.OperationDockerImageInspect: true,
|
|
||||||
portainer.OperationDockerImageLoad: true,
|
|
||||||
portainer.OperationDockerImageCreate: true,
|
|
||||||
portainer.OperationDockerImagePush: true,
|
|
||||||
portainer.OperationDockerImageTag: true,
|
|
||||||
portainer.OperationDockerImageDelete: true,
|
|
||||||
portainer.OperationDockerImageCommit: true,
|
|
||||||
portainer.OperationDockerImageBuild: true,
|
|
||||||
portainer.OperationDockerNetworkList: true,
|
|
||||||
portainer.OperationDockerNetworkInspect: true,
|
|
||||||
portainer.OperationDockerNetworkCreate: true,
|
|
||||||
portainer.OperationDockerNetworkConnect: true,
|
|
||||||
portainer.OperationDockerNetworkDisconnect: true,
|
|
||||||
portainer.OperationDockerNetworkDelete: true,
|
|
||||||
portainer.OperationDockerVolumeList: true,
|
|
||||||
portainer.OperationDockerVolumeInspect: true,
|
|
||||||
portainer.OperationDockerVolumeCreate: true,
|
|
||||||
portainer.OperationDockerVolumeDelete: true,
|
|
||||||
portainer.OperationDockerExecInspect: true,
|
|
||||||
portainer.OperationDockerExecStart: true,
|
|
||||||
portainer.OperationDockerExecResize: true,
|
|
||||||
portainer.OperationDockerSwarmInspect: true,
|
|
||||||
portainer.OperationDockerSwarmUnlockKey: true,
|
|
||||||
portainer.OperationDockerSwarmInit: true,
|
|
||||||
portainer.OperationDockerSwarmJoin: true,
|
|
||||||
portainer.OperationDockerSwarmLeave: true,
|
|
||||||
portainer.OperationDockerSwarmUpdate: true,
|
|
||||||
portainer.OperationDockerSwarmUnlock: true,
|
|
||||||
portainer.OperationDockerNodeList: true,
|
|
||||||
portainer.OperationDockerNodeInspect: true,
|
|
||||||
portainer.OperationDockerNodeUpdate: true,
|
|
||||||
portainer.OperationDockerNodeDelete: true,
|
|
||||||
portainer.OperationDockerServiceList: true,
|
|
||||||
portainer.OperationDockerServiceInspect: true,
|
|
||||||
portainer.OperationDockerServiceLogs: true,
|
|
||||||
portainer.OperationDockerServiceCreate: true,
|
|
||||||
portainer.OperationDockerServiceUpdate: true,
|
|
||||||
portainer.OperationDockerServiceDelete: true,
|
|
||||||
portainer.OperationDockerSecretList: true,
|
|
||||||
portainer.OperationDockerSecretInspect: true,
|
|
||||||
portainer.OperationDockerSecretCreate: true,
|
|
||||||
portainer.OperationDockerSecretUpdate: true,
|
|
||||||
portainer.OperationDockerSecretDelete: true,
|
|
||||||
portainer.OperationDockerConfigList: true,
|
|
||||||
portainer.OperationDockerConfigInspect: true,
|
|
||||||
portainer.OperationDockerConfigCreate: true,
|
|
||||||
portainer.OperationDockerConfigUpdate: true,
|
|
||||||
portainer.OperationDockerConfigDelete: true,
|
|
||||||
portainer.OperationDockerTaskList: true,
|
|
||||||
portainer.OperationDockerTaskInspect: true,
|
|
||||||
portainer.OperationDockerTaskLogs: true,
|
|
||||||
portainer.OperationDockerPluginList: true,
|
|
||||||
portainer.OperationDockerPluginPrivileges: true,
|
|
||||||
portainer.OperationDockerPluginInspect: true,
|
|
||||||
portainer.OperationDockerPluginPull: true,
|
|
||||||
portainer.OperationDockerPluginCreate: true,
|
|
||||||
portainer.OperationDockerPluginEnable: true,
|
|
||||||
portainer.OperationDockerPluginDisable: true,
|
|
||||||
portainer.OperationDockerPluginPush: true,
|
|
||||||
portainer.OperationDockerPluginUpgrade: true,
|
|
||||||
portainer.OperationDockerPluginSet: true,
|
|
||||||
portainer.OperationDockerPluginDelete: true,
|
|
||||||
portainer.OperationDockerSessionStart: true,
|
|
||||||
portainer.OperationDockerDistributionInspect: true,
|
|
||||||
portainer.OperationDockerBuildPrune: true,
|
|
||||||
portainer.OperationDockerBuildCancel: true,
|
|
||||||
portainer.OperationDockerPing: true,
|
|
||||||
portainer.OperationDockerInfo: true,
|
|
||||||
portainer.OperationDockerVersion: true,
|
|
||||||
portainer.OperationDockerEvents: true,
|
|
||||||
portainer.OperationDockerSystem: true,
|
|
||||||
portainer.OperationDockerUndefined: true,
|
|
||||||
portainer.OperationDockerAgentPing: true,
|
|
||||||
portainer.OperationDockerAgentList: true,
|
|
||||||
portainer.OperationDockerAgentHostInfo: true,
|
|
||||||
portainer.OperationDockerAgentUndefined: true,
|
|
||||||
portainer.OperationPortainerResourceControlCreate: true,
|
|
||||||
portainer.OperationPortainerResourceControlUpdate: true,
|
|
||||||
portainer.OperationPortainerResourceControlDelete: true,
|
|
||||||
portainer.OperationPortainerStackList: true,
|
|
||||||
portainer.OperationPortainerStackInspect: true,
|
|
||||||
portainer.OperationPortainerStackFile: true,
|
|
||||||
portainer.OperationPortainerStackCreate: true,
|
|
||||||
portainer.OperationPortainerStackMigrate: true,
|
|
||||||
portainer.OperationPortainerStackUpdate: true,
|
|
||||||
portainer.OperationPortainerStackDelete: true,
|
|
||||||
portainer.OperationPortainerWebsocketExec: true,
|
|
||||||
portainer.OperationPortainerWebhookList: true,
|
|
||||||
portainer.OperationPortainerWebhookCreate: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = store.RoleService.CreateRole(standardUserRole)
|
err = store.RoleService.CreateRole(standardUserRole)
|
||||||
|
@ -363,54 +68,10 @@ func (store *Store) Init() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
readOnlyUserRole := &portainer.Role{
|
readOnlyUserRole := &portainer.Role{
|
||||||
Name: "Read-only user",
|
Name: "Read-only user",
|
||||||
Description: "Read-only access of assigned resources in an endpoint",
|
Description: "Read-only access of assigned resources in an endpoint",
|
||||||
Authorizations: map[portainer.Authorization]bool{
|
Priority: 4,
|
||||||
portainer.OperationDockerContainerArchiveInfo: true,
|
Authorizations: portainer.DefaultEndpointAuthorizationsForReadOnlyUserRole(false),
|
||||||
portainer.OperationDockerContainerList: true,
|
|
||||||
portainer.OperationDockerContainerChanges: true,
|
|
||||||
portainer.OperationDockerContainerInspect: true,
|
|
||||||
portainer.OperationDockerContainerTop: true,
|
|
||||||
portainer.OperationDockerContainerLogs: true,
|
|
||||||
portainer.OperationDockerContainerStats: true,
|
|
||||||
portainer.OperationDockerImageList: true,
|
|
||||||
portainer.OperationDockerImageSearch: true,
|
|
||||||
portainer.OperationDockerImageGetAll: true,
|
|
||||||
portainer.OperationDockerImageGet: true,
|
|
||||||
portainer.OperationDockerImageHistory: true,
|
|
||||||
portainer.OperationDockerImageInspect: true,
|
|
||||||
portainer.OperationDockerNetworkList: true,
|
|
||||||
portainer.OperationDockerNetworkInspect: true,
|
|
||||||
portainer.OperationDockerVolumeList: true,
|
|
||||||
portainer.OperationDockerVolumeInspect: true,
|
|
||||||
portainer.OperationDockerSwarmInspect: true,
|
|
||||||
portainer.OperationDockerNodeList: true,
|
|
||||||
portainer.OperationDockerNodeInspect: true,
|
|
||||||
portainer.OperationDockerServiceList: true,
|
|
||||||
portainer.OperationDockerServiceInspect: true,
|
|
||||||
portainer.OperationDockerServiceLogs: true,
|
|
||||||
portainer.OperationDockerSecretList: true,
|
|
||||||
portainer.OperationDockerSecretInspect: true,
|
|
||||||
portainer.OperationDockerConfigList: true,
|
|
||||||
portainer.OperationDockerConfigInspect: true,
|
|
||||||
portainer.OperationDockerTaskList: true,
|
|
||||||
portainer.OperationDockerTaskInspect: true,
|
|
||||||
portainer.OperationDockerTaskLogs: true,
|
|
||||||
portainer.OperationDockerPluginList: true,
|
|
||||||
portainer.OperationDockerDistributionInspect: true,
|
|
||||||
portainer.OperationDockerPing: true,
|
|
||||||
portainer.OperationDockerInfo: true,
|
|
||||||
portainer.OperationDockerVersion: true,
|
|
||||||
portainer.OperationDockerEvents: true,
|
|
||||||
portainer.OperationDockerSystem: true,
|
|
||||||
portainer.OperationDockerAgentPing: true,
|
|
||||||
portainer.OperationDockerAgentList: true,
|
|
||||||
portainer.OperationDockerAgentHostInfo: true,
|
|
||||||
portainer.OperationPortainerStackList: true,
|
|
||||||
portainer.OperationPortainerStackInspect: true,
|
|
||||||
portainer.OperationPortainerStackFile: true,
|
|
||||||
portainer.OperationPortainerWebhookList: true,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = store.RoleService.CreateRole(readOnlyUserRole)
|
err = store.RoleService.CreateRole(readOnlyUserRole)
|
||||||
|
|
|
@ -82,13 +82,15 @@ func DeleteObject(db *bolt.DB, bucketName string, key []byte) error {
|
||||||
func GetNextIdentifier(db *bolt.DB, bucketName string) int {
|
func GetNextIdentifier(db *bolt.DB, bucketName string) int {
|
||||||
var identifier int
|
var identifier int
|
||||||
|
|
||||||
db.View(func(tx *bolt.Tx) error {
|
db.Update(func(tx *bolt.Tx) error {
|
||||||
bucket := tx.Bucket([]byte(bucketName))
|
bucket := tx.Bucket([]byte(bucketName))
|
||||||
id := bucket.Sequence()
|
id, err := bucket.NextSequence()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
identifier = int(id)
|
identifier = int(id)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
identifier++
|
|
||||||
return identifier
|
return identifier
|
||||||
}
|
}
|
||||||
|
|
89
api/bolt/migrator/migrate_dbversion20.go
Normal file
89
api/bolt/migrator/migrate_dbversion20.go
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
package migrator
|
||||||
|
|
||||||
|
import portainer "github.com/portainer/portainer/api"
|
||||||
|
|
||||||
|
func (m *Migrator) updateResourceControlsToDBVersion22() error {
|
||||||
|
legacyResourceControls, err := m.resourceControlService.ResourceControls()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, resourceControl := range legacyResourceControls {
|
||||||
|
resourceControl.AdministratorsOnly = false
|
||||||
|
|
||||||
|
err := m.resourceControlService.UpdateResourceControl(resourceControl.ID, &resourceControl)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Migrator) updateUsersAndRolesToDBVersion22() error {
|
||||||
|
legacyUsers, err := m.userService.Users()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings, err := m.settingsService.Settings()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, user := range legacyUsers {
|
||||||
|
user.PortainerAuthorizations = portainer.DefaultPortainerAuthorizations()
|
||||||
|
err = m.userService.UpdateUser(user.ID, &user)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointAdministratorRole, err := m.roleService.Role(portainer.RoleID(1))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
endpointAdministratorRole.Priority = 1
|
||||||
|
endpointAdministratorRole.Authorizations = portainer.DefaultEndpointAuthorizationsForEndpointAdministratorRole()
|
||||||
|
|
||||||
|
err = m.roleService.UpdateRole(endpointAdministratorRole.ID, endpointAdministratorRole)
|
||||||
|
|
||||||
|
helpDeskRole, err := m.roleService.Role(portainer.RoleID(2))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
helpDeskRole.Priority = 2
|
||||||
|
helpDeskRole.Authorizations = portainer.DefaultEndpointAuthorizationsForHelpDeskRole(settings.AllowVolumeBrowserForRegularUsers)
|
||||||
|
|
||||||
|
err = m.roleService.UpdateRole(helpDeskRole.ID, helpDeskRole)
|
||||||
|
|
||||||
|
standardUserRole, err := m.roleService.Role(portainer.RoleID(3))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
standardUserRole.Priority = 3
|
||||||
|
standardUserRole.Authorizations = portainer.DefaultEndpointAuthorizationsForStandardUserRole(settings.AllowVolumeBrowserForRegularUsers)
|
||||||
|
|
||||||
|
err = m.roleService.UpdateRole(standardUserRole.ID, standardUserRole)
|
||||||
|
|
||||||
|
readOnlyUserRole, err := m.roleService.Role(portainer.RoleID(4))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
readOnlyUserRole.Priority = 4
|
||||||
|
readOnlyUserRole.Authorizations = portainer.DefaultEndpointAuthorizationsForReadOnlyUserRole(settings.AllowVolumeBrowserForRegularUsers)
|
||||||
|
|
||||||
|
err = m.roleService.UpdateRole(readOnlyUserRole.ID, readOnlyUserRole)
|
||||||
|
|
||||||
|
authorizationServiceParameters := &portainer.AuthorizationServiceParameters{
|
||||||
|
EndpointService: m.endpointService,
|
||||||
|
EndpointGroupService: m.endpointGroupService,
|
||||||
|
RegistryService: m.registryService,
|
||||||
|
RoleService: m.roleService,
|
||||||
|
TeamMembershipService: m.teamMembershipService,
|
||||||
|
UserService: m.userService,
|
||||||
|
}
|
||||||
|
|
||||||
|
authorizationService := portainer.NewAuthorizationService(authorizationServiceParameters)
|
||||||
|
return authorizationService.UpdateUsersAuthorizations()
|
||||||
|
}
|
|
@ -287,5 +287,19 @@ func (m *Migrator) Migrate() error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Portainer 1.23.0
|
||||||
|
// DBVersion 21 is missing as it was shipped as via hotfix 1.22.2
|
||||||
|
if m.currentDBVersion < 22 {
|
||||||
|
err := m.updateResourceControlsToDBVersion22()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = m.updateUsersAndRolesToDBVersion22()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
return m.versionService.StoreDBVersion(portainer.DBVersion)
|
||||||
}
|
}
|
||||||
|
|
|
@ -42,9 +42,10 @@ func (service *Service) ResourceControl(ID portainer.ResourceControlID) (*portai
|
||||||
return &resourceControl, nil
|
return &resourceControl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResourceControlByResourceID returns a ResourceControl object by checking if the resourceID is equal
|
// ResourceControlByResourceIDAndType returns a ResourceControl object by checking if the resourceID is equal
|
||||||
// to the main ResourceID or in SubResourceIDs
|
// to the main ResourceID or in SubResourceIDs. It also performs a check on the resource type. Return nil
|
||||||
func (service *Service) ResourceControlByResourceID(resourceID string) (*portainer.ResourceControl, error) {
|
// if no ResourceControl was found.
|
||||||
|
func (service *Service) ResourceControlByResourceIDAndType(resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
|
||||||
var resourceControl *portainer.ResourceControl
|
var resourceControl *portainer.ResourceControl
|
||||||
|
|
||||||
err := service.db.View(func(tx *bolt.Tx) error {
|
err := service.db.View(func(tx *bolt.Tx) error {
|
||||||
|
@ -58,7 +59,7 @@ func (service *Service) ResourceControlByResourceID(resourceID string) (*portain
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if rc.ResourceID == resourceID {
|
if rc.ResourceID == resourceID && rc.Type == resourceType {
|
||||||
resourceControl = &rc
|
resourceControl = &rc
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
@ -71,10 +72,6 @@ func (service *Service) ResourceControlByResourceID(resourceID string) (*portain
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if resourceControl == nil {
|
|
||||||
return portainer.ErrObjectNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -489,26 +489,11 @@ func initJobService(dockerClientFactory *docker.ClientFactory) portainer.JobServ
|
||||||
func initExtensionManager(fileService portainer.FileService, extensionService portainer.ExtensionService) (portainer.ExtensionManager, error) {
|
func initExtensionManager(fileService portainer.FileService, extensionService portainer.ExtensionService) (portainer.ExtensionManager, error) {
|
||||||
extensionManager := exec.NewExtensionManager(fileService, extensionService)
|
extensionManager := exec.NewExtensionManager(fileService, extensionService)
|
||||||
|
|
||||||
extensions, err := extensionService.Extensions()
|
err := extensionManager.StartExtensions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, extension := range extensions {
|
|
||||||
err := extensionManager.EnableExtension(&extension, extension.License.LicenseKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Unable to enable extension: %s [extension: %s]", err.Error(), extension.Name)
|
|
||||||
extension.Enabled = false
|
|
||||||
extension.License.Valid = false
|
|
||||||
}
|
|
||||||
|
|
||||||
err = extensionService.Persist(&extension)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return extensionManager, nil
|
return extensionManager, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -639,7 +624,7 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(users) == 0 {
|
if len(users) == 0 {
|
||||||
log.Printf("Creating admin user with password hash %s", adminPasswordHash)
|
log.Println("Created admin user with the given password.")
|
||||||
user := &portainer.User{
|
user := &portainer.User{
|
||||||
Username: "admin",
|
Username: "admin",
|
||||||
Role: portainer.AdministratorRole,
|
Role: portainer.AdministratorRole,
|
||||||
|
|
|
@ -14,6 +14,7 @@ import (
|
||||||
const (
|
const (
|
||||||
unsupportedEnvironmentType = portainer.Error("Environment not supported")
|
unsupportedEnvironmentType = portainer.Error("Environment not supported")
|
||||||
defaultDockerRequestTimeout = 60
|
defaultDockerRequestTimeout = 60
|
||||||
|
dockerClientVersion = "1.40"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ClientFactory is used to create Docker clients
|
// ClientFactory is used to create Docker clients
|
||||||
|
@ -51,7 +52,7 @@ func (factory *ClientFactory) CreateClient(endpoint *portainer.Endpoint, nodeNam
|
||||||
func createLocalClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
func createLocalClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||||
return client.NewClientWithOpts(
|
return client.NewClientWithOpts(
|
||||||
client.WithHost(endpoint.URL),
|
client.WithHost(endpoint.URL),
|
||||||
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
client.WithVersion(dockerClientVersion),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,7 +64,7 @@ func createTCPClient(endpoint *portainer.Endpoint) (*client.Client, error) {
|
||||||
|
|
||||||
return client.NewClientWithOpts(
|
return client.NewClientWithOpts(
|
||||||
client.WithHost(endpoint.URL),
|
client.WithHost(endpoint.URL),
|
||||||
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
client.WithVersion(dockerClientVersion),
|
||||||
client.WithHTTPClient(httpCli),
|
client.WithHTTPClient(httpCli),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -84,7 +85,7 @@ func createEdgeClient(endpoint *portainer.Endpoint, reverseTunnelService portain
|
||||||
|
|
||||||
return client.NewClientWithOpts(
|
return client.NewClientWithOpts(
|
||||||
client.WithHost(endpointURL),
|
client.WithHost(endpointURL),
|
||||||
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
client.WithVersion(dockerClientVersion),
|
||||||
client.WithHTTPClient(httpCli),
|
client.WithHTTPClient(httpCli),
|
||||||
client.WithHTTPHeaders(headers),
|
client.WithHTTPHeaders(headers),
|
||||||
)
|
)
|
||||||
|
@ -112,7 +113,7 @@ func createAgentClient(endpoint *portainer.Endpoint, signatureService portainer.
|
||||||
|
|
||||||
return client.NewClientWithOpts(
|
return client.NewClientWithOpts(
|
||||||
client.WithHost(endpoint.URL),
|
client.WithHost(endpoint.URL),
|
||||||
client.WithVersion(portainer.SupportedDockerAPIVersion),
|
client.WithVersion(dockerClientVersion),
|
||||||
client.WithHTTPClient(httpCli),
|
client.WithHTTPClient(httpCli),
|
||||||
client.WithHTTPHeaders(headers),
|
client.WithHTTPHeaders(headers),
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,20 +4,26 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
|
"regexp"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/coreos/go-semver/semver"
|
||||||
|
|
||||||
"github.com/orcaman/concurrent-map"
|
"github.com/orcaman/concurrent-map"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/client"
|
"github.com/portainer/portainer/api/http/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
var extensionDownloadBaseURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com/extensions/"
|
var extensionDownloadBaseURL = portainer.AssetsServerURL + "/extensions/"
|
||||||
|
var extensionVersionRegexp = regexp.MustCompile(`\d+(\.\d+)+`)
|
||||||
|
|
||||||
var extensionBinaryMap = map[portainer.ExtensionID]string{
|
var extensionBinaryMap = map[portainer.ExtensionID]string{
|
||||||
portainer.RegistryManagementExtension: "extension-registry-management",
|
portainer.RegistryManagementExtension: "extension-registry-management",
|
||||||
|
@ -47,20 +53,11 @@ func processKey(ID portainer.ExtensionID) string {
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildExtensionURL(extension *portainer.Extension) string {
|
func buildExtensionURL(extension *portainer.Extension) string {
|
||||||
extensionURL := extensionDownloadBaseURL
|
return fmt.Sprintf("%s%s-%s-%s-%s.zip", extensionDownloadBaseURL, extensionBinaryMap[extension.ID], runtime.GOOS, runtime.GOARCH, extension.Version)
|
||||||
extensionURL += extensionBinaryMap[extension.ID]
|
|
||||||
extensionURL += "-" + runtime.GOOS + "-" + runtime.GOARCH
|
|
||||||
extensionURL += "-" + extension.Version
|
|
||||||
extensionURL += ".zip"
|
|
||||||
return extensionURL
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildExtensionPath(binaryPath string, extension *portainer.Extension) string {
|
func buildExtensionPath(binaryPath string, extension *portainer.Extension) string {
|
||||||
|
extensionFilename := fmt.Sprintf("%s-%s-%s-%s", extensionBinaryMap[extension.ID], runtime.GOOS, runtime.GOARCH, extension.Version)
|
||||||
extensionFilename := extensionBinaryMap[extension.ID]
|
|
||||||
extensionFilename += "-" + runtime.GOOS + "-" + runtime.GOARCH
|
|
||||||
extensionFilename += "-" + extension.Version
|
|
||||||
|
|
||||||
if runtime.GOOS == "windows" {
|
if runtime.GOOS == "windows" {
|
||||||
extensionFilename += ".exe"
|
extensionFilename += ".exe"
|
||||||
}
|
}
|
||||||
|
@ -73,11 +70,20 @@ func buildExtensionPath(binaryPath string, extension *portainer.Extension) strin
|
||||||
}
|
}
|
||||||
|
|
||||||
// FetchExtensionDefinitions will fetch the list of available
|
// FetchExtensionDefinitions will fetch the list of available
|
||||||
// extension definitions from the official Portainer assets server
|
// 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) {
|
func (manager *ExtensionManager) FetchExtensionDefinitions() ([]portainer.Extension, error) {
|
||||||
extensionData, err := client.Get(portainer.ExtensionDefinitionsURL, 30)
|
var extensionData []byte
|
||||||
|
|
||||||
|
extensionData, err := client.Get(portainer.ExtensionDefinitionsURL, 5)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
var extensions []portainer.Extension
|
||||||
|
@ -89,6 +95,37 @@ func (manager *ExtensionManager) FetchExtensionDefinitions() ([]portainer.Extens
|
||||||
return extensions, nil
|
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
|
// 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.
|
// 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
|
// After installing the binary on the filesystem, it will execute the binary in license check
|
||||||
|
@ -145,6 +182,61 @@ func (manager *ExtensionManager) DisableExtension(extension *portainer.Extension
|
||||||
return manager.fileService.RemoveDirectory(extensionBinaryPath)
|
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.extensionService.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.extensionService.Persist(&extension)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// UpdateExtension will download the new extension binary from the official Portainer assets
|
// UpdateExtension will download the new extension binary from the official Portainer assets
|
||||||
// server, disable the previous extension via DisableExtension, trigger a license check
|
// server, disable the previous extension via DisableExtension, trigger a license check
|
||||||
// and then start the extension process and add it to the processes map
|
// and then start the extension process and add it to the processes map
|
||||||
|
@ -195,7 +287,7 @@ func validateLicense(binaryPath, licenseKey string) ([]string, error) {
|
||||||
err := licenseCheckProcess.Run()
|
err := licenseCheckProcess.Run()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("[DEBUG] [exec,extension] [message: unable to run extension process] [err: %s]", err)
|
log.Printf("[DEBUG] [exec,extension] [message: unable to run extension process] [err: %s]", err)
|
||||||
return nil, errors.New("Invalid extension license key")
|
return nil, errors.New("invalid extension license key")
|
||||||
}
|
}
|
||||||
|
|
||||||
output := string(cmdOutput.Bytes())
|
output := string(cmdOutput.Bytes())
|
||||||
|
@ -205,8 +297,12 @@ func validateLicense(binaryPath, licenseKey string) ([]string, error) {
|
||||||
|
|
||||||
func (manager *ExtensionManager) startExtensionProcess(extension *portainer.Extension, binaryPath string) error {
|
func (manager *ExtensionManager) startExtensionProcess(extension *portainer.Extension, binaryPath string) error {
|
||||||
extensionProcess := exec.Command(binaryPath, "-license", extension.License.LicenseKey)
|
extensionProcess := exec.Command(binaryPath, "-license", extension.License.LicenseKey)
|
||||||
|
extensionProcess.Stdout = os.Stdout
|
||||||
|
extensionProcess.Stderr = os.Stderr
|
||||||
|
|
||||||
err := extensionProcess.Start()
|
err := extensionProcess.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Printf("[DEBUG] [exec,extension] [message: unable to start extension process] [err: %s]", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -87,12 +87,7 @@ func (service *Service) GetBinaryFolder() string {
|
||||||
// ExtractExtensionArchive extracts the content of an extension archive
|
// ExtractExtensionArchive extracts the content of an extension archive
|
||||||
// specified as raw data into the binary store on the filesystem
|
// specified as raw data into the binary store on the filesystem
|
||||||
func (service *Service) ExtractExtensionArchive(data []byte) error {
|
func (service *Service) ExtractExtensionArchive(data []byte) error {
|
||||||
err := archive.UnzipArchive(data, path.Join(service.fileStorePath, BinaryStorePath))
|
return archive.UnzipArchive(data, path.Join(service.fileStorePath, BinaryStorePath))
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RemoveDirectory removes a directory on the filesystem.
|
// RemoveDirectory removes a directory on the filesystem.
|
||||||
|
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -87,6 +88,7 @@ func Get(url string, timeout int) ([]byte, error) {
|
||||||
defer response.Body.Close()
|
defer response.Body.Close()
|
||||||
|
|
||||||
if response.StatusCode != http.StatusOK {
|
if response.StatusCode != http.StatusOK {
|
||||||
|
log.Printf("[ERROR] [http,client] [message: unexpected status code] [status_code: %d]", response.StatusCode)
|
||||||
return nil, errInvalidResponseStatus
|
return nil, errInvalidResponseStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -113,6 +113,11 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use
|
||||||
log.Printf("Warning: unable to automatically add user into teams: %s\n", err.Error())
|
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)
|
return handler.writeToken(w, user)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -133,6 +133,11 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team membership inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist team membership inside the database", err}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update user authorizations", err}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return handler.writeToken(w, user)
|
return handler.writeToken(w, user)
|
||||||
|
|
|
@ -34,6 +34,7 @@ type Handler struct {
|
||||||
EndpointGroupService portainer.EndpointGroupService
|
EndpointGroupService portainer.EndpointGroupService
|
||||||
RoleService portainer.RoleService
|
RoleService portainer.RoleService
|
||||||
ProxyManager *proxy.Manager
|
ProxyManager *proxy.Manager
|
||||||
|
AuthorizationService *portainer.AuthorizationService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage authentication operations.
|
// NewHandler creates a handler to manage authentication operations.
|
||||||
|
|
|
@ -29,9 +29,9 @@ func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.R
|
||||||
}
|
}
|
||||||
|
|
||||||
var proxy http.Handler
|
var proxy http.Handler
|
||||||
proxy = handler.ProxyManager.GetProxy(endpoint)
|
proxy = handler.ProxyManager.GetEndpointProxy(endpoint)
|
||||||
if proxy == nil {
|
if proxy == nil {
|
||||||
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
proxy, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -37,7 +37,7 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
||||||
|
|
||||||
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
|
tunnel := handler.ReverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||||
if tunnel.Status == portainer.EdgeAgentIdle {
|
if tunnel.Status == portainer.EdgeAgentIdle {
|
||||||
handler.ProxyManager.DeleteProxy(endpoint)
|
handler.ProxyManager.DeleteEndpointProxy(endpoint)
|
||||||
|
|
||||||
err := handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
|
err := handler.ReverseTunnelService.SetTunnelStatusToRequired(endpoint.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -55,9 +55,9 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.
|
||||||
}
|
}
|
||||||
|
|
||||||
var proxy http.Handler
|
var proxy http.Handler
|
||||||
proxy = handler.ProxyManager.GetProxy(endpoint)
|
proxy = handler.ProxyManager.GetEndpointProxy(endpoint)
|
||||||
if proxy == nil {
|
if proxy == nil {
|
||||||
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
proxy, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create proxy", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ func (handler *Handler) endpointDelete(w http.ResponseWriter, r *http.Request) *
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove endpoint from the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
handler.ProxyManager.DeleteProxy(endpoint)
|
handler.ProxyManager.DeleteEndpointProxy(endpoint)
|
||||||
|
|
||||||
if len(endpoint.UserAccessPolicies) > 0 || len(endpoint.TeamAccessPolicies) > 0 {
|
if len(endpoint.UserAccessPolicies) > 0 || len(endpoint.TeamAccessPolicies) > 0 {
|
||||||
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
err = handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||||
|
|
|
@ -166,7 +166,7 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) *
|
||||||
}
|
}
|
||||||
|
|
||||||
if payload.URL != nil || payload.TLS != nil || endpoint.Type == portainer.AzureEnvironment {
|
if payload.URL != nil || payload.TLS != nil || endpoint.Type == portainer.AzureEnvironment {
|
||||||
_, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
_, err = handler.ProxyManager.CreateAndRegisterEndpointProxy(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register HTTP proxy for the endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,3 +59,59 @@ func (handler *Handler) upgradeRBACData() error {
|
||||||
|
|
||||||
return handler.AuthorizationService.UpdateUsersAuthorizations()
|
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.EndpointGroupService.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.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpoints, err := handler.EndpointService.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.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler.AuthorizationService.UpdateUsersAuthorizations()
|
||||||
|
}
|
|
@ -29,6 +29,13 @@ func (handler *Handler) extensionDelete(w http.ResponseWriter, r *http.Request)
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete extension", err}
|
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.ExtensionService.DeleteExtension(extensionID)
|
err = handler.ExtensionService.DeleteExtension(extensionID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete the extension from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to delete the extension from the database", err}
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
package extensions
|
package extensions
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/coreos/go-semver/semver"
|
"github.com/portainer/portainer/api/http/client"
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/client"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GET request on /api/extensions/:id
|
// GET request on /api/extensions/:id
|
||||||
|
@ -18,46 +17,39 @@ func (handler *Handler) extensionInspect(w http.ResponseWriter, r *http.Request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid extension identifier route variable", err}
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid extension identifier route variable", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
extensionID := portainer.ExtensionID(extensionIdentifier)
|
extensionID := portainer.ExtensionID(extensionIdentifier)
|
||||||
|
|
||||||
extensionData, err := client.Get(portainer.ExtensionDefinitionsURL, 30)
|
definitions, err := handler.ExtensionManager.FetchExtensionDefinitions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extension definitions", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions informations", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
var extensions []portainer.Extension
|
localExtension, err := handler.ExtensionService.Extension(extensionID)
|
||||||
err = json.Unmarshal(extensionData, &extensions)
|
if err != nil && err != portainer.ErrObjectNotFound {
|
||||||
if err != nil {
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extension information from the database", err}
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to parse external extension definitions", err}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var extension portainer.Extension
|
var extension portainer.Extension
|
||||||
for _, p := range extensions {
|
var extensionDefinition portainer.Extension
|
||||||
if p.ID == extensionID {
|
|
||||||
extension = p
|
for _, definition := range definitions {
|
||||||
if extension.DescriptionURL != "" {
|
if definition.ID == extensionID {
|
||||||
description, _ := client.Get(extension.DescriptionURL, 10)
|
extensionDefinition = definition
|
||||||
extension.Description = string(description)
|
|
||||||
}
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
storedExtension, err := handler.ExtensionService.Extension(extensionID)
|
if localExtension == nil {
|
||||||
if err == portainer.ErrObjectNotFound {
|
extension = extensionDefinition
|
||||||
return response.JSON(w, extension)
|
} else {
|
||||||
} else if err != nil {
|
extension = *localExtension
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension.Enabled = storedExtension.Enabled
|
mergeExtensionAndDefinition(&extension, &extensionDefinition)
|
||||||
|
|
||||||
extensionVer := semver.New(extension.Version)
|
description, _ := client.Get(extension.DescriptionURL, 5)
|
||||||
pVer := semver.New(storedExtension.Version)
|
extension.Description = string(description)
|
||||||
|
|
||||||
if pVer.LessThan(*extensionVer) {
|
|
||||||
extension.UpdateAvailable = true
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.JSON(w, extension)
|
return response.JSON(w, extension)
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,54 +3,28 @@ package extensions
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/coreos/go-semver/semver"
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// GET request on /api/extensions?store=<store>
|
// GET request on /api/extensions?store=<store>
|
||||||
func (handler *Handler) extensionList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
func (handler *Handler) extensionList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
storeDetails, _ := request.RetrieveBooleanQueryParameter(r, "store", true)
|
fetchManifestInformation, _ := request.RetrieveBooleanQueryParameter(r, "store", true)
|
||||||
|
|
||||||
extensions, err := handler.ExtensionService.Extensions()
|
extensions, err := handler.ExtensionService.Extensions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions from the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if storeDetails {
|
if fetchManifestInformation {
|
||||||
definitions, err := handler.ExtensionManager.FetchExtensionDefinitions()
|
definitions, err := handler.ExtensionManager.FetchExtensionDefinitions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve extensions informations", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
for idx := range definitions {
|
extensions = mergeExtensionsAndDefinitions(extensions, definitions)
|
||||||
associateExtensionData(&definitions[idx], extensions)
|
|
||||||
}
|
|
||||||
|
|
||||||
extensions = definitions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, extensions)
|
return response.JSON(w, extensions)
|
||||||
}
|
}
|
||||||
|
|
||||||
func associateExtensionData(definition *portainer.Extension, extensions []portainer.Extension) {
|
|
||||||
for _, extension := range extensions {
|
|
||||||
if extension.ID == definition.ID {
|
|
||||||
|
|
||||||
definition.Enabled = extension.Enabled
|
|
||||||
definition.License.Company = extension.License.Company
|
|
||||||
definition.License.Expiration = extension.License.Expiration
|
|
||||||
definition.License.Valid = extension.License.Valid
|
|
||||||
|
|
||||||
definitionVersion := semver.New(definition.Version)
|
|
||||||
extensionVersion := semver.New(extension.Version)
|
|
||||||
if extensionVersion.LessThan(*definitionVersion) {
|
|
||||||
definition.UpdateAvailable = true
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
75
api/http/handler/extensions/extension_upload.go
Normal file
75
api/http/handler/extensions/extension_upload.go
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
package extensions
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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 portainer.Error("Invalid license")
|
||||||
|
}
|
||||||
|
payload.License = license
|
||||||
|
|
||||||
|
fileData, fileName, err := request.RetrieveMultiPartFormFile(r, "file")
|
||||||
|
if err != nil {
|
||||||
|
return portainer.Error("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.ExtensionService.Persist(extension)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist extension status inside the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.Empty(w)
|
||||||
|
}
|
|
@ -3,6 +3,8 @@ package extensions
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/coreos/go-semver/semver"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
|
@ -30,6 +32,8 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
bouncer.RestrictedAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet)
|
bouncer.RestrictedAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet)
|
||||||
h.Handle("/extensions",
|
h.Handle("/extensions",
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.extensionCreate))).Methods(http.MethodPost)
|
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}",
|
h.Handle("/extensions/{id}",
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.extensionInspect))).Methods(http.MethodGet)
|
bouncer.AdminAccess(httperror.LoggerHandler(h.extensionInspect))).Methods(http.MethodGet)
|
||||||
h.Handle("/extensions/{id}",
|
h.Handle("/extensions/{id}",
|
||||||
|
@ -39,3 +43,44 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
|
|
||||||
return h
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -46,6 +46,9 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
bouncer.AdminAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete)
|
bouncer.AdminAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete)
|
||||||
h.PathPrefix("/registries/{id}/v2").Handler(
|
h.PathPrefix("/registries/{id}/v2").Handler(
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI)))
|
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
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -41,7 +41,7 @@ func (handler *Handler) proxyRequestsToRegistryAPI(w http.ResponseWriter, r *htt
|
||||||
if proxy == nil {
|
if proxy == nil {
|
||||||
proxy, err = handler.ProxyManager.CreateExtensionProxy(portainer.RegistryManagementExtension)
|
proxy, err = handler.ProxyManager.CreateExtensionProxy(portainer.RegistryManagementExtension)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to register registry proxy", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create extension proxy for registry manager", err}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
23
api/http/handler/registries/proxy_gitlab.go
Normal file
23
api/http/handler/registries/proxy_gitlab.go
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
package registries
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
)
|
||||||
|
|
||||||
|
// request on /api/registries/proxies/gitlab
|
||||||
|
func (handler *Handler) proxyRequestsToGitlabAPIWithoutRegistry(w http.ResponseWriter, r *http.Request) *httperror.HandlerError {
|
||||||
|
domain := r.Header.Get("X-Gitlab-Domain")
|
||||||
|
if domain == "" {
|
||||||
|
return &httperror.HandlerError{http.StatusBadRequest, "No Gitlab domain provided", nil}
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy, err := handler.ProxyManager.CreateGitlabProxy(domain)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to create gitlab proxy", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
http.StripPrefix("/registries/proxies/gitlab", proxy).ServeHTTP(w, r)
|
||||||
|
return nil
|
||||||
|
}
|
66
api/http/handler/registries/proxy_management_gitlab.go
Normal file
66
api/http/handler/registries/proxy_management_gitlab.go
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
package registries
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
"github.com/portainer/libhttp/request"
|
||||||
|
"github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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.RegistryService.Registry(portainer.RegistryID(registryID))
|
||||||
|
if err == portainer.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", portainer.ErrEndpointAccessDenied}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension, err := handler.ExtensionService.Extension(portainer.RegistryManagementExtension)
|
||||||
|
if err == portainer.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
|
||||||
|
}
|
|
@ -12,11 +12,12 @@ import (
|
||||||
|
|
||||||
type registryCreatePayload struct {
|
type registryCreatePayload struct {
|
||||||
Name string
|
Name string
|
||||||
Type int
|
Type portainer.RegistryType
|
||||||
URL string
|
URL string
|
||||||
Authentication bool
|
Authentication bool
|
||||||
Username string
|
Username string
|
||||||
Password string
|
Password string
|
||||||
|
Gitlab portainer.GitlabRegistryData
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *registryCreatePayload) Validate(r *http.Request) error {
|
func (payload *registryCreatePayload) Validate(r *http.Request) error {
|
||||||
|
@ -29,8 +30,8 @@ func (payload *registryCreatePayload) Validate(r *http.Request) error {
|
||||||
if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) {
|
if payload.Authentication && (govalidator.IsNull(payload.Username) || govalidator.IsNull(payload.Password)) {
|
||||||
return portainer.Error("Invalid credentials. Username and password must be specified when authentication is enabled")
|
return portainer.Error("Invalid credentials. Username and password must be specified when authentication is enabled")
|
||||||
}
|
}
|
||||||
if payload.Type != 1 && payload.Type != 2 && payload.Type != 3 {
|
if payload.Type != portainer.QuayRegistry && payload.Type != portainer.AzureRegistry && payload.Type != portainer.CustomRegistry && payload.Type != portainer.GitlabRegistry {
|
||||||
return portainer.Error("Invalid registry type. Valid values are: 1 (Quay.io), 2 (Azure container registry) or 3 (custom registry)")
|
return portainer.Error("Invalid registry type. Valid values are: 1 (Quay.io), 2 (Azure container registry), 3 (custom registry) or 4 (Gitlab registry)")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -42,16 +43,6 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
registries, err := handler.RegistryService.Registries()
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve registries from the database", err}
|
|
||||||
}
|
|
||||||
for _, r := range registries {
|
|
||||||
if r.URL == payload.URL {
|
|
||||||
return &httperror.HandlerError{http.StatusConflict, "A registry with the same URL already exists", portainer.ErrRegistryAlreadyExists}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
registry := &portainer.Registry{
|
registry := &portainer.Registry{
|
||||||
Type: portainer.RegistryType(payload.Type),
|
Type: portainer.RegistryType(payload.Type),
|
||||||
Name: payload.Name,
|
Name: payload.Name,
|
||||||
|
@ -61,6 +52,7 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) *
|
||||||
Password: payload.Password,
|
Password: payload.Password,
|
||||||
UserAccessPolicies: portainer.UserAccessPolicies{},
|
UserAccessPolicies: portainer.UserAccessPolicies{},
|
||||||
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
TeamAccessPolicies: portainer.TeamAccessPolicies{},
|
||||||
|
Gitlab: payload.Gitlab,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.RegistryService.CreateRegistry(registry)
|
err = handler.RegistryService.CreateRegistry(registry)
|
||||||
|
|
|
@ -21,11 +21,10 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
Router: mux.NewRouter(),
|
Router: mux.NewRouter(),
|
||||||
}
|
}
|
||||||
h.Handle("/resource_controls",
|
h.Handle("/resource_controls",
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.resourceControlCreate))).Methods(http.MethodPost)
|
bouncer.AdminAccess(httperror.LoggerHandler(h.resourceControlCreate))).Methods(http.MethodPost)
|
||||||
h.Handle("/resource_controls/{id}",
|
h.Handle("/resource_controls/{id}",
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.resourceControlUpdate))).Methods(http.MethodPut)
|
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.resourceControlUpdate))).Methods(http.MethodPut)
|
||||||
h.Handle("/resource_controls/{id}",
|
h.Handle("/resource_controls/{id}",
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.resourceControlDelete))).Methods(http.MethodDelete)
|
bouncer.AdminAccess(httperror.LoggerHandler(h.resourceControlDelete))).Methods(http.MethodDelete)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package resourcecontrols
|
package resourcecontrols
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
|
@ -8,29 +9,33 @@ import (
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type resourceControlCreatePayload struct {
|
type resourceControlCreatePayload struct {
|
||||||
ResourceID string
|
ResourceID string
|
||||||
Type string
|
Type string
|
||||||
Public bool
|
Public bool
|
||||||
Users []int
|
AdministratorsOnly bool
|
||||||
Teams []int
|
Users []int
|
||||||
SubResourceIDs []string
|
Teams []int
|
||||||
|
SubResourceIDs []string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *resourceControlCreatePayload) Validate(r *http.Request) error {
|
func (payload *resourceControlCreatePayload) Validate(r *http.Request) error {
|
||||||
if govalidator.IsNull(payload.ResourceID) {
|
if govalidator.IsNull(payload.ResourceID) {
|
||||||
return portainer.Error("Invalid resource identifier")
|
return errors.New("invalid payload: invalid resource identifier")
|
||||||
}
|
}
|
||||||
|
|
||||||
if govalidator.IsNull(payload.Type) {
|
if govalidator.IsNull(payload.Type) {
|
||||||
return portainer.Error("Invalid type")
|
return errors.New("invalid payload: invalid type")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public {
|
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public && !payload.AdministratorsOnly {
|
||||||
return portainer.Error("Invalid resource control declaration. Must specify Users, Teams or Public")
|
return errors.New("invalid payload: must specify Users, Teams, Public or AdministratorsOnly")
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.Public && payload.AdministratorsOnly {
|
||||||
|
return errors.New("invalid payload: cannot set both public and administrators only flags to true")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -63,8 +68,8 @@ func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Req
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid type value. Value must be one of: container, service, volume, network, secret, stack or config", portainer.ErrInvalidResourceControlType}
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid type value. Value must be one of: container, service, volume, network, secret, stack or config", portainer.ErrInvalidResourceControlType}
|
||||||
}
|
}
|
||||||
|
|
||||||
rc, err := handler.ResourceControlService.ResourceControlByResourceID(payload.ResourceID)
|
rc, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(payload.ResourceID, resourceControlType)
|
||||||
if err != nil && err != portainer.ErrObjectNotFound {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve resource controls from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve resource controls from the database", err}
|
||||||
}
|
}
|
||||||
if rc != nil {
|
if rc != nil {
|
||||||
|
@ -90,21 +95,13 @@ func (handler *Handler) resourceControlCreate(w http.ResponseWriter, r *http.Req
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceControl := portainer.ResourceControl{
|
resourceControl := portainer.ResourceControl{
|
||||||
ResourceID: payload.ResourceID,
|
ResourceID: payload.ResourceID,
|
||||||
SubResourceIDs: payload.SubResourceIDs,
|
SubResourceIDs: payload.SubResourceIDs,
|
||||||
Type: resourceControlType,
|
Type: resourceControlType,
|
||||||
Public: payload.Public,
|
Public: payload.Public,
|
||||||
UserAccesses: userAccesses,
|
AdministratorsOnly: payload.AdministratorsOnly,
|
||||||
TeamAccesses: teamAccesses,
|
UserAccesses: userAccesses,
|
||||||
}
|
TeamAccesses: teamAccesses,
|
||||||
|
|
||||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !security.AuthorizedResourceControlCreation(&resourceControl, securityContext) {
|
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to create a resource control for the specified resource", portainer.ErrResourceAccessDenied}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.ResourceControlService.CreateResourceControl(&resourceControl)
|
err = handler.ResourceControlService.CreateResourceControl(&resourceControl)
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// DELETE request on /api/resource_controls/:id
|
// DELETE request on /api/resource_controls/:id
|
||||||
|
@ -17,22 +16,13 @@ func (handler *Handler) resourceControlDelete(w http.ResponseWriter, r *http.Req
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid resource control identifier route variable", err}
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid resource control identifier route variable", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceControl, err := handler.ResourceControlService.ResourceControl(portainer.ResourceControlID(resourceControlID))
|
_, err = handler.ResourceControlService.ResourceControl(portainer.ResourceControlID(resourceControlID))
|
||||||
if err == portainer.ErrObjectNotFound {
|
if err == portainer.ErrObjectNotFound {
|
||||||
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a resource control with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusNotFound, "Unable to find a resource control with the specified identifier inside the database", err}
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a resource control with with the specified identifier inside the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a resource control with with the specified identifier inside the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
|
||||||
if err != nil {
|
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !security.AuthorizedResourceControlDeletion(resourceControl, securityContext) {
|
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to delete the resource control", portainer.ErrResourceAccessDenied}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = handler.ResourceControlService.DeleteResourceControl(portainer.ResourceControlID(resourceControlID))
|
err = handler.ResourceControlService.DeleteResourceControl(portainer.ResourceControlID(resourceControlID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the resource control from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the resource control from the database", err}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package resourcecontrols
|
package resourcecontrols
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
@ -11,14 +12,19 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type resourceControlUpdatePayload struct {
|
type resourceControlUpdatePayload struct {
|
||||||
Public bool
|
Public bool
|
||||||
Users []int
|
Users []int
|
||||||
Teams []int
|
Teams []int
|
||||||
|
AdministratorsOnly bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (payload *resourceControlUpdatePayload) Validate(r *http.Request) error {
|
func (payload *resourceControlUpdatePayload) Validate(r *http.Request) error {
|
||||||
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public {
|
if len(payload.Users) == 0 && len(payload.Teams) == 0 && !payload.Public && !payload.AdministratorsOnly {
|
||||||
return portainer.Error("Invalid resource control declaration. Must specify Users, Teams or Public")
|
return errors.New("invalid payload: must specify Users, Teams, Public or AdministratorsOnly")
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.Public && payload.AdministratorsOnly {
|
||||||
|
return errors.New("invalid payload: cannot set public and administrators only")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -49,10 +55,11 @@ func (handler *Handler) resourceControlUpdate(w http.ResponseWriter, r *http.Req
|
||||||
}
|
}
|
||||||
|
|
||||||
if !security.AuthorizedResourceControlAccess(resourceControl, securityContext) {
|
if !security.AuthorizedResourceControlAccess(resourceControl, securityContext) {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to update the resource control", portainer.ErrResourceAccessDenied}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access the resource control", portainer.ErrResourceAccessDenied}
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceControl.Public = payload.Public
|
resourceControl.Public = payload.Public
|
||||||
|
resourceControl.AdministratorsOnly = payload.AdministratorsOnly
|
||||||
|
|
||||||
var userAccesses = make([]portainer.UserResourceAccess, 0)
|
var userAccesses = make([]portainer.UserResourceAccess, 0)
|
||||||
for _, v := range payload.Users {
|
for _, v := range payload.Users {
|
||||||
|
|
|
@ -4,18 +4,25 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"path"
|
"path"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// this is coming from libcompose
|
||||||
|
// https://github.com/portainer/libcompose/blob/master/project/context.go#L117-L120
|
||||||
|
func normalizeStackName(name string) string {
|
||||||
|
r := regexp.MustCompile("[^a-z0-9]+")
|
||||||
|
return r.ReplaceAllString(strings.ToLower(name), "")
|
||||||
|
}
|
||||||
|
|
||||||
type composeStackFromFileContentPayload struct {
|
type composeStackFromFileContentPayload struct {
|
||||||
Name string
|
Name string
|
||||||
StackFileContent string
|
StackFileContent string
|
||||||
|
@ -26,13 +33,14 @@ func (payload *composeStackFromFileContentPayload) Validate(r *http.Request) err
|
||||||
if govalidator.IsNull(payload.Name) {
|
if govalidator.IsNull(payload.Name) {
|
||||||
return portainer.Error("Invalid stack name")
|
return portainer.Error("Invalid stack name")
|
||||||
}
|
}
|
||||||
|
payload.Name = normalizeStackName(payload.Name)
|
||||||
if govalidator.IsNull(payload.StackFileContent) {
|
if govalidator.IsNull(payload.StackFileContent) {
|
||||||
return portainer.Error("Invalid stack file content")
|
return portainer.Error("Invalid stack file content")
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||||
var payload composeStackFromFileContentPayload
|
var payload composeStackFromFileContentPayload
|
||||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -86,7 +94,7 @@ func (handler *Handler) createComposeStackFromFileContent(w http.ResponseWriter,
|
||||||
}
|
}
|
||||||
|
|
||||||
doCleanUp = false
|
doCleanUp = false
|
||||||
return response.JSON(w, stack)
|
return handler.decorateStackResponse(w, stack, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type composeStackFromGitRepositoryPayload struct {
|
type composeStackFromGitRepositoryPayload struct {
|
||||||
|
@ -104,6 +112,7 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e
|
||||||
if govalidator.IsNull(payload.Name) {
|
if govalidator.IsNull(payload.Name) {
|
||||||
return portainer.Error("Invalid stack name")
|
return portainer.Error("Invalid stack name")
|
||||||
}
|
}
|
||||||
|
payload.Name = normalizeStackName(payload.Name)
|
||||||
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
|
if govalidator.IsNull(payload.RepositoryURL) || !govalidator.IsURL(payload.RepositoryURL) {
|
||||||
return portainer.Error("Invalid repository URL. Must correspond to a valid URL format")
|
return portainer.Error("Invalid repository URL. Must correspond to a valid URL format")
|
||||||
}
|
}
|
||||||
|
@ -116,7 +125,7 @@ func (payload *composeStackFromGitRepositoryPayload) Validate(r *http.Request) e
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||||
var payload composeStackFromGitRepositoryPayload
|
var payload composeStackFromGitRepositoryPayload
|
||||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -180,7 +189,7 @@ func (handler *Handler) createComposeStackFromGitRepository(w http.ResponseWrite
|
||||||
}
|
}
|
||||||
|
|
||||||
doCleanUp = false
|
doCleanUp = false
|
||||||
return response.JSON(w, stack)
|
return handler.decorateStackResponse(w, stack, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type composeStackFromFileUploadPayload struct {
|
type composeStackFromFileUploadPayload struct {
|
||||||
|
@ -194,7 +203,7 @@ func (payload *composeStackFromFileUploadPayload) Validate(r *http.Request) erro
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return portainer.Error("Invalid stack name")
|
return portainer.Error("Invalid stack name")
|
||||||
}
|
}
|
||||||
payload.Name = name
|
payload.Name = normalizeStackName(name)
|
||||||
|
|
||||||
composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file")
|
composeFileContent, _, err := request.RetrieveMultiPartFormFile(r, "file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -211,7 +220,7 @@ func (payload *composeStackFromFileUploadPayload) Validate(r *http.Request) erro
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||||
payload := &composeStackFromFileUploadPayload{}
|
payload := &composeStackFromFileUploadPayload{}
|
||||||
err := payload.Validate(r)
|
err := payload.Validate(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -265,7 +274,7 @@ func (handler *Handler) createComposeStackFromFileUpload(w http.ResponseWriter,
|
||||||
}
|
}
|
||||||
|
|
||||||
doCleanUp = false
|
doCleanUp = false
|
||||||
return response.JSON(w, stack)
|
return handler.decorateStackResponse(w, stack, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type composeStackDeploymentConfig struct {
|
type composeStackDeploymentConfig struct {
|
||||||
|
|
|
@ -10,7 +10,6 @@ import (
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/filesystem"
|
"github.com/portainer/portainer/api/filesystem"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
@ -36,7 +35,7 @@ func (payload *swarmStackFromFileContentPayload) Validate(r *http.Request) error
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||||
var payload swarmStackFromFileContentPayload
|
var payload swarmStackFromFileContentPayload
|
||||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -91,7 +90,7 @@ func (handler *Handler) createSwarmStackFromFileContent(w http.ResponseWriter, r
|
||||||
}
|
}
|
||||||
|
|
||||||
doCleanUp = false
|
doCleanUp = false
|
||||||
return response.JSON(w, stack)
|
return handler.decorateStackResponse(w, stack, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type swarmStackFromGitRepositoryPayload struct {
|
type swarmStackFromGitRepositoryPayload struct {
|
||||||
|
@ -125,7 +124,7 @@ func (payload *swarmStackFromGitRepositoryPayload) Validate(r *http.Request) err
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||||
var payload swarmStackFromGitRepositoryPayload
|
var payload swarmStackFromGitRepositoryPayload
|
||||||
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
err := request.DecodeAndValidateJSONPayload(r, &payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -190,7 +189,7 @@ func (handler *Handler) createSwarmStackFromGitRepository(w http.ResponseWriter,
|
||||||
}
|
}
|
||||||
|
|
||||||
doCleanUp = false
|
doCleanUp = false
|
||||||
return response.JSON(w, stack)
|
return handler.decorateStackResponse(w, stack, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type swarmStackFromFileUploadPayload struct {
|
type swarmStackFromFileUploadPayload struct {
|
||||||
|
@ -228,7 +227,7 @@ func (payload *swarmStackFromFileUploadPayload) Validate(r *http.Request) error
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r *http.Request, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||||
payload := &swarmStackFromFileUploadPayload{}
|
payload := &swarmStackFromFileUploadPayload{}
|
||||||
err := payload.Validate(r)
|
err := payload.Validate(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -283,7 +282,7 @@ func (handler *Handler) createSwarmStackFromFileUpload(w http.ResponseWriter, r
|
||||||
}
|
}
|
||||||
|
|
||||||
doCleanUp = false
|
doCleanUp = false
|
||||||
return response.JSON(w, stack)
|
return handler.decorateStackResponse(w, stack, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
type swarmStackDeploymentConfig struct {
|
type swarmStackDeploymentConfig struct {
|
||||||
|
|
|
@ -26,6 +26,8 @@ type Handler struct {
|
||||||
SwarmStackManager portainer.SwarmStackManager
|
SwarmStackManager portainer.SwarmStackManager
|
||||||
ComposeStackManager portainer.ComposeStackManager
|
ComposeStackManager portainer.ComposeStackManager
|
||||||
SettingsService portainer.SettingsService
|
SettingsService portainer.SettingsService
|
||||||
|
UserService portainer.UserService
|
||||||
|
ExtensionService portainer.ExtensionService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewHandler creates a handler to manage stack operations.
|
// NewHandler creates a handler to manage stack operations.
|
||||||
|
@ -52,3 +54,36 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler {
|
||||||
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackMigrate))).Methods(http.MethodPost)
|
bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.stackMigrate))).Methods(http.MethodPost)
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) userCanAccessStack(securityContext *security.RestrictedRequestContext, endpointID portainer.EndpointID, resourceControl *portainer.ResourceControl) (bool, error) {
|
||||||
|
if securityContext.IsAdmin {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
userTeamIDs := make([]portainer.TeamID, 0)
|
||||||
|
for _, membership := range securityContext.UserMemberships {
|
||||||
|
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resourceControl != nil && portainer.UserCanAccessResource(securityContext.UserID, userTeamIDs, resourceControl) {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := handler.ExtensionService.Extension(portainer.RBACExtension)
|
||||||
|
if err == portainer.ErrObjectNotFound {
|
||||||
|
return false, nil
|
||||||
|
} else if err != nil && err != portainer.ErrObjectNotFound {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := handler.UserService.User(securityContext.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := user.EndpointAuthorizations[endpointID][portainer.EndpointResourcesAccess]
|
||||||
|
if ok {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
|
@ -5,12 +5,13 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/compose/types"
|
|
||||||
|
|
||||||
"github.com/docker/cli/cli/compose/loader"
|
"github.com/docker/cli/cli/compose/loader"
|
||||||
|
"github.com/docker/cli/cli/compose/types"
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (handler *Handler) cleanUp(stack *portainer.Stack, doCleanUp *bool) error {
|
func (handler *Handler) cleanUp(stack *portainer.Stack, doCleanUp *bool) error {
|
||||||
|
@ -54,38 +55,43 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tokenData, err := security.RetrieveTokenData(r)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user details from authentication token", err}
|
||||||
|
}
|
||||||
|
|
||||||
switch portainer.StackType(stackType) {
|
switch portainer.StackType(stackType) {
|
||||||
case portainer.DockerSwarmStack:
|
case portainer.DockerSwarmStack:
|
||||||
return handler.createSwarmStack(w, r, method, endpoint)
|
return handler.createSwarmStack(w, r, method, endpoint, tokenData.ID)
|
||||||
case portainer.DockerComposeStack:
|
case portainer.DockerComposeStack:
|
||||||
return handler.createComposeStack(w, r, method, endpoint)
|
return handler.createComposeStack(w, r, method, endpoint, tokenData.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: type. Value must be one of: 1 (Swarm stack) or 2 (Compose stack)", errors.New(request.ErrInvalidQueryParameter)}
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: type. Value must be one of: 1 (Swarm stack) or 2 (Compose stack)", errors.New(request.ErrInvalidQueryParameter)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (handler *Handler) createComposeStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
func (handler *Handler) createComposeStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||||
|
|
||||||
switch method {
|
switch method {
|
||||||
case "string":
|
case "string":
|
||||||
return handler.createComposeStackFromFileContent(w, r, endpoint)
|
return handler.createComposeStackFromFileContent(w, r, endpoint, userID)
|
||||||
case "repository":
|
case "repository":
|
||||||
return handler.createComposeStackFromGitRepository(w, r, endpoint)
|
return handler.createComposeStackFromGitRepository(w, r, endpoint, userID)
|
||||||
case "file":
|
case "file":
|
||||||
return handler.createComposeStackFromFileUpload(w, r, endpoint)
|
return handler.createComposeStackFromFileUpload(w, r, endpoint, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)}
|
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) createSwarmStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint) *httperror.HandlerError {
|
func (handler *Handler) createSwarmStack(w http.ResponseWriter, r *http.Request, method string, endpoint *portainer.Endpoint, userID portainer.UserID) *httperror.HandlerError {
|
||||||
switch method {
|
switch method {
|
||||||
case "string":
|
case "string":
|
||||||
return handler.createSwarmStackFromFileContent(w, r, endpoint)
|
return handler.createSwarmStackFromFileContent(w, r, endpoint, userID)
|
||||||
case "repository":
|
case "repository":
|
||||||
return handler.createSwarmStackFromGitRepository(w, r, endpoint)
|
return handler.createSwarmStackFromGitRepository(w, r, endpoint, userID)
|
||||||
case "file":
|
case "file":
|
||||||
return handler.createSwarmStackFromFileUpload(w, r, endpoint)
|
return handler.createSwarmStackFromFileUpload(w, r, endpoint, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)}
|
return &httperror.HandlerError{http.StatusBadRequest, "Invalid value for query parameter: method. Value must be one of: string, repository or file", errors.New(request.ErrInvalidQueryParameter)}
|
||||||
|
@ -125,3 +131,15 @@ func (handler *Handler) isValidStackFile(stackFileContent []byte) (bool, error)
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (handler *Handler) decorateStackResponse(w http.ResponseWriter, stack *portainer.Stack, userID portainer.UserID) *httperror.HandlerError {
|
||||||
|
resourceControl := portainer.NewPrivateResourceControl(stack.Name, portainer.StackResourceControl, userID)
|
||||||
|
|
||||||
|
err := handler.ResourceControlService.CreateResourceControl(resourceControl)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist resource control inside the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
stack.ResourceControl = resourceControl
|
||||||
|
return response.JSON(w, stack)
|
||||||
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
@ -64,8 +63,8 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||||
if err != nil && err != portainer.ErrObjectNotFound {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,10 +73,12 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !securityContext.IsAdmin {
|
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||||
if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
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", portainer.ErrResourceAccessDenied}
|
||||||
}
|
}
|
||||||
|
|
||||||
err = handler.deleteStack(stack, endpoint)
|
err = handler.deleteStack(stack, endpoint)
|
||||||
|
@ -90,6 +91,13 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the stack from the database", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the stack from the database", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if resourceControl != nil {
|
||||||
|
err = handler.ResourceControlService.DeleteResourceControl(resourceControl.ID)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove the associated resource control from the database", err}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
err = handler.FileService.RemoveDirectory(stack.ProjectPath)
|
err = handler.FileService.RemoveDirectory(stack.ProjectPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove stack files from disk", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to remove stack files from disk", err}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import (
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -42,8 +41,8 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||||
if err != nil && err != portainer.ErrObjectNotFound {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,17 +51,12 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
extendedStack := proxy.ExtendedStack{*stack, portainer.ResourceControl{}}
|
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||||
if !securityContext.IsAdmin && resourceControl == nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||||
}
|
}
|
||||||
|
if !access {
|
||||||
if resourceControl != nil {
|
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||||
if securityContext.IsAdmin || proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
|
||||||
extendedStack.ResourceControl = *resourceControl
|
|
||||||
} else {
|
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, stack.EntryPoint))
|
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, stack.EntryPoint))
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -37,28 +36,27 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||||
if err != nil && err != portainer.ErrObjectNotFound {
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user info from request context", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||||
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to verify user authorizations to validate stack access", err}
|
||||||
}
|
}
|
||||||
|
if !access {
|
||||||
extendedStack := proxy.ExtendedStack{*stack, portainer.ResourceControl{}}
|
|
||||||
if !securityContext.IsAdmin && resourceControl == nil {
|
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
||||||
}
|
}
|
||||||
|
|
||||||
if resourceControl != nil {
|
if resourceControl != nil {
|
||||||
if securityContext.IsAdmin || proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
stack.ResourceControl = resourceControl
|
||||||
extendedStack.ResourceControl = *resourceControl
|
|
||||||
} else {
|
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(w, extendedStack)
|
return response.JSON(w, stack)
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -40,10 +39,31 @@ func (handler *Handler) stackList(w http.ResponseWriter, r *http.Request) *httpe
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
filteredStacks := proxy.FilterStacks(stacks, resourceControls, securityContext.IsAdmin,
|
stacks = portainer.DecorateStacks(stacks, resourceControls)
|
||||||
securityContext.UserID, securityContext.UserMemberships)
|
|
||||||
|
|
||||||
return response.JSON(w, filteredStacks)
|
if !securityContext.IsAdmin {
|
||||||
|
rbacExtensionEnabled := true
|
||||||
|
_, err := handler.ExtensionService.Extension(portainer.RBACExtension)
|
||||||
|
if err == portainer.ErrObjectNotFound {
|
||||||
|
rbacExtensionEnabled = false
|
||||||
|
} else if err != nil && err != portainer.ErrObjectNotFound {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to check if RBAC extension is enabled", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := handler.UserService.User(securityContext.UserID)
|
||||||
|
if err != nil {
|
||||||
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve user information from the database", err}
|
||||||
|
}
|
||||||
|
|
||||||
|
userTeamIDs := make([]portainer.TeamID, 0)
|
||||||
|
for _, membership := range securityContext.UserMemberships {
|
||||||
|
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
||||||
|
}
|
||||||
|
|
||||||
|
stacks = portainer.FilterAuthorizedStacks(stacks, user, userTeamIDs, rbacExtensionEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.JSON(w, stacks)
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterStacks(stacks []portainer.Stack, filters *stackListOperationFilters) []portainer.Stack {
|
func filterStacks(stacks []portainer.Stack, filters *stackListOperationFilters) []portainer.Stack {
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"github.com/portainer/libhttp/request"
|
"github.com/portainer/libhttp/request"
|
||||||
"github.com/portainer/libhttp/response"
|
"github.com/portainer/libhttp/response"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -56,8 +55,8 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||||
if err != nil && err != portainer.ErrObjectNotFound {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -66,10 +65,12 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !securityContext.IsAdmin {
|
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||||
if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
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", portainer.ErrResourceAccessDenied}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
|
// TODO: this is a work-around for stacks created with Portainer version >= 1.17.1
|
||||||
|
|
|
@ -4,7 +4,6 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/portainer/portainer/api/http/proxy"
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
|
||||||
"github.com/asaskevich/govalidator"
|
"github.com/asaskevich/govalidator"
|
||||||
|
@ -76,8 +75,8 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceIDAndType(stack.Name, portainer.StackResourceControl)
|
||||||
if err != nil && err != portainer.ErrObjectNotFound {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -86,10 +85,12 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt
|
||||||
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !securityContext.IsAdmin {
|
access, err := handler.userCanAccessStack(securityContext, endpoint.ID, resourceControl)
|
||||||
if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
if err != nil {
|
||||||
return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied}
|
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", portainer.ErrResourceAccessDenied}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateError := handler.updateAndDeployStack(r, stack, endpoint)
|
updateError := handler.updateAndDeployStack(r, stack, endpoint)
|
||||||
|
|
|
@ -1,166 +0,0 @@
|
||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
type (
|
|
||||||
// ExtendedStack represents a stack combined with its associated access control
|
|
||||||
ExtendedStack struct {
|
|
||||||
portainer.Stack
|
|
||||||
ResourceControl portainer.ResourceControl `json:"ResourceControl"`
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
// applyResourceAccessControlFromLabel returns an optionally decorated object as the first return value and the
|
|
||||||
// access level for the user (granted or denied) as the second return value.
|
|
||||||
// It will retrieve an identifier from the labels object. If an identifier exists, it will check for
|
|
||||||
// an existing resource control associated to it.
|
|
||||||
// Returns a decorated object and authorized access (true) when a resource control is found and the user can access the resource.
|
|
||||||
// Returns the original object and denied access (false) when no resource control is found.
|
|
||||||
// Returns the original object and denied access (false) when a resource control is found and the user cannot access the resource.
|
|
||||||
func applyResourceAccessControlFromLabel(labelsObject, resourceObject map[string]interface{}, labelIdentifier string,
|
|
||||||
context *restrictedDockerOperationContext) (map[string]interface{}, bool) {
|
|
||||||
|
|
||||||
if labelsObject != nil && labelsObject[labelIdentifier] != nil {
|
|
||||||
resourceIdentifier := labelsObject[labelIdentifier].(string)
|
|
||||||
return applyResourceAccessControl(resourceObject, resourceIdentifier, context)
|
|
||||||
}
|
|
||||||
return resourceObject, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// applyResourceAccessControl returns an optionally decorated object as the first return value and the
|
|
||||||
// access level for the user (granted or denied) as the second return value.
|
|
||||||
// Returns a decorated object and authorized access (true) when a resource control is found to the specified resource
|
|
||||||
// identifier and the user can access the resource.
|
|
||||||
// Returns the original object and authorized access (false) when no resource control is found for the specified
|
|
||||||
// resource identifier.
|
|
||||||
// Returns the original object and denied access (false) when a resource control is associated to the resource
|
|
||||||
// and the user cannot access the resource.
|
|
||||||
func applyResourceAccessControl(resourceObject map[string]interface{}, resourceIdentifier string,
|
|
||||||
context *restrictedDockerOperationContext) (map[string]interface{}, bool) {
|
|
||||||
|
|
||||||
resourceControl := getResourceControlByResourceID(resourceIdentifier, context.resourceControls)
|
|
||||||
if resourceControl == nil {
|
|
||||||
return resourceObject, context.isAdmin || context.endpointResourceAccess
|
|
||||||
}
|
|
||||||
|
|
||||||
if context.isAdmin || context.endpointResourceAccess || resourceControl.Public || canUserAccessResource(context.userID, context.userTeamIDs, resourceControl) {
|
|
||||||
resourceObject = decorateObject(resourceObject, resourceControl)
|
|
||||||
return resourceObject, true
|
|
||||||
}
|
|
||||||
|
|
||||||
return resourceObject, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// decorateResourceWithAccessControlFromLabel will retrieve an identifier from the labels object. If an identifier exists,
|
|
||||||
// it will check for an existing resource control associated to it. If a resource control is found, the resource object will be
|
|
||||||
// decorated. If no identifier can be found in the labels or no resource control is associated to the identifier, the resource
|
|
||||||
// object will not be changed.
|
|
||||||
func decorateResourceWithAccessControlFromLabel(labelsObject, resourceObject map[string]interface{}, labelIdentifier string,
|
|
||||||
resourceControls []portainer.ResourceControl) map[string]interface{} {
|
|
||||||
|
|
||||||
if labelsObject != nil && labelsObject[labelIdentifier] != nil {
|
|
||||||
resourceIdentifier := labelsObject[labelIdentifier].(string)
|
|
||||||
resourceObject = decorateResourceWithAccessControl(resourceObject, resourceIdentifier, resourceControls)
|
|
||||||
}
|
|
||||||
|
|
||||||
return resourceObject
|
|
||||||
}
|
|
||||||
|
|
||||||
// decorateResourceWithAccessControl will check if a resource control is associated to the specified resource identifier.
|
|
||||||
// If a resource control is found, the resource object will be decorated, otherwise it will not be changed.
|
|
||||||
func decorateResourceWithAccessControl(resourceObject map[string]interface{}, resourceIdentifier string,
|
|
||||||
resourceControls []portainer.ResourceControl) map[string]interface{} {
|
|
||||||
|
|
||||||
resourceControl := getResourceControlByResourceID(resourceIdentifier, resourceControls)
|
|
||||||
if resourceControl != nil {
|
|
||||||
return decorateObject(resourceObject, resourceControl)
|
|
||||||
}
|
|
||||||
return resourceObject
|
|
||||||
}
|
|
||||||
|
|
||||||
func canUserAccessResource(userID portainer.UserID, userTeamIDs []portainer.TeamID, resourceControl *portainer.ResourceControl) bool {
|
|
||||||
for _, authorizedUserAccess := range resourceControl.UserAccesses {
|
|
||||||
if userID == authorizedUserAccess.UserID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, authorizedTeamAccess := range resourceControl.TeamAccesses {
|
|
||||||
for _, userTeamID := range userTeamIDs {
|
|
||||||
if userTeamID == authorizedTeamAccess.TeamID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return resourceControl.Public
|
|
||||||
}
|
|
||||||
|
|
||||||
func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} {
|
|
||||||
if object["Portainer"] == nil {
|
|
||||||
object["Portainer"] = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
portainerMetadata := object["Portainer"].(map[string]interface{})
|
|
||||||
portainerMetadata["ResourceControl"] = resourceControl
|
|
||||||
return object
|
|
||||||
}
|
|
||||||
|
|
||||||
func getResourceControlByResourceID(resourceID string, resourceControls []portainer.ResourceControl) *portainer.ResourceControl {
|
|
||||||
for _, resourceControl := range resourceControls {
|
|
||||||
if resourceID == resourceControl.ResourceID {
|
|
||||||
return &resourceControl
|
|
||||||
}
|
|
||||||
for _, subResourceID := range resourceControl.SubResourceIDs {
|
|
||||||
if resourceID == subResourceID {
|
|
||||||
return &resourceControl
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CanAccessStack checks if a user can access a stack
|
|
||||||
func CanAccessStack(stack *portainer.Stack, resourceControl *portainer.ResourceControl, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
|
||||||
if resourceControl == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
userTeamIDs := make([]portainer.TeamID, 0)
|
|
||||||
for _, membership := range memberships {
|
|
||||||
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if canUserAccessResource(userID, userTeamIDs, resourceControl) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return resourceControl.Public
|
|
||||||
}
|
|
||||||
|
|
||||||
// FilterStacks filters stacks based on user role and resource controls.
|
|
||||||
func FilterStacks(stacks []portainer.Stack, resourceControls []portainer.ResourceControl, isAdmin bool,
|
|
||||||
userID portainer.UserID, memberships []portainer.TeamMembership) []ExtendedStack {
|
|
||||||
|
|
||||||
filteredStacks := make([]ExtendedStack, 0)
|
|
||||||
|
|
||||||
userTeamIDs := make([]portainer.TeamID, 0)
|
|
||||||
for _, membership := range memberships {
|
|
||||||
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, stack := range stacks {
|
|
||||||
extendedStack := ExtendedStack{stack, portainer.ResourceControl{}}
|
|
||||||
resourceControl := getResourceControlByResourceID(stack.Name, resourceControls)
|
|
||||||
if resourceControl == nil && isAdmin {
|
|
||||||
filteredStacks = append(filteredStacks, extendedStack)
|
|
||||||
} else if resourceControl != nil && (isAdmin || resourceControl.Public || canUserAccessResource(userID, userTeamIDs, resourceControl)) {
|
|
||||||
extendedStack.ResourceControl = *resourceControl
|
|
||||||
filteredStacks = append(filteredStacks, extendedStack)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredStacks
|
|
||||||
}
|
|
|
@ -1,107 +0,0 @@
|
||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ErrDockerConfigIdentifierNotFound defines an error raised when Portainer is unable to find a config identifier
|
|
||||||
ErrDockerConfigIdentifierNotFound = portainer.Error("Docker config identifier not found")
|
|
||||||
configIdentifier = "ID"
|
|
||||||
)
|
|
||||||
|
|
||||||
// configListOperation extracts the response as a JSON object, loop through the configs array
|
|
||||||
// decorate and/or filter the configs based on resource controls before rewriting the response
|
|
||||||
func configListOperation(response *http.Response, executor *operationExecutor) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// ConfigList response is a JSON array
|
|
||||||
// https://docs.docker.com/engine/api/v1.30/#operation/ConfigList
|
|
||||||
responseArray, err := getResponseAsJSONArray(response)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
|
|
||||||
responseArray, err = decorateConfigList(responseArray, executor.operationContext.resourceControls)
|
|
||||||
} else {
|
|
||||||
responseArray, err = filterConfigList(responseArray, executor.operationContext)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return rewriteResponse(response, responseArray, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// configInspectOperation extracts the response as a JSON object, verify that the user
|
|
||||||
// has access to the config based on resource control (check are done based on the configID and optional Swarm service ID)
|
|
||||||
// and either rewrite an access denied response or a decorated config.
|
|
||||||
func configInspectOperation(response *http.Response, executor *operationExecutor) error {
|
|
||||||
// ConfigInspect response is a JSON object
|
|
||||||
// https://docs.docker.com/engine/api/v1.30/#operation/ConfigInspect
|
|
||||||
responseObject, err := getResponseAsJSONOBject(response)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if responseObject[configIdentifier] == nil {
|
|
||||||
return ErrDockerConfigIdentifierNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
configID := responseObject[configIdentifier].(string)
|
|
||||||
responseObject, access := applyResourceAccessControl(responseObject, configID, executor.operationContext)
|
|
||||||
if !access {
|
|
||||||
return rewriteAccessDeniedResponse(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// decorateConfigList loops through all configs and decorates any config with an existing resource control.
|
|
||||||
// Resource controls checks are based on: resource identifier.
|
|
||||||
// Config object schema reference: https://docs.docker.com/engine/api/v1.30/#operation/ConfigList
|
|
||||||
func decorateConfigList(configData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
|
|
||||||
decoratedConfigData := make([]interface{}, 0)
|
|
||||||
|
|
||||||
for _, config := range configData {
|
|
||||||
|
|
||||||
configObject := config.(map[string]interface{})
|
|
||||||
if configObject[configIdentifier] == nil {
|
|
||||||
return nil, ErrDockerConfigIdentifierNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
configID := configObject[configIdentifier].(string)
|
|
||||||
configObject = decorateResourceWithAccessControl(configObject, configID, resourceControls)
|
|
||||||
|
|
||||||
decoratedConfigData = append(decoratedConfigData, configObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
return decoratedConfigData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterConfigList loops through all configs and filters public configs (no associated resource control)
|
|
||||||
// as well as authorized configs (access granted to the user based on existing resource control).
|
|
||||||
// Authorized configs are decorated during the process.
|
|
||||||
// Resource controls checks are based on: resource identifier.
|
|
||||||
// Config object schema reference: https://docs.docker.com/engine/api/v1.30/#operation/ConfigList
|
|
||||||
func filterConfigList(configData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
|
|
||||||
filteredConfigData := make([]interface{}, 0)
|
|
||||||
|
|
||||||
for _, config := range configData {
|
|
||||||
configObject := config.(map[string]interface{})
|
|
||||||
if configObject[configIdentifier] == nil {
|
|
||||||
return nil, ErrDockerConfigIdentifierNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
configID := configObject[configIdentifier].(string)
|
|
||||||
configObject, access := applyResourceAccessControl(configObject, configID, context)
|
|
||||||
if access {
|
|
||||||
filteredConfigData = append(filteredConfigData, configObject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredConfigData, nil
|
|
||||||
}
|
|
|
@ -1,204 +0,0 @@
|
||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ErrDockerContainerIdentifierNotFound defines an error raised when Portainer is unable to find a container identifier
|
|
||||||
ErrDockerContainerIdentifierNotFound = portainer.Error("Docker container identifier not found")
|
|
||||||
containerIdentifier = "Id"
|
|
||||||
containerLabelForServiceIdentifier = "com.docker.swarm.service.id"
|
|
||||||
containerLabelForSwarmStackIdentifier = "com.docker.stack.namespace"
|
|
||||||
containerLabelForComposeStackIdentifier = "com.docker.compose.project"
|
|
||||||
)
|
|
||||||
|
|
||||||
// containerListOperation extracts the response as a JSON object, loop through the containers array
|
|
||||||
// decorate and/or filter the containers based on resource controls before rewriting the response
|
|
||||||
func containerListOperation(response *http.Response, executor *operationExecutor) error {
|
|
||||||
var err error
|
|
||||||
// ContainerList response is a JSON array
|
|
||||||
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
|
|
||||||
responseArray, err := getResponseAsJSONArray(response)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
|
|
||||||
responseArray, err = decorateContainerList(responseArray, executor.operationContext.resourceControls)
|
|
||||||
} else {
|
|
||||||
responseArray, err = filterContainerList(responseArray, executor.operationContext)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if executor.labelBlackList != nil {
|
|
||||||
responseArray, err = filterContainersWithBlackListedLabels(responseArray, executor.labelBlackList)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rewriteResponse(response, responseArray, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// containerInspectOperation extracts the response as a JSON object, verify that the user
|
|
||||||
// has access to the container based on resource control (check are done based on the containerID and optional Swarm service ID)
|
|
||||||
// and either rewrite an access denied response or a decorated container.
|
|
||||||
func containerInspectOperation(response *http.Response, executor *operationExecutor) error {
|
|
||||||
// ContainerInspect response is a JSON object
|
|
||||||
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect
|
|
||||||
responseObject, err := getResponseAsJSONOBject(response)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if responseObject[containerIdentifier] == nil {
|
|
||||||
return ErrDockerContainerIdentifierNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
containerID := responseObject[containerIdentifier].(string)
|
|
||||||
responseObject, access := applyResourceAccessControl(responseObject, containerID, executor.operationContext)
|
|
||||||
if access {
|
|
||||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
containerLabels := extractContainerLabelsFromContainerInspectObject(responseObject)
|
|
||||||
responseObject, access = applyResourceAccessControlFromLabel(containerLabels, responseObject, containerLabelForServiceIdentifier, executor.operationContext)
|
|
||||||
if access {
|
|
||||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
responseObject, access = applyResourceAccessControlFromLabel(containerLabels, responseObject, containerLabelForSwarmStackIdentifier, executor.operationContext)
|
|
||||||
if access {
|
|
||||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
responseObject, access = applyResourceAccessControlFromLabel(containerLabels, responseObject, containerLabelForComposeStackIdentifier, executor.operationContext)
|
|
||||||
if access {
|
|
||||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rewriteAccessDeniedResponse(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractContainerLabelsFromContainerInspectObject retrieve the Labels of the container if present.
|
|
||||||
// Container schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect
|
|
||||||
func extractContainerLabelsFromContainerInspectObject(responseObject map[string]interface{}) map[string]interface{} {
|
|
||||||
// Labels are stored under Config.Labels
|
|
||||||
containerConfigObject := extractJSONField(responseObject, "Config")
|
|
||||||
if containerConfigObject != nil {
|
|
||||||
containerLabelsObject := extractJSONField(containerConfigObject, "Labels")
|
|
||||||
return containerLabelsObject
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractContainerLabelsFromContainerListObject retrieve the Labels of the container if present.
|
|
||||||
// Container schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
|
|
||||||
func extractContainerLabelsFromContainerListObject(responseObject map[string]interface{}) map[string]interface{} {
|
|
||||||
// Labels are stored under Labels
|
|
||||||
containerLabelsObject := extractJSONField(responseObject, "Labels")
|
|
||||||
return containerLabelsObject
|
|
||||||
}
|
|
||||||
|
|
||||||
// decorateContainerList loops through all containers and decorates any container with an existing resource control.
|
|
||||||
// Resource controls checks are based on: resource identifier, service identifier (from label), stack identifier (from label).
|
|
||||||
// Container object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
|
|
||||||
func decorateContainerList(containerData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
|
|
||||||
decoratedContainerData := make([]interface{}, 0)
|
|
||||||
|
|
||||||
for _, container := range containerData {
|
|
||||||
|
|
||||||
containerObject := container.(map[string]interface{})
|
|
||||||
if containerObject[containerIdentifier] == nil {
|
|
||||||
return nil, ErrDockerContainerIdentifierNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
containerID := containerObject[containerIdentifier].(string)
|
|
||||||
containerObject = decorateResourceWithAccessControl(containerObject, containerID, resourceControls)
|
|
||||||
|
|
||||||
containerLabels := extractContainerLabelsFromContainerListObject(containerObject)
|
|
||||||
containerObject = decorateResourceWithAccessControlFromLabel(containerLabels, containerObject, containerLabelForServiceIdentifier, resourceControls)
|
|
||||||
containerObject = decorateResourceWithAccessControlFromLabel(containerLabels, containerObject, containerLabelForSwarmStackIdentifier, resourceControls)
|
|
||||||
containerObject = decorateResourceWithAccessControlFromLabel(containerLabels, containerObject, containerLabelForComposeStackIdentifier, resourceControls)
|
|
||||||
|
|
||||||
decoratedContainerData = append(decoratedContainerData, containerObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
return decoratedContainerData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterContainerList loops through all containers and filters public containers (no associated resource control)
|
|
||||||
// as well as authorized containers (access granted to the user based on existing resource control).
|
|
||||||
// Authorized containers are decorated during the process.
|
|
||||||
// Resource controls checks are based on: resource identifier, service identifier (from label), stack identifier (from label).
|
|
||||||
// Container object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
|
|
||||||
func filterContainerList(containerData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
|
|
||||||
filteredContainerData := make([]interface{}, 0)
|
|
||||||
|
|
||||||
for _, container := range containerData {
|
|
||||||
containerObject := container.(map[string]interface{})
|
|
||||||
if containerObject[containerIdentifier] == nil {
|
|
||||||
return nil, ErrDockerContainerIdentifierNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
containerID := containerObject[containerIdentifier].(string)
|
|
||||||
containerObject, access := applyResourceAccessControl(containerObject, containerID, context)
|
|
||||||
if !access {
|
|
||||||
containerLabels := extractContainerLabelsFromContainerListObject(containerObject)
|
|
||||||
containerObject, access = applyResourceAccessControlFromLabel(containerLabels, containerObject, containerLabelForComposeStackIdentifier, context)
|
|
||||||
if !access {
|
|
||||||
containerObject, access = applyResourceAccessControlFromLabel(containerLabels, containerObject, containerLabelForServiceIdentifier, context)
|
|
||||||
if !access {
|
|
||||||
containerObject, access = applyResourceAccessControlFromLabel(containerLabels, containerObject, containerLabelForSwarmStackIdentifier, context)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if access {
|
|
||||||
filteredContainerData = append(filteredContainerData, containerObject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredContainerData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterContainersWithLabels loops through a list of containers, and filters containers that do not contains
|
|
||||||
// any labels in the labels black list.
|
|
||||||
func filterContainersWithBlackListedLabels(containerData []interface{}, labelBlackList []portainer.Pair) ([]interface{}, error) {
|
|
||||||
filteredContainerData := make([]interface{}, 0)
|
|
||||||
|
|
||||||
for _, container := range containerData {
|
|
||||||
containerObject := container.(map[string]interface{})
|
|
||||||
|
|
||||||
containerLabels := extractContainerLabelsFromContainerListObject(containerObject)
|
|
||||||
if containerLabels != nil {
|
|
||||||
if !containerHasBlackListedLabel(containerLabels, labelBlackList) {
|
|
||||||
filteredContainerData = append(filteredContainerData, containerObject)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
filteredContainerData = append(filteredContainerData, containerObject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredContainerData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelBlackList []portainer.Pair) bool {
|
|
||||||
for key, value := range containerLabels {
|
|
||||||
labelName := key
|
|
||||||
labelValue := value.(string)
|
|
||||||
|
|
||||||
for _, blackListedLabel := range labelBlackList {
|
|
||||||
if blackListedLabel.Name == labelName && blackListedLabel.Value == labelValue {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
|
@ -1,600 +0,0 @@
|
||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"path"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
"github.com/portainer/portainer/api/http/security"
|
|
||||||
)
|
|
||||||
|
|
||||||
var apiVersionRe = regexp.MustCompile(`(/v[0-9]\.[0-9]*)?`)
|
|
||||||
|
|
||||||
type (
|
|
||||||
proxyTransport struct {
|
|
||||||
dockerTransport *http.Transport
|
|
||||||
enableSignature bool
|
|
||||||
ResourceControlService portainer.ResourceControlService
|
|
||||||
UserService portainer.UserService
|
|
||||||
TeamMembershipService portainer.TeamMembershipService
|
|
||||||
RegistryService portainer.RegistryService
|
|
||||||
DockerHubService portainer.DockerHubService
|
|
||||||
SettingsService portainer.SettingsService
|
|
||||||
SignatureService portainer.DigitalSignatureService
|
|
||||||
ReverseTunnelService portainer.ReverseTunnelService
|
|
||||||
ExtensionService portainer.ExtensionService
|
|
||||||
endpointIdentifier portainer.EndpointID
|
|
||||||
endpointType portainer.EndpointType
|
|
||||||
}
|
|
||||||
restrictedDockerOperationContext struct {
|
|
||||||
isAdmin bool
|
|
||||||
endpointResourceAccess bool
|
|
||||||
userID portainer.UserID
|
|
||||||
userTeamIDs []portainer.TeamID
|
|
||||||
resourceControls []portainer.ResourceControl
|
|
||||||
}
|
|
||||||
registryAccessContext struct {
|
|
||||||
isAdmin bool
|
|
||||||
userID portainer.UserID
|
|
||||||
teamMemberships []portainer.TeamMembership
|
|
||||||
registries []portainer.Registry
|
|
||||||
dockerHub *portainer.DockerHub
|
|
||||||
}
|
|
||||||
registryAuthenticationHeader struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
Password string `json:"password"`
|
|
||||||
Serveraddress string `json:"serveraddress"`
|
|
||||||
}
|
|
||||||
operationExecutor struct {
|
|
||||||
operationContext *restrictedDockerOperationContext
|
|
||||||
labelBlackList []portainer.Pair
|
|
||||||
}
|
|
||||||
restrictedOperationRequest func(*http.Response, *operationExecutor) error
|
|
||||||
operationRequest func(*http.Request) error
|
|
||||||
)
|
|
||||||
|
|
||||||
func (p *proxyTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
return p.proxyDockerRequest(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *proxyTransport) executeDockerRequest(request *http.Request) (*http.Response, error) {
|
|
||||||
response, err := p.dockerTransport.RoundTrip(request)
|
|
||||||
|
|
||||||
if p.endpointType != portainer.EdgeAgentEnvironment {
|
|
||||||
return response, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
p.ReverseTunnelService.SetTunnelStatusToActive(p.endpointIdentifier)
|
|
||||||
} else {
|
|
||||||
p.ReverseTunnelService.SetTunnelStatusToIdle(p.endpointIdentifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Response, error) {
|
|
||||||
path := apiVersionRe.ReplaceAllString(request.URL.Path, "")
|
|
||||||
request.URL.Path = path
|
|
||||||
|
|
||||||
if p.enableSignature {
|
|
||||||
signature, err := p.SignatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
request.Header.Set(portainer.PortainerAgentPublicKeyHeader, p.SignatureService.EncodedPublicKey())
|
|
||||||
request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(path, "/configs"):
|
|
||||||
return p.proxyConfigRequest(request)
|
|
||||||
case strings.HasPrefix(path, "/containers"):
|
|
||||||
return p.proxyContainerRequest(request)
|
|
||||||
case strings.HasPrefix(path, "/services"):
|
|
||||||
return p.proxyServiceRequest(request)
|
|
||||||
case strings.HasPrefix(path, "/volumes"):
|
|
||||||
return p.proxyVolumeRequest(request)
|
|
||||||
case strings.HasPrefix(path, "/networks"):
|
|
||||||
return p.proxyNetworkRequest(request)
|
|
||||||
case strings.HasPrefix(path, "/secrets"):
|
|
||||||
return p.proxySecretRequest(request)
|
|
||||||
case strings.HasPrefix(path, "/swarm"):
|
|
||||||
return p.proxySwarmRequest(request)
|
|
||||||
case strings.HasPrefix(path, "/nodes"):
|
|
||||||
return p.proxyNodeRequest(request)
|
|
||||||
case strings.HasPrefix(path, "/tasks"):
|
|
||||||
return p.proxyTaskRequest(request)
|
|
||||||
case strings.HasPrefix(path, "/build"):
|
|
||||||
return p.proxyBuildRequest(request)
|
|
||||||
case strings.HasPrefix(path, "/images"):
|
|
||||||
return p.proxyImageRequest(request)
|
|
||||||
case strings.HasPrefix(path, "/v2"):
|
|
||||||
return p.proxyAgentRequest(request)
|
|
||||||
default:
|
|
||||||
return p.executeDockerRequest(request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *proxyTransport) proxyAgentRequest(r *http.Request) (*http.Response, error) {
|
|
||||||
requestPath := strings.TrimPrefix(r.URL.Path, "/v2")
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case strings.HasPrefix(requestPath, "/browse"):
|
|
||||||
volumeIDParameter, found := r.URL.Query()["volumeID"]
|
|
||||||
if !found || len(volumeIDParameter) < 1 {
|
|
||||||
return p.administratorOperation(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.restrictedVolumeBrowserOperation(r, volumeIDParameter[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.executeDockerRequest(r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *proxyTransport) proxyConfigRequest(request *http.Request) (*http.Response, error) {
|
|
||||||
switch requestPath := request.URL.Path; requestPath {
|
|
||||||
case "/configs/create":
|
|
||||||
return p.executeDockerRequest(request)
|
|
||||||
|
|
||||||
case "/configs":
|
|
||||||
return p.rewriteOperation(request, configListOperation)
|
|
||||||
|
|
||||||
default:
|
|
||||||
// assume /configs/{id}
|
|
||||||
if request.Method == http.MethodGet {
|
|
||||||
return p.rewriteOperation(request, configInspectOperation)
|
|
||||||
}
|
|
||||||
configID := path.Base(requestPath)
|
|
||||||
return p.restrictedOperation(request, configID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *proxyTransport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
|
|
||||||
switch requestPath := request.URL.Path; requestPath {
|
|
||||||
case "/containers/create":
|
|
||||||
return p.executeDockerRequest(request)
|
|
||||||
|
|
||||||
case "/containers/prune":
|
|
||||||
return p.administratorOperation(request)
|
|
||||||
|
|
||||||
case "/containers/json":
|
|
||||||
return p.rewriteOperationWithLabelFiltering(request, containerListOperation)
|
|
||||||
|
|
||||||
default:
|
|
||||||
// This section assumes /containers/**
|
|
||||||
if match, _ := path.Match("/containers/*/*", requestPath); match {
|
|
||||||
// Handle /containers/{id}/{action} requests
|
|
||||||
containerID := path.Base(path.Dir(requestPath))
|
|
||||||
action := path.Base(requestPath)
|
|
||||||
|
|
||||||
if action == "json" {
|
|
||||||
return p.rewriteOperation(request, containerInspectOperation)
|
|
||||||
}
|
|
||||||
return p.restrictedOperation(request, containerID)
|
|
||||||
} else if match, _ := path.Match("/containers/*", requestPath); match {
|
|
||||||
// Handle /containers/{id} requests
|
|
||||||
containerID := path.Base(requestPath)
|
|
||||||
return p.restrictedOperation(request, containerID)
|
|
||||||
}
|
|
||||||
return p.executeDockerRequest(request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *proxyTransport) proxyServiceRequest(request *http.Request) (*http.Response, error) {
|
|
||||||
switch requestPath := request.URL.Path; requestPath {
|
|
||||||
case "/services/create":
|
|
||||||
return p.replaceRegistryAuthenticationHeader(request)
|
|
||||||
|
|
||||||
case "/services":
|
|
||||||
return p.rewriteOperation(request, serviceListOperation)
|
|
||||||
|
|
||||||
default:
|
|
||||||
// This section assumes /services/**
|
|
||||||
if match, _ := path.Match("/services/*/*", requestPath); match {
|
|
||||||
// Handle /services/{id}/{action} requests
|
|
||||||
serviceID := path.Base(path.Dir(requestPath))
|
|
||||||
return p.restrictedOperation(request, serviceID)
|
|
||||||
} else if match, _ := path.Match("/services/*", requestPath); match {
|
|
||||||
// Handle /services/{id} requests
|
|
||||||
serviceID := path.Base(requestPath)
|
|
||||||
|
|
||||||
if request.Method == http.MethodGet {
|
|
||||||
return p.rewriteOperation(request, serviceInspectOperation)
|
|
||||||
}
|
|
||||||
return p.restrictedOperation(request, serviceID)
|
|
||||||
}
|
|
||||||
return p.executeDockerRequest(request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *proxyTransport) proxyVolumeRequest(request *http.Request) (*http.Response, error) {
|
|
||||||
switch requestPath := request.URL.Path; requestPath {
|
|
||||||
case "/volumes/create":
|
|
||||||
return p.executeDockerRequest(request)
|
|
||||||
|
|
||||||
case "/volumes/prune":
|
|
||||||
return p.administratorOperation(request)
|
|
||||||
|
|
||||||
case "/volumes":
|
|
||||||
return p.rewriteOperation(request, volumeListOperation)
|
|
||||||
|
|
||||||
default:
|
|
||||||
// assume /volumes/{name}
|
|
||||||
if request.Method == http.MethodGet {
|
|
||||||
return p.rewriteOperation(request, volumeInspectOperation)
|
|
||||||
}
|
|
||||||
volumeID := path.Base(requestPath)
|
|
||||||
return p.restrictedOperation(request, volumeID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *proxyTransport) proxyNetworkRequest(request *http.Request) (*http.Response, error) {
|
|
||||||
switch requestPath := request.URL.Path; requestPath {
|
|
||||||
case "/networks/create":
|
|
||||||
return p.executeDockerRequest(request)
|
|
||||||
|
|
||||||
case "/networks":
|
|
||||||
return p.rewriteOperation(request, networkListOperation)
|
|
||||||
|
|
||||||
default:
|
|
||||||
// assume /networks/{id}
|
|
||||||
if request.Method == http.MethodGet {
|
|
||||||
return p.rewriteOperation(request, networkInspectOperation)
|
|
||||||
}
|
|
||||||
networkID := path.Base(requestPath)
|
|
||||||
return p.restrictedOperation(request, networkID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *proxyTransport) proxySecretRequest(request *http.Request) (*http.Response, error) {
|
|
||||||
switch requestPath := request.URL.Path; requestPath {
|
|
||||||
case "/secrets/create":
|
|
||||||
return p.executeDockerRequest(request)
|
|
||||||
|
|
||||||
case "/secrets":
|
|
||||||
return p.rewriteOperation(request, secretListOperation)
|
|
||||||
|
|
||||||
default:
|
|
||||||
// assume /secrets/{id}
|
|
||||||
if request.Method == http.MethodGet {
|
|
||||||
return p.rewriteOperation(request, secretInspectOperation)
|
|
||||||
}
|
|
||||||
secretID := path.Base(requestPath)
|
|
||||||
return p.restrictedOperation(request, secretID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *proxyTransport) proxyNodeRequest(request *http.Request) (*http.Response, error) {
|
|
||||||
requestPath := request.URL.Path
|
|
||||||
|
|
||||||
// assume /nodes/{id}
|
|
||||||
if path.Base(requestPath) != "nodes" {
|
|
||||||
return p.administratorOperation(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.executeDockerRequest(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *proxyTransport) proxySwarmRequest(request *http.Request) (*http.Response, error) {
|
|
||||||
switch requestPath := request.URL.Path; requestPath {
|
|
||||||
case "/swarm":
|
|
||||||
return p.rewriteOperation(request, swarmInspectOperation)
|
|
||||||
default:
|
|
||||||
// assume /swarm/{action}
|
|
||||||
return p.administratorOperation(request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *proxyTransport) proxyTaskRequest(request *http.Request) (*http.Response, error) {
|
|
||||||
switch requestPath := request.URL.Path; requestPath {
|
|
||||||
case "/tasks":
|
|
||||||
return p.rewriteOperation(request, taskListOperation)
|
|
||||||
default:
|
|
||||||
// assume /tasks/{id}
|
|
||||||
return p.executeDockerRequest(request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *proxyTransport) proxyBuildRequest(request *http.Request) (*http.Response, error) {
|
|
||||||
return p.interceptAndRewriteRequest(request, buildOperation)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *proxyTransport) proxyImageRequest(request *http.Request) (*http.Response, error) {
|
|
||||||
switch requestPath := request.URL.Path; requestPath {
|
|
||||||
case "/images/create":
|
|
||||||
return p.replaceRegistryAuthenticationHeader(request)
|
|
||||||
default:
|
|
||||||
if path.Base(requestPath) == "push" && request.Method == http.MethodPost {
|
|
||||||
return p.replaceRegistryAuthenticationHeader(request)
|
|
||||||
}
|
|
||||||
return p.executeDockerRequest(request)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *proxyTransport) replaceRegistryAuthenticationHeader(request *http.Request) (*http.Response, error) {
|
|
||||||
accessContext, err := p.createRegistryAccessContext(request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
originalHeader := request.Header.Get("X-Registry-Auth")
|
|
||||||
|
|
||||||
if originalHeader != "" {
|
|
||||||
|
|
||||||
decodedHeaderData, err := base64.StdEncoding.DecodeString(originalHeader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var originalHeaderData registryAuthenticationHeader
|
|
||||||
err = json.Unmarshal(decodedHeaderData, &originalHeaderData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
authenticationHeader := createRegistryAuthenticationHeader(originalHeaderData.Serveraddress, accessContext)
|
|
||||||
|
|
||||||
headerData, err := json.Marshal(authenticationHeader)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
header := base64.StdEncoding.EncodeToString(headerData)
|
|
||||||
|
|
||||||
request.Header.Set("X-Registry-Auth", header)
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.executeDockerRequest(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
// restrictedOperation ensures that the current user has the required authorizations
|
|
||||||
// before executing the original request.
|
|
||||||
func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) {
|
|
||||||
var err error
|
|
||||||
tokenData, err := security.RetrieveTokenData(request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if tokenData.Role != portainer.AdministratorRole {
|
|
||||||
|
|
||||||
teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
userTeamIDs := make([]portainer.TeamID, 0)
|
|
||||||
for _, membership := range teamMemberships {
|
|
||||||
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceControls, err := p.ResourceControlService.ResourceControls()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceControl := getResourceControlByResourceID(resourceID, resourceControls)
|
|
||||||
if resourceControl == nil || !canUserAccessResource(tokenData.ID, userTeamIDs, resourceControl) {
|
|
||||||
return writeAccessDeniedResponse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.executeDockerRequest(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
// restrictedVolumeBrowserOperation is similar to restrictedOperation but adds an extra check on a specific setting
|
|
||||||
func (p *proxyTransport) restrictedVolumeBrowserOperation(request *http.Request, resourceID string) (*http.Response, error) {
|
|
||||||
var err error
|
|
||||||
tokenData, err := security.RetrieveTokenData(request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if tokenData.Role != portainer.AdministratorRole {
|
|
||||||
settings, err := p.SettingsService.Settings()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = p.ExtensionService.Extension(portainer.RBACExtension)
|
|
||||||
if err == portainer.ErrObjectNotFound && !settings.AllowVolumeBrowserForRegularUsers {
|
|
||||||
return writeAccessDeniedResponse()
|
|
||||||
} else if err != nil && err != portainer.ErrObjectNotFound {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
user, err := p.UserService.User(tokenData.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
endpointResourceAccess := false
|
|
||||||
_, ok := user.EndpointAuthorizations[p.endpointIdentifier][portainer.EndpointResourcesAccess]
|
|
||||||
if ok {
|
|
||||||
endpointResourceAccess = true
|
|
||||||
}
|
|
||||||
|
|
||||||
teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
userTeamIDs := make([]portainer.TeamID, 0)
|
|
||||||
for _, membership := range teamMemberships {
|
|
||||||
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceControls, err := p.ResourceControlService.ResourceControls()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceControl := getResourceControlByResourceID(resourceID, resourceControls)
|
|
||||||
if !endpointResourceAccess && (resourceControl == nil || !canUserAccessResource(tokenData.ID, userTeamIDs, resourceControl)) {
|
|
||||||
return writeAccessDeniedResponse()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.executeDockerRequest(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
// rewriteOperationWithLabelFiltering will create a new operation context with data that will be used
|
|
||||||
// to decorate the original request's response as well as retrieve all the black listed labels
|
|
||||||
// to filter the resources.
|
|
||||||
func (p *proxyTransport) rewriteOperationWithLabelFiltering(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) {
|
|
||||||
operationContext, err := p.createOperationContext(request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
settings, err := p.SettingsService.Settings()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
executor := &operationExecutor{
|
|
||||||
operationContext: operationContext,
|
|
||||||
labelBlackList: settings.BlackListedLabels,
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.executeRequestAndRewriteResponse(request, operation, executor)
|
|
||||||
}
|
|
||||||
|
|
||||||
// rewriteOperation will create a new operation context with data that will be used
|
|
||||||
// to decorate the original request's response.
|
|
||||||
func (p *proxyTransport) rewriteOperation(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) {
|
|
||||||
operationContext, err := p.createOperationContext(request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
executor := &operationExecutor{
|
|
||||||
operationContext: operationContext,
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.executeRequestAndRewriteResponse(request, operation, executor)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *proxyTransport) interceptAndRewriteRequest(request *http.Request, operation operationRequest) (*http.Response, error) {
|
|
||||||
err := operation(request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.executeDockerRequest(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *proxyTransport) executeRequestAndRewriteResponse(request *http.Request, operation restrictedOperationRequest, executor *operationExecutor) (*http.Response, error) {
|
|
||||||
response, err := p.executeDockerRequest(request)
|
|
||||||
if err != nil {
|
|
||||||
return response, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = operation(response, executor)
|
|
||||||
return response, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// administratorOperation ensures that the user has administrator privileges
|
|
||||||
// before executing the original request.
|
|
||||||
func (p *proxyTransport) administratorOperation(request *http.Request) (*http.Response, error) {
|
|
||||||
tokenData, err := security.RetrieveTokenData(request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if tokenData.Role != portainer.AdministratorRole {
|
|
||||||
return writeAccessDeniedResponse()
|
|
||||||
}
|
|
||||||
|
|
||||||
return p.executeDockerRequest(request)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *proxyTransport) createRegistryAccessContext(request *http.Request) (*registryAccessContext, error) {
|
|
||||||
tokenData, err := security.RetrieveTokenData(request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
accessContext := ®istryAccessContext{
|
|
||||||
isAdmin: true,
|
|
||||||
userID: tokenData.ID,
|
|
||||||
}
|
|
||||||
|
|
||||||
hub, err := p.DockerHubService.DockerHub()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
accessContext.dockerHub = hub
|
|
||||||
|
|
||||||
registries, err := p.RegistryService.Registries()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
accessContext.registries = registries
|
|
||||||
|
|
||||||
if tokenData.Role != portainer.AdministratorRole {
|
|
||||||
accessContext.isAdmin = false
|
|
||||||
|
|
||||||
teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
accessContext.teamMemberships = teamMemberships
|
|
||||||
}
|
|
||||||
|
|
||||||
return accessContext, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *proxyTransport) createOperationContext(request *http.Request) (*restrictedDockerOperationContext, error) {
|
|
||||||
var err error
|
|
||||||
tokenData, err := security.RetrieveTokenData(request)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resourceControls, err := p.ResourceControlService.ResourceControls()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
operationContext := &restrictedDockerOperationContext{
|
|
||||||
isAdmin: true,
|
|
||||||
userID: tokenData.ID,
|
|
||||||
resourceControls: resourceControls,
|
|
||||||
endpointResourceAccess: false,
|
|
||||||
}
|
|
||||||
|
|
||||||
if tokenData.Role != portainer.AdministratorRole {
|
|
||||||
operationContext.isAdmin = false
|
|
||||||
|
|
||||||
user, err := p.UserService.User(operationContext.userID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, ok := user.EndpointAuthorizations[p.endpointIdentifier][portainer.EndpointResourcesAccess]
|
|
||||||
if ok {
|
|
||||||
operationContext.endpointResourceAccess = true
|
|
||||||
}
|
|
||||||
|
|
||||||
teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
userTeamIDs := make([]portainer.TeamID, 0)
|
|
||||||
for _, membership := range teamMemberships {
|
|
||||||
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
|
||||||
}
|
|
||||||
operationContext.userTeamIDs = userTeamIDs
|
|
||||||
}
|
|
||||||
|
|
||||||
return operationContext, nil
|
|
||||||
}
|
|
|
@ -1,101 +0,0 @@
|
||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httputil"
|
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
"github.com/portainer/portainer/api/crypto"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AzureAPIBaseURL is the URL where Azure API requests will be proxied.
|
|
||||||
const AzureAPIBaseURL = "https://management.azure.com"
|
|
||||||
|
|
||||||
// proxyFactory is a factory to create reverse proxies to Docker endpoints
|
|
||||||
type proxyFactory struct {
|
|
||||||
ResourceControlService portainer.ResourceControlService
|
|
||||||
UserService portainer.UserService
|
|
||||||
TeamMembershipService portainer.TeamMembershipService
|
|
||||||
SettingsService portainer.SettingsService
|
|
||||||
RegistryService portainer.RegistryService
|
|
||||||
DockerHubService portainer.DockerHubService
|
|
||||||
SignatureService portainer.DigitalSignatureService
|
|
||||||
ReverseTunnelService portainer.ReverseTunnelService
|
|
||||||
ExtensionService portainer.ExtensionService
|
|
||||||
}
|
|
||||||
|
|
||||||
func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
|
|
||||||
u.Scheme = "http"
|
|
||||||
return httputil.NewSingleHostReverseProxy(u)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newAzureProxy(credentials *portainer.AzureCredentials) (http.Handler, error) {
|
|
||||||
remoteURL, err := url.Parse(AzureAPIBaseURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy := newSingleHostReverseProxyWithHostHeader(remoteURL)
|
|
||||||
proxy.Transport = NewAzureTransport(credentials)
|
|
||||||
|
|
||||||
return proxy, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portainer.TLSConfiguration, endpoint *portainer.Endpoint) (http.Handler, error) {
|
|
||||||
u.Scheme = "https"
|
|
||||||
|
|
||||||
proxy := factory.createDockerReverseProxy(u, endpoint)
|
|
||||||
config, err := crypto.CreateTLSConfigurationFromDisk(tlsConfig.TLSCACertPath, tlsConfig.TLSCertPath, tlsConfig.TLSKeyPath, tlsConfig.TLSSkipVerify)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy.Transport.(*proxyTransport).dockerTransport.TLSClientConfig = config
|
|
||||||
return proxy, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL, endpoint *portainer.Endpoint) http.Handler {
|
|
||||||
u.Scheme = "http"
|
|
||||||
return factory.createDockerReverseProxy(u, endpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, endpoint *portainer.Endpoint) *httputil.ReverseProxy {
|
|
||||||
proxy := newSingleHostReverseProxyWithHostHeader(u)
|
|
||||||
|
|
||||||
enableSignature := false
|
|
||||||
if endpoint.Type == portainer.AgentOnDockerEnvironment {
|
|
||||||
enableSignature = true
|
|
||||||
}
|
|
||||||
|
|
||||||
transport := &proxyTransport{
|
|
||||||
enableSignature: enableSignature,
|
|
||||||
ResourceControlService: factory.ResourceControlService,
|
|
||||||
UserService: factory.UserService,
|
|
||||||
TeamMembershipService: factory.TeamMembershipService,
|
|
||||||
SettingsService: factory.SettingsService,
|
|
||||||
RegistryService: factory.RegistryService,
|
|
||||||
DockerHubService: factory.DockerHubService,
|
|
||||||
ReverseTunnelService: factory.ReverseTunnelService,
|
|
||||||
ExtensionService: factory.ExtensionService,
|
|
||||||
dockerTransport: &http.Transport{},
|
|
||||||
endpointIdentifier: endpoint.ID,
|
|
||||||
endpointType: endpoint.Type,
|
|
||||||
}
|
|
||||||
|
|
||||||
if enableSignature {
|
|
||||||
transport.SignatureService = factory.SignatureService
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy.Transport = transport
|
|
||||||
return proxy
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSocketTransport(socketPath string) *http.Transport {
|
|
||||||
return &http.Transport{
|
|
||||||
Dial: func(proto, addr string) (conn net.Conn, err error) {
|
|
||||||
return net.Dial("unix", socketPath)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
20
api/http/proxy/factory/azure.go
Normal file
20
api/http/proxy/factory/azure.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package factory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory/azure"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newAzureProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||||
|
remoteURL, err := url.Parse(azureAPIBaseURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := newSingleHostReverseProxyWithHostHeader(remoteURL)
|
||||||
|
proxy.Transport = azure.NewTransport(&endpoint.AzureCredentials)
|
||||||
|
return proxy, nil
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package proxy
|
package azure
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -16,9 +16,7 @@ type (
|
||||||
expirationTime time.Time
|
expirationTime time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
// AzureTransport represents a transport used when executing HTTP requests
|
Transport struct {
|
||||||
// against the Azure API.
|
|
||||||
AzureTransport struct {
|
|
||||||
credentials *portainer.AzureCredentials
|
credentials *portainer.AzureCredentials
|
||||||
client *client.HTTPClient
|
client *client.HTTPClient
|
||||||
token *azureAPIToken
|
token *azureAPIToken
|
||||||
|
@ -26,15 +24,27 @@ type (
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewAzureTransport returns a pointer to an AzureTransport instance.
|
// NewTransport returns a pointer to a new instance of Transport that implements the HTTP Transport
|
||||||
func NewAzureTransport(credentials *portainer.AzureCredentials) *AzureTransport {
|
// interface for proxying requests to the Azure API.
|
||||||
return &AzureTransport{
|
func NewTransport(credentials *portainer.AzureCredentials) *Transport {
|
||||||
|
return &Transport{
|
||||||
credentials: credentials,
|
credentials: credentials,
|
||||||
client: client.NewHTTPClient(),
|
client: client.NewHTTPClient(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (transport *AzureTransport) authenticate() error {
|
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||||
|
func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||||
|
err := transport.retrieveAuthenticationToken()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Header.Set("Authorization", "Bearer "+transport.token.value)
|
||||||
|
return http.DefaultTransport.RoundTrip(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) authenticate() error {
|
||||||
token, err := transport.client.ExecuteAzureAuthenticationRequest(transport.credentials)
|
token, err := transport.client.ExecuteAzureAuthenticationRequest(transport.credentials)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -53,7 +63,7 @@ func (transport *AzureTransport) authenticate() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (transport *AzureTransport) retrieveAuthenticationToken() error {
|
func (transport *Transport) retrieveAuthenticationToken() error {
|
||||||
transport.mutex.Lock()
|
transport.mutex.Lock()
|
||||||
defer transport.mutex.Unlock()
|
defer transport.mutex.Unlock()
|
||||||
|
|
||||||
|
@ -68,14 +78,3 @@ func (transport *AzureTransport) retrieveAuthenticationToken() error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RoundTrip is the implementation of the Transport interface.
|
|
||||||
func (transport *AzureTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
|
||||||
err := transport.retrieveAuthenticationToken()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
request.Header.Set("Authorization", "Bearer "+transport.token.value)
|
|
||||||
return http.DefaultTransport.RoundTrip(request)
|
|
||||||
}
|
|
118
api/http/proxy/factory/docker.go
Normal file
118
api/http/proxy/factory/docker.go
Normal file
|
@ -0,0 +1,118 @@
|
||||||
|
package factory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
httperror "github.com/portainer/libhttp/error"
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/crypto"
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory/docker"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (factory *ProxyFactory) newDockerProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||||
|
if strings.HasPrefix(endpoint.URL, "unix://") || strings.HasPrefix(endpoint.URL, "npipe://") {
|
||||||
|
return factory.newDockerLocalProxy(endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
return factory.newDockerHTTPProxy(endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (factory *ProxyFactory) newDockerLocalProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||||
|
endpointURL, err := url.Parse(endpoint.URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return factory.newOSBasedLocalProxy(endpointURL.Path, endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (factory *ProxyFactory) newDockerHTTPProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||||
|
if endpoint.Type == portainer.EdgeAgentEnvironment {
|
||||||
|
tunnel := factory.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
||||||
|
endpoint.URL = fmt.Sprintf("http://localhost:%d", tunnel.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointURL, err := url.Parse(endpoint.URL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointURL.Scheme = "http"
|
||||||
|
httpTransport := &http.Transport{}
|
||||||
|
|
||||||
|
if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify {
|
||||||
|
config, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
httpTransport.TLSClientConfig = config
|
||||||
|
endpointURL.Scheme = "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
transportParameters := &docker.TransportParameters{
|
||||||
|
Endpoint: endpoint,
|
||||||
|
ResourceControlService: factory.resourceControlService,
|
||||||
|
UserService: factory.userService,
|
||||||
|
TeamService: factory.teamService,
|
||||||
|
TeamMembershipService: factory.teamMembershipService,
|
||||||
|
RegistryService: factory.registryService,
|
||||||
|
DockerHubService: factory.dockerHubService,
|
||||||
|
SettingsService: factory.settingsService,
|
||||||
|
ReverseTunnelService: factory.reverseTunnelService,
|
||||||
|
ExtensionService: factory.extensionService,
|
||||||
|
SignatureService: factory.signatureService,
|
||||||
|
DockerClientFactory: factory.dockerClientFactory,
|
||||||
|
}
|
||||||
|
|
||||||
|
dockerTransport, err := docker.NewTransport(transportParameters, httpTransport)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := newSingleHostReverseProxyWithHostHeader(endpointURL)
|
||||||
|
proxy.Transport = dockerTransport
|
||||||
|
return proxy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type dockerLocalProxy struct {
|
||||||
|
transport *docker.Transport
|
||||||
|
}
|
||||||
|
|
||||||
|
// ServeHTTP is the http.Handler interface implementation
|
||||||
|
// for a local (Unix socket or Windows named pipe) Docker proxy.
|
||||||
|
func (proxy *dockerLocalProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
// Force URL/domain to http/unixsocket to be able to
|
||||||
|
// use http.transport RoundTrip to do the requests via the socket
|
||||||
|
r.URL.Scheme = "http"
|
||||||
|
r.URL.Host = "unixsocket"
|
||||||
|
|
||||||
|
res, err := proxy.transport.ProxyDockerRequest(r)
|
||||||
|
if err != nil {
|
||||||
|
code := http.StatusInternalServerError
|
||||||
|
if res != nil && res.StatusCode != 0 {
|
||||||
|
code = res.StatusCode
|
||||||
|
}
|
||||||
|
|
||||||
|
httperror.WriteError(w, code, "Unable to proxy the request via the Docker socket", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer res.Body.Close()
|
||||||
|
|
||||||
|
for k, vv := range res.Header {
|
||||||
|
for _, v := range vv {
|
||||||
|
w.Header().Add(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(res.StatusCode)
|
||||||
|
|
||||||
|
if _, err := io.Copy(w, res.Body); err != nil {
|
||||||
|
log.Printf("proxy error: %s\n", err)
|
||||||
|
}
|
||||||
|
}
|
306
api/http/proxy/factory/docker/access_control.go
Normal file
306
api/http/proxy/factory/docker/access_control.go
Normal file
|
@ -0,0 +1,306 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
resourceLabelForPortainerTeamResourceControl = "io.portainer.accesscontrol.teams"
|
||||||
|
resourceLabelForPortainerUserResourceControl = "io.portainer.accesscontrol.users"
|
||||||
|
resourceLabelForPortainerPublicResourceControl = "io.portainer.accesscontrol.public"
|
||||||
|
resourceLabelForDockerSwarmStackName = "com.docker.stack.namespace"
|
||||||
|
resourceLabelForDockerServiceID = "com.docker.swarm.service.id"
|
||||||
|
resourceLabelForDockerComposeStackName = "com.docker.compose.project"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
resourceLabelsObjectSelector func(map[string]interface{}) map[string]interface{}
|
||||||
|
resourceOperationParameters struct {
|
||||||
|
resourceIdentifierAttribute string
|
||||||
|
resourceType portainer.ResourceControlType
|
||||||
|
labelsObjectSelector resourceLabelsObjectSelector
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (transport *Transport) newResourceControlFromPortainerLabels(labelsObject map[string]interface{}, resourceID string, resourceType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
|
||||||
|
if labelsObject[resourceLabelForPortainerPublicResourceControl] != nil {
|
||||||
|
resourceControl := portainer.NewPublicResourceControl(resourceID, resourceType)
|
||||||
|
|
||||||
|
err := transport.resourceControlService.CreateResourceControl(resourceControl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourceControl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
teamNames := make([]string, 0)
|
||||||
|
userNames := make([]string, 0)
|
||||||
|
if labelsObject[resourceLabelForPortainerTeamResourceControl] != nil {
|
||||||
|
concatenatedTeamNames := labelsObject[resourceLabelForPortainerTeamResourceControl].(string)
|
||||||
|
teamNames = strings.Split(concatenatedTeamNames, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
if labelsObject[resourceLabelForPortainerUserResourceControl] != nil {
|
||||||
|
concatenatedUserNames := labelsObject[resourceLabelForPortainerUserResourceControl].(string)
|
||||||
|
userNames = strings.Split(concatenatedUserNames, ",")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(teamNames) > 0 || len(userNames) > 0 {
|
||||||
|
teamIDs := make([]portainer.TeamID, 0)
|
||||||
|
userIDs := make([]portainer.UserID, 0)
|
||||||
|
|
||||||
|
for _, name := range teamNames {
|
||||||
|
team, err := transport.teamService.TeamByName(name)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[WARN] [http,proxy,docker] [message: unknown team name in access control label, ignoring access control rule for this team] [name: %s] [resource_id: %s]", name, resourceID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
teamIDs = append(teamIDs, team.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, name := range userNames {
|
||||||
|
user, err := transport.userService.UserByUsername(name)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[WARN] [http,proxy,docker] [message: unknown user name in access control label, ignoring access control rule for this user] [name: %s] [resource_id: %s]", name, resourceID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
userIDs = append(userIDs, user.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceControl := portainer.NewRestrictedResourceControl(resourceID, resourceType, userIDs, teamIDs)
|
||||||
|
|
||||||
|
err := transport.resourceControlService.CreateResourceControl(resourceControl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourceControl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) createPrivateResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, userID portainer.UserID) (*portainer.ResourceControl, error) {
|
||||||
|
resourceControl := portainer.NewPrivateResourceControl(resourceIdentifier, resourceType, userID)
|
||||||
|
|
||||||
|
err := transport.resourceControlService.CreateResourceControl(resourceControl)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[ERROR] [http,proxy,docker,transport] [message: unable to persist resource control] [resource: %s] [err: %s]", resourceIdentifier, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourceControl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) getInheritedResourceControlFromServiceOrStack(resourceIdentifier, nodeName string, resourceType portainer.ResourceControlType, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||||
|
client := transport.dockerClient
|
||||||
|
|
||||||
|
if nodeName != "" {
|
||||||
|
dockerClient, err := transport.dockerClientFactory.CreateClient(transport.endpoint, nodeName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer dockerClient.Close()
|
||||||
|
|
||||||
|
client = dockerClient
|
||||||
|
}
|
||||||
|
|
||||||
|
switch resourceType {
|
||||||
|
case portainer.ContainerResourceControl:
|
||||||
|
return getInheritedResourceControlFromContainerLabels(client, resourceIdentifier, resourceControls)
|
||||||
|
case portainer.NetworkResourceControl:
|
||||||
|
return getInheritedResourceControlFromNetworkLabels(client, resourceIdentifier, resourceControls)
|
||||||
|
case portainer.VolumeResourceControl:
|
||||||
|
return getInheritedResourceControlFromVolumeLabels(client, resourceIdentifier, resourceControls)
|
||||||
|
case portainer.ServiceResourceControl:
|
||||||
|
return getInheritedResourceControlFromServiceLabels(client, resourceIdentifier, resourceControls)
|
||||||
|
case portainer.ConfigResourceControl:
|
||||||
|
return getInheritedResourceControlFromConfigLabels(client, resourceIdentifier, resourceControls)
|
||||||
|
case portainer.SecretResourceControl:
|
||||||
|
return getInheritedResourceControlFromSecretLabels(client, resourceIdentifier, resourceControls)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) applyAccessControlOnResource(parameters *resourceOperationParameters, responseObject map[string]interface{}, response *http.Response, executor *operationExecutor) error {
|
||||||
|
if responseObject[parameters.resourceIdentifierAttribute] == nil {
|
||||||
|
log.Printf("[WARN] [message: unable to find resource identifier property in resource object] [identifier_attribute: %s]", parameters.resourceIdentifierAttribute)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if parameters.resourceType == portainer.NetworkResourceControl {
|
||||||
|
systemResourceControl := findSystemNetworkResourceControl(responseObject)
|
||||||
|
if systemResourceControl != nil {
|
||||||
|
responseObject = decorateObject(responseObject, systemResourceControl)
|
||||||
|
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceIdentifier := responseObject[parameters.resourceIdentifierAttribute].(string)
|
||||||
|
resourceLabelsObject := parameters.labelsObjectSelector(responseObject)
|
||||||
|
|
||||||
|
resourceControl, err := transport.findResourceControl(resourceIdentifier, parameters.resourceType, resourceLabelsObject, executor.operationContext.resourceControls)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resourceControl == nil && (executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess) {
|
||||||
|
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess || portainer.UserCanAccessResource(executor.operationContext.userID, executor.operationContext.userTeamIDs, resourceControl) {
|
||||||
|
responseObject = decorateObject(responseObject, resourceControl)
|
||||||
|
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseutils.RewriteAccessDeniedResponse(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) applyAccessControlOnResourceList(parameters *resourceOperationParameters, resourceData []interface{}, executor *operationExecutor) ([]interface{}, error) {
|
||||||
|
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
|
||||||
|
return transport.decorateResourceList(parameters, resourceData, executor.operationContext.resourceControls)
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.filterResourceList(parameters, resourceData, executor.operationContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) decorateResourceList(parameters *resourceOperationParameters, resourceData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
|
||||||
|
decoratedResourceData := make([]interface{}, 0)
|
||||||
|
|
||||||
|
for _, resource := range resourceData {
|
||||||
|
resourceObject := resource.(map[string]interface{})
|
||||||
|
|
||||||
|
if resourceObject[parameters.resourceIdentifierAttribute] == nil {
|
||||||
|
log.Printf("[WARN] [http,proxy,docker,decorate] [message: unable to find resource identifier property in resource list element] [identifier_attribute: %s]", parameters.resourceIdentifierAttribute)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if parameters.resourceType == portainer.NetworkResourceControl {
|
||||||
|
systemResourceControl := findSystemNetworkResourceControl(resourceObject)
|
||||||
|
if systemResourceControl != nil {
|
||||||
|
resourceObject = decorateObject(resourceObject, systemResourceControl)
|
||||||
|
decoratedResourceData = append(decoratedResourceData, resourceObject)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceIdentifier := resourceObject[parameters.resourceIdentifierAttribute].(string)
|
||||||
|
resourceLabelsObject := parameters.labelsObjectSelector(resourceObject)
|
||||||
|
|
||||||
|
resourceControl, err := transport.findResourceControl(resourceIdentifier, parameters.resourceType, resourceLabelsObject, resourceControls)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resourceControl != nil {
|
||||||
|
resourceObject = decorateObject(resourceObject, resourceControl)
|
||||||
|
}
|
||||||
|
|
||||||
|
decoratedResourceData = append(decoratedResourceData, resourceObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoratedResourceData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) filterResourceList(parameters *resourceOperationParameters, resourceData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
|
||||||
|
filteredResourceData := make([]interface{}, 0)
|
||||||
|
|
||||||
|
for _, resource := range resourceData {
|
||||||
|
resourceObject := resource.(map[string]interface{})
|
||||||
|
if resourceObject[parameters.resourceIdentifierAttribute] == nil {
|
||||||
|
log.Printf("[WARN] [http,proxy,docker,filter] [message: unable to find resource identifier property in resource list element] [identifier_attribute: %s]", parameters.resourceIdentifierAttribute)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceIdentifier := resourceObject[parameters.resourceIdentifierAttribute].(string)
|
||||||
|
resourceLabelsObject := parameters.labelsObjectSelector(resourceObject)
|
||||||
|
|
||||||
|
if parameters.resourceType == portainer.NetworkResourceControl {
|
||||||
|
systemResourceControl := findSystemNetworkResourceControl(resourceObject)
|
||||||
|
if systemResourceControl != nil {
|
||||||
|
resourceObject = decorateObject(resourceObject, systemResourceControl)
|
||||||
|
filteredResourceData = append(filteredResourceData, resourceObject)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceControl, err := transport.findResourceControl(resourceIdentifier, parameters.resourceType, resourceLabelsObject, context.resourceControls)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resourceControl == nil {
|
||||||
|
if context.isAdmin || context.endpointResourceAccess {
|
||||||
|
filteredResourceData = append(filteredResourceData, resourceObject)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if context.isAdmin || context.endpointResourceAccess || portainer.UserCanAccessResource(context.userID, context.userTeamIDs, resourceControl) {
|
||||||
|
resourceObject = decorateObject(resourceObject, resourceControl)
|
||||||
|
filteredResourceData = append(filteredResourceData, resourceObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredResourceData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) findResourceControl(resourceIdentifier string, resourceType portainer.ResourceControlType, resourceLabelsObject map[string]interface{}, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||||
|
resourceControl := portainer.GetResourceControlByResourceIDAndType(resourceIdentifier, resourceType, resourceControls)
|
||||||
|
if resourceControl != nil {
|
||||||
|
return resourceControl, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if resourceLabelsObject != nil {
|
||||||
|
if resourceLabelsObject[resourceLabelForDockerServiceID] != nil {
|
||||||
|
inheritedServiceIdentifier := resourceLabelsObject[resourceLabelForDockerServiceID].(string)
|
||||||
|
resourceControl = portainer.GetResourceControlByResourceIDAndType(inheritedServiceIdentifier, portainer.ServiceResourceControl, resourceControls)
|
||||||
|
|
||||||
|
if resourceControl != nil {
|
||||||
|
return resourceControl, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if resourceLabelsObject[resourceLabelForDockerSwarmStackName] != nil {
|
||||||
|
inheritedSwarmStackIdentifier := resourceLabelsObject[resourceLabelForDockerSwarmStackName].(string)
|
||||||
|
resourceControl = portainer.GetResourceControlByResourceIDAndType(inheritedSwarmStackIdentifier, portainer.StackResourceControl, resourceControls)
|
||||||
|
|
||||||
|
if resourceControl != nil {
|
||||||
|
return resourceControl, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if resourceLabelsObject[resourceLabelForDockerComposeStackName] != nil {
|
||||||
|
inheritedComposeStackIdentifier := resourceLabelsObject[resourceLabelForDockerComposeStackName].(string)
|
||||||
|
resourceControl = portainer.GetResourceControlByResourceIDAndType(inheritedComposeStackIdentifier, portainer.StackResourceControl, resourceControls)
|
||||||
|
|
||||||
|
if resourceControl != nil {
|
||||||
|
return resourceControl, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.newResourceControlFromPortainerLabels(resourceLabelsObject, resourceIdentifier, resourceType)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} {
|
||||||
|
if object["Portainer"] == nil {
|
||||||
|
object["Portainer"] = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
portainerMetadata := object["Portainer"].(map[string]interface{})
|
||||||
|
portainerMetadata["ResourceControl"] = resourceControl
|
||||||
|
return object
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package proxy
|
package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
86
api/http/proxy/factory/docker/configs.go
Normal file
86
api/http/proxy/factory/docker/configs.go
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
configObjectIdentifier = "ID"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getInheritedResourceControlFromConfigLabels(dockerClient *client.Client, configID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||||
|
config, _, err := dockerClient.ConfigInspectWithRaw(context.Background(), configID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
swarmStackName := config.Spec.Labels[resourceLabelForDockerSwarmStackName]
|
||||||
|
if swarmStackName != "" {
|
||||||
|
return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// configListOperation extracts the response as a JSON object, loop through the configs array
|
||||||
|
// decorate and/or filter the configs based on resource controls before rewriting the response.
|
||||||
|
func (transport *Transport) configListOperation(response *http.Response, executor *operationExecutor) error {
|
||||||
|
// ConfigList response is a JSON array
|
||||||
|
// https://docs.docker.com/engine/api/v1.30/#operation/ConfigList
|
||||||
|
responseArray, err := responseutils.GetResponseAsJSONArray(response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceOperationParameters := &resourceOperationParameters{
|
||||||
|
resourceIdentifierAttribute: configObjectIdentifier,
|
||||||
|
resourceType: portainer.ConfigResourceControl,
|
||||||
|
labelsObjectSelector: selectorConfigLabels,
|
||||||
|
}
|
||||||
|
|
||||||
|
responseArray, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, responseArray, executor)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// configInspectOperation extracts the response as a JSON object, verify that the user
|
||||||
|
// has access to the config based on resource control and either rewrite an access denied response or a decorated config.
|
||||||
|
func (transport *Transport) configInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||||
|
// ConfigInspect response is a JSON object
|
||||||
|
// https://docs.docker.com/engine/api/v1.30/#operation/ConfigInspect
|
||||||
|
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceOperationParameters := &resourceOperationParameters{
|
||||||
|
resourceIdentifierAttribute: configObjectIdentifier,
|
||||||
|
resourceType: portainer.ConfigResourceControl,
|
||||||
|
labelsObjectSelector: selectorConfigLabels,
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.applyAccessControlOnResource(resourceOperationParameters, responseObject, response, executor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectorConfigLabels retrieve the labels object associated to the config object.
|
||||||
|
// Labels are available under the "Spec.Labels" property.
|
||||||
|
// API schema references:
|
||||||
|
// https://docs.docker.com/engine/api/v1.40/#operation/ConfigList
|
||||||
|
// https://docs.docker.com/engine/api/v1.40/#operation/ConfigInspect
|
||||||
|
func selectorConfigLabels(responseObject map[string]interface{}) map[string]interface{} {
|
||||||
|
secretSpec := responseutils.GetJSONObject(responseObject, "Spec")
|
||||||
|
if secretSpec != nil {
|
||||||
|
secretLabelsObject := responseutils.GetJSONObject(secretSpec, "Labels")
|
||||||
|
return secretLabelsObject
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
146
api/http/proxy/factory/docker/containers.go
Normal file
146
api/http/proxy/factory/docker/containers.go
Normal file
|
@ -0,0 +1,146 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
containerObjectIdentifier = "Id"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getInheritedResourceControlFromContainerLabels(dockerClient *client.Client, containerID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||||
|
container, err := dockerClient.ContainerInspect(context.Background(), containerID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
swarmStackName := container.Config.Labels[resourceLabelForDockerSwarmStackName]
|
||||||
|
if swarmStackName != "" {
|
||||||
|
return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceName := container.Config.Labels[resourceLabelForDockerServiceID]
|
||||||
|
if serviceName != "" {
|
||||||
|
return portainer.GetResourceControlByResourceIDAndType(serviceName, portainer.ServiceResourceControl, resourceControls), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
composeStackName := container.Config.Labels[resourceLabelForDockerComposeStackName]
|
||||||
|
if composeStackName != "" {
|
||||||
|
return portainer.GetResourceControlByResourceIDAndType(composeStackName, portainer.StackResourceControl, resourceControls), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// containerListOperation extracts the response as a JSON array, loop through the containers array
|
||||||
|
// decorate and/or filter the containers based on resource controls before rewriting the response.
|
||||||
|
func (transport *Transport) containerListOperation(response *http.Response, executor *operationExecutor) error {
|
||||||
|
// ContainerList response is a JSON array
|
||||||
|
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
|
||||||
|
responseArray, err := responseutils.GetResponseAsJSONArray(response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceOperationParameters := &resourceOperationParameters{
|
||||||
|
resourceIdentifierAttribute: containerObjectIdentifier,
|
||||||
|
resourceType: portainer.ContainerResourceControl,
|
||||||
|
labelsObjectSelector: selectorContainerLabelsFromContainerListOperation,
|
||||||
|
}
|
||||||
|
|
||||||
|
responseArray, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, responseArray, executor)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if executor.labelBlackList != nil {
|
||||||
|
responseArray, err = filterContainersWithBlackListedLabels(responseArray, executor.labelBlackList)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// containerInspectOperation extracts the response as a JSON object, verify that the user
|
||||||
|
// has access to the container based on resource control and either rewrite an access denied response or a decorated container.
|
||||||
|
func (transport *Transport) containerInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||||
|
//ContainerInspect response is a JSON object
|
||||||
|
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect
|
||||||
|
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceOperationParameters := &resourceOperationParameters{
|
||||||
|
resourceIdentifierAttribute: containerObjectIdentifier,
|
||||||
|
resourceType: portainer.ContainerResourceControl,
|
||||||
|
labelsObjectSelector: selectorContainerLabelsFromContainerInspectOperation,
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.applyAccessControlOnResource(resourceOperationParameters, responseObject, response, executor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectorContainerLabelsFromContainerInspectOperation retrieve the labels object associated to the container object.
|
||||||
|
// This selector is specific to the containerInspect Docker operation.
|
||||||
|
// Labels are available under the "Config.Labels" property.
|
||||||
|
// API schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect
|
||||||
|
func selectorContainerLabelsFromContainerInspectOperation(responseObject map[string]interface{}) map[string]interface{} {
|
||||||
|
containerConfigObject := responseutils.GetJSONObject(responseObject, "Config")
|
||||||
|
if containerConfigObject != nil {
|
||||||
|
containerLabelsObject := responseutils.GetJSONObject(containerConfigObject, "Labels")
|
||||||
|
return containerLabelsObject
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectorContainerLabelsFromContainerListOperation retrieve the labels object associated to the container object.
|
||||||
|
// This selector is specific to the containerList Docker operation.
|
||||||
|
// Labels are available under the "Labels" property.
|
||||||
|
// API schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
|
||||||
|
func selectorContainerLabelsFromContainerListOperation(responseObject map[string]interface{}) map[string]interface{} {
|
||||||
|
containerLabelsObject := responseutils.GetJSONObject(responseObject, "Labels")
|
||||||
|
return containerLabelsObject
|
||||||
|
}
|
||||||
|
|
||||||
|
// filterContainersWithLabels loops through a list of containers, and filters containers that do not contains
|
||||||
|
// any labels in the labels black list.
|
||||||
|
func filterContainersWithBlackListedLabels(containerData []interface{}, labelBlackList []portainer.Pair) ([]interface{}, error) {
|
||||||
|
filteredContainerData := make([]interface{}, 0)
|
||||||
|
|
||||||
|
for _, container := range containerData {
|
||||||
|
containerObject := container.(map[string]interface{})
|
||||||
|
|
||||||
|
containerLabels := selectorContainerLabelsFromContainerListOperation(containerObject)
|
||||||
|
if containerLabels != nil {
|
||||||
|
if !containerHasBlackListedLabel(containerLabels, labelBlackList) {
|
||||||
|
filteredContainerData = append(filteredContainerData, containerObject)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filteredContainerData = append(filteredContainerData, containerObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredContainerData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelBlackList []portainer.Pair) bool {
|
||||||
|
for key, value := range containerLabels {
|
||||||
|
labelName := key
|
||||||
|
labelValue := value.(string)
|
||||||
|
|
||||||
|
for _, blackListedLabel := range labelBlackList {
|
||||||
|
if blackListedLabel.Name == labelName && blackListedLabel.Value == labelValue {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
101
api/http/proxy/factory/docker/networks.go
Normal file
101
api/http/proxy/factory/docker/networks.go
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
networkObjectIdentifier = "Id"
|
||||||
|
networkObjectName = "Name"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getInheritedResourceControlFromNetworkLabels(dockerClient *client.Client, networkID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||||
|
network, err := dockerClient.NetworkInspect(context.Background(), networkID, types.NetworkInspectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
swarmStackName := network.Labels[resourceLabelForDockerSwarmStackName]
|
||||||
|
if swarmStackName != "" {
|
||||||
|
return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// networkListOperation extracts the response as a JSON object, loop through the networks array
|
||||||
|
// decorate and/or filter the networks based on resource controls before rewriting the response.
|
||||||
|
func (transport *Transport) networkListOperation(response *http.Response, executor *operationExecutor) error {
|
||||||
|
// NetworkList response is a JSON array
|
||||||
|
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
||||||
|
responseArray, err := responseutils.GetResponseAsJSONArray(response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceOperationParameters := &resourceOperationParameters{
|
||||||
|
resourceIdentifierAttribute: networkObjectIdentifier,
|
||||||
|
resourceType: portainer.NetworkResourceControl,
|
||||||
|
labelsObjectSelector: selectorNetworkLabels,
|
||||||
|
}
|
||||||
|
|
||||||
|
responseArray, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, responseArray, executor)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// networkInspectOperation extracts the response as a JSON object, verify that the user
|
||||||
|
// has access to the network based on resource control and either rewrite an access denied response or a decorated network.
|
||||||
|
func (transport *Transport) networkInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||||
|
// NetworkInspect response is a JSON object
|
||||||
|
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect
|
||||||
|
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceOperationParameters := &resourceOperationParameters{
|
||||||
|
resourceIdentifierAttribute: networkObjectIdentifier,
|
||||||
|
resourceType: portainer.NetworkResourceControl,
|
||||||
|
labelsObjectSelector: selectorNetworkLabels,
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.applyAccessControlOnResource(resourceOperationParameters, responseObject, response, executor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// findSystemNetworkResourceControl will check if the network object is a system network
|
||||||
|
// and will return a system resource control if that's the case.
|
||||||
|
func findSystemNetworkResourceControl(networkObject map[string]interface{}) *portainer.ResourceControl {
|
||||||
|
if networkObject[networkObjectName] == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
networkID := networkObject[networkObjectIdentifier].(string)
|
||||||
|
networkName := networkObject[networkObjectName].(string)
|
||||||
|
|
||||||
|
if networkName == "bridge" || networkName == "host" || networkName == "none" {
|
||||||
|
return portainer.NewSystemResourceControl(networkID, portainer.NetworkResourceControl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectorNetworkLabels retrieve the labels object associated to the network object.
|
||||||
|
// Labels are available under the "Labels" property.
|
||||||
|
// API schema references:
|
||||||
|
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect
|
||||||
|
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
||||||
|
func selectorNetworkLabels(responseObject map[string]interface{}) map[string]interface{} {
|
||||||
|
return responseutils.GetJSONObject(responseObject, "Labels")
|
||||||
|
}
|
|
@ -1,10 +1,25 @@
|
||||||
package proxy
|
package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
"github.com/portainer/portainer/api/http/security"
|
"github.com/portainer/portainer/api/http/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
registryAccessContext struct {
|
||||||
|
isAdmin bool
|
||||||
|
userID portainer.UserID
|
||||||
|
teamMemberships []portainer.TeamMembership
|
||||||
|
registries []portainer.Registry
|
||||||
|
dockerHub *portainer.DockerHub
|
||||||
|
}
|
||||||
|
registryAuthenticationHeader struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Serveraddress string `json:"serveraddress"`
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func createRegistryAuthenticationHeader(serverAddress string, accessContext *registryAccessContext) *registryAuthenticationHeader {
|
func createRegistryAuthenticationHeader(serverAddress string, accessContext *registryAccessContext) *registryAuthenticationHeader {
|
||||||
var authenticationHeader *registryAuthenticationHeader
|
var authenticationHeader *registryAuthenticationHeader
|
||||||
|
|
87
api/http/proxy/factory/docker/secrets.go
Normal file
87
api/http/proxy/factory/docker/secrets.go
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
secretObjectIdentifier = "ID"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getInheritedResourceControlFromSecretLabels(dockerClient *client.Client, secretID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||||
|
secret, _, err := dockerClient.SecretInspectWithRaw(context.Background(), secretID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
swarmStackName := secret.Spec.Labels[resourceLabelForDockerSwarmStackName]
|
||||||
|
if swarmStackName != "" {
|
||||||
|
return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// secretListOperation extracts the response as a JSON object, loop through the secrets array
|
||||||
|
// decorate and/or filter the secrets based on resource controls before rewriting the response.
|
||||||
|
func (transport *Transport) secretListOperation(response *http.Response, executor *operationExecutor) error {
|
||||||
|
// SecretList response is a JSON array
|
||||||
|
// https://docs.docker.com/engine/api/v1.28/#operation/SecretList
|
||||||
|
responseArray, err := responseutils.GetResponseAsJSONArray(response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceOperationParameters := &resourceOperationParameters{
|
||||||
|
resourceIdentifierAttribute: secretObjectIdentifier,
|
||||||
|
resourceType: portainer.SecretResourceControl,
|
||||||
|
labelsObjectSelector: selectorSecretLabels,
|
||||||
|
}
|
||||||
|
|
||||||
|
responseArray, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, responseArray, executor)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// secretInspectOperation extracts the response as a JSON object, verify that the user
|
||||||
|
// has access to the secret based on resource control and either rewrite an access denied response or a decorated secret.
|
||||||
|
func (transport *Transport) secretInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||||
|
// SecretInspect response is a JSON object
|
||||||
|
// https://docs.docker.com/engine/api/v1.28/#operation/SecretInspect
|
||||||
|
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceOperationParameters := &resourceOperationParameters{
|
||||||
|
resourceIdentifierAttribute: secretObjectIdentifier,
|
||||||
|
resourceType: portainer.SecretResourceControl,
|
||||||
|
labelsObjectSelector: selectorSecretLabels,
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.applyAccessControlOnResource(resourceOperationParameters, responseObject, response, executor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectorSecretLabels retrieve the labels object associated to the secret object.
|
||||||
|
// Labels are available under the "Spec.Labels" property.
|
||||||
|
// API schema references:
|
||||||
|
// https://docs.docker.com/engine/api/v1.40/#operation/SecretList
|
||||||
|
// https://docs.docker.com/engine/api/v1.40/#operation/SecretInspect
|
||||||
|
func selectorSecretLabels(responseObject map[string]interface{}) map[string]interface{} {
|
||||||
|
secretSpec := responseutils.GetJSONObject(responseObject, "Spec")
|
||||||
|
if secretSpec != nil {
|
||||||
|
secretLabelsObject := responseutils.GetJSONObject(secretSpec, "Labels")
|
||||||
|
return secretLabelsObject
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
86
api/http/proxy/factory/docker/services.go
Normal file
86
api/http/proxy/factory/docker/services.go
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
serviceObjectIdentifier = "ID"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getInheritedResourceControlFromServiceLabels(dockerClient *client.Client, serviceID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||||
|
service, _, err := dockerClient.ServiceInspectWithRaw(context.Background(), serviceID, types.ServiceInspectOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
swarmStackName := service.Spec.Labels[resourceLabelForDockerSwarmStackName]
|
||||||
|
if swarmStackName != "" {
|
||||||
|
return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceListOperation extracts the response as a JSON array, loop through the service array
|
||||||
|
// decorate and/or filter the services based on resource controls before rewriting the response.
|
||||||
|
func (transport *Transport) serviceListOperation(response *http.Response, executor *operationExecutor) error {
|
||||||
|
// ServiceList response is a JSON array
|
||||||
|
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
||||||
|
responseArray, err := responseutils.GetResponseAsJSONArray(response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceOperationParameters := &resourceOperationParameters{
|
||||||
|
resourceIdentifierAttribute: serviceObjectIdentifier,
|
||||||
|
resourceType: portainer.ServiceResourceControl,
|
||||||
|
labelsObjectSelector: selectorServiceLabels,
|
||||||
|
}
|
||||||
|
|
||||||
|
responseArray, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, responseArray, executor)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// serviceInspectOperation extracts the response as a JSON object, verify that the user
|
||||||
|
// has access to the service based on resource control and either rewrite an access denied response or a decorated service.
|
||||||
|
func (transport *Transport) serviceInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||||
|
//ServiceInspect response is a JSON object
|
||||||
|
//https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
|
||||||
|
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceOperationParameters := &resourceOperationParameters{
|
||||||
|
resourceIdentifierAttribute: serviceObjectIdentifier,
|
||||||
|
resourceType: portainer.ServiceResourceControl,
|
||||||
|
labelsObjectSelector: selectorServiceLabels,
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.applyAccessControlOnResource(resourceOperationParameters, responseObject, response, executor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectorServiceLabels retrieve the labels object associated to the service object.
|
||||||
|
// Labels are available under the "Spec.Labels" property.
|
||||||
|
// API schema references:
|
||||||
|
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
|
||||||
|
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
||||||
|
func selectorServiceLabels(responseObject map[string]interface{}) map[string]interface{} {
|
||||||
|
serviceSpecObject := responseutils.GetJSONObject(responseObject, "Spec")
|
||||||
|
if serviceSpecObject != nil {
|
||||||
|
return responseutils.GetJSONObject(serviceSpecObject, "Labels")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,7 +1,9 @@
|
||||||
package proxy
|
package docker
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
// swarmInspectOperation extracts the response as a JSON object and rewrites the response based
|
// swarmInspectOperation extracts the response as a JSON object and rewrites the response based
|
||||||
|
@ -9,7 +11,7 @@ import (
|
||||||
func swarmInspectOperation(response *http.Response, executor *operationExecutor) error {
|
func swarmInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||||
// SwarmInspect response is a JSON object
|
// SwarmInspect response is a JSON object
|
||||||
// https://docs.docker.com/engine/api/v1.30/#operation/SwarmInspect
|
// https://docs.docker.com/engine/api/v1.30/#operation/SwarmInspect
|
||||||
responseObject, err := getResponseAsJSONOBject(response)
|
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -19,5 +21,5 @@ func swarmInspectOperation(response *http.Response, executor *operationExecutor)
|
||||||
delete(responseObject, "TLSInfo")
|
delete(responseObject, "TLSInfo")
|
||||||
}
|
}
|
||||||
|
|
||||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||||
}
|
}
|
50
api/http/proxy/factory/docker/tasks.go
Normal file
50
api/http/proxy/factory/docker/tasks.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
taskServiceObjectIdentifier = "ServiceID"
|
||||||
|
)
|
||||||
|
|
||||||
|
// taskListOperation extracts the response as a JSON array, loop through the tasks array
|
||||||
|
// and filter the containers based on resource controls before rewriting the response.
|
||||||
|
func (transport *Transport) taskListOperation(response *http.Response, executor *operationExecutor) error {
|
||||||
|
// TaskList response is a JSON array
|
||||||
|
// https://docs.docker.com/engine/api/v1.28/#operation/TaskList
|
||||||
|
responseArray, err := responseutils.GetResponseAsJSONArray(response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceOperationParameters := &resourceOperationParameters{
|
||||||
|
resourceIdentifierAttribute: taskServiceObjectIdentifier,
|
||||||
|
resourceType: portainer.ServiceResourceControl,
|
||||||
|
labelsObjectSelector: selectorTaskLabels,
|
||||||
|
}
|
||||||
|
|
||||||
|
responseArray, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, responseArray, executor)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseutils.RewriteResponse(response, responseArray, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectorServiceLabels retrieve the labels object associated to the task object.
|
||||||
|
// Labels are available under the "Spec.ContainerSpec.Labels" property.
|
||||||
|
// API schema reference: https://docs.docker.com/engine/api/v1.28/#operation/TaskList
|
||||||
|
func selectorTaskLabels(responseObject map[string]interface{}) map[string]interface{} {
|
||||||
|
taskSpecObject := responseutils.GetJSONObject(responseObject, "Spec")
|
||||||
|
if taskSpecObject != nil {
|
||||||
|
containerSpecObject := responseutils.GetJSONObject(taskSpecObject, "ContainerSpec")
|
||||||
|
if containerSpecObject != nil {
|
||||||
|
return responseutils.GetJSONObject(containerSpecObject, "Labels")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
727
api/http/proxy/factory/docker/transport.go
Normal file
727
api/http/proxy/factory/docker/transport.go
Normal file
|
@ -0,0 +1,727 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/docker"
|
||||||
|
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||||
|
"github.com/portainer/portainer/api/http/security"
|
||||||
|
)
|
||||||
|
|
||||||
|
var apiVersionRe = regexp.MustCompile(`(/v[0-9]\.[0-9]*)?`)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// Transport is a custom transport for Docker API reverse proxy. It allows
|
||||||
|
// interception of requests and rewriting of responses.
|
||||||
|
Transport struct {
|
||||||
|
HTTPTransport *http.Transport
|
||||||
|
endpoint *portainer.Endpoint
|
||||||
|
resourceControlService portainer.ResourceControlService
|
||||||
|
userService portainer.UserService
|
||||||
|
teamService portainer.TeamService
|
||||||
|
teamMembershipService portainer.TeamMembershipService
|
||||||
|
registryService portainer.RegistryService
|
||||||
|
dockerHubService portainer.DockerHubService
|
||||||
|
settingsService portainer.SettingsService
|
||||||
|
signatureService portainer.DigitalSignatureService
|
||||||
|
reverseTunnelService portainer.ReverseTunnelService
|
||||||
|
extensionService portainer.ExtensionService
|
||||||
|
dockerClient *client.Client
|
||||||
|
dockerClientFactory *docker.ClientFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
// TransportParameters is used to create a new Transport
|
||||||
|
TransportParameters struct {
|
||||||
|
Endpoint *portainer.Endpoint
|
||||||
|
ResourceControlService portainer.ResourceControlService
|
||||||
|
UserService portainer.UserService
|
||||||
|
TeamService portainer.TeamService
|
||||||
|
TeamMembershipService portainer.TeamMembershipService
|
||||||
|
RegistryService portainer.RegistryService
|
||||||
|
DockerHubService portainer.DockerHubService
|
||||||
|
SettingsService portainer.SettingsService
|
||||||
|
SignatureService portainer.DigitalSignatureService
|
||||||
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
|
ExtensionService portainer.ExtensionService
|
||||||
|
DockerClientFactory *docker.ClientFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
restrictedDockerOperationContext struct {
|
||||||
|
isAdmin bool
|
||||||
|
endpointResourceAccess bool
|
||||||
|
userID portainer.UserID
|
||||||
|
userTeamIDs []portainer.TeamID
|
||||||
|
resourceControls []portainer.ResourceControl
|
||||||
|
}
|
||||||
|
|
||||||
|
operationExecutor struct {
|
||||||
|
operationContext *restrictedDockerOperationContext
|
||||||
|
labelBlackList []portainer.Pair
|
||||||
|
}
|
||||||
|
restrictedOperationRequest func(*http.Response, *operationExecutor) error
|
||||||
|
operationRequest func(*http.Request) error
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewTransport returns a pointer to a new Transport instance.
|
||||||
|
func NewTransport(parameters *TransportParameters, httpTransport *http.Transport) (*Transport, error) {
|
||||||
|
dockerClient, err := parameters.DockerClientFactory.CreateClient(parameters.Endpoint, "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
transport := &Transport{
|
||||||
|
endpoint: parameters.Endpoint,
|
||||||
|
resourceControlService: parameters.ResourceControlService,
|
||||||
|
userService: parameters.UserService,
|
||||||
|
teamService: parameters.TeamService,
|
||||||
|
teamMembershipService: parameters.TeamMembershipService,
|
||||||
|
registryService: parameters.RegistryService,
|
||||||
|
dockerHubService: parameters.DockerHubService,
|
||||||
|
settingsService: parameters.SettingsService,
|
||||||
|
signatureService: parameters.SignatureService,
|
||||||
|
reverseTunnelService: parameters.ReverseTunnelService,
|
||||||
|
extensionService: parameters.ExtensionService,
|
||||||
|
dockerClientFactory: parameters.DockerClientFactory,
|
||||||
|
HTTPTransport: httpTransport,
|
||||||
|
dockerClient: dockerClient,
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||||
|
func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||||
|
return transport.ProxyDockerRequest(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProxyDockerRequest intercepts a Docker API request and apply logic based
|
||||||
|
// on the requested operation.
|
||||||
|
func (transport *Transport) ProxyDockerRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
requestPath := apiVersionRe.ReplaceAllString(request.URL.Path, "")
|
||||||
|
request.URL.Path = requestPath
|
||||||
|
|
||||||
|
if transport.endpoint.Type == portainer.AgentOnDockerEnvironment {
|
||||||
|
signature, err := transport.signatureService.CreateSignature(portainer.PortainerAgentSignatureMessage)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
request.Header.Set(portainer.PortainerAgentPublicKeyHeader, transport.signatureService.EncodedPublicKey())
|
||||||
|
request.Header.Set(portainer.PortainerAgentSignatureHeader, signature)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(requestPath, "/configs"):
|
||||||
|
return transport.proxyConfigRequest(request)
|
||||||
|
case strings.HasPrefix(requestPath, "/containers"):
|
||||||
|
return transport.proxyContainerRequest(request)
|
||||||
|
case strings.HasPrefix(requestPath, "/services"):
|
||||||
|
return transport.proxyServiceRequest(request)
|
||||||
|
case strings.HasPrefix(requestPath, "/volumes"):
|
||||||
|
return transport.proxyVolumeRequest(request)
|
||||||
|
case strings.HasPrefix(requestPath, "/networks"):
|
||||||
|
return transport.proxyNetworkRequest(request)
|
||||||
|
case strings.HasPrefix(requestPath, "/secrets"):
|
||||||
|
return transport.proxySecretRequest(request)
|
||||||
|
case strings.HasPrefix(requestPath, "/swarm"):
|
||||||
|
return transport.proxySwarmRequest(request)
|
||||||
|
case strings.HasPrefix(requestPath, "/nodes"):
|
||||||
|
return transport.proxyNodeRequest(request)
|
||||||
|
case strings.HasPrefix(requestPath, "/tasks"):
|
||||||
|
return transport.proxyTaskRequest(request)
|
||||||
|
case strings.HasPrefix(requestPath, "/build"):
|
||||||
|
return transport.proxyBuildRequest(request)
|
||||||
|
case strings.HasPrefix(requestPath, "/images"):
|
||||||
|
return transport.proxyImageRequest(request)
|
||||||
|
case strings.HasPrefix(requestPath, "/v2"):
|
||||||
|
return transport.proxyAgentRequest(request)
|
||||||
|
default:
|
||||||
|
return transport.executeDockerRequest(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) executeDockerRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
response, err := transport.HTTPTransport.RoundTrip(request)
|
||||||
|
|
||||||
|
if transport.endpoint.Type != portainer.EdgeAgentEnvironment {
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
transport.reverseTunnelService.SetTunnelStatusToActive(transport.endpoint.ID)
|
||||||
|
} else {
|
||||||
|
transport.reverseTunnelService.SetTunnelStatusToIdle(transport.endpoint.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) proxyAgentRequest(r *http.Request) (*http.Response, error) {
|
||||||
|
requestPath := strings.TrimPrefix(r.URL.Path, "/v2")
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(requestPath, "/browse"):
|
||||||
|
volumeIDParameter, found := r.URL.Query()["volumeID"]
|
||||||
|
if !found || len(volumeIDParameter) < 1 {
|
||||||
|
return transport.administratorOperation(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.restrictedResourceOperation(r, volumeIDParameter[0], portainer.VolumeResourceControl, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.executeDockerRequest(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) proxyConfigRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
switch requestPath := request.URL.Path; requestPath {
|
||||||
|
case "/configs/create":
|
||||||
|
return transport.decorateGenericResourceCreationOperation(request, configObjectIdentifier, portainer.ConfigResourceControl)
|
||||||
|
|
||||||
|
case "/configs":
|
||||||
|
return transport.rewriteOperation(request, transport.configListOperation)
|
||||||
|
|
||||||
|
default:
|
||||||
|
// assume /configs/{id}
|
||||||
|
configID := path.Base(requestPath)
|
||||||
|
|
||||||
|
if request.Method == http.MethodGet {
|
||||||
|
return transport.rewriteOperation(request, transport.configInspectOperation)
|
||||||
|
} else if request.Method == http.MethodDelete {
|
||||||
|
return transport.executeGenericResourceDeletionOperation(request, configID, portainer.ConfigResourceControl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.restrictedResourceOperation(request, configID, portainer.ConfigResourceControl, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
case "/containers/prune":
|
||||||
|
return transport.administratorOperation(request)
|
||||||
|
|
||||||
|
case "/containers/json":
|
||||||
|
return transport.rewriteOperationWithLabelFiltering(request, transport.containerListOperation)
|
||||||
|
|
||||||
|
default:
|
||||||
|
// This section assumes /containers/**
|
||||||
|
if match, _ := path.Match("/containers/*/*", requestPath); match {
|
||||||
|
// Handle /containers/{id}/{action} requests
|
||||||
|
containerID := path.Base(path.Dir(requestPath))
|
||||||
|
action := path.Base(requestPath)
|
||||||
|
|
||||||
|
if action == "json" {
|
||||||
|
return transport.rewriteOperation(request, transport.containerInspectOperation)
|
||||||
|
}
|
||||||
|
return transport.restrictedResourceOperation(request, containerID, portainer.ContainerResourceControl, false)
|
||||||
|
} else if match, _ := path.Match("/containers/*", requestPath); match {
|
||||||
|
// Handle /containers/{id} requests
|
||||||
|
containerID := path.Base(requestPath)
|
||||||
|
|
||||||
|
if request.Method == http.MethodDelete {
|
||||||
|
return transport.executeGenericResourceDeletionOperation(request, containerID, portainer.ContainerResourceControl)
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.restrictedResourceOperation(request, containerID, portainer.ContainerResourceControl, false)
|
||||||
|
}
|
||||||
|
return transport.executeDockerRequest(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) proxyServiceRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
switch requestPath := request.URL.Path; requestPath {
|
||||||
|
case "/services/create":
|
||||||
|
return transport.replaceRegistryAuthenticationHeader(request)
|
||||||
|
|
||||||
|
case "/services":
|
||||||
|
return transport.rewriteOperation(request, transport.serviceListOperation)
|
||||||
|
|
||||||
|
default:
|
||||||
|
// This section assumes /services/**
|
||||||
|
if match, _ := path.Match("/services/*/*", requestPath); match {
|
||||||
|
// Handle /services/{id}/{action} requests
|
||||||
|
serviceID := path.Base(path.Dir(requestPath))
|
||||||
|
return transport.restrictedResourceOperation(request, serviceID, portainer.ServiceResourceControl, false)
|
||||||
|
} else if match, _ := path.Match("/services/*", requestPath); match {
|
||||||
|
// Handle /services/{id} requests
|
||||||
|
serviceID := path.Base(requestPath)
|
||||||
|
|
||||||
|
switch request.Method {
|
||||||
|
case http.MethodGet:
|
||||||
|
return transport.rewriteOperation(request, transport.serviceInspectOperation)
|
||||||
|
case http.MethodDelete:
|
||||||
|
return transport.executeGenericResourceDeletionOperation(request, serviceID, portainer.ServiceResourceControl)
|
||||||
|
}
|
||||||
|
return transport.restrictedResourceOperation(request, serviceID, portainer.ServiceResourceControl, false)
|
||||||
|
}
|
||||||
|
return transport.executeDockerRequest(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) proxyVolumeRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
switch requestPath := request.URL.Path; requestPath {
|
||||||
|
case "/volumes/create":
|
||||||
|
return transport.decorateGenericResourceCreationOperation(request, volumeObjectIdentifier, portainer.VolumeResourceControl)
|
||||||
|
|
||||||
|
case "/volumes/prune":
|
||||||
|
return transport.administratorOperation(request)
|
||||||
|
|
||||||
|
case "/volumes":
|
||||||
|
return transport.rewriteOperation(request, transport.volumeListOperation)
|
||||||
|
|
||||||
|
default:
|
||||||
|
// assume /volumes/{name}
|
||||||
|
volumeID := path.Base(requestPath)
|
||||||
|
|
||||||
|
if request.Method == http.MethodGet {
|
||||||
|
return transport.rewriteOperation(request, transport.volumeInspectOperation)
|
||||||
|
} else if request.Method == http.MethodDelete {
|
||||||
|
return transport.executeGenericResourceDeletionOperation(request, volumeID, portainer.VolumeResourceControl)
|
||||||
|
}
|
||||||
|
return transport.restrictedResourceOperation(request, volumeID, portainer.VolumeResourceControl, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) proxyNetworkRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
switch requestPath := request.URL.Path; requestPath {
|
||||||
|
case "/networks/create":
|
||||||
|
return transport.decorateGenericResourceCreationOperation(request, networkObjectIdentifier, portainer.NetworkResourceControl)
|
||||||
|
|
||||||
|
case "/networks":
|
||||||
|
return transport.rewriteOperation(request, transport.networkListOperation)
|
||||||
|
|
||||||
|
default:
|
||||||
|
// assume /networks/{id}
|
||||||
|
networkID := path.Base(requestPath)
|
||||||
|
|
||||||
|
if request.Method == http.MethodGet {
|
||||||
|
return transport.rewriteOperation(request, transport.networkInspectOperation)
|
||||||
|
} else if request.Method == http.MethodDelete {
|
||||||
|
return transport.executeGenericResourceDeletionOperation(request, networkID, portainer.NetworkResourceControl)
|
||||||
|
}
|
||||||
|
return transport.restrictedResourceOperation(request, networkID, portainer.NetworkResourceControl, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) proxySecretRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
switch requestPath := request.URL.Path; requestPath {
|
||||||
|
case "/secrets/create":
|
||||||
|
return transport.decorateGenericResourceCreationOperation(request, secretObjectIdentifier, portainer.SecretResourceControl)
|
||||||
|
|
||||||
|
case "/secrets":
|
||||||
|
return transport.rewriteOperation(request, transport.secretListOperation)
|
||||||
|
|
||||||
|
default:
|
||||||
|
// assume /secrets/{id}
|
||||||
|
secretID := path.Base(requestPath)
|
||||||
|
|
||||||
|
if request.Method == http.MethodGet {
|
||||||
|
return transport.rewriteOperation(request, transport.secretInspectOperation)
|
||||||
|
} else if request.Method == http.MethodDelete {
|
||||||
|
return transport.executeGenericResourceDeletionOperation(request, secretID, portainer.SecretResourceControl)
|
||||||
|
}
|
||||||
|
return transport.restrictedResourceOperation(request, secretID, portainer.SecretResourceControl, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) proxyNodeRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
requestPath := request.URL.Path
|
||||||
|
|
||||||
|
// assume /nodes/{id}
|
||||||
|
if path.Base(requestPath) != "nodes" {
|
||||||
|
return transport.administratorOperation(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.executeDockerRequest(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) proxySwarmRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
switch requestPath := request.URL.Path; requestPath {
|
||||||
|
case "/swarm":
|
||||||
|
return transport.rewriteOperation(request, swarmInspectOperation)
|
||||||
|
default:
|
||||||
|
// assume /swarm/{action}
|
||||||
|
return transport.administratorOperation(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) proxyTaskRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
switch requestPath := request.URL.Path; requestPath {
|
||||||
|
case "/tasks":
|
||||||
|
return transport.rewriteOperation(request, transport.taskListOperation)
|
||||||
|
default:
|
||||||
|
// assume /tasks/{id}
|
||||||
|
return transport.executeDockerRequest(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) proxyBuildRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
return transport.interceptAndRewriteRequest(request, buildOperation)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) proxyImageRequest(request *http.Request) (*http.Response, error) {
|
||||||
|
switch requestPath := request.URL.Path; requestPath {
|
||||||
|
case "/images/create":
|
||||||
|
return transport.replaceRegistryAuthenticationHeader(request)
|
||||||
|
default:
|
||||||
|
if path.Base(requestPath) == "push" && request.Method == http.MethodPost {
|
||||||
|
return transport.replaceRegistryAuthenticationHeader(request)
|
||||||
|
}
|
||||||
|
return transport.executeDockerRequest(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) replaceRegistryAuthenticationHeader(request *http.Request) (*http.Response, error) {
|
||||||
|
accessContext, err := transport.createRegistryAccessContext(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
originalHeader := request.Header.Get("X-Registry-Auth")
|
||||||
|
|
||||||
|
if originalHeader != "" {
|
||||||
|
|
||||||
|
decodedHeaderData, err := base64.StdEncoding.DecodeString(originalHeader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var originalHeaderData registryAuthenticationHeader
|
||||||
|
err = json.Unmarshal(decodedHeaderData, &originalHeaderData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
authenticationHeader := createRegistryAuthenticationHeader(originalHeaderData.Serveraddress, accessContext)
|
||||||
|
|
||||||
|
headerData, err := json.Marshal(authenticationHeader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
header := base64.StdEncoding.EncodeToString(headerData)
|
||||||
|
|
||||||
|
request.Header.Set("X-Registry-Auth", header)
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.decorateGenericResourceCreationOperation(request, serviceObjectIdentifier, portainer.ServiceResourceControl)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) restrictedResourceOperation(request *http.Request, resourceID string, resourceType portainer.ResourceControlType, volumeBrowseRestrictionCheck bool) (*http.Response, error) {
|
||||||
|
var err error
|
||||||
|
tokenData, err := security.RetrieveTokenData(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenData.Role != portainer.AdministratorRole {
|
||||||
|
rbacExtension, err := transport.extensionService.Extension(portainer.RBACExtension)
|
||||||
|
if err != nil && err != portainer.ErrObjectNotFound {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
user, err := transport.userService.User(tokenData.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if volumeBrowseRestrictionCheck {
|
||||||
|
settings, err := transport.settingsService.Settings()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return access denied for all roles except endpoint-administrator
|
||||||
|
_, userCanBrowse := user.EndpointAuthorizations[transport.endpoint.ID][portainer.OperationDockerAgentBrowseList]
|
||||||
|
if rbacExtension != nil && !settings.AllowVolumeBrowserForRegularUsers && !userCanBrowse {
|
||||||
|
return responseutils.WriteAccessDeniedResponse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
endpointResourceAccess := false
|
||||||
|
_, ok := user.EndpointAuthorizations[transport.endpoint.ID][portainer.EndpointResourcesAccess]
|
||||||
|
if ok {
|
||||||
|
endpointResourceAccess = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if rbacExtension != nil && endpointResourceAccess {
|
||||||
|
return transport.executeDockerRequest(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
teamMemberships, err := transport.teamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userTeamIDs := make([]portainer.TeamID, 0)
|
||||||
|
for _, membership := range teamMemberships {
|
||||||
|
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceControls, err := transport.resourceControlService.ResourceControls()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceControl := portainer.GetResourceControlByResourceIDAndType(resourceID, resourceType, resourceControls)
|
||||||
|
if resourceControl == nil {
|
||||||
|
agentTargetHeader := request.Header.Get(portainer.PortainerAgentTargetHeader)
|
||||||
|
|
||||||
|
// This resource was created outside of portainer,
|
||||||
|
// is part of a Docker service or part of a Docker Swarm/Compose stack.
|
||||||
|
inheritedResourceControl, err := transport.getInheritedResourceControlFromServiceOrStack(resourceID, agentTargetHeader, resourceType, resourceControls)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if inheritedResourceControl == nil || !portainer.UserCanAccessResource(tokenData.ID, userTeamIDs, inheritedResourceControl) {
|
||||||
|
return responseutils.WriteAccessDeniedResponse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if resourceControl != nil && !portainer.UserCanAccessResource(tokenData.ID, userTeamIDs, resourceControl) {
|
||||||
|
return responseutils.WriteAccessDeniedResponse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.executeDockerRequest(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rewriteOperationWithLabelFiltering will create a new operation context with data that will be used
|
||||||
|
// to decorate the original request's response as well as retrieve all the black listed labels
|
||||||
|
// to filter the resources.
|
||||||
|
func (transport *Transport) rewriteOperationWithLabelFiltering(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) {
|
||||||
|
operationContext, err := transport.createOperationContext(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
settings, err := transport.settingsService.Settings()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
executor := &operationExecutor{
|
||||||
|
operationContext: operationContext,
|
||||||
|
labelBlackList: settings.BlackListedLabels,
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.executeRequestAndRewriteResponse(request, operation, executor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rewriteOperation will create a new operation context with data that will be used
|
||||||
|
// to decorate the original request's response.
|
||||||
|
func (transport *Transport) rewriteOperation(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) {
|
||||||
|
operationContext, err := transport.createOperationContext(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
executor := &operationExecutor{
|
||||||
|
operationContext: operationContext,
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.executeRequestAndRewriteResponse(request, operation, executor)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) interceptAndRewriteRequest(request *http.Request, operation operationRequest) (*http.Response, error) {
|
||||||
|
err := operation(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.executeDockerRequest(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decorateGenericResourceCreationResponse extracts the response as a JSON object, extracts the resource identifier from that object based
|
||||||
|
// on the resourceIdentifierAttribute parameter then generate a new resource control associated to that resource
|
||||||
|
// with a random token and rewrites the response by decorating the original response with a ResourceControl object.
|
||||||
|
// The generic Docker API response format is JSON object:
|
||||||
|
// https://docs.docker.com/engine/api/v1.40/#operation/ContainerCreate
|
||||||
|
// https://docs.docker.com/engine/api/v1.40/#operation/NetworkCreate
|
||||||
|
// https://docs.docker.com/engine/api/v1.40/#operation/VolumeCreate
|
||||||
|
// https://docs.docker.com/engine/api/v1.40/#operation/ServiceCreate
|
||||||
|
// https://docs.docker.com/engine/api/v1.40/#operation/SecretCreate
|
||||||
|
// https://docs.docker.com/engine/api/v1.40/#operation/ConfigCreate
|
||||||
|
func (transport *Transport) decorateGenericResourceCreationResponse(response *http.Response, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType, userID portainer.UserID) error {
|
||||||
|
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if responseObject[resourceIdentifierAttribute] == nil {
|
||||||
|
log.Printf("[ERROR] [proxy,docker]")
|
||||||
|
return errors.New("missing identifier in Docker resource creation response")
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceID := responseObject[resourceIdentifierAttribute].(string)
|
||||||
|
|
||||||
|
resourceControl, err := transport.createPrivateResourceControl(resourceID, resourceType, userID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
responseObject = decorateObject(responseObject, resourceControl)
|
||||||
|
|
||||||
|
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) decorateGenericResourceCreationOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) {
|
||||||
|
tokenData, err := security.RetrieveTokenData(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) executeGenericResourceDeletionOperation(request *http.Request, resourceIdentifierAttribute string, resourceType portainer.ResourceControlType) (*http.Response, error) {
|
||||||
|
response, err := transport.restrictedResourceOperation(request, resourceIdentifierAttribute, resourceType, false)
|
||||||
|
if err != nil {
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceControl, err := transport.resourceControlService.ResourceControlByResourceIDAndType(resourceIdentifierAttribute, resourceType)
|
||||||
|
if err != nil {
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if resourceControl != nil {
|
||||||
|
err = transport.resourceControlService.DeleteResourceControl(resourceControl.ID)
|
||||||
|
if err != nil {
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) executeRequestAndRewriteResponse(request *http.Request, operation restrictedOperationRequest, executor *operationExecutor) (*http.Response, error) {
|
||||||
|
response, err := transport.executeDockerRequest(request)
|
||||||
|
if err != nil {
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = operation(response, executor)
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// administratorOperation ensures that the user has administrator privileges
|
||||||
|
// before executing the original request.
|
||||||
|
func (transport *Transport) administratorOperation(request *http.Request) (*http.Response, error) {
|
||||||
|
tokenData, err := security.RetrieveTokenData(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenData.Role != portainer.AdministratorRole {
|
||||||
|
return responseutils.WriteAccessDeniedResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.executeDockerRequest(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) createRegistryAccessContext(request *http.Request) (*registryAccessContext, error) {
|
||||||
|
tokenData, err := security.RetrieveTokenData(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accessContext := ®istryAccessContext{
|
||||||
|
isAdmin: true,
|
||||||
|
userID: tokenData.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
hub, err := transport.dockerHubService.DockerHub()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
accessContext.dockerHub = hub
|
||||||
|
|
||||||
|
registries, err := transport.registryService.Registries()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
accessContext.registries = registries
|
||||||
|
|
||||||
|
if tokenData.Role != portainer.AdministratorRole {
|
||||||
|
accessContext.isAdmin = false
|
||||||
|
|
||||||
|
teamMemberships, err := transport.teamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
accessContext.teamMemberships = teamMemberships
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessContext, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (transport *Transport) createOperationContext(request *http.Request) (*restrictedDockerOperationContext, error) {
|
||||||
|
var err error
|
||||||
|
tokenData, err := security.RetrieveTokenData(request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceControls, err := transport.resourceControlService.ResourceControls()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
operationContext := &restrictedDockerOperationContext{
|
||||||
|
isAdmin: true,
|
||||||
|
userID: tokenData.ID,
|
||||||
|
resourceControls: resourceControls,
|
||||||
|
endpointResourceAccess: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenData.Role != portainer.AdministratorRole {
|
||||||
|
operationContext.isAdmin = false
|
||||||
|
|
||||||
|
user, err := transport.userService.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.teamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userTeamIDs := make([]portainer.TeamID, 0)
|
||||||
|
for _, membership := range teamMemberships {
|
||||||
|
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
||||||
|
}
|
||||||
|
operationContext.userTeamIDs = userTeamIDs
|
||||||
|
}
|
||||||
|
|
||||||
|
return operationContext, nil
|
||||||
|
}
|
89
api/http/proxy/factory/docker/volumes.go
Normal file
89
api/http/proxy/factory/docker/volumes.go
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
package docker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory/responseutils"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
volumeObjectIdentifier = "Name"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getInheritedResourceControlFromVolumeLabels(dockerClient *client.Client, volumeID string, resourceControls []portainer.ResourceControl) (*portainer.ResourceControl, error) {
|
||||||
|
volume, err := dockerClient.VolumeInspect(context.Background(), volumeID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
swarmStackName := volume.Labels[resourceLabelForDockerSwarmStackName]
|
||||||
|
if swarmStackName != "" {
|
||||||
|
return portainer.GetResourceControlByResourceIDAndType(swarmStackName, portainer.StackResourceControl, resourceControls), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// volumeListOperation extracts the response as a JSON object, loop through the volume array
|
||||||
|
// decorate and/or filter the volumes based on resource controls before rewriting the response.
|
||||||
|
func (transport *Transport) volumeListOperation(response *http.Response, executor *operationExecutor) error {
|
||||||
|
// VolumeList response is a JSON object
|
||||||
|
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
||||||
|
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// The "Volumes" field contains the list of volumes as an array of JSON objects
|
||||||
|
if responseObject["Volumes"] != nil {
|
||||||
|
volumeData := responseObject["Volumes"].([]interface{})
|
||||||
|
|
||||||
|
resourceOperationParameters := &resourceOperationParameters{
|
||||||
|
resourceIdentifierAttribute: volumeObjectIdentifier,
|
||||||
|
resourceType: portainer.VolumeResourceControl,
|
||||||
|
labelsObjectSelector: selectorVolumeLabels,
|
||||||
|
}
|
||||||
|
|
||||||
|
volumeData, err = transport.applyAccessControlOnResourceList(resourceOperationParameters, volumeData, executor)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overwrite the original volume list
|
||||||
|
responseObject["Volumes"] = volumeData
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseutils.RewriteResponse(response, responseObject, http.StatusOK)
|
||||||
|
}
|
||||||
|
|
||||||
|
// volumeInspectOperation extracts the response as a JSON object, verify that the user
|
||||||
|
// has access to the volume based on any existing resource control and either rewrite an access denied response or a decorated volume.
|
||||||
|
func (transport *Transport) volumeInspectOperation(response *http.Response, executor *operationExecutor) error {
|
||||||
|
// VolumeInspect response is a JSON object
|
||||||
|
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect
|
||||||
|
responseObject, err := responseutils.GetResponseAsJSONOBject(response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
resourceOperationParameters := &resourceOperationParameters{
|
||||||
|
resourceIdentifierAttribute: volumeObjectIdentifier,
|
||||||
|
resourceType: portainer.VolumeResourceControl,
|
||||||
|
labelsObjectSelector: selectorVolumeLabels,
|
||||||
|
}
|
||||||
|
|
||||||
|
return transport.applyAccessControlOnResource(resourceOperationParameters, responseObject, response, executor)
|
||||||
|
}
|
||||||
|
|
||||||
|
// selectorVolumeLabels retrieve the labels object associated to the volume object.
|
||||||
|
// Labels are available under the "Labels" property.
|
||||||
|
// API schema references:
|
||||||
|
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect
|
||||||
|
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
||||||
|
func selectorVolumeLabels(responseObject map[string]interface{}) map[string]interface{} {
|
||||||
|
return responseutils.GetJSONObject(responseObject, "Labels")
|
||||||
|
}
|
46
api/http/proxy/factory/docker_unix.go
Normal file
46
api/http/proxy/factory/docker_unix.go
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
// +build aix darwin dragonfly freebsd linux netbsd openbsd solaris
|
||||||
|
|
||||||
|
package factory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory/docker"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (factory ProxyFactory) newOSBasedLocalProxy(path string, endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||||
|
transportParameters := &docker.TransportParameters{
|
||||||
|
Endpoint: endpoint,
|
||||||
|
ResourceControlService: factory.resourceControlService,
|
||||||
|
UserService: factory.userService,
|
||||||
|
TeamService: factory.teamService,
|
||||||
|
TeamMembershipService: factory.teamMembershipService,
|
||||||
|
RegistryService: factory.registryService,
|
||||||
|
DockerHubService: factory.dockerHubService,
|
||||||
|
SettingsService: factory.settingsService,
|
||||||
|
ReverseTunnelService: factory.reverseTunnelService,
|
||||||
|
ExtensionService: factory.extensionService,
|
||||||
|
SignatureService: factory.signatureService,
|
||||||
|
DockerClientFactory: factory.dockerClientFactory,
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := &dockerLocalProxy{}
|
||||||
|
|
||||||
|
dockerTransport, err := docker.NewTransport(transportParameters, newSocketTransport(path))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy.transport = dockerTransport
|
||||||
|
return proxy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newSocketTransport(socketPath string) *http.Transport {
|
||||||
|
return &http.Transport{
|
||||||
|
Dial: func(proto, addr string) (conn net.Conn, err error) {
|
||||||
|
return net.Dial("unix", socketPath)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
47
api/http/proxy/factory/docker_windows.go
Normal file
47
api/http/proxy/factory/docker_windows.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package factory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/Microsoft/go-winio"
|
||||||
|
portainer "github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory/docker"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (factory ProxyFactory) newOSBasedLocalProxy(path string, endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||||
|
transportParameters := &docker.TransportParameters{
|
||||||
|
Endpoint: endpoint,
|
||||||
|
ResourceControlService: factory.resourceControlService,
|
||||||
|
UserService: factory.userService,
|
||||||
|
TeamService: factory.teamService,
|
||||||
|
TeamMembershipService: factory.teamMembershipService,
|
||||||
|
RegistryService: factory.registryService,
|
||||||
|
DockerHubService: factory.dockerHubService,
|
||||||
|
SettingsService: factory.settingsService,
|
||||||
|
ReverseTunnelService: factory.reverseTunnelService,
|
||||||
|
ExtensionService: factory.extensionService,
|
||||||
|
SignatureService: factory.signatureService,
|
||||||
|
DockerClientFactory: factory.dockerClientFactory,
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := &dockerLocalProxy{}
|
||||||
|
|
||||||
|
dockerTransport, err := docker.NewTransport(transportParameters, newNamedPipeTransport(path))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy.transport = dockerTransport
|
||||||
|
return proxy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNamedPipeTransport(namedPipePath string) *http.Transport {
|
||||||
|
return &http.Transport{
|
||||||
|
Dial: func(proto, addr string) (conn net.Conn, err error) {
|
||||||
|
return winio.DialPipe(namedPipePath, nil)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
114
api/http/proxy/factory/factory.go
Normal file
114
api/http/proxy/factory/factory.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
package factory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httputil"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/docker"
|
||||||
|
)
|
||||||
|
|
||||||
|
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 struct {
|
||||||
|
resourceControlService portainer.ResourceControlService
|
||||||
|
userService portainer.UserService
|
||||||
|
teamService portainer.TeamService
|
||||||
|
teamMembershipService portainer.TeamMembershipService
|
||||||
|
settingsService portainer.SettingsService
|
||||||
|
registryService portainer.RegistryService
|
||||||
|
dockerHubService portainer.DockerHubService
|
||||||
|
signatureService portainer.DigitalSignatureService
|
||||||
|
reverseTunnelService portainer.ReverseTunnelService
|
||||||
|
extensionService portainer.ExtensionService
|
||||||
|
dockerClientFactory *docker.ClientFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProxyFactoryParameters is used to create a new ProxyFactory
|
||||||
|
ProxyFactoryParameters struct {
|
||||||
|
ResourceControlService portainer.ResourceControlService
|
||||||
|
UserService portainer.UserService
|
||||||
|
TeamService portainer.TeamService
|
||||||
|
TeamMembershipService portainer.TeamMembershipService
|
||||||
|
SettingsService portainer.SettingsService
|
||||||
|
RegistryService portainer.RegistryService
|
||||||
|
DockerHubService portainer.DockerHubService
|
||||||
|
SignatureService portainer.DigitalSignatureService
|
||||||
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
|
ExtensionService portainer.ExtensionService
|
||||||
|
DockerClientFactory *docker.ClientFactory
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewProxyFactory returns a pointer to a new instance of a ProxyFactory
|
||||||
|
func NewProxyFactory(parameters *ProxyFactoryParameters) *ProxyFactory {
|
||||||
|
return &ProxyFactory{
|
||||||
|
resourceControlService: parameters.ResourceControlService,
|
||||||
|
userService: parameters.UserService,
|
||||||
|
teamService: parameters.TeamService,
|
||||||
|
teamMembershipService: parameters.TeamMembershipService,
|
||||||
|
settingsService: parameters.SettingsService,
|
||||||
|
registryService: parameters.RegistryService,
|
||||||
|
dockerHubService: parameters.DockerHubService,
|
||||||
|
signatureService: parameters.SignatureService,
|
||||||
|
reverseTunnelService: parameters.ReverseTunnelService,
|
||||||
|
extensionService: parameters.ExtensionService,
|
||||||
|
dockerClientFactory: parameters.DockerClientFactory,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
extensionURL.Scheme = "http"
|
||||||
|
proxy := httputil.NewSingleHostReverseProxy(extensionURL)
|
||||||
|
return proxy, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEndpointProxy returns a new reverse proxy (filesystem based or HTTP) to an endpoint API server
|
||||||
|
func (factory *ProxyFactory) NewEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||||
|
switch endpoint.Type {
|
||||||
|
case portainer.AzureEnvironment:
|
||||||
|
return newAzureProxy(endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
return factory.newDockerProxy(endpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewGitlabProxy returns a new HTTP proxy to a Gitlab API server
|
||||||
|
func (factory *ProxyFactory) NewGitlabProxy(gitlabAPIUri string) (http.Handler, error) {
|
||||||
|
return newGitlabProxy(gitlabAPIUri)
|
||||||
|
}
|
19
api/http/proxy/factory/gitlab.go
Normal file
19
api/http/proxy/factory/gitlab.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package factory
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory/gitlab"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newGitlabProxy(uri string) (http.Handler, error) {
|
||||||
|
url, err := url.Parse(uri)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := newSingleHostReverseProxyWithHostHeader(url)
|
||||||
|
proxy.Transport = gitlab.NewTransport()
|
||||||
|
return proxy, nil
|
||||||
|
}
|
34
api/http/proxy/factory/gitlab/transport.go
Normal file
34
api/http/proxy/factory/gitlab/transport.go
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
package gitlab
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Transport struct {
|
||||||
|
httpTransport *http.Transport
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTransport returns a pointer to a new instance of Transport that implements the HTTP Transport
|
||||||
|
// interface for proxying requests to the Gitlab API.
|
||||||
|
func NewTransport() *Transport {
|
||||||
|
return &Transport{
|
||||||
|
httpTransport: &http.Transport{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoundTrip is the implementation of the the http.RoundTripper interface
|
||||||
|
func (transport *Transport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||||
|
token := request.Header.Get("Private-Token")
|
||||||
|
if token == "" {
|
||||||
|
return nil, errors.New("no gitlab token provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := http.NewRequest(request.Method, request.URL.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Set("Private-Token", token)
|
||||||
|
return transport.httpTransport.RoundTrip(r)
|
||||||
|
}
|
11
api/http/proxy/factory/responseutils/json.go
Normal file
11
api/http/proxy/factory/responseutils/json.go
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
package responseutils
|
||||||
|
|
||||||
|
// GetJSONObject will extract an object from a specific property of another JSON object.
|
||||||
|
// Returns nil if nothing is associated to the specified key.
|
||||||
|
func GetJSONObject(jsonObject map[string]interface{}, property string) map[string]interface{} {
|
||||||
|
object := jsonObject[property]
|
||||||
|
if object != nil {
|
||||||
|
return object.(map[string]interface{})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
106
api/http/proxy/factory/responseutils/response.go
Normal file
106
api/http/proxy/factory/responseutils/response.go
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
package responseutils
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetResponseAsJSONOBject returns the response content as a generic JSON object
|
||||||
|
func GetResponseAsJSONOBject(response *http.Response) (map[string]interface{}, error) {
|
||||||
|
responseData, err := getResponseBodyAsGenericJSON(response)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
responseObject := responseData.(map[string]interface{})
|
||||||
|
return responseObject, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetResponseAsJSONArray returns the response content as an array of generic JSON object
|
||||||
|
func GetResponseAsJSONArray(response *http.Response) ([]interface{}, error) {
|
||||||
|
responseData, err := getResponseBodyAsGenericJSON(response)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch responseObject := responseData.(type) {
|
||||||
|
case []interface{}:
|
||||||
|
return responseObject, nil
|
||||||
|
case map[string]interface{}:
|
||||||
|
if responseObject["message"] != nil {
|
||||||
|
return nil, errors.New(responseObject["message"].(string))
|
||||||
|
}
|
||||||
|
log.Printf("[ERROR] [http,proxy,response] [message: invalid response format, expecting JSON array] [response: %+v]", responseObject)
|
||||||
|
return nil, errors.New("unable to parse response: expected JSON array, got JSON object")
|
||||||
|
default:
|
||||||
|
log.Printf("[ERROR] [http,proxy,response] [message: invalid response format, expecting JSON array] [response: %+v]", responseObject)
|
||||||
|
return nil, errors.New("unable to parse response: expected JSON array")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getResponseBodyAsGenericJSON(response *http.Response) (interface{}, error) {
|
||||||
|
if response.Body == nil {
|
||||||
|
return nil, errors.New("unable to parse response: empty response body")
|
||||||
|
}
|
||||||
|
|
||||||
|
var data interface{}
|
||||||
|
body, err := ioutil.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = response.Body.Close()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type dockerErrorResponse struct {
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteAccessDeniedResponse will create a new access denied response
|
||||||
|
func WriteAccessDeniedResponse() (*http.Response, error) {
|
||||||
|
response := &http.Response{}
|
||||||
|
err := RewriteResponse(response, dockerErrorResponse{Message: "access denied to resource"}, http.StatusForbidden)
|
||||||
|
return response, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RewriteAccessDeniedResponse will overwrite the existing response with an access denied response
|
||||||
|
func RewriteAccessDeniedResponse(response *http.Response) error {
|
||||||
|
return RewriteResponse(response, dockerErrorResponse{Message: "access denied to resource"}, http.StatusForbidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RewriteResponse will replace the existing response body and status code with the one specified
|
||||||
|
// in parameters
|
||||||
|
func RewriteResponse(response *http.Response, newResponseData interface{}, statusCode int) error {
|
||||||
|
jsonData, err := json.Marshal(newResponseData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
body := ioutil.NopCloser(bytes.NewReader(jsonData))
|
||||||
|
|
||||||
|
response.StatusCode = statusCode
|
||||||
|
response.Body = body
|
||||||
|
response.ContentLength = int64(len(jsonData))
|
||||||
|
|
||||||
|
if response.Header == nil {
|
||||||
|
response.Header = make(http.Header)
|
||||||
|
}
|
||||||
|
response.Header.Set("Content-Length", strconv.Itoa(len(jsonData)))
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
package proxy
|
package factory
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
|
@ -1,29 +0,0 @@
|
||||||
// +build !windows
|
|
||||||
|
|
||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (factory *proxyFactory) newLocalProxy(path string, endpoint *portainer.Endpoint) http.Handler {
|
|
||||||
proxy := &localProxy{}
|
|
||||||
transport := &proxyTransport{
|
|
||||||
enableSignature: false,
|
|
||||||
ResourceControlService: factory.ResourceControlService,
|
|
||||||
UserService: factory.UserService,
|
|
||||||
TeamMembershipService: factory.TeamMembershipService,
|
|
||||||
SettingsService: factory.SettingsService,
|
|
||||||
RegistryService: factory.RegistryService,
|
|
||||||
DockerHubService: factory.DockerHubService,
|
|
||||||
ExtensionService: factory.ExtensionService,
|
|
||||||
dockerTransport: newSocketTransport(path),
|
|
||||||
ReverseTunnelService: factory.ReverseTunnelService,
|
|
||||||
endpointIdentifier: endpoint.ID,
|
|
||||||
endpointType: endpoint.Type,
|
|
||||||
}
|
|
||||||
proxy.Transport = transport
|
|
||||||
return proxy
|
|
||||||
}
|
|
|
@ -1,40 +0,0 @@
|
||||||
// +build windows
|
|
||||||
|
|
||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/Microsoft/go-winio"
|
|
||||||
|
|
||||||
portainer "github.com/portainer/portainer/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
func (factory *proxyFactory) newLocalProxy(path string, endpoint *portainer.Endpoint) http.Handler {
|
|
||||||
proxy := &localProxy{}
|
|
||||||
transport := &proxyTransport{
|
|
||||||
enableSignature: false,
|
|
||||||
ResourceControlService: factory.ResourceControlService,
|
|
||||||
UserService: factory.UserService,
|
|
||||||
TeamMembershipService: factory.TeamMembershipService,
|
|
||||||
SettingsService: factory.SettingsService,
|
|
||||||
RegistryService: factory.RegistryService,
|
|
||||||
DockerHubService: factory.DockerHubService,
|
|
||||||
ReverseTunnelService: factory.ReverseTunnelService,
|
|
||||||
ExtensionService: factory.ExtensionService,
|
|
||||||
dockerTransport: newNamedPipeTransport(path),
|
|
||||||
endpointIdentifier: endpoint.ID,
|
|
||||||
endpointType: endpoint.Type,
|
|
||||||
}
|
|
||||||
proxy.Transport = transport
|
|
||||||
return proxy
|
|
||||||
}
|
|
||||||
|
|
||||||
func newNamedPipeTransport(namedPipePath string) *http.Transport {
|
|
||||||
return &http.Transport{
|
|
||||||
Dial: func(proto, addr string) (conn net.Conn, err error) {
|
|
||||||
return winio.DialPipe(namedPipePath, nil)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,43 +0,0 @@
|
||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
httperror "github.com/portainer/libhttp/error"
|
|
||||||
)
|
|
||||||
|
|
||||||
type localProxy struct {
|
|
||||||
Transport *proxyTransport
|
|
||||||
}
|
|
||||||
|
|
||||||
func (proxy *localProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
||||||
// Force URL/domain to http/unixsocket to be able to
|
|
||||||
// use http.Transport RoundTrip to do the requests via the socket
|
|
||||||
r.URL.Scheme = "http"
|
|
||||||
r.URL.Host = "unixsocket"
|
|
||||||
|
|
||||||
res, err := proxy.Transport.proxyDockerRequest(r)
|
|
||||||
if err != nil {
|
|
||||||
code := http.StatusInternalServerError
|
|
||||||
if res != nil && res.StatusCode != 0 {
|
|
||||||
code = res.StatusCode
|
|
||||||
}
|
|
||||||
httperror.WriteError(w, code, "Unable to proxy the request via the Docker socket", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer res.Body.Close()
|
|
||||||
|
|
||||||
for k, vv := range res.Header {
|
|
||||||
for _, v := range vv {
|
|
||||||
w.Header().Add(k, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(res.StatusCode)
|
|
||||||
|
|
||||||
if _, err := io.Copy(w, res.Body); err != nil {
|
|
||||||
log.Printf("proxy error: %s\n", err)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,29 +1,22 @@
|
||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"github.com/orcaman/concurrent-map"
|
"github.com/orcaman/concurrent-map"
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
|
"github.com/portainer/portainer/api/docker"
|
||||||
|
"github.com/portainer/portainer/api/http/proxy/factory"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TODO: contain code related to legacy extension management
|
// TODO: contain code related to legacy extension management
|
||||||
|
|
||||||
var extensionPorts = map[portainer.ExtensionID]string{
|
|
||||||
portainer.RegistryManagementExtension: "7001",
|
|
||||||
portainer.OAuthAuthenticationExtension: "7002",
|
|
||||||
portainer.RBACExtension: "7003",
|
|
||||||
}
|
|
||||||
|
|
||||||
type (
|
type (
|
||||||
// Manager represents a service used to manage Docker proxies.
|
// Manager represents a service used to manage proxies to endpoints and extensions.
|
||||||
Manager struct {
|
Manager struct {
|
||||||
proxyFactory *proxyFactory
|
proxyFactory *factory.ProxyFactory
|
||||||
reverseTunnelService portainer.ReverseTunnelService
|
endpointProxies cmap.ConcurrentMap
|
||||||
proxies cmap.ConcurrentMap
|
|
||||||
extensionProxies cmap.ConcurrentMap
|
extensionProxies cmap.ConcurrentMap
|
||||||
legacyExtensionProxies cmap.ConcurrentMap
|
legacyExtensionProxies cmap.ConcurrentMap
|
||||||
}
|
}
|
||||||
|
@ -32,6 +25,7 @@ type (
|
||||||
ManagerParams struct {
|
ManagerParams struct {
|
||||||
ResourceControlService portainer.ResourceControlService
|
ResourceControlService portainer.ResourceControlService
|
||||||
UserService portainer.UserService
|
UserService portainer.UserService
|
||||||
|
TeamService portainer.TeamService
|
||||||
TeamMembershipService portainer.TeamMembershipService
|
TeamMembershipService portainer.TeamMembershipService
|
||||||
SettingsService portainer.SettingsService
|
SettingsService portainer.SettingsService
|
||||||
RegistryService portainer.RegistryService
|
RegistryService portainer.RegistryService
|
||||||
|
@ -39,54 +33,71 @@ type (
|
||||||
SignatureService portainer.DigitalSignatureService
|
SignatureService portainer.DigitalSignatureService
|
||||||
ReverseTunnelService portainer.ReverseTunnelService
|
ReverseTunnelService portainer.ReverseTunnelService
|
||||||
ExtensionService portainer.ExtensionService
|
ExtensionService portainer.ExtensionService
|
||||||
|
DockerClientFactory *docker.ClientFactory
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewManager initializes a new proxy Service
|
// NewManager initializes a new proxy Service
|
||||||
func NewManager(parameters *ManagerParams) *Manager {
|
func NewManager(parameters *ManagerParams) *Manager {
|
||||||
|
proxyFactoryParameters := &factory.ProxyFactoryParameters{
|
||||||
|
ResourceControlService: parameters.ResourceControlService,
|
||||||
|
UserService: parameters.UserService,
|
||||||
|
TeamService: parameters.TeamService,
|
||||||
|
TeamMembershipService: parameters.TeamMembershipService,
|
||||||
|
SettingsService: parameters.SettingsService,
|
||||||
|
RegistryService: parameters.RegistryService,
|
||||||
|
DockerHubService: parameters.DockerHubService,
|
||||||
|
SignatureService: parameters.SignatureService,
|
||||||
|
ReverseTunnelService: parameters.ReverseTunnelService,
|
||||||
|
ExtensionService: parameters.ExtensionService,
|
||||||
|
DockerClientFactory: parameters.DockerClientFactory,
|
||||||
|
}
|
||||||
|
|
||||||
return &Manager{
|
return &Manager{
|
||||||
proxies: cmap.New(),
|
endpointProxies: cmap.New(),
|
||||||
extensionProxies: cmap.New(),
|
extensionProxies: cmap.New(),
|
||||||
legacyExtensionProxies: cmap.New(),
|
legacyExtensionProxies: cmap.New(),
|
||||||
proxyFactory: &proxyFactory{
|
proxyFactory: factory.NewProxyFactory(proxyFactoryParameters),
|
||||||
ResourceControlService: parameters.ResourceControlService,
|
|
||||||
UserService: parameters.UserService,
|
|
||||||
TeamMembershipService: parameters.TeamMembershipService,
|
|
||||||
SettingsService: parameters.SettingsService,
|
|
||||||
RegistryService: parameters.RegistryService,
|
|
||||||
DockerHubService: parameters.DockerHubService,
|
|
||||||
SignatureService: parameters.SignatureService,
|
|
||||||
ReverseTunnelService: parameters.ReverseTunnelService,
|
|
||||||
ExtensionService: parameters.ExtensionService,
|
|
||||||
},
|
|
||||||
reverseTunnelService: parameters.ReverseTunnelService,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetProxy returns the proxy associated to a key
|
// CreateAndRegisterEndpointProxy creates a new HTTP reverse proxy based on endpoint properties and and adds it to the registered proxies.
|
||||||
func (manager *Manager) GetProxy(endpoint *portainer.Endpoint) http.Handler {
|
|
||||||
proxy, ok := manager.proxies.Get(string(endpoint.ID))
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return proxy.(http.Handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateAndRegisterProxy creates a new HTTP reverse proxy based on endpoint properties and and adds it to the registered proxies.
|
|
||||||
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
|
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
|
||||||
func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
func (manager *Manager) CreateAndRegisterEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||||
proxy, err := manager.createProxy(endpoint)
|
proxy, err := manager.proxyFactory.NewEndpointProxy(endpoint)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
manager.proxies.Set(string(endpoint.ID), proxy)
|
manager.endpointProxies.Set(string(endpoint.ID), proxy)
|
||||||
return proxy, nil
|
return proxy, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteProxy deletes the proxy associated to a key
|
// GetEndpointProxy returns the proxy associated to a key
|
||||||
func (manager *Manager) DeleteProxy(endpoint *portainer.Endpoint) {
|
func (manager *Manager) GetEndpointProxy(endpoint *portainer.Endpoint) http.Handler {
|
||||||
manager.proxies.Remove(string(endpoint.ID))
|
proxy, ok := manager.endpointProxies.Get(string(endpoint.ID))
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return proxy.(http.Handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteEndpointProxy deletes the proxy associated to a key
|
||||||
|
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
|
// GetExtensionProxy returns an extension proxy associated to an extension identifier
|
||||||
|
@ -95,28 +106,13 @@ func (manager *Manager) GetExtensionProxy(extensionID portainer.ExtensionID) htt
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return proxy.(http.Handler)
|
return proxy.(http.Handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
|
||||||
address := "http://" + portainer.ExtensionServer + ":" + extensionPorts[extensionID]
|
|
||||||
|
|
||||||
extensionURL, err := url.Parse(address)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy := manager.proxyFactory.newHTTPProxy(extensionURL)
|
|
||||||
manager.extensionProxies.Set(strconv.Itoa(int(extensionID)), proxy)
|
|
||||||
|
|
||||||
return proxy, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetExtensionURL retrieves the URL of an extension running locally based on the extension port table
|
// GetExtensionURL retrieves the URL of an extension running locally based on the extension port table
|
||||||
func (manager *Manager) GetExtensionURL(extensionID portainer.ExtensionID) string {
|
func (manager *Manager) GetExtensionURL(extensionID portainer.ExtensionID) string {
|
||||||
return "http://localhost:" + extensionPorts[extensionID]
|
return factory.BuildExtensionURL(extensionID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteExtensionProxy deletes the extension proxy associated to an extension identifier
|
// DeleteExtensionProxy deletes the extension proxy associated to an extension identifier
|
||||||
|
@ -124,6 +120,17 @@ func (manager *Manager) DeleteExtensionProxy(extensionID portainer.ExtensionID)
|
||||||
manager.extensionProxies.Remove(strconv.Itoa(int(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)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
manager.legacyExtensionProxies.Set(key, proxy)
|
||||||
|
return proxy, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetLegacyExtensionProxy returns a legacy extension proxy associated to a key
|
// GetLegacyExtensionProxy returns a legacy extension proxy associated to a key
|
||||||
func (manager *Manager) GetLegacyExtensionProxy(key string) http.Handler {
|
func (manager *Manager) GetLegacyExtensionProxy(key string) http.Handler {
|
||||||
proxy, ok := manager.legacyExtensionProxies.Get(key)
|
proxy, ok := manager.legacyExtensionProxies.Get(key)
|
||||||
|
@ -133,52 +140,7 @@ func (manager *Manager) GetLegacyExtensionProxy(key string) http.Handler {
|
||||||
return proxy.(http.Handler)
|
return proxy.(http.Handler)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateLegacyExtensionProxy creates a new HTTP reverse proxy for a legacy extension and adds it to the registered proxies.
|
// CreateGitlabProxy creates a new HTTP reverse proxy that can be used to send requests to the Gitlab API
|
||||||
func (manager *Manager) CreateLegacyExtensionProxy(key, extensionAPIURL string) (http.Handler, error) {
|
func (manager *Manager) CreateGitlabProxy(url string) (http.Handler, error) {
|
||||||
extensionURL, err := url.Parse(extensionAPIURL)
|
return manager.proxyFactory.NewGitlabProxy(url)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
proxy := manager.proxyFactory.newHTTPProxy(extensionURL)
|
|
||||||
manager.legacyExtensionProxies.Set(key, proxy)
|
|
||||||
return proxy, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *Manager) createDockerProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
|
||||||
baseURL := endpoint.URL
|
|
||||||
if endpoint.Type == portainer.EdgeAgentEnvironment {
|
|
||||||
tunnel := manager.reverseTunnelService.GetTunnelDetails(endpoint.ID)
|
|
||||||
baseURL = fmt.Sprintf("http://localhost:%d", tunnel.Port)
|
|
||||||
}
|
|
||||||
|
|
||||||
endpointURL, err := url.Parse(baseURL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch endpoint.Type {
|
|
||||||
case portainer.AgentOnDockerEnvironment:
|
|
||||||
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, endpoint)
|
|
||||||
case portainer.EdgeAgentEnvironment:
|
|
||||||
return manager.proxyFactory.newDockerHTTPProxy(endpointURL, endpoint), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if endpointURL.Scheme == "tcp" {
|
|
||||||
if endpoint.TLSConfig.TLS || endpoint.TLSConfig.TLSSkipVerify {
|
|
||||||
return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, endpoint)
|
|
||||||
}
|
|
||||||
|
|
||||||
return manager.proxyFactory.newDockerHTTPProxy(endpointURL, endpoint), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return manager.proxyFactory.newLocalProxy(endpointURL.Path, endpoint), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
|
||||||
if endpoint.Type == portainer.AzureEnvironment {
|
|
||||||
return newAzureProxy(&endpoint.AzureCredentials)
|
|
||||||
}
|
|
||||||
|
|
||||||
return manager.createDockerProxy(endpoint)
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,135 +0,0 @@
|
||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ErrDockerNetworkIdentifierNotFound defines an error raised when Portainer is unable to find a network identifier
|
|
||||||
ErrDockerNetworkIdentifierNotFound = portainer.Error("Docker network identifier not found")
|
|
||||||
networkIdentifier = "Id"
|
|
||||||
networkLabelForStackIdentifier = "com.docker.stack.namespace"
|
|
||||||
)
|
|
||||||
|
|
||||||
// networkListOperation extracts the response as a JSON object, loop through the networks array
|
|
||||||
// decorate and/or filter the networks based on resource controls before rewriting the response
|
|
||||||
func networkListOperation(response *http.Response, executor *operationExecutor) error {
|
|
||||||
var err error
|
|
||||||
// NetworkList response is a JSON array
|
|
||||||
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
|
||||||
responseArray, err := getResponseAsJSONArray(response)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
|
|
||||||
responseArray, err = decorateNetworkList(responseArray, executor.operationContext.resourceControls)
|
|
||||||
} else {
|
|
||||||
responseArray, err = filterNetworkList(responseArray, executor.operationContext)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return rewriteResponse(response, responseArray, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// networkInspectOperation extracts the response as a JSON object, verify that the user
|
|
||||||
// has access to the network based on resource control and either rewrite an access denied response
|
|
||||||
// or a decorated network.
|
|
||||||
func networkInspectOperation(response *http.Response, executor *operationExecutor) error {
|
|
||||||
// NetworkInspect response is a JSON object
|
|
||||||
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect
|
|
||||||
responseObject, err := getResponseAsJSONOBject(response)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if responseObject[networkIdentifier] == nil {
|
|
||||||
return ErrDockerNetworkIdentifierNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
networkID := responseObject[networkIdentifier].(string)
|
|
||||||
responseObject, access := applyResourceAccessControl(responseObject, networkID, executor.operationContext)
|
|
||||||
if access {
|
|
||||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
networkLabels := extractNetworkLabelsFromNetworkInspectObject(responseObject)
|
|
||||||
responseObject, access = applyResourceAccessControlFromLabel(networkLabels, responseObject, networkLabelForStackIdentifier, executor.operationContext)
|
|
||||||
if access {
|
|
||||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rewriteAccessDeniedResponse(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractNetworkLabelsFromNetworkInspectObject retrieve the Labels of the network if present.
|
|
||||||
// Container schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect
|
|
||||||
func extractNetworkLabelsFromNetworkInspectObject(responseObject map[string]interface{}) map[string]interface{} {
|
|
||||||
// Labels are stored under Labels
|
|
||||||
return extractJSONField(responseObject, "Labels")
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractNetworkLabelsFromNetworkListObject retrieve the Labels of the network if present.
|
|
||||||
// Network schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
|
||||||
func extractNetworkLabelsFromNetworkListObject(responseObject map[string]interface{}) map[string]interface{} {
|
|
||||||
// Labels are stored under Labels
|
|
||||||
return extractJSONField(responseObject, "Labels")
|
|
||||||
}
|
|
||||||
|
|
||||||
// decorateNetworkList loops through all networks and decorates any network with an existing resource control.
|
|
||||||
// Resource controls checks are based on: resource identifier, stack identifier (from label).
|
|
||||||
// Network object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
|
||||||
func decorateNetworkList(networkData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
|
|
||||||
decoratedNetworkData := make([]interface{}, 0)
|
|
||||||
|
|
||||||
for _, network := range networkData {
|
|
||||||
|
|
||||||
networkObject := network.(map[string]interface{})
|
|
||||||
if networkObject[networkIdentifier] == nil {
|
|
||||||
return nil, ErrDockerNetworkIdentifierNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
networkID := networkObject[networkIdentifier].(string)
|
|
||||||
networkObject = decorateResourceWithAccessControl(networkObject, networkID, resourceControls)
|
|
||||||
|
|
||||||
networkLabels := extractNetworkLabelsFromNetworkListObject(networkObject)
|
|
||||||
networkObject = decorateResourceWithAccessControlFromLabel(networkLabels, networkObject, networkLabelForStackIdentifier, resourceControls)
|
|
||||||
|
|
||||||
decoratedNetworkData = append(decoratedNetworkData, networkObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
return decoratedNetworkData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterNetworkList loops through all networks and filters public networks (no associated resource control)
|
|
||||||
// as well as authorized networks (access granted to the user based on existing resource control).
|
|
||||||
// Authorized networks are decorated during the process.
|
|
||||||
// Resource controls checks are based on: resource identifier, stack identifier (from label).
|
|
||||||
// Network object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
|
||||||
func filterNetworkList(networkData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
|
|
||||||
filteredNetworkData := make([]interface{}, 0)
|
|
||||||
|
|
||||||
for _, network := range networkData {
|
|
||||||
networkObject := network.(map[string]interface{})
|
|
||||||
if networkObject[networkIdentifier] == nil {
|
|
||||||
return nil, ErrDockerNetworkIdentifierNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
networkID := networkObject[networkIdentifier].(string)
|
|
||||||
networkObject, access := applyResourceAccessControl(networkObject, networkID, context)
|
|
||||||
if !access {
|
|
||||||
networkLabels := extractNetworkLabelsFromNetworkListObject(networkObject)
|
|
||||||
networkObject, access = applyResourceAccessControlFromLabel(networkLabels, networkObject, networkLabelForStackIdentifier, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
if access {
|
|
||||||
filteredNetworkData = append(filteredNetworkData, networkObject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredNetworkData, nil
|
|
||||||
}
|
|
|
@ -1,109 +0,0 @@
|
||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ErrEmptyResponseBody defines an error raised when portainer excepts to parse the body of a HTTP response and there is nothing to parse
|
|
||||||
ErrEmptyResponseBody = portainer.Error("Empty response body")
|
|
||||||
// ErrInvalidResponseContent defines an error raised when Portainer excepts a JSON array and get something else.
|
|
||||||
ErrInvalidResponseContent = portainer.Error("Invalid Docker response")
|
|
||||||
)
|
|
||||||
|
|
||||||
func extractJSONField(jsonObject map[string]interface{}, key string) map[string]interface{} {
|
|
||||||
object := jsonObject[key]
|
|
||||||
if object != nil {
|
|
||||||
return object.(map[string]interface{})
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getResponseAsJSONOBject(response *http.Response) (map[string]interface{}, error) {
|
|
||||||
responseData, err := getResponseBodyAsGenericJSON(response)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
responseObject := responseData.(map[string]interface{})
|
|
||||||
return responseObject, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getResponseAsJSONArray(response *http.Response) ([]interface{}, error) {
|
|
||||||
responseData, err := getResponseBodyAsGenericJSON(response)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch responseObject := responseData.(type) {
|
|
||||||
case []interface{}:
|
|
||||||
return responseObject, nil
|
|
||||||
case map[string]interface{}:
|
|
||||||
if responseObject["message"] != nil {
|
|
||||||
return nil, portainer.Error(responseObject["message"].(string))
|
|
||||||
}
|
|
||||||
log.Printf("Response: %+v\n", responseObject)
|
|
||||||
return nil, ErrInvalidResponseContent
|
|
||||||
default:
|
|
||||||
log.Printf("Response: %+v\n", responseObject)
|
|
||||||
return nil, ErrInvalidResponseContent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getResponseBodyAsGenericJSON(response *http.Response) (interface{}, error) {
|
|
||||||
var data interface{}
|
|
||||||
if response.Body != nil {
|
|
||||||
body, err := ioutil.ReadAll(response.Body)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = response.Body.Close()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(body, &data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return data, nil
|
|
||||||
}
|
|
||||||
return nil, ErrEmptyResponseBody
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeAccessDeniedResponse() (*http.Response, error) {
|
|
||||||
response := &http.Response{}
|
|
||||||
err := rewriteResponse(response, portainer.ErrResourceAccessDenied, http.StatusForbidden)
|
|
||||||
return response, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func rewriteAccessDeniedResponse(response *http.Response) error {
|
|
||||||
return rewriteResponse(response, portainer.ErrResourceAccessDenied, http.StatusForbidden)
|
|
||||||
}
|
|
||||||
|
|
||||||
func rewriteResponse(response *http.Response, newResponseData interface{}, statusCode int) error {
|
|
||||||
jsonData, err := json.Marshal(newResponseData)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
body := ioutil.NopCloser(bytes.NewReader(jsonData))
|
|
||||||
response.StatusCode = statusCode
|
|
||||||
response.Body = body
|
|
||||||
response.ContentLength = int64(len(jsonData))
|
|
||||||
|
|
||||||
if response.Header == nil {
|
|
||||||
response.Header = make(http.Header)
|
|
||||||
}
|
|
||||||
response.Header.Set("Content-Length", strconv.Itoa(len(jsonData)))
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
|
@ -1,107 +0,0 @@
|
||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ErrDockerSecretIdentifierNotFound defines an error raised when Portainer is unable to find a secret identifier
|
|
||||||
ErrDockerSecretIdentifierNotFound = portainer.Error("Docker secret identifier not found")
|
|
||||||
secretIdentifier = "ID"
|
|
||||||
)
|
|
||||||
|
|
||||||
// secretListOperation extracts the response as a JSON object, loop through the secrets array
|
|
||||||
// decorate and/or filter the secrets based on resource controls before rewriting the response
|
|
||||||
func secretListOperation(response *http.Response, executor *operationExecutor) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// SecretList response is a JSON array
|
|
||||||
// https://docs.docker.com/engine/api/v1.28/#operation/SecretList
|
|
||||||
responseArray, err := getResponseAsJSONArray(response)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
|
|
||||||
responseArray, err = decorateSecretList(responseArray, executor.operationContext.resourceControls)
|
|
||||||
} else {
|
|
||||||
responseArray, err = filterSecretList(responseArray, executor.operationContext)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return rewriteResponse(response, responseArray, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// secretInspectOperation extracts the response as a JSON object, verify that the user
|
|
||||||
// has access to the secret based on resource control (check are done based on the secretID and optional Swarm service ID)
|
|
||||||
// and either rewrite an access denied response or a decorated secret.
|
|
||||||
func secretInspectOperation(response *http.Response, executor *operationExecutor) error {
|
|
||||||
// SecretInspect response is a JSON object
|
|
||||||
// https://docs.docker.com/engine/api/v1.28/#operation/SecretInspect
|
|
||||||
responseObject, err := getResponseAsJSONOBject(response)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if responseObject[secretIdentifier] == nil {
|
|
||||||
return ErrDockerSecretIdentifierNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
secretID := responseObject[secretIdentifier].(string)
|
|
||||||
responseObject, access := applyResourceAccessControl(responseObject, secretID, executor.operationContext)
|
|
||||||
if !access {
|
|
||||||
return rewriteAccessDeniedResponse(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// decorateSecretList loops through all secrets and decorates any secret with an existing resource control.
|
|
||||||
// Resource controls checks are based on: resource identifier.
|
|
||||||
// Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList
|
|
||||||
func decorateSecretList(secretData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
|
|
||||||
decoratedSecretData := make([]interface{}, 0)
|
|
||||||
|
|
||||||
for _, secret := range secretData {
|
|
||||||
|
|
||||||
secretObject := secret.(map[string]interface{})
|
|
||||||
if secretObject[secretIdentifier] == nil {
|
|
||||||
return nil, ErrDockerSecretIdentifierNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
secretID := secretObject[secretIdentifier].(string)
|
|
||||||
secretObject = decorateResourceWithAccessControl(secretObject, secretID, resourceControls)
|
|
||||||
|
|
||||||
decoratedSecretData = append(decoratedSecretData, secretObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
return decoratedSecretData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterSecretList loops through all secrets and filters public secrets (no associated resource control)
|
|
||||||
// as well as authorized secrets (access granted to the user based on existing resource control).
|
|
||||||
// Authorized secrets are decorated during the process.
|
|
||||||
// Resource controls checks are based on: resource identifier.
|
|
||||||
// Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList
|
|
||||||
func filterSecretList(secretData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
|
|
||||||
filteredSecretData := make([]interface{}, 0)
|
|
||||||
|
|
||||||
for _, secret := range secretData {
|
|
||||||
secretObject := secret.(map[string]interface{})
|
|
||||||
if secretObject[secretIdentifier] == nil {
|
|
||||||
return nil, ErrDockerSecretIdentifierNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
secretID := secretObject[secretIdentifier].(string)
|
|
||||||
secretObject, access := applyResourceAccessControl(secretObject, secretID, context)
|
|
||||||
if access {
|
|
||||||
filteredSecretData = append(filteredSecretData, secretObject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredSecretData, nil
|
|
||||||
}
|
|
|
@ -1,143 +0,0 @@
|
||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ErrDockerServiceIdentifierNotFound defines an error raised when Portainer is unable to find a service identifier
|
|
||||||
ErrDockerServiceIdentifierNotFound = portainer.Error("Docker service identifier not found")
|
|
||||||
serviceIdentifier = "ID"
|
|
||||||
serviceLabelForStackIdentifier = "com.docker.stack.namespace"
|
|
||||||
)
|
|
||||||
|
|
||||||
// serviceListOperation extracts the response as a JSON array, loop through the service array
|
|
||||||
// decorate and/or filter the services based on resource controls before rewriting the response
|
|
||||||
func serviceListOperation(response *http.Response, executor *operationExecutor) error {
|
|
||||||
var err error
|
|
||||||
// ServiceList response is a JSON array
|
|
||||||
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
|
||||||
responseArray, err := getResponseAsJSONArray(response)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
|
|
||||||
responseArray, err = decorateServiceList(responseArray, executor.operationContext.resourceControls)
|
|
||||||
} else {
|
|
||||||
responseArray, err = filterServiceList(responseArray, executor.operationContext)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return rewriteResponse(response, responseArray, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// serviceInspectOperation extracts the response as a JSON object, verify that the user
|
|
||||||
// has access to the service based on resource control and either rewrite an access denied response
|
|
||||||
// or a decorated service.
|
|
||||||
func serviceInspectOperation(response *http.Response, executor *operationExecutor) error {
|
|
||||||
// ServiceInspect response is a JSON object
|
|
||||||
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
|
|
||||||
responseObject, err := getResponseAsJSONOBject(response)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if responseObject[serviceIdentifier] == nil {
|
|
||||||
return ErrDockerServiceIdentifierNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceID := responseObject[serviceIdentifier].(string)
|
|
||||||
responseObject, access := applyResourceAccessControl(responseObject, serviceID, executor.operationContext)
|
|
||||||
if access {
|
|
||||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceLabels := extractServiceLabelsFromServiceInspectObject(responseObject)
|
|
||||||
responseObject, access = applyResourceAccessControlFromLabel(serviceLabels, responseObject, serviceLabelForStackIdentifier, executor.operationContext)
|
|
||||||
if access {
|
|
||||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rewriteAccessDeniedResponse(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractServiceLabelsFromServiceInspectObject retrieve the Labels of the service if present.
|
|
||||||
// Service schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
|
|
||||||
func extractServiceLabelsFromServiceInspectObject(responseObject map[string]interface{}) map[string]interface{} {
|
|
||||||
// Labels are stored under Spec.Labels
|
|
||||||
serviceSpecObject := extractJSONField(responseObject, "Spec")
|
|
||||||
if serviceSpecObject != nil {
|
|
||||||
return extractJSONField(serviceSpecObject, "Labels")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractServiceLabelsFromServiceListObject retrieve the Labels of the service if present.
|
|
||||||
// Service schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
|
||||||
func extractServiceLabelsFromServiceListObject(responseObject map[string]interface{}) map[string]interface{} {
|
|
||||||
// Labels are stored under Spec.Labels
|
|
||||||
serviceSpecObject := extractJSONField(responseObject, "Spec")
|
|
||||||
if serviceSpecObject != nil {
|
|
||||||
return extractJSONField(serviceSpecObject, "Labels")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// decorateServiceList loops through all services and decorates any service with an existing resource control.
|
|
||||||
// Resource controls checks are based on: resource identifier, stack identifier (from label).
|
|
||||||
// Service object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
|
||||||
func decorateServiceList(serviceData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
|
|
||||||
decoratedServiceData := make([]interface{}, 0)
|
|
||||||
|
|
||||||
for _, service := range serviceData {
|
|
||||||
|
|
||||||
serviceObject := service.(map[string]interface{})
|
|
||||||
if serviceObject[serviceIdentifier] == nil {
|
|
||||||
return nil, ErrDockerServiceIdentifierNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceID := serviceObject[serviceIdentifier].(string)
|
|
||||||
serviceObject = decorateResourceWithAccessControl(serviceObject, serviceID, resourceControls)
|
|
||||||
|
|
||||||
serviceLabels := extractServiceLabelsFromServiceListObject(serviceObject)
|
|
||||||
serviceObject = decorateResourceWithAccessControlFromLabel(serviceLabels, serviceObject, serviceLabelForStackIdentifier, resourceControls)
|
|
||||||
|
|
||||||
decoratedServiceData = append(decoratedServiceData, serviceObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
return decoratedServiceData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterServiceList loops through all services and filters public services (no associated resource control)
|
|
||||||
// as well as authorized services (access granted to the user based on existing resource control).
|
|
||||||
// Authorized services are decorated during the process.
|
|
||||||
// Resource controls checks are based on: resource identifier, stack identifier (from label).
|
|
||||||
// Service object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
|
||||||
func filterServiceList(serviceData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
|
|
||||||
filteredServiceData := make([]interface{}, 0)
|
|
||||||
|
|
||||||
for _, service := range serviceData {
|
|
||||||
serviceObject := service.(map[string]interface{})
|
|
||||||
if serviceObject[serviceIdentifier] == nil {
|
|
||||||
return nil, ErrDockerServiceIdentifierNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceID := serviceObject[serviceIdentifier].(string)
|
|
||||||
serviceObject, access := applyResourceAccessControl(serviceObject, serviceID, context)
|
|
||||||
if !access {
|
|
||||||
serviceLabels := extractServiceLabelsFromServiceListObject(serviceObject)
|
|
||||||
serviceObject, access = applyResourceAccessControlFromLabel(serviceLabels, serviceObject, serviceLabelForStackIdentifier, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
if access {
|
|
||||||
filteredServiceData = append(filteredServiceData, serviceObject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredServiceData, nil
|
|
||||||
}
|
|
|
@ -1,79 +0,0 @@
|
||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ErrDockerTaskServiceIdentifierNotFound defines an error raised when Portainer is unable to find the service identifier associated to a task
|
|
||||||
ErrDockerTaskServiceIdentifierNotFound = portainer.Error("Docker task service identifier not found")
|
|
||||||
taskServiceIdentifier = "ServiceID"
|
|
||||||
taskLabelForStackIdentifier = "com.docker.stack.namespace"
|
|
||||||
)
|
|
||||||
|
|
||||||
// taskListOperation extracts the response as a JSON object, loop through the tasks array
|
|
||||||
// and filter the tasks based on resource controls before rewriting the response
|
|
||||||
func taskListOperation(response *http.Response, executor *operationExecutor) error {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
// TaskList response is a JSON array
|
|
||||||
// https://docs.docker.com/engine/api/v1.28/#operation/TaskList
|
|
||||||
responseArray, err := getResponseAsJSONArray(response)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if !executor.operationContext.isAdmin && !executor.operationContext.endpointResourceAccess {
|
|
||||||
responseArray, err = filterTaskList(responseArray, executor.operationContext)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rewriteResponse(response, responseArray, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractTaskLabelsFromTaskListObject retrieve the Labels of the task if present.
|
|
||||||
// Task schema reference: https://docs.docker.com/engine/api/v1.28/#operation/TaskList
|
|
||||||
func extractTaskLabelsFromTaskListObject(responseObject map[string]interface{}) map[string]interface{} {
|
|
||||||
// Labels are stored under Spec.ContainerSpec.Labels
|
|
||||||
taskSpecObject := extractJSONField(responseObject, "Spec")
|
|
||||||
if taskSpecObject != nil {
|
|
||||||
containerSpecObject := extractJSONField(taskSpecObject, "ContainerSpec")
|
|
||||||
if containerSpecObject != nil {
|
|
||||||
return extractJSONField(containerSpecObject, "Labels")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterTaskList loops through all tasks and filters public tasks (no associated resource control)
|
|
||||||
// as well as authorized tasks (access granted to the user based on existing resource control).
|
|
||||||
// Resource controls checks are based on: service identifier, stack identifier (from label).
|
|
||||||
// Task object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/TaskList
|
|
||||||
// any resource control giving access to the user based on the associated service identifier.
|
|
||||||
func filterTaskList(taskData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
|
|
||||||
filteredTaskData := make([]interface{}, 0)
|
|
||||||
|
|
||||||
for _, task := range taskData {
|
|
||||||
taskObject := task.(map[string]interface{})
|
|
||||||
if taskObject[taskServiceIdentifier] == nil {
|
|
||||||
return nil, ErrDockerTaskServiceIdentifierNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceID := taskObject[taskServiceIdentifier].(string)
|
|
||||||
taskObject, access := applyResourceAccessControl(taskObject, serviceID, context)
|
|
||||||
if !access {
|
|
||||||
taskLabels := extractTaskLabelsFromTaskListObject(taskObject)
|
|
||||||
taskObject, access = applyResourceAccessControlFromLabel(taskLabels, taskObject, taskLabelForStackIdentifier, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
if access {
|
|
||||||
filteredTaskData = append(filteredTaskData, taskObject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredTaskData, nil
|
|
||||||
}
|
|
|
@ -1,144 +0,0 @@
|
||||||
package proxy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/portainer/portainer/api"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// ErrDockerVolumeIdentifierNotFound defines an error raised when Portainer is unable to find a volume identifier
|
|
||||||
ErrDockerVolumeIdentifierNotFound = portainer.Error("Docker volume identifier not found")
|
|
||||||
volumeIdentifier = "Name"
|
|
||||||
volumeLabelForStackIdentifier = "com.docker.stack.namespace"
|
|
||||||
)
|
|
||||||
|
|
||||||
// volumeListOperation extracts the response as a JSON object, loop through the volume array
|
|
||||||
// decorate and/or filter the volumes based on resource controls before rewriting the response
|
|
||||||
func volumeListOperation(response *http.Response, executor *operationExecutor) error {
|
|
||||||
var err error
|
|
||||||
// VolumeList response is a JSON object
|
|
||||||
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
|
||||||
responseObject, err := getResponseAsJSONOBject(response)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// The "Volumes" field contains the list of volumes as an array of JSON objects
|
|
||||||
// Response schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
|
||||||
if responseObject["Volumes"] != nil {
|
|
||||||
volumeData := responseObject["Volumes"].([]interface{})
|
|
||||||
|
|
||||||
if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess {
|
|
||||||
volumeData, err = decorateVolumeList(volumeData, executor.operationContext.resourceControls)
|
|
||||||
} else {
|
|
||||||
volumeData, err = filterVolumeList(volumeData, executor.operationContext)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overwrite the original volume list
|
|
||||||
responseObject["Volumes"] = volumeData
|
|
||||||
}
|
|
||||||
|
|
||||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
// volumeInspectOperation extracts the response as a JSON object, verify that the user
|
|
||||||
// has access to the volume based on any existing resource control and either rewrite an access denied response
|
|
||||||
// or a decorated volume.
|
|
||||||
func volumeInspectOperation(response *http.Response, executor *operationExecutor) error {
|
|
||||||
// VolumeInspect response is a JSON object
|
|
||||||
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect
|
|
||||||
responseObject, err := getResponseAsJSONOBject(response)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if responseObject[volumeIdentifier] == nil {
|
|
||||||
return ErrDockerVolumeIdentifierNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
volumeID := responseObject[volumeIdentifier].(string)
|
|
||||||
responseObject, access := applyResourceAccessControl(responseObject, volumeID, executor.operationContext)
|
|
||||||
if access {
|
|
||||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
volumeLabels := extractVolumeLabelsFromVolumeInspectObject(responseObject)
|
|
||||||
responseObject, access = applyResourceAccessControlFromLabel(volumeLabels, responseObject, volumeLabelForStackIdentifier, executor.operationContext)
|
|
||||||
if access {
|
|
||||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
||||||
return rewriteAccessDeniedResponse(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractVolumeLabelsFromVolumeInspectObject retrieve the Labels of the volume if present.
|
|
||||||
// Volume schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect
|
|
||||||
func extractVolumeLabelsFromVolumeInspectObject(responseObject map[string]interface{}) map[string]interface{} {
|
|
||||||
// Labels are stored under Labels
|
|
||||||
return extractJSONField(responseObject, "Labels")
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractVolumeLabelsFromVolumeListObject retrieve the Labels of the volume if present.
|
|
||||||
// Volume schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
|
||||||
func extractVolumeLabelsFromVolumeListObject(responseObject map[string]interface{}) map[string]interface{} {
|
|
||||||
// Labels are stored under Labels
|
|
||||||
return extractJSONField(responseObject, "Labels")
|
|
||||||
}
|
|
||||||
|
|
||||||
// decorateVolumeList loops through all volumes and decorates any volume with an existing resource control.
|
|
||||||
// Resource controls checks are based on: resource identifier, stack identifier (from label).
|
|
||||||
// Volume object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
|
||||||
func decorateVolumeList(volumeData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
|
|
||||||
decoratedVolumeData := make([]interface{}, 0)
|
|
||||||
|
|
||||||
for _, volume := range volumeData {
|
|
||||||
|
|
||||||
volumeObject := volume.(map[string]interface{})
|
|
||||||
if volumeObject[volumeIdentifier] == nil {
|
|
||||||
return nil, ErrDockerVolumeIdentifierNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
volumeID := volumeObject[volumeIdentifier].(string)
|
|
||||||
volumeObject = decorateResourceWithAccessControl(volumeObject, volumeID, resourceControls)
|
|
||||||
|
|
||||||
volumeLabels := extractVolumeLabelsFromVolumeListObject(volumeObject)
|
|
||||||
volumeObject = decorateResourceWithAccessControlFromLabel(volumeLabels, volumeObject, volumeLabelForStackIdentifier, resourceControls)
|
|
||||||
|
|
||||||
decoratedVolumeData = append(decoratedVolumeData, volumeObject)
|
|
||||||
}
|
|
||||||
|
|
||||||
return decoratedVolumeData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// filterVolumeList loops through all volumes and filters public volumes (no associated resource control)
|
|
||||||
// as well as authorized volumes (access granted to the user based on existing resource control).
|
|
||||||
// Authorized volumes are decorated during the process.
|
|
||||||
// Resource controls checks are based on: resource identifier, stack identifier (from label).
|
|
||||||
// Volume object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
|
||||||
func filterVolumeList(volumeData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) {
|
|
||||||
filteredVolumeData := make([]interface{}, 0)
|
|
||||||
|
|
||||||
for _, volume := range volumeData {
|
|
||||||
volumeObject := volume.(map[string]interface{})
|
|
||||||
if volumeObject[volumeIdentifier] == nil {
|
|
||||||
return nil, ErrDockerVolumeIdentifierNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
volumeID := volumeObject[volumeIdentifier].(string)
|
|
||||||
volumeObject, access := applyResourceAccessControl(volumeObject, volumeID, context)
|
|
||||||
if !access {
|
|
||||||
volumeLabels := extractVolumeLabelsFromVolumeListObject(volumeObject)
|
|
||||||
volumeObject, access = applyResourceAccessControlFromLabel(volumeLabels, volumeObject, volumeLabelForStackIdentifier, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
if access {
|
|
||||||
filteredVolumeData = append(filteredVolumeData, volumeObject)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filteredVolumeData, nil
|
|
||||||
}
|
|
|
@ -4,40 +4,6 @@ import (
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthorizedResourceControlDeletion ensure that the user can delete a resource control object.
|
|
||||||
// A non-administrator user cannot delete a resource control where:
|
|
||||||
// * the Public flag is false
|
|
||||||
// * he is not one of the users in the user accesses
|
|
||||||
// * he is not a member of any team within the team accesses
|
|
||||||
func AuthorizedResourceControlDeletion(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
|
||||||
if context.IsAdmin || resourceControl.Public {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
userAccessesCount := len(resourceControl.UserAccesses)
|
|
||||||
teamAccessesCount := len(resourceControl.TeamAccesses)
|
|
||||||
|
|
||||||
if teamAccessesCount > 0 {
|
|
||||||
for _, access := range resourceControl.TeamAccesses {
|
|
||||||
for _, membership := range context.UserMemberships {
|
|
||||||
if membership.TeamID == access.TeamID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if userAccessesCount > 0 {
|
|
||||||
for _, access := range resourceControl.UserAccesses {
|
|
||||||
if access.UserID == context.UserID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthorizedResourceControlAccess checks whether the user can alter an existing resource control.
|
// AuthorizedResourceControlAccess checks whether the user can alter an existing resource control.
|
||||||
func AuthorizedResourceControlAccess(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
func AuthorizedResourceControlAccess(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
||||||
if context.IsAdmin || resourceControl.Public {
|
if context.IsAdmin || resourceControl.Public {
|
||||||
|
@ -62,30 +28,22 @@ func AuthorizedResourceControlAccess(resourceControl *portainer.ResourceControl,
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizedResourceControlUpdate ensure that the user can update a resource control object.
|
// AuthorizedResourceControlUpdate ensure that the user can update a resource control object.
|
||||||
// It reuses the creation restrictions and adds extra checks.
|
|
||||||
// A non-administrator user cannot update a resource control where:
|
|
||||||
// * he wants to put one or more user in the user accesses
|
|
||||||
func AuthorizedResourceControlUpdate(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
|
||||||
userAccessesCount := len(resourceControl.UserAccesses)
|
|
||||||
if !context.IsAdmin && userAccessesCount > 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return AuthorizedResourceControlCreation(resourceControl, context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuthorizedResourceControlCreation ensure that the user can create a resource control object.
|
|
||||||
// A non-administrator user cannot create a resource control where:
|
// A non-administrator user cannot create a resource control where:
|
||||||
// * the Public flag is set false
|
// * the Public flag is set false
|
||||||
|
// * the AdministatorsOnly flag is set to true
|
||||||
// * he wants to create a resource control without any user/team accesses
|
// * he wants to create a resource control without any user/team accesses
|
||||||
// * he wants to add more than one user in the user accesses
|
// * he wants to add more than one user in the user accesses
|
||||||
// * he wants to add a user in the user accesses that is not corresponding to its id
|
// * he wants to add a user in the user accesses that is not corresponding to its id
|
||||||
// * he wants to add a team he is not a member of
|
// * he wants to add a team he is not a member of
|
||||||
func AuthorizedResourceControlCreation(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
func AuthorizedResourceControlUpdate(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
||||||
if context.IsAdmin || resourceControl.Public {
|
if context.IsAdmin || resourceControl.Public {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if resourceControl.AdministratorsOnly {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
userAccessesCount := len(resourceControl.UserAccesses)
|
userAccessesCount := len(resourceControl.UserAccesses)
|
||||||
teamAccessesCount := len(resourceControl.TeamAccesses)
|
teamAccessesCount := len(resourceControl.TeamAccesses)
|
||||||
|
|
||||||
|
@ -133,15 +91,6 @@ func AuthorizedTeamManagement(teamID portainer.TeamID, context *RestrictedReques
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthorizedUserManagement ensure that access to the management of the specified user is granted.
|
|
||||||
// It will check if the user is either administrator or the owner of the user account.
|
|
||||||
func AuthorizedUserManagement(userID portainer.UserID, context *RestrictedRequestContext) bool {
|
|
||||||
if context.IsAdmin || context.UserID == userID {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// authorizedEndpointAccess ensure that the user can access the specified endpoint.
|
// authorizedEndpointAccess ensure that the user can access the specified endpoint.
|
||||||
// It will check if the user is part of the authorized users or part of a team that is
|
// It will check if the user is part of the authorized users or part of a team that is
|
||||||
// listed in the authorized teams of the endpoint and the associated group.
|
// listed in the authorized teams of the endpoint and the associated group.
|
||||||
|
|
|
@ -87,6 +87,7 @@ func (server *Server) Start() error {
|
||||||
proxyManagerParameters := &proxy.ManagerParams{
|
proxyManagerParameters := &proxy.ManagerParams{
|
||||||
ResourceControlService: server.ResourceControlService,
|
ResourceControlService: server.ResourceControlService,
|
||||||
UserService: server.UserService,
|
UserService: server.UserService,
|
||||||
|
TeamService: server.TeamService,
|
||||||
TeamMembershipService: server.TeamMembershipService,
|
TeamMembershipService: server.TeamMembershipService,
|
||||||
SettingsService: server.SettingsService,
|
SettingsService: server.SettingsService,
|
||||||
RegistryService: server.RegistryService,
|
RegistryService: server.RegistryService,
|
||||||
|
@ -94,6 +95,7 @@ func (server *Server) Start() error {
|
||||||
SignatureService: server.SignatureService,
|
SignatureService: server.SignatureService,
|
||||||
ReverseTunnelService: server.ReverseTunnelService,
|
ReverseTunnelService: server.ReverseTunnelService,
|
||||||
ExtensionService: server.ExtensionService,
|
ExtensionService: server.ExtensionService,
|
||||||
|
DockerClientFactory: server.DockerClientFactory,
|
||||||
}
|
}
|
||||||
proxyManager := proxy.NewManager(proxyManagerParameters)
|
proxyManager := proxy.NewManager(proxyManagerParameters)
|
||||||
|
|
||||||
|
@ -134,6 +136,7 @@ func (server *Server) Start() error {
|
||||||
authHandler.EndpointGroupService = server.EndpointGroupService
|
authHandler.EndpointGroupService = server.EndpointGroupService
|
||||||
authHandler.RoleService = server.RoleService
|
authHandler.RoleService = server.RoleService
|
||||||
authHandler.ProxyManager = proxyManager
|
authHandler.ProxyManager = proxyManager
|
||||||
|
authHandler.AuthorizationService = authorizationService
|
||||||
|
|
||||||
var roleHandler = roles.NewHandler(requestBouncer)
|
var roleHandler = roles.NewHandler(requestBouncer)
|
||||||
roleHandler.RoleService = server.RoleService
|
roleHandler.RoleService = server.RoleService
|
||||||
|
@ -214,6 +217,8 @@ func (server *Server) Start() error {
|
||||||
stackHandler.RegistryService = server.RegistryService
|
stackHandler.RegistryService = server.RegistryService
|
||||||
stackHandler.DockerHubService = server.DockerHubService
|
stackHandler.DockerHubService = server.DockerHubService
|
||||||
stackHandler.SettingsService = server.SettingsService
|
stackHandler.SettingsService = server.SettingsService
|
||||||
|
stackHandler.UserService = server.UserService
|
||||||
|
stackHandler.ExtensionService = server.ExtensionService
|
||||||
|
|
||||||
var tagHandler = tags.NewHandler(requestBouncer)
|
var tagHandler = tags.NewHandler(requestBouncer)
|
||||||
tagHandler.TagService = server.TagService
|
tagHandler.TagService = server.TagService
|
||||||
|
|
|
@ -16,6 +16,10 @@ import (
|
||||||
"github.com/portainer/portainer/api"
|
"github.com/portainer/portainer/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
dockerClientVersion = "1.24"
|
||||||
|
)
|
||||||
|
|
||||||
// ComposeStackManager represents a service for managing compose stacks.
|
// ComposeStackManager represents a service for managing compose stacks.
|
||||||
type ComposeStackManager struct {
|
type ComposeStackManager struct {
|
||||||
dataPath string
|
dataPath string
|
||||||
|
@ -40,7 +44,7 @@ func (manager *ComposeStackManager) createClient(endpoint *portainer.Endpoint) (
|
||||||
|
|
||||||
clientOpts := client.Options{
|
clientOpts := client.Options{
|
||||||
Host: endpointURL,
|
Host: endpointURL,
|
||||||
APIVersion: portainer.SupportedDockerAPIVersion,
|
APIVersion: dockerClientVersion,
|
||||||
}
|
}
|
||||||
|
|
||||||
if endpoint.TLSConfig.TLS {
|
if endpoint.TLSConfig.TLS {
|
||||||
|
|
|
@ -175,14 +175,15 @@ type (
|
||||||
|
|
||||||
// Stack represents a Docker stack created via docker stack deploy
|
// Stack represents a Docker stack created via docker stack deploy
|
||||||
Stack struct {
|
Stack struct {
|
||||||
ID StackID `json:"Id"`
|
ID StackID `json:"Id"`
|
||||||
Name string `json:"Name"`
|
Name string `json:"Name"`
|
||||||
Type StackType `json:"Type"`
|
Type StackType `json:"Type"`
|
||||||
EndpointID EndpointID `json:"EndpointId"`
|
EndpointID EndpointID `json:"EndpointId"`
|
||||||
SwarmID string `json:"SwarmId"`
|
SwarmID string `json:"SwarmId"`
|
||||||
EntryPoint string `json:"EntryPoint"`
|
EntryPoint string `json:"EntryPoint"`
|
||||||
Env []Pair `json:"Env"`
|
Env []Pair `json:"Env"`
|
||||||
ProjectPath string
|
ResourceControl *ResourceControl `json:"ResourceControl"`
|
||||||
|
ProjectPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegistryID represents a registry identifier
|
// RegistryID represents a registry identifier
|
||||||
|
@ -191,6 +192,13 @@ type (
|
||||||
// RegistryType represents a type of registry
|
// RegistryType represents a type of registry
|
||||||
RegistryType int
|
RegistryType int
|
||||||
|
|
||||||
|
// GitlabRegistryData represents data required for gitlab registry to work
|
||||||
|
GitlabRegistryData struct {
|
||||||
|
ProjectID int `json:"ProjectId"`
|
||||||
|
InstanceURL string `json:"InstanceURL"`
|
||||||
|
ProjectPath string `json:"ProjectPath"`
|
||||||
|
}
|
||||||
|
|
||||||
// Registry represents a Docker registry with all the info required
|
// Registry represents a Docker registry with all the info required
|
||||||
// to connect to it
|
// to connect to it
|
||||||
Registry struct {
|
Registry struct {
|
||||||
|
@ -202,6 +210,7 @@ type (
|
||||||
Username string `json:"Username"`
|
Username string `json:"Username"`
|
||||||
Password string `json:"Password,omitempty"`
|
Password string `json:"Password,omitempty"`
|
||||||
ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"`
|
ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"`
|
||||||
|
Gitlab GitlabRegistryData `json:"Gitlab"`
|
||||||
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
|
UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"`
|
||||||
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
|
TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"`
|
||||||
|
|
||||||
|
@ -295,6 +304,7 @@ type (
|
||||||
Name string `json:"Name"`
|
Name string `json:"Name"`
|
||||||
Description string `json:"Description"`
|
Description string `json:"Description"`
|
||||||
Authorizations Authorizations `json:"Authorizations"`
|
Authorizations Authorizations `json:"Authorizations"`
|
||||||
|
Priority int `json:"Priority"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// AccessPolicy represent a policy that can be associated to a user or team
|
// AccessPolicy represent a policy that can be associated to a user or team
|
||||||
|
@ -439,21 +449,20 @@ type (
|
||||||
|
|
||||||
// ResourceControl represent a reference to a Docker resource with specific access controls
|
// ResourceControl represent a reference to a Docker resource with specific access controls
|
||||||
ResourceControl struct {
|
ResourceControl struct {
|
||||||
ID ResourceControlID `json:"Id"`
|
ID ResourceControlID `json:"Id"`
|
||||||
ResourceID string `json:"ResourceId"`
|
ResourceID string `json:"ResourceId"`
|
||||||
SubResourceIDs []string `json:"SubResourceIds"`
|
SubResourceIDs []string `json:"SubResourceIds"`
|
||||||
Type ResourceControlType `json:"Type"`
|
Type ResourceControlType `json:"Type"`
|
||||||
UserAccesses []UserResourceAccess `json:"UserAccesses"`
|
UserAccesses []UserResourceAccess `json:"UserAccesses"`
|
||||||
TeamAccesses []TeamResourceAccess `json:"TeamAccesses"`
|
TeamAccesses []TeamResourceAccess `json:"TeamAccesses"`
|
||||||
Public bool `json:"Public"`
|
Public bool `json:"Public"`
|
||||||
|
AdministratorsOnly bool `json:"AdministratorsOnly"`
|
||||||
|
System bool `json:"System"`
|
||||||
|
|
||||||
// Deprecated fields
|
// Deprecated fields
|
||||||
// Deprecated in DBVersion == 2
|
// Deprecated in DBVersion == 2
|
||||||
OwnerID UserID `json:"OwnerId,omitempty"`
|
OwnerID UserID `json:"OwnerId,omitempty"`
|
||||||
AccessLevel ResourceAccessLevel `json:"AccessLevel,omitempty"`
|
AccessLevel ResourceAccessLevel `json:"AccessLevel,omitempty"`
|
||||||
|
|
||||||
// Deprecated in DBVersion == 14
|
|
||||||
AdministratorsOnly bool `json:"AdministratorsOnly,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResourceControlType represents the type of resource associated to the resource control (volume, container, service...)
|
// ResourceControlType represents the type of resource associated to the resource control (volume, container, service...)
|
||||||
|
@ -742,7 +751,7 @@ type (
|
||||||
// ResourceControlService represents a service for managing resource control data
|
// ResourceControlService represents a service for managing resource control data
|
||||||
ResourceControlService interface {
|
ResourceControlService interface {
|
||||||
ResourceControl(ID ResourceControlID) (*ResourceControl, error)
|
ResourceControl(ID ResourceControlID) (*ResourceControl, error)
|
||||||
ResourceControlByResourceID(resourceID string) (*ResourceControl, error)
|
ResourceControlByResourceIDAndType(resourceID string, resourceType ResourceControlType) (*ResourceControl, error)
|
||||||
ResourceControls() ([]ResourceControl, error)
|
ResourceControls() ([]ResourceControl, error)
|
||||||
CreateResourceControl(rc *ResourceControl) error
|
CreateResourceControl(rc *ResourceControl) error
|
||||||
UpdateResourceControl(ID ResourceControlID, resourceControl *ResourceControl) error
|
UpdateResourceControl(ID ResourceControlID, resourceControl *ResourceControl) error
|
||||||
|
@ -883,9 +892,11 @@ type (
|
||||||
// ExtensionManager represents a service used to manage extensions
|
// ExtensionManager represents a service used to manage extensions
|
||||||
ExtensionManager interface {
|
ExtensionManager interface {
|
||||||
FetchExtensionDefinitions() ([]Extension, error)
|
FetchExtensionDefinitions() ([]Extension, error)
|
||||||
|
InstallExtension(extension *Extension, licenseKey string, archiveFileName string, extensionArchive []byte) error
|
||||||
EnableExtension(extension *Extension, licenseKey string) error
|
EnableExtension(extension *Extension, licenseKey string) error
|
||||||
DisableExtension(extension *Extension) error
|
DisableExtension(extension *Extension) error
|
||||||
UpdateExtension(extension *Extension, version string) error
|
UpdateExtension(extension *Extension, version string) error
|
||||||
|
StartExtensions() error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReverseTunnelService represensts a service used to manage reverse tunnel connections.
|
// ReverseTunnelService represensts a service used to manage reverse tunnel connections.
|
||||||
|
@ -903,9 +914,9 @@ type (
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// APIVersion is the version number of the Portainer API
|
// APIVersion is the version number of the Portainer API
|
||||||
APIVersion = "1.22.1"
|
APIVersion = "1.23.0"
|
||||||
// DBVersion is the version number of the Portainer database
|
// DBVersion is the version number of the Portainer database
|
||||||
DBVersion = 20
|
DBVersion = 22
|
||||||
// AssetsServerURL represents the URL of the Portainer asset server
|
// AssetsServerURL represents the URL of the Portainer asset server
|
||||||
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com"
|
||||||
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
// MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved
|
||||||
|
@ -913,7 +924,7 @@ const (
|
||||||
// VersionCheckURL represents the URL used to retrieve the latest version of Portainer
|
// VersionCheckURL represents the URL used to retrieve the latest version of Portainer
|
||||||
VersionCheckURL = "https://api.github.com/repos/portainer/portainer/releases/latest"
|
VersionCheckURL = "https://api.github.com/repos/portainer/portainer/releases/latest"
|
||||||
// ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved
|
// ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved
|
||||||
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.22.1.json"
|
ExtensionDefinitionsURL = AssetsServerURL + "/extensions-" + APIVersion + ".json"
|
||||||
// SupportProductsURL represents the URL where Portainer support products can be retrieved
|
// SupportProductsURL represents the URL where Portainer support products can be retrieved
|
||||||
SupportProductsURL = AssetsServerURL + "/support.json"
|
SupportProductsURL = AssetsServerURL + "/support.json"
|
||||||
// PortainerAgentHeader represents the name of the header available in any agent response
|
// PortainerAgentHeader represents the name of the header available in any agent response
|
||||||
|
@ -929,12 +940,12 @@ const (
|
||||||
// PortainerAgentSignatureMessage represents the message used to create a digital signature
|
// PortainerAgentSignatureMessage represents the message used to create a digital signature
|
||||||
// to be used when communicating with an agent
|
// to be used when communicating with an agent
|
||||||
PortainerAgentSignatureMessage = "Portainer-App"
|
PortainerAgentSignatureMessage = "Portainer-App"
|
||||||
// SupportedDockerAPIVersion is the minimum Docker API version supported by Portainer
|
|
||||||
SupportedDockerAPIVersion = "1.24"
|
|
||||||
// ExtensionServer represents the server used by Portainer to communicate with extensions
|
// ExtensionServer represents the server used by Portainer to communicate with extensions
|
||||||
ExtensionServer = "localhost"
|
ExtensionServer = "localhost"
|
||||||
// DefaultEdgeAgentCheckinIntervalInSeconds represents the default interval (in seconds) used by Edge agents to checkin with the Portainer instance
|
// DefaultEdgeAgentCheckinIntervalInSeconds represents the default interval (in seconds) used by Edge agents to checkin with the Portainer instance
|
||||||
DefaultEdgeAgentCheckinIntervalInSeconds = 5
|
DefaultEdgeAgentCheckinIntervalInSeconds = 5
|
||||||
|
// LocalExtensionManifestFile represents the name of the local manifest file for extensions
|
||||||
|
LocalExtensionManifestFile = "/extensions.json"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -1076,6 +1087,8 @@ const (
|
||||||
AzureRegistry
|
AzureRegistry
|
||||||
// CustomRegistry represents a custom registry
|
// CustomRegistry represents a custom registry
|
||||||
CustomRegistry
|
CustomRegistry
|
||||||
|
// GitlabRegistry represents a gitlab registry
|
||||||
|
GitlabRegistry
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -54,7 +54,7 @@ info:
|
||||||
|
|
||||||
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
|
**NOTE**: You can find more information on how to query the Docker API in the [Docker official documentation](https://docs.docker.com/engine/api/v1.30/) as well as in [this Portainer example](https://gist.github.com/deviantony/77026d402366b4b43fa5918d41bc42f8).
|
||||||
|
|
||||||
version: "1.22.1"
|
version: "1.23.0"
|
||||||
title: "Portainer API"
|
title: "Portainer API"
|
||||||
contact:
|
contact:
|
||||||
email: "info@portainer.io"
|
email: "info@portainer.io"
|
||||||
|
@ -1235,7 +1235,7 @@ paths:
|
||||||
summary: "Create a new resource control"
|
summary: "Create a new resource control"
|
||||||
description: |
|
description: |
|
||||||
Create a new resource control to restrict access to a Docker resource.
|
Create a new resource control to restrict access to a Docker resource.
|
||||||
**Access policy**: restricted
|
**Access policy**: administrator
|
||||||
operationId: "ResourceControlCreate"
|
operationId: "ResourceControlCreate"
|
||||||
consumes:
|
consumes:
|
||||||
- "application/json"
|
- "application/json"
|
||||||
|
@ -1343,7 +1343,7 @@ paths:
|
||||||
summary: "Remove a resource control"
|
summary: "Remove a resource control"
|
||||||
description: |
|
description: |
|
||||||
Remove a resource control.
|
Remove a resource control.
|
||||||
**Access policy**: restricted
|
**Access policy**: administrator
|
||||||
operationId: "ResourceControlDelete"
|
operationId: "ResourceControlDelete"
|
||||||
security:
|
security:
|
||||||
- jwt: []
|
- jwt: []
|
||||||
|
@ -3174,7 +3174,7 @@ definitions:
|
||||||
description: "Is analytics enabled"
|
description: "Is analytics enabled"
|
||||||
Version:
|
Version:
|
||||||
type: "string"
|
type: "string"
|
||||||
example: "1.22.1"
|
example: "1.23.0"
|
||||||
description: "Portainer API version"
|
description: "Portainer API version"
|
||||||
PublicSettingsInspectResponse:
|
PublicSettingsInspectResponse:
|
||||||
type: "object"
|
type: "object"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"packageName": "portainer",
|
"packageName": "portainer",
|
||||||
"packageVersion": "1.22.1",
|
"packageVersion": "1.23.0",
|
||||||
"projectName": "portainer"
|
"projectName": "portainer"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import '../assets/css/app.css';
|
import '../assets/css/app.css';
|
||||||
import './libraries/isteven-angular-multiselect/isteven-multi-select.css';
|
|
||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
|
|
||||||
import './agent/_module';
|
import './agent/_module';
|
||||||
|
|
57
app/app.js
57
app/app.js
|
@ -1,25 +1,13 @@
|
||||||
import _ from 'lodash-es';
|
import $ from 'jquery';
|
||||||
|
import '@babel/polyfill'
|
||||||
|
|
||||||
angular.module('portainer')
|
angular.module('portainer')
|
||||||
.run(['$rootScope', '$state', '$interval', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', 'SystemService', 'cfpLoadingBar', '$transitions', 'HttpRequestHelper',
|
.run(['$rootScope', '$state', '$interval', 'LocalStorage', 'EndpointProvider', 'SystemService', 'cfpLoadingBar', '$transitions', 'HttpRequestHelper',
|
||||||
function ($rootScope, $state, $interval, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics, SystemService, cfpLoadingBar, $transitions, HttpRequestHelper) {
|
function ($rootScope, $state, $interval, LocalStorage, EndpointProvider, SystemService, cfpLoadingBar, $transitions, HttpRequestHelper) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
EndpointProvider.initialize();
|
EndpointProvider.initialize();
|
||||||
|
|
||||||
StateManager.initialize()
|
|
||||||
.then(function success(state) {
|
|
||||||
if (state.application.authentication) {
|
|
||||||
initAuthentication(authManager, Authentication, $rootScope, $state);
|
|
||||||
}
|
|
||||||
if (state.application.analytics) {
|
|
||||||
initAnalytics(Analytics, $rootScope);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function error(err) {
|
|
||||||
Notifications.error('Failure', err, 'Unable to retrieve application settings');
|
|
||||||
});
|
|
||||||
|
|
||||||
$rootScope.$state = $state;
|
$rootScope.$state = $state;
|
||||||
|
|
||||||
// Workaround to prevent the loading bar from going backward
|
// Workaround to prevent the loading bar from going backward
|
||||||
|
@ -35,11 +23,23 @@ function ($rootScope, $state, $interval, Authentication, authManager, StateManag
|
||||||
HttpRequestHelper.resetAgentHeaders();
|
HttpRequestHelper.resetAgentHeaders();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$state.defaultErrorHandler(function() {
|
||||||
|
// Do not log transitionTo errors
|
||||||
|
});
|
||||||
|
|
||||||
// Keep-alive Edge endpoints by sending a ping request every minute
|
// Keep-alive Edge endpoints by sending a ping request every minute
|
||||||
$interval(function() {
|
$interval(function() {
|
||||||
ping(EndpointProvider, SystemService);
|
ping(EndpointProvider, SystemService);
|
||||||
}, 60 * 1000)
|
}, 60 * 1000)
|
||||||
|
|
||||||
|
$(document).ajaxSend(function (event, jqXhr, jqOpts) {
|
||||||
|
const type = jqOpts.type === 'POST' || jqOpts.type === 'PUT' || jqOpts.type === 'PATCH';
|
||||||
|
const hasNoContentType = jqOpts.contentType !== 'application/json' && jqOpts.headers && !jqOpts.headers['Content-Type'];
|
||||||
|
if (type && hasNoContentType) {
|
||||||
|
jqXhr.setRequestHeader('Content-Type', 'application/json');
|
||||||
|
}
|
||||||
|
jqXhr.setRequestHeader('Authorization', 'Bearer ' + LocalStorage.getJWT());
|
||||||
|
});
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
function ping(EndpointProvider, SystemService) {
|
function ping(EndpointProvider, SystemService) {
|
||||||
|
@ -48,28 +48,3 @@ function ping(EndpointProvider, SystemService) {
|
||||||
SystemService.ping(endpoint.Id);
|
SystemService.ping(endpoint.Id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function initAuthentication(authManager, Authentication, $rootScope, $state) {
|
|
||||||
authManager.checkAuthOnRefresh();
|
|
||||||
Authentication.init();
|
|
||||||
|
|
||||||
// The unauthenticated event is broadcasted by the jwtInterceptor when
|
|
||||||
// hitting a 401. We're using this instead of the usual combination of
|
|
||||||
// authManager.redirectWhenUnauthenticated() + unauthenticatedRedirector
|
|
||||||
// to have more controls on which URL should trigger the unauthenticated state.
|
|
||||||
$rootScope.$on('unauthenticated', function (event, data) {
|
|
||||||
if (!_.includes(data.config.url, '/v2/')) {
|
|
||||||
$state.go('portainer.auth', { error: 'Your session has expired' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function initAnalytics(Analytics, $rootScope) {
|
|
||||||
Analytics.offline(false);
|
|
||||||
Analytics.registerScriptTags();
|
|
||||||
Analytics.registerTrackers();
|
|
||||||
$rootScope.$on('$stateChangeSuccess', function (event, toState) {
|
|
||||||
Analytics.trackPage(toState.url);
|
|
||||||
Analytics.pageView();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
|
@ -101,7 +101,7 @@
|
||||||
<td ng-if="$ctrl.showOwnershipColumn">
|
<td ng-if="$ctrl.showOwnershipColumn">
|
||||||
<span>
|
<span>
|
||||||
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
||||||
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
|
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -272,7 +272,7 @@
|
||||||
<td ng-if="$ctrl.showOwnershipColumn" ng-show="$ctrl.columnVisibility.columns.ownership.display">
|
<td ng-if="$ctrl.showOwnershipColumn" ng-show="$ctrl.columnVisibility.columns.ownership.display">
|
||||||
<span>
|
<span>
|
||||||
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
||||||
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
|
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
<td>
|
<td>
|
||||||
<a ng-if="!parentCtrl.offlineMode" ui-sref="docker.networks.network({ id: item.Id, nodeName: item.NodeName })" title="{{ item.Name }}">{{ item.Name | truncate:40 }}</a>
|
<a ng-if="!parentCtrl.offlineMode" ui-sref="docker.networks.network({ id: item.Id, nodeName: item.NodeName })" title="{{ item.Name }}">{{ item.Name | truncate:40 }}</a>
|
||||||
<span ng-if="parentCtrl.offlineMode">{{ item.Name | truncate:40 }}</span>
|
<span ng-if="parentCtrl.offlineMode">{{ item.Name | truncate:40 }}</span>
|
||||||
|
<span style="margin-left: 10px;" class="label label-info image-tag space-left" ng-if="item.ResourceControl.System">System</span>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ item.StackName ? item.StackName : '-' }}</td>
|
<td>{{ item.StackName ? item.StackName : '-' }}</td>
|
||||||
<td>{{ item.Scope }}</td>
|
<td>{{ item.Scope }}</td>
|
||||||
|
@ -22,6 +23,6 @@
|
||||||
<td ng-if="parentCtrl.showOwnershipColumn">
|
<td ng-if="parentCtrl.showOwnershipColumn">
|
||||||
<span>
|
<span>
|
||||||
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
||||||
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
|
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = RCO.ADMINISTRATORS }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { ResourceControlOwnership as RCO } from 'Portainer/models/resourceControl/resourceControlOwnership';
|
||||||
|
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.directive('networkRowContent', [function networkRowContent() {
|
.directive('networkRowContent', [function networkRowContent() {
|
||||||
var directive = {
|
var directive = {
|
||||||
|
@ -9,6 +11,9 @@ angular.module('portainer.docker')
|
||||||
parentCtrl: '<',
|
parentCtrl: '<',
|
||||||
allowCheckbox: '<',
|
allowCheckbox: '<',
|
||||||
allowExpand: '<'
|
allowExpand: '<'
|
||||||
|
},
|
||||||
|
controller: ($scope) => {
|
||||||
|
$scope.RCO = RCO;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return directive;
|
return directive;
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import _ from 'lodash-es';
|
import _ from 'lodash-es';
|
||||||
|
|
||||||
angular.module('portainer.docker')
|
angular.module('portainer.docker')
|
||||||
.controller('NetworksDatatableController', ['$scope', '$controller', 'PREDEFINED_NETWORKS', 'DatatableService',
|
.controller('NetworksDatatableController', ['$scope', '$controller', 'NetworkHelper', 'DatatableService',
|
||||||
function ($scope, $controller, PREDEFINED_NETWORKS, DatatableService) {
|
function ($scope, $controller, NetworkHelper, DatatableService) {
|
||||||
|
|
||||||
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
|
angular.extend(this, $controller('GenericDatatableController', {$scope: $scope}));
|
||||||
|
|
||||||
this.disableRemove = function(item) {
|
this.disableRemove = function(item) {
|
||||||
return PREDEFINED_NETWORKS.includes(item.Name);
|
return NetworkHelper.isSystemNetwork(item);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.state = Object.assign(this.state, {
|
this.state = Object.assign(this.state, {
|
||||||
|
@ -15,7 +15,7 @@ angular.module('portainer.docker')
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Do not allow PREDEFINED_NETWORKS to be selected
|
* Do not allow system networks to be selected
|
||||||
*/
|
*/
|
||||||
this.allowSelection = function(item) {
|
this.allowSelection = function(item) {
|
||||||
return !this.disableRemove(item);
|
return !this.disableRemove(item);
|
||||||
|
|
|
@ -101,7 +101,7 @@
|
||||||
<td ng-if="$ctrl.showOwnershipColumn">
|
<td ng-if="$ctrl.showOwnershipColumn">
|
||||||
<span>
|
<span>
|
||||||
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
||||||
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
|
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
@ -156,7 +156,7 @@
|
||||||
<td ng-if="$ctrl.showOwnershipColumn">
|
<td ng-if="$ctrl.showOwnershipColumn">
|
||||||
<span>
|
<span>
|
||||||
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
<i ng-class="item.ResourceControl.Ownership | ownershipicon" aria-hidden="true"></i>
|
||||||
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = 'administrators' }}
|
{{ item.ResourceControl.Ownership ? item.ResourceControl.Ownership : item.ResourceControl.Ownership = $ctrl.RCO.ADMINISTRATORS }}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue