diff --git a/.babelrc b/.babelrc new file mode 100644 index 000000000..32beabad5 --- /dev/null +++ b/.babelrc @@ -0,0 +1,12 @@ +{ + "plugins": ["lodash", "angularjs-annotate"], + "presets": [ + [ + "@babel/preset-env", + { + "modules": false, + "useBuiltIns": "usage" + } + ] + ] +} diff --git a/.codeclimate.yml b/.codeclimate.yml index 845dacc08..07eb34e8b 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -53,6 +53,7 @@ plugins: mass_threshold: 80 eslint: enabled: true + channel: "eslint-5" config: config: .eslintrc.yml fixme: diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..3e98e7f99 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +test/ \ No newline at end of file diff --git a/.eslintrc.yml b/.eslintrc.yml index bc803c2e5..eb1b1779a 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -1,287 +1,292 @@ env: browser: true jquery: true + node: true + es6: true + +globals: + angular: true + __CONFIG_GA_ID: true -# globals: -# angular: true -# $: true -# _: true -# moment: true -# filesize: true -# splitargs: true extends: - 'eslint:recommended' -# http://eslint.org/docs/rules/ +parserOptions: + ecmaVersion: 2018 + sourceType: module + ecmaFeatures: + modules: true + +# # http://eslint.org/docs/rules/ rules: - # Possible Errors - no-await-in-loop: off - no-cond-assign: error - no-console: off - no-constant-condition: error - no-control-regex: error - no-debugger: error - no-dupe-args: error - no-dupe-keys: error - no-duplicate-case: error - no-empty-character-class: error - no-empty: error - no-ex-assign: error - no-extra-boolean-cast: error - no-extra-parens: off - no-extra-semi: error - no-func-assign: error - no-inner-declarations: - - error - - functions - no-invalid-regexp: error - no-irregular-whitespace: error - no-negated-in-lhs: error - no-obj-calls: error - no-prototype-builtins: off - no-regex-spaces: error - no-sparse-arrays: error - no-template-curly-in-string: off - no-unexpected-multiline: error - no-unreachable: error - no-unsafe-finally: off - no-unsafe-negation: off - use-isnan: error - valid-jsdoc: off - valid-typeof: error +# # Possible Errors +# no-await-in-loop: off +# no-cond-assign: error +# no-console: off +# no-constant-condition: error +# no-control-regex: error +# no-debugger: error +# no-dupe-args: error +# no-dupe-keys: error +# no-duplicate-case: error +# no-empty-character-class: error + no-empty: warn +# no-ex-assign: error +# no-extra-boolean-cast: error +# no-extra-parens: off +# no-extra-semi: error +# no-func-assign: error +# no-inner-declarations: +# - error +# - functions +# no-invalid-regexp: error +# no-irregular-whitespace: error +# no-negated-in-lhs: error +# no-obj-calls: error +# no-prototype-builtins: off +# no-regex-spaces: error +# no-sparse-arrays: error +# no-template-curly-in-string: off +# no-unexpected-multiline: error +# no-unreachable: error +# no-unsafe-finally: off +# no-unsafe-negation: off +# use-isnan: error +# valid-jsdoc: off +# valid-typeof: error - # Best Practices - accessor-pairs: error - array-callback-return: off - block-scoped-var: off - class-methods-use-this: off - complexity: - - error - - 6 - consistent-return: off - curly: off - default-case: off - dot-location: off - dot-notation: off - eqeqeq: error - guard-for-in: error - no-alert: error - no-caller: error - no-case-declarations: error - no-div-regex: error - no-else-return: off - no-empty-function: off - no-empty-pattern: error - no-eq-null: error - no-eval: error - no-extend-native: error - no-extra-bind: error - no-extra-label: off - no-fallthrough: error - no-floating-decimal: off - no-global-assign: off - no-implicit-coercion: off - no-implied-eval: error - no-invalid-this: off - no-iterator: error - no-labels: - - error - - allowLoop: true - allowSwitch: true - no-lone-blocks: error - no-loop-func: error - no-magic-number: off - no-multi-spaces: off - no-multi-str: off - no-native-reassign: error - no-new-func: error - no-new-wrappers: error - no-new: error - no-octal-escape: error - no-octal: error - no-param-reassign: off - no-proto: error - no-redeclare: error - no-restricted-properties: off - no-return-assign: error - no-return-await: off - no-script-url: error - no-self-assign: off - no-self-compare: error - no-sequences: off - no-throw-literal: off - no-unmodified-loop-condition: off - no-unused-expressions: error - no-unused-labels: off - no-useless-call: error - no-useless-concat: error +# # Best Practices +# accessor-pairs: error +# array-callback-return: off +# block-scoped-var: off +# class-methods-use-this: off +# complexity: +# - error +# - 6 +# consistent-return: off +# curly: off +# default-case: off +# dot-location: off +# dot-notation: off +# eqeqeq: error +# guard-for-in: error +# no-alert: error +# no-caller: error +# no-case-declarations: error +# no-div-regex: error +# no-else-return: off + no-empty-function: warn +# no-empty-pattern: error +# no-eq-null: error +# no-eval: error +# no-extend-native: error +# no-extra-bind: error +# no-extra-label: off +# no-fallthrough: error +# no-floating-decimal: off +# no-global-assign: off +# no-implicit-coercion: off +# no-implied-eval: error +# no-invalid-this: off +# no-iterator: error +# no-labels: +# - error +# - allowLoop: true +# allowSwitch: true +# no-lone-blocks: error +# no-loop-func: error +# no-magic-number: off +# no-multi-spaces: off +# no-multi-str: off +# no-native-reassign: error +# no-new-func: error +# no-new-wrappers: error +# no-new: error +# no-octal-escape: error +# no-octal: error +# no-param-reassign: off +# no-proto: error +# no-redeclare: error +# no-restricted-properties: off +# no-return-assign: error +# no-return-await: off +# no-script-url: error +# no-self-assign: off +# no-self-compare: error +# no-sequences: off +# no-throw-literal: off +# no-unmodified-loop-condition: off +# no-unused-expressions: error +# no-unused-labels: off +# no-useless-call: error +# no-useless-concat: error no-useless-escape: off - no-useless-return: off - no-void: error - no-warning-comments: off - no-with: error - prefer-promise-reject-errors: off - radix: error - require-await: off - vars-on-top: off - wrap-iife: error - yoda: off +# no-useless-return: off +# no-void: error +# no-warning-comments: off +# no-with: error +# prefer-promise-reject-errors: off +# radix: error +# require-await: off +# vars-on-top: off +# wrap-iife: error +# yoda: off - # Strict - strict: off +# # Strict +# strict: off - # Variables - init-declarations: off - no-catch-shadow: error - no-delete-var: error - no-label-var: error - no-restricted-globals: off - no-shadow-restricted-names: error - no-shadow: off - no-undef-init: error - no-undef: off - no-undefined: off - no-unused-vars: - - warn - - - vars: local - no-use-before-define: off +# # Variables +# init-declarations: off +# no-catch-shadow: error +# no-delete-var: error +# no-label-var: error +# no-restricted-globals: off +# no-shadow-restricted-names: error +# no-shadow: off +# no-undef-init: error +# no-undef: off +# no-undefined: off +# no-unused-vars: +# - warn +# - +# vars: local +# no-use-before-define: off - # Node.js and CommonJS - callback-return: error - global-require: error - handle-callback-err: error - no-mixed-requires: off - no-new-require: off - no-path-concat: error - no-process-env: off - no-process-exit: error - no-restricted-modules: off - no-sync: off +# # Node.js and CommonJS +# callback-return: error +# global-require: error +# handle-callback-err: error +# no-mixed-requires: off +# no-new-require: off +# no-path-concat: error +# no-process-env: off +# no-process-exit: error +# no-restricted-modules: off +# no-sync: off - # Stylistic Issues - array-bracket-spacing: off - block-spacing: off - brace-style: off - camelcase: off - capitalized-comments: off - comma-dangle: - - error - - never - comma-spacing: off - comma-style: off - computed-property-spacing: off - consistent-this: off - eol-last: off - func-call-spacing: off - func-name-matching: off - func-names: off - func-style: off - id-length: off - id-match: off - indent: off - jsx-quotes: off - key-spacing: off - keyword-spacing: off - line-comment-position: off - linebreak-style: - - error - - unix - lines-around-comment: off - lines-around-directive: off - max-depth: off - max-len: off - max-nested-callbacks: off - max-params: off - max-statements-per-line: off - max-statements: - - error - - 30 - multiline-ternary: off - new-cap: off - new-parens: off - newline-after-var: off - newline-before-return: off - newline-per-chained-call: off - no-array-constructor: off - no-bitwise: off - no-continue: off - no-inline-comments: off - no-lonely-if: off - no-mixed-operators: off - no-mixed-spaces-and-tabs: off - no-multi-assign: off - no-multiple-empty-lines: off - no-negated-condition: off - no-nested-ternary: off - no-new-object: off - no-plusplus: off - no-restricted-syntax: off - no-spaced-func: off - no-tabs: off - no-ternary: off - no-trailing-spaces: off - no-underscore-dangle: off - no-unneeded-ternary: off - object-curly-newline: off - object-curly-spacing: off - object-property-newline: off - one-var-declaration-per-line: off - one-var: off - operator-assignment: off - operator-linebreak: off - padded-blocks: off - quote-props: off - quotes: - - error - - single - require-jsdoc: off - semi-spacing: off - semi: - - error - - always - sort-keys: off - sort-vars: off - space-before-blocks: off - space-before-function-paren: off - space-in-parens: off - space-infix-ops: off - space-unary-ops: off - spaced-comment: off - template-tag-spacing: off - unicode-bom: off - wrap-regex: off +# # Stylistic Issues +# array-bracket-spacing: off +# block-spacing: off +# brace-style: off +# camelcase: off +# capitalized-comments: off +# comma-dangle: +# - error +# - never +# comma-spacing: off +# comma-style: off +# computed-property-spacing: off +# consistent-this: off +# eol-last: off +# func-call-spacing: off +# func-name-matching: off +# func-names: off +# func-style: off +# id-length: off +# id-match: off +# indent: off +# jsx-quotes: off +# key-spacing: off +# keyword-spacing: off +# line-comment-position: off +# linebreak-style: +# - error +# - unix +# lines-around-comment: off +# lines-around-directive: off +# max-depth: off +# max-len: off +# max-nested-callbacks: off +# max-params: off +# max-statements-per-line: off +# max-statements: +# - error +# - 30 +# multiline-ternary: off +# new-cap: off +# new-parens: off +# newline-after-var: off +# newline-before-return: off +# newline-per-chained-call: off +# no-array-constructor: off +# no-bitwise: off +# no-continue: off +# no-inline-comments: off +# no-lonely-if: off +# no-mixed-operators: off +# no-mixed-spaces-and-tabs: off +# no-multi-assign: off +# no-multiple-empty-lines: off +# no-negated-condition: off +# no-nested-ternary: off +# no-new-object: off +# no-plusplus: off +# no-restricted-syntax: off +# no-spaced-func: off +# no-tabs: off +# no-ternary: off +# no-trailing-spaces: off +# no-underscore-dangle: off +# no-unneeded-ternary: off +# object-curly-newline: off +# object-curly-spacing: off +# object-property-newline: off +# one-var-declaration-per-line: off +# one-var: off +# operator-assignment: off +# operator-linebreak: off +# padded-blocks: off +# quote-props: off +# quotes: +# - error +# - single +# require-jsdoc: off +# semi-spacing: off +# semi: +# - error +# - always +# sort-keys: off +# sort-vars: off +# space-before-blocks: off +# space-before-function-paren: off +# space-in-parens: off +# space-infix-ops: off +# space-unary-ops: off +# spaced-comment: off +# template-tag-spacing: off +# unicode-bom: off +# wrap-regex: off - # ECMAScript 6 - arrow-body-style: off - arrow-parens: off - arrow-spacing: off - constructor-super: off - generator-star-spacing: off - no-class-assign: off - no-confusing-arrow: off - no-const-assign: off - no-dupe-class-members: off - no-duplicate-imports: off - no-new-symbol: off - no-restricted-imports: off - no-this-before-super: off - no-useless-computed-key: off - no-useless-constructor: off - no-useless-rename: off - no-var: off - object-shorthand: off - prefer-arrow-callback: off - prefer-const: off - prefer-destructuring: off - prefer-numeric-literals: off - prefer-rest-params: off - prefer-reflect: off - prefer-spread: off - prefer-template: off - require-yield: off - rest-spread-spacing: off - sort-imports: off - symbol-description: off - template-curly-spacing: off - yield-star-spacing: off +# # ECMAScript 6 +# arrow-body-style: off +# arrow-parens: off +# arrow-spacing: off +# constructor-super: off +# generator-star-spacing: off +# no-class-assign: off +# no-confusing-arrow: off +# no-const-assign: off +# no-dupe-class-members: off +# no-duplicate-imports: off +# no-new-symbol: off +# no-restricted-imports: off +# no-this-before-super: off +# no-useless-computed-key: off +# no-useless-constructor: off +# no-useless-rename: off +# no-var: off +# object-shorthand: off +# prefer-arrow-callback: off +# prefer-const: off +# prefer-destructuring: off +# prefer-numeric-literals: off +# prefer-rest-params: off +# prefer-reflect: off +# prefer-spread: off +# prefer-template: off +# require-yield: off +# rest-spread-spacing: off +# sort-imports: off +# symbol-description: off +# template-curly-spacing: off +# yield-star-spacing: off diff --git a/.github/ISSUE_TEMPLATE/Bug_report.md b/.github/ISSUE_TEMPLATE/Bug_report.md index 986b47357..4da716c9a 100644 --- a/.github/ISSUE_TEMPLATE/Bug_report.md +++ b/.github/ISSUE_TEMPLATE/Bug_report.md @@ -8,7 +8,9 @@ about: Create a bug report Thanks for reporting a bug for Portainer ! -Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/ or gitter https://gitter.im/portainer/Lobby. +You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/ + +Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/. Before opening a new issue, make sure that we do not have any duplicates already open. You can ensure this by searching the issue list for this diff --git a/.github/ISSUE_TEMPLATE/Custom.md b/.github/ISSUE_TEMPLATE/Custom.md index e43b1e6f7..b4d55f559 100644 --- a/.github/ISSUE_TEMPLATE/Custom.md +++ b/.github/ISSUE_TEMPLATE/Custom.md @@ -6,7 +6,9 @@ about: Ask us a question about Portainer usage or deployment diff --git a/.github/ISSUE_TEMPLATE/Feature_request.md b/.github/ISSUE_TEMPLATE/Feature_request.md index 76a157fe0..6da5f4265 100644 --- a/.github/ISSUE_TEMPLATE/Feature_request.md +++ b/.github/ISSUE_TEMPLATE/Feature_request.md @@ -8,7 +8,7 @@ about: Suggest a feature/enhancement that should be added in Portainer Thanks for opening a feature request for Portainer ! -Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/ or gitter https://gitter.im/portainer/Lobby. +Do you need help or have a question? Come chat with us on Slack http://portainer.io/slack/ Before opening a new issue, make sure that we do not have any duplicates already open. You can ensure this by searching the issue list for this diff --git a/README.md b/README.md index 6a6a41c22..e1297a611 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,3 @@ -

@@ -11,10 +10,8 @@ [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=YHXZJQNJQ36H6) **_Portainer_** is a lightweight management UI which allows you to **easily** manage your different Docker environments (Docker hosts or Swarm clusters). - -**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (can be deployed as Linux container or a Windows native container). - -**_Portainer_** allows you to manage your Docker containers, images, volumes, networks and more ! It is compatible with the *standalone Docker* engine and with *Docker Swarm mode*. +**_Portainer_** is meant to be as **simple** to deploy as it is to use. It consists of a single container that can run on any Docker engine (can be deployed as Linux container or a Windows native container, supports other platforms too). +**_Portainer_** allows you to manage your all your Docker resources (containers, images, volumes, networks and more) ! It is compatible with the *standalone Docker* engine and with *Docker Swarm mode*. ## Demo @@ -37,6 +34,8 @@ Unlike the public demo, the playground sessions are deleted after 4 hours. Apart ## Getting help +**NOTE**: You can find more information about Portainer support framework policy here: https://www.portainer.io/2019/04/portainer-support-policy/ + * Issues: https://github.com/portainer/portainer/issues * FAQ: https://portainer.readthedocs.io/en/latest/faq.html * Slack (chat): https://portainer.io/slack/ diff --git a/api/bolt/datastore.go b/api/bolt/datastore.go index 85273c23b..adc3df1e7 100644 --- a/api/bolt/datastore.go +++ b/api/bolt/datastore.go @@ -6,24 +6,25 @@ import ( "time" "github.com/boltdb/bolt" - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/dockerhub" - "github.com/portainer/portainer/bolt/endpoint" - "github.com/portainer/portainer/bolt/endpointgroup" - "github.com/portainer/portainer/bolt/extension" - "github.com/portainer/portainer/bolt/migrator" - "github.com/portainer/portainer/bolt/registry" - "github.com/portainer/portainer/bolt/resourcecontrol" - "github.com/portainer/portainer/bolt/schedule" - "github.com/portainer/portainer/bolt/settings" - "github.com/portainer/portainer/bolt/stack" - "github.com/portainer/portainer/bolt/tag" - "github.com/portainer/portainer/bolt/team" - "github.com/portainer/portainer/bolt/teammembership" - "github.com/portainer/portainer/bolt/template" - "github.com/portainer/portainer/bolt/user" - "github.com/portainer/portainer/bolt/version" - "github.com/portainer/portainer/bolt/webhook" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/dockerhub" + "github.com/portainer/portainer/api/bolt/endpoint" + "github.com/portainer/portainer/api/bolt/endpointgroup" + "github.com/portainer/portainer/api/bolt/extension" + "github.com/portainer/portainer/api/bolt/migrator" + "github.com/portainer/portainer/api/bolt/registry" + "github.com/portainer/portainer/api/bolt/resourcecontrol" + "github.com/portainer/portainer/api/bolt/role" + "github.com/portainer/portainer/api/bolt/schedule" + "github.com/portainer/portainer/api/bolt/settings" + "github.com/portainer/portainer/api/bolt/stack" + "github.com/portainer/portainer/api/bolt/tag" + "github.com/portainer/portainer/api/bolt/team" + "github.com/portainer/portainer/api/bolt/teammembership" + "github.com/portainer/portainer/api/bolt/template" + "github.com/portainer/portainer/api/bolt/user" + "github.com/portainer/portainer/api/bolt/version" + "github.com/portainer/portainer/api/bolt/webhook" ) const ( @@ -37,6 +38,7 @@ type Store struct { db *bolt.DB checkForDataMigration bool fileService portainer.FileService + RoleService *role.Service DockerHubService *dockerhub.Service EndpointGroupService *endpointgroup.Service EndpointService *endpoint.Service @@ -89,29 +91,6 @@ func (store *Store) Open() error { return store.initServices() } -// Init creates the default data set. -func (store *Store) Init() error { - groups, err := store.EndpointGroupService.EndpointGroups() - if err != nil { - return err - } - - if len(groups) == 0 { - unassignedGroup := &portainer.EndpointGroup{ - Name: "Unassigned", - Description: "Unassigned endpoints", - Labels: []portainer.Pair{}, - AuthorizedUsers: []portainer.UserID{}, - AuthorizedTeams: []portainer.TeamID{}, - Tags: []string{}, - } - - return store.EndpointGroupService.CreateEndpointGroup(unassignedGroup) - } - - return nil -} - // Close closes the BoltDB database. func (store *Store) Close() error { if store.db != nil { @@ -140,6 +119,7 @@ func (store *Store) MigrateData() error { EndpointGroupService: store.EndpointGroupService, EndpointService: store.EndpointService, ExtensionService: store.ExtensionService, + RegistryService: store.RegistryService, ResourceControlService: store.ResourceControlService, SettingsService: store.SettingsService, StackService: store.StackService, @@ -162,6 +142,12 @@ func (store *Store) MigrateData() error { } func (store *Store) initServices() error { + authorizationsetService, err := role.NewService(store.db) + if err != nil { + return err + } + store.RoleService = authorizationsetService + dockerhubService, err := dockerhub.NewService(store.db) if err != nil { return err diff --git a/api/bolt/dockerhub/dockerhub.go b/api/bolt/dockerhub/dockerhub.go index 0e4b4858c..a225be462 100644 --- a/api/bolt/dockerhub/dockerhub.go +++ b/api/bolt/dockerhub/dockerhub.go @@ -1,8 +1,8 @@ package dockerhub import ( - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/internal" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" "github.com/boltdb/bolt" ) diff --git a/api/bolt/endpoint/endpoint.go b/api/bolt/endpoint/endpoint.go index 6dfff48df..723c2046b 100644 --- a/api/bolt/endpoint/endpoint.go +++ b/api/bolt/endpoint/endpoint.go @@ -1,8 +1,8 @@ package endpoint import ( - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/internal" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" "github.com/boltdb/bolt" ) diff --git a/api/bolt/endpointgroup/endpointgroup.go b/api/bolt/endpointgroup/endpointgroup.go index 89398dbd5..3311e88cb 100644 --- a/api/bolt/endpointgroup/endpointgroup.go +++ b/api/bolt/endpointgroup/endpointgroup.go @@ -1,8 +1,8 @@ package endpointgroup import ( - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/internal" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" "github.com/boltdb/bolt" ) diff --git a/api/bolt/extension/extension.go b/api/bolt/extension/extension.go index e60963f97..83e225414 100644 --- a/api/bolt/extension/extension.go +++ b/api/bolt/extension/extension.go @@ -1,8 +1,8 @@ package extension import ( - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/internal" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" "github.com/boltdb/bolt" ) diff --git a/api/bolt/init.go b/api/bolt/init.go new file mode 100644 index 000000000..b8b731bf2 --- /dev/null +++ b/api/bolt/init.go @@ -0,0 +1,432 @@ +package bolt + +import portainer "github.com/portainer/portainer/api" + +// Init creates the default data set. +func (store *Store) Init() error { + groups, err := store.EndpointGroupService.EndpointGroups() + if err != nil { + return err + } + + if len(groups) == 0 { + unassignedGroup := &portainer.EndpointGroup{ + Name: "Unassigned", + Description: "Unassigned endpoints", + Labels: []portainer.Pair{}, + UserAccessPolicies: portainer.UserAccessPolicies{}, + TeamAccessPolicies: portainer.TeamAccessPolicies{}, + Tags: []string{}, + } + + err = store.EndpointGroupService.CreateEndpointGroup(unassignedGroup) + if err != nil { + return err + } + } + + roles, err := store.RoleService.Roles() + if err != nil { + return err + } + + if len(roles) == 0 { + environmentAdministratorRole := &portainer.Role{ + Name: "Endpoint administrator", + Description: "Full control of all resources in an endpoint", + Authorizations: map[portainer.Authorization]bool{ + portainer.OperationDockerContainerArchiveInfo: true, + 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) + if err != nil { + return err + } + + environmentReadOnlyUserRole := &portainer.Role{ + Name: "Helpdesk", + Description: "Read-only access of all resources in an endpoint", + Authorizations: map[portainer.Authorization]bool{ + portainer.OperationDockerContainerArchiveInfo: true, + 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.OperationDockerAgentBrowseGet: true, + portainer.OperationDockerAgentBrowseList: true, + portainer.OperationPortainerStackList: true, + portainer.OperationPortainerStackInspect: true, + portainer.OperationPortainerStackFile: true, + portainer.OperationPortainerWebhookList: true, + portainer.EndpointResourcesAccess: true, + }, + } + + err = store.RoleService.CreateRole(environmentReadOnlyUserRole) + if err != nil { + return err + } + + standardUserRole := &portainer.Role{ + Name: "Standard user", + Description: "Full control of assigned resources in an endpoint", + Authorizations: map[portainer.Authorization]bool{ + portainer.OperationDockerContainerArchiveInfo: true, + 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.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, + }, + } + + err = store.RoleService.CreateRole(standardUserRole) + if err != nil { + return err + } + + readOnlyUserRole := &portainer.Role{ + Name: "Read-only user", + Description: "Read-only access of assigned resources in an endpoint", + Authorizations: map[portainer.Authorization]bool{ + portainer.OperationDockerContainerArchiveInfo: true, + 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.OperationDockerAgentBrowseGet: true, + portainer.OperationDockerAgentBrowseList: true, + portainer.OperationPortainerStackList: true, + portainer.OperationPortainerStackInspect: true, + portainer.OperationPortainerStackFile: true, + portainer.OperationPortainerWebhookList: true, + }, + } + + err = store.RoleService.CreateRole(readOnlyUserRole) + if err != nil { + return err + } + } + + return nil +} diff --git a/api/bolt/internal/db.go b/api/bolt/internal/db.go index 59ca7c87d..8cbb1d2d0 100644 --- a/api/bolt/internal/db.go +++ b/api/bolt/internal/db.go @@ -4,7 +4,7 @@ import ( "encoding/binary" "github.com/boltdb/bolt" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // Itob returns an 8-byte big endian representation of v. diff --git a/api/bolt/migrator/migrate_dbversion0.go b/api/bolt/migrator/migrate_dbversion0.go index 4c2bdff12..04d1a93b5 100644 --- a/api/bolt/migrator/migrate_dbversion0.go +++ b/api/bolt/migrator/migrate_dbversion0.go @@ -2,8 +2,8 @@ package migrator import ( "github.com/boltdb/bolt" - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/user" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/user" ) func (m *Migrator) updateAdminUserToDBVersion1() error { diff --git a/api/bolt/migrator/migrate_dbversion1.go b/api/bolt/migrator/migrate_dbversion1.go index 063f58dc7..0b52d83b8 100644 --- a/api/bolt/migrator/migrate_dbversion1.go +++ b/api/bolt/migrator/migrate_dbversion1.go @@ -2,8 +2,8 @@ package migrator import ( "github.com/boltdb/bolt" - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/internal" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" ) func (m *Migrator) updateResourceControlsToDBVersion2() error { diff --git a/api/bolt/migrator/migrate_dbversion10.go b/api/bolt/migrator/migrate_dbversion10.go index da55e3962..38a989207 100644 --- a/api/bolt/migrator/migrate_dbversion10.go +++ b/api/bolt/migrator/migrate_dbversion10.go @@ -1,6 +1,6 @@ package migrator -import "github.com/portainer/portainer" +import "github.com/portainer/portainer/api" func (m *Migrator) updateEndpointsToVersion11() error { legacyEndpoints, err := m.endpointService.Endpoints() diff --git a/api/bolt/migrator/migrate_dbversion11.go b/api/bolt/migrator/migrate_dbversion11.go index 168d23984..8acaefc2e 100644 --- a/api/bolt/migrator/migrate_dbversion11.go +++ b/api/bolt/migrator/migrate_dbversion11.go @@ -5,9 +5,9 @@ import ( "strings" "github.com/boltdb/bolt" - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/internal" - "github.com/portainer/portainer/bolt/stack" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" + "github.com/portainer/portainer/api/bolt/stack" ) func (m *Migrator) updateEndpointsToVersion12() error { diff --git a/api/bolt/migrator/migrate_dbversion12.go b/api/bolt/migrator/migrate_dbversion12.go index 0d2bf0554..ba180ca56 100644 --- a/api/bolt/migrator/migrate_dbversion12.go +++ b/api/bolt/migrator/migrate_dbversion12.go @@ -1,6 +1,6 @@ package migrator -import "github.com/portainer/portainer" +import "github.com/portainer/portainer/api" func (m *Migrator) updateSettingsToVersion13() error { legacySettings, err := m.settingsService.Settings() diff --git a/api/bolt/migrator/migrate_dbversion14.go b/api/bolt/migrator/migrate_dbversion14.go index ebaa4495e..5ec13cd9f 100644 --- a/api/bolt/migrator/migrate_dbversion14.go +++ b/api/bolt/migrator/migrate_dbversion14.go @@ -3,7 +3,7 @@ package migrator import ( "strings" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) func (m *Migrator) updateSettingsToDBVersion15() error { diff --git a/api/bolt/migrator/migrate_dbversion17.go b/api/bolt/migrator/migrate_dbversion17.go new file mode 100644 index 000000000..4e17090a8 --- /dev/null +++ b/api/bolt/migrator/migrate_dbversion17.go @@ -0,0 +1,125 @@ +package migrator + +import ( + portainer "github.com/portainer/portainer/api" +) + +func (m *Migrator) updateUsersToDBVersion18() error { + legacyUsers, err := m.userService.Users() + if err != nil { + return err + } + + for _, user := range legacyUsers { + user.PortainerAuthorizations = map[portainer.Authorization]bool{ + portainer.OperationPortainerDockerHubInspect: true, + portainer.OperationPortainerEndpointGroupList: true, + portainer.OperationPortainerEndpointList: true, + portainer.OperationPortainerEndpointInspect: true, + portainer.OperationPortainerEndpointExtensionAdd: true, + portainer.OperationPortainerEndpointExtensionRemove: true, + portainer.OperationPortainerExtensionList: true, + portainer.OperationPortainerMOTD: true, + portainer.OperationPortainerRegistryList: true, + portainer.OperationPortainerRegistryInspect: true, + portainer.OperationPortainerTeamList: true, + portainer.OperationPortainerTemplateList: true, + portainer.OperationPortainerTemplateInspect: true, + portainer.OperationPortainerUserList: true, + portainer.OperationPortainerUserMemberships: true, + } + + err = m.userService.UpdateUser(user.ID, &user) + if err != nil { + return err + } + } + + return nil +} + +func (m *Migrator) updateEndpointsToDBVersion18() error { + legacyEndpoints, err := m.endpointService.Endpoints() + if err != nil { + return err + } + + for _, endpoint := range legacyEndpoints { + endpoint.UserAccessPolicies = make(portainer.UserAccessPolicies) + for _, userID := range endpoint.AuthorizedUsers { + endpoint.UserAccessPolicies[userID] = portainer.AccessPolicy{ + RoleID: 4, + } + } + + endpoint.TeamAccessPolicies = make(portainer.TeamAccessPolicies) + for _, teamID := range endpoint.AuthorizedTeams { + endpoint.TeamAccessPolicies[teamID] = portainer.AccessPolicy{ + RoleID: 4, + } + } + + err = m.endpointService.UpdateEndpoint(endpoint.ID, &endpoint) + if err != nil { + return err + } + } + + return nil +} + +func (m *Migrator) updateEndpointGroupsToDBVersion18() error { + legacyEndpointGroups, err := m.endpointGroupService.EndpointGroups() + if err != nil { + return err + } + + for _, endpointGroup := range legacyEndpointGroups { + endpointGroup.UserAccessPolicies = make(portainer.UserAccessPolicies) + for _, userID := range endpointGroup.AuthorizedUsers { + endpointGroup.UserAccessPolicies[userID] = portainer.AccessPolicy{ + RoleID: 4, + } + } + + endpointGroup.TeamAccessPolicies = make(portainer.TeamAccessPolicies) + for _, teamID := range endpointGroup.AuthorizedTeams { + endpointGroup.TeamAccessPolicies[teamID] = portainer.AccessPolicy{ + RoleID: 4, + } + } + + err = m.endpointGroupService.UpdateEndpointGroup(endpointGroup.ID, &endpointGroup) + if err != nil { + return err + } + } + + return nil +} + +func (m *Migrator) updateRegistriesToDBVersion18() error { + legacyRegistries, err := m.registryService.Registries() + if err != nil { + return err + } + + for _, registry := range legacyRegistries { + registry.UserAccessPolicies = make(portainer.UserAccessPolicies) + for _, userID := range registry.AuthorizedUsers { + registry.UserAccessPolicies[userID] = portainer.AccessPolicy{} + } + + registry.TeamAccessPolicies = make(portainer.TeamAccessPolicies) + for _, teamID := range registry.AuthorizedTeams { + registry.TeamAccessPolicies[teamID] = portainer.AccessPolicy{} + } + + err = m.registryService.UpdateRegistry(registry.ID, ®istry) + if err != nil { + return err + } + } + + return nil +} diff --git a/api/bolt/migrator/migrate_dbversion2.go b/api/bolt/migrator/migrate_dbversion2.go index 9488f50f9..07eaf1d24 100644 --- a/api/bolt/migrator/migrate_dbversion2.go +++ b/api/bolt/migrator/migrate_dbversion2.go @@ -1,6 +1,6 @@ package migrator -import "github.com/portainer/portainer" +import "github.com/portainer/portainer/api" func (m *Migrator) updateSettingsToDBVersion3() error { legacySettings, err := m.settingsService.Settings() diff --git a/api/bolt/migrator/migrate_dbversion3.go b/api/bolt/migrator/migrate_dbversion3.go index 75636dc97..cfe9d5715 100644 --- a/api/bolt/migrator/migrate_dbversion3.go +++ b/api/bolt/migrator/migrate_dbversion3.go @@ -1,6 +1,6 @@ package migrator -import "github.com/portainer/portainer" +import "github.com/portainer/portainer/api" func (m *Migrator) updateEndpointsToDBVersion4() error { legacyEndpoints, err := m.endpointService.Endpoints() diff --git a/api/bolt/migrator/migrate_dbversion7.go b/api/bolt/migrator/migrate_dbversion7.go index b248e9e42..b0672a609 100644 --- a/api/bolt/migrator/migrate_dbversion7.go +++ b/api/bolt/migrator/migrate_dbversion7.go @@ -1,6 +1,6 @@ package migrator -import "github.com/portainer/portainer" +import "github.com/portainer/portainer/api" func (m *Migrator) updateEndpointsToVersion8() error { legacyEndpoints, err := m.endpointService.Endpoints() diff --git a/api/bolt/migrator/migrate_dbversion8.go b/api/bolt/migrator/migrate_dbversion8.go index 99a73bf11..a9fff2da4 100644 --- a/api/bolt/migrator/migrate_dbversion8.go +++ b/api/bolt/migrator/migrate_dbversion8.go @@ -1,6 +1,6 @@ package migrator -import "github.com/portainer/portainer" +import "github.com/portainer/portainer/api" func (m *Migrator) updateEndpointsToVersion9() error { legacyEndpoints, err := m.endpointService.Endpoints() diff --git a/api/bolt/migrator/migrate_dbversion9.go b/api/bolt/migrator/migrate_dbversion9.go index f4a52d398..1b3fbadfe 100644 --- a/api/bolt/migrator/migrate_dbversion9.go +++ b/api/bolt/migrator/migrate_dbversion9.go @@ -1,6 +1,6 @@ package migrator -import "github.com/portainer/portainer" +import "github.com/portainer/portainer/api" func (m *Migrator) updateEndpointsToVersion10() error { legacyEndpoints, err := m.endpointService.Endpoints() diff --git a/api/bolt/migrator/migrator.go b/api/bolt/migrator/migrator.go index ccee735ff..570e8be5e 100644 --- a/api/bolt/migrator/migrator.go +++ b/api/bolt/migrator/migrator.go @@ -2,16 +2,17 @@ package migrator import ( "github.com/boltdb/bolt" - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/endpoint" - "github.com/portainer/portainer/bolt/endpointgroup" - "github.com/portainer/portainer/bolt/extension" - "github.com/portainer/portainer/bolt/resourcecontrol" - "github.com/portainer/portainer/bolt/settings" - "github.com/portainer/portainer/bolt/stack" - "github.com/portainer/portainer/bolt/template" - "github.com/portainer/portainer/bolt/user" - "github.com/portainer/portainer/bolt/version" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/endpoint" + "github.com/portainer/portainer/api/bolt/endpointgroup" + "github.com/portainer/portainer/api/bolt/extension" + "github.com/portainer/portainer/api/bolt/registry" + "github.com/portainer/portainer/api/bolt/resourcecontrol" + "github.com/portainer/portainer/api/bolt/settings" + "github.com/portainer/portainer/api/bolt/stack" + "github.com/portainer/portainer/api/bolt/template" + "github.com/portainer/portainer/api/bolt/user" + "github.com/portainer/portainer/api/bolt/version" ) type ( @@ -22,6 +23,7 @@ type ( endpointGroupService *endpointgroup.Service endpointService *endpoint.Service extensionService *extension.Service + registryService *registry.Service resourceControlService *resourcecontrol.Service settingsService *settings.Service stackService *stack.Service @@ -38,6 +40,7 @@ type ( EndpointGroupService *endpointgroup.Service EndpointService *endpoint.Service ExtensionService *extension.Service + RegistryService *registry.Service ResourceControlService *resourcecontrol.Service SettingsService *settings.Service StackService *stack.Service @@ -56,6 +59,7 @@ func NewMigrator(parameters *Parameters) *Migrator { endpointGroupService: parameters.EndpointGroupService, endpointService: parameters.EndpointService, extensionService: parameters.ExtensionService, + registryService: parameters.RegistryService, resourceControlService: parameters.ResourceControlService, settingsService: parameters.SettingsService, templateService: parameters.TemplateService, @@ -222,5 +226,28 @@ func (m *Migrator) Migrate() error { } } + // Portainer 1.21.0 + if m.currentDBVersion < 18 { + err := m.updateUsersToDBVersion18() + if err != nil { + return err + } + + err = m.updateEndpointsToDBVersion18() + if err != nil { + return err + } + + err = m.updateEndpointGroupsToDBVersion18() + if err != nil { + return err + } + + err = m.updateRegistriesToDBVersion18() + if err != nil { + return err + } + } + return m.versionService.StoreDBVersion(portainer.DBVersion) } diff --git a/api/bolt/registry/registry.go b/api/bolt/registry/registry.go index 2fbfbeb90..428c6957e 100644 --- a/api/bolt/registry/registry.go +++ b/api/bolt/registry/registry.go @@ -1,8 +1,8 @@ package registry import ( - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/internal" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" "github.com/boltdb/bolt" ) diff --git a/api/bolt/resourcecontrol/resourcecontrol.go b/api/bolt/resourcecontrol/resourcecontrol.go index 222bafd79..d4fdb3026 100644 --- a/api/bolt/resourcecontrol/resourcecontrol.go +++ b/api/bolt/resourcecontrol/resourcecontrol.go @@ -1,8 +1,8 @@ package resourcecontrol import ( - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/internal" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" "github.com/boltdb/bolt" ) diff --git a/api/bolt/role/role.go b/api/bolt/role/role.go new file mode 100644 index 000000000..8a4e3e975 --- /dev/null +++ b/api/bolt/role/role.go @@ -0,0 +1,83 @@ +package role + +import ( + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" + + "github.com/boltdb/bolt" +) + +const ( + // BucketName represents the name of the bucket where this service stores data. + BucketName = "roles" +) + +// Service represents a service for managing endpoint data. +type Service struct { + db *bolt.DB +} + +// NewService creates a new instance of a service. +func NewService(db *bolt.DB) (*Service, error) { + err := internal.CreateBucket(db, BucketName) + if err != nil { + return nil, err + } + + return &Service{ + db: db, + }, nil +} + +// Role returns a Role by ID +func (service *Service) Role(ID portainer.RoleID) (*portainer.Role, error) { + var set portainer.Role + identifier := internal.Itob(int(ID)) + + err := internal.GetObject(service.db, BucketName, identifier, &set) + if err != nil { + return nil, err + } + + return &set, nil +} + +// Roles return an array containing all the sets. +func (service *Service) Roles() ([]portainer.Role, error) { + var sets = make([]portainer.Role, 0) + + err := service.db.View(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(BucketName)) + + cursor := bucket.Cursor() + for k, v := cursor.First(); k != nil; k, v = cursor.Next() { + var set portainer.Role + err := internal.UnmarshalObject(v, &set) + if err != nil { + return err + } + sets = append(sets, set) + } + + return nil + }) + + return sets, err +} + +// CreateRole creates a new Role. +func (service *Service) CreateRole(set *portainer.Role) error { + return service.db.Update(func(tx *bolt.Tx) error { + bucket := tx.Bucket([]byte(BucketName)) + + id, _ := bucket.NextSequence() + set.ID = portainer.RoleID(id) + + data, err := internal.MarshalObject(set) + if err != nil { + return err + } + + return bucket.Put(internal.Itob(int(set.ID)), data) + }) +} diff --git a/api/bolt/schedule/schedule.go b/api/bolt/schedule/schedule.go index db824768f..25b996373 100644 --- a/api/bolt/schedule/schedule.go +++ b/api/bolt/schedule/schedule.go @@ -1,8 +1,8 @@ package schedule import ( - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/internal" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" "github.com/boltdb/bolt" ) diff --git a/api/bolt/settings/settings.go b/api/bolt/settings/settings.go index 6e1d4bc82..c14032b25 100644 --- a/api/bolt/settings/settings.go +++ b/api/bolt/settings/settings.go @@ -1,8 +1,8 @@ package settings import ( - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/internal" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" "github.com/boltdb/bolt" ) diff --git a/api/bolt/stack/stack.go b/api/bolt/stack/stack.go index 1bd8e159b..54d5facd1 100644 --- a/api/bolt/stack/stack.go +++ b/api/bolt/stack/stack.go @@ -1,8 +1,8 @@ package stack import ( - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/internal" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" "github.com/boltdb/bolt" ) diff --git a/api/bolt/tag/tag.go b/api/bolt/tag/tag.go index 796b463dd..d54ee6b76 100644 --- a/api/bolt/tag/tag.go +++ b/api/bolt/tag/tag.go @@ -1,8 +1,8 @@ package tag import ( - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/internal" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" "github.com/boltdb/bolt" ) diff --git a/api/bolt/team/team.go b/api/bolt/team/team.go index 189a16a8a..d77aed90e 100644 --- a/api/bolt/team/team.go +++ b/api/bolt/team/team.go @@ -1,8 +1,8 @@ package team import ( - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/internal" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" "github.com/boltdb/bolt" ) diff --git a/api/bolt/teammembership/teammembership.go b/api/bolt/teammembership/teammembership.go index 2f09e0ad7..af60db0d2 100644 --- a/api/bolt/teammembership/teammembership.go +++ b/api/bolt/teammembership/teammembership.go @@ -1,8 +1,8 @@ package teammembership import ( - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/internal" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" "github.com/boltdb/bolt" ) diff --git a/api/bolt/template/template.go b/api/bolt/template/template.go index d60f64c0e..e5f7a4cf5 100644 --- a/api/bolt/template/template.go +++ b/api/bolt/template/template.go @@ -1,8 +1,8 @@ package template import ( - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/internal" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" "github.com/boltdb/bolt" ) diff --git a/api/bolt/user/user.go b/api/bolt/user/user.go index 03a3a8d88..63e3dfdc9 100644 --- a/api/bolt/user/user.go +++ b/api/bolt/user/user.go @@ -1,8 +1,8 @@ package user import ( - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/internal" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" "github.com/boltdb/bolt" ) diff --git a/api/bolt/version/version.go b/api/bolt/version/version.go index 52a7b4be4..18e9ab6d5 100644 --- a/api/bolt/version/version.go +++ b/api/bolt/version/version.go @@ -4,8 +4,8 @@ import ( "strconv" "github.com/boltdb/bolt" - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/internal" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" ) const ( diff --git a/api/bolt/webhook/webhook.go b/api/bolt/webhook/webhook.go index 94ebe61c5..38377cdb4 100644 --- a/api/bolt/webhook/webhook.go +++ b/api/bolt/webhook/webhook.go @@ -1,8 +1,8 @@ package webhook import ( - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt/internal" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt/internal" "github.com/boltdb/bolt" ) diff --git a/api/cli/cli.go b/api/cli/cli.go index 7c6febac2..6eef1a432 100644 --- a/api/cli/cli.go +++ b/api/cli/cli.go @@ -3,7 +3,7 @@ package cli import ( "time" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" "os" "path/filepath" diff --git a/api/cli/pairlist.go b/api/cli/pairlist.go index 7c1d4ea58..1eb6d10df 100644 --- a/api/cli/pairlist.go +++ b/api/cli/pairlist.go @@ -1,7 +1,7 @@ package cli import ( - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" "fmt" "gopkg.in/alecthomas/kingpin.v2" diff --git a/api/cmd/portainer/main.go b/api/cmd/portainer/main.go index f759285da..8b34fc070 100644 --- a/api/cmd/portainer/main.go +++ b/api/cmd/portainer/main.go @@ -1,4 +1,4 @@ -package main // import "github.com/portainer/portainer" +package main import ( "encoding/json" @@ -6,20 +6,20 @@ import ( "strings" "time" - "github.com/portainer/portainer" - "github.com/portainer/portainer/bolt" - "github.com/portainer/portainer/cli" - "github.com/portainer/portainer/cron" - "github.com/portainer/portainer/crypto" - "github.com/portainer/portainer/docker" - "github.com/portainer/portainer/exec" - "github.com/portainer/portainer/filesystem" - "github.com/portainer/portainer/git" - "github.com/portainer/portainer/http" - "github.com/portainer/portainer/http/client" - "github.com/portainer/portainer/jwt" - "github.com/portainer/portainer/ldap" - "github.com/portainer/portainer/libcompose" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/bolt" + "github.com/portainer/portainer/api/cli" + "github.com/portainer/portainer/api/cron" + "github.com/portainer/portainer/api/crypto" + "github.com/portainer/portainer/api/docker" + "github.com/portainer/portainer/api/exec" + "github.com/portainer/portainer/api/filesystem" + "github.com/portainer/portainer/api/git" + "github.com/portainer/portainer/api/http" + "github.com/portainer/portainer/api/http/client" + "github.com/portainer/portainer/api/jwt" + "github.com/portainer/portainer/api/ldap" + "github.com/portainer/portainer/api/libcompose" "log" ) @@ -377,18 +377,18 @@ func createTLSSecuredEndpoint(flags *portainer.CLIFlags, endpointService portain endpointID := endpointService.GetNextIdentifier() endpoint := &portainer.Endpoint{ - ID: portainer.EndpointID(endpointID), - Name: "primary", - URL: *flags.EndpointURL, - GroupID: portainer.EndpointGroupID(1), - Type: portainer.DockerEnvironment, - TLSConfig: tlsConfiguration, - AuthorizedUsers: []portainer.UserID{}, - AuthorizedTeams: []portainer.TeamID{}, - Extensions: []portainer.EndpointExtension{}, - Tags: []string{}, - Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + ID: portainer.EndpointID(endpointID), + Name: "primary", + URL: *flags.EndpointURL, + GroupID: portainer.EndpointGroupID(1), + Type: portainer.DockerEnvironment, + TLSConfig: tlsConfiguration, + UserAccessPolicies: portainer.UserAccessPolicies{}, + TeamAccessPolicies: portainer.TeamAccessPolicies{}, + Extensions: []portainer.EndpointExtension{}, + Tags: []string{}, + Status: portainer.EndpointStatusUp, + Snapshots: []portainer.Snapshot{}, } if strings.HasPrefix(endpoint.URL, "tcp://") { @@ -420,18 +420,18 @@ func createUnsecuredEndpoint(endpointURL string, endpointService portainer.Endpo endpointID := endpointService.GetNextIdentifier() endpoint := &portainer.Endpoint{ - ID: portainer.EndpointID(endpointID), - Name: "primary", - URL: endpointURL, - GroupID: portainer.EndpointGroupID(1), - Type: portainer.DockerEnvironment, - TLSConfig: portainer.TLSConfiguration{}, - AuthorizedUsers: []portainer.UserID{}, - AuthorizedTeams: []portainer.TeamID{}, - Extensions: []portainer.EndpointExtension{}, - Tags: []string{}, - Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + ID: portainer.EndpointID(endpointID), + Name: "primary", + URL: endpointURL, + GroupID: portainer.EndpointGroupID(1), + Type: portainer.DockerEnvironment, + TLSConfig: portainer.TLSConfiguration{}, + UserAccessPolicies: portainer.UserAccessPolicies{}, + TeamAccessPolicies: portainer.TeamAccessPolicies{}, + Extensions: []portainer.EndpointExtension{}, + Tags: []string{}, + Status: portainer.EndpointStatusUp, + Snapshots: []portainer.Snapshot{}, } return snapshotAndPersistEndpoint(endpoint, endpointService, snapshotter) @@ -627,6 +627,23 @@ func main() { Username: "admin", Role: portainer.AdministratorRole, Password: adminPasswordHash, + PortainerAuthorizations: map[portainer.Authorization]bool{ + portainer.OperationPortainerDockerHubInspect: true, + portainer.OperationPortainerEndpointGroupList: true, + portainer.OperationPortainerEndpointList: true, + portainer.OperationPortainerEndpointInspect: true, + portainer.OperationPortainerEndpointExtensionAdd: true, + portainer.OperationPortainerEndpointExtensionRemove: true, + portainer.OperationPortainerExtensionList: true, + portainer.OperationPortainerMOTD: true, + portainer.OperationPortainerRegistryList: true, + portainer.OperationPortainerRegistryInspect: true, + portainer.OperationPortainerTeamList: true, + portainer.OperationPortainerTemplateList: true, + portainer.OperationPortainerTemplateInspect: true, + portainer.OperationPortainerUserList: true, + portainer.OperationPortainerUserMemberships: true, + }, } err := store.UserService.CreateUser(user) if err != nil { @@ -647,6 +664,7 @@ func main() { AssetsPath: *flags.Assets, AuthDisabled: *flags.NoAuth, EndpointManagement: endpointManagement, + RoleService: store.RoleService, UserService: store.UserService, TeamService: store.TeamService, TeamMembershipService: store.TeamMembershipService, diff --git a/api/cron/job_endpoint_sync.go b/api/cron/job_endpoint_sync.go index ef13a4970..361698649 100644 --- a/api/cron/job_endpoint_sync.go +++ b/api/cron/job_endpoint_sync.go @@ -6,7 +6,7 @@ import ( "log" "strings" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // EndpointSyncJobRunner is used to run a EndpointSyncJob diff --git a/api/cron/job_script_execution.go b/api/cron/job_script_execution.go index 2143a83f5..f9634813f 100644 --- a/api/cron/job_script_execution.go +++ b/api/cron/job_script_execution.go @@ -4,7 +4,7 @@ import ( "log" "time" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // ScriptExecutionJobRunner is used to run a ScriptExecutionJob diff --git a/api/cron/job_snapshot.go b/api/cron/job_snapshot.go index 513a324b9..c7828b164 100644 --- a/api/cron/job_snapshot.go +++ b/api/cron/job_snapshot.go @@ -3,7 +3,7 @@ package cron import ( "log" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // SnapshotJobRunner is used to run a SnapshotJob diff --git a/api/cron/scheduler.go b/api/cron/scheduler.go index 144e5ed2f..9a2de7450 100644 --- a/api/cron/scheduler.go +++ b/api/cron/scheduler.go @@ -1,7 +1,7 @@ package cron import ( - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" "github.com/robfig/cron" ) diff --git a/api/docker/client.go b/api/docker/client.go index 421f92c04..9dcdf33dc 100644 --- a/api/docker/client.go +++ b/api/docker/client.go @@ -6,8 +6,8 @@ import ( "time" "github.com/docker/docker/client" - "github.com/portainer/portainer" - "github.com/portainer/portainer/crypto" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/crypto" ) const ( diff --git a/api/docker/job.go b/api/docker/job.go index 7765f5f2e..ff6dae2c2 100644 --- a/api/docker/job.go +++ b/api/docker/job.go @@ -12,8 +12,8 @@ import ( "github.com/docker/docker/api/types/network" "github.com/docker/docker/api/types/strslice" "github.com/docker/docker/client" - "github.com/portainer/portainer" - "github.com/portainer/portainer/archive" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/archive" ) // JobService represents a service that handles the execution of jobs diff --git a/api/docker/snapshot.go b/api/docker/snapshot.go index 342465b1f..e03a76496 100644 --- a/api/docker/snapshot.go +++ b/api/docker/snapshot.go @@ -7,7 +7,7 @@ import ( "github.com/docker/docker/api/types" "github.com/docker/docker/api/types/filters" "github.com/docker/docker/client" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) func snapshot(cli *client.Client) (*portainer.Snapshot, error) { diff --git a/api/docker/snapshotter.go b/api/docker/snapshotter.go index ee095f6d5..24d121845 100644 --- a/api/docker/snapshotter.go +++ b/api/docker/snapshotter.go @@ -1,7 +1,7 @@ package docker import ( - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // Snapshotter represents a service used to create endpoint snapshots diff --git a/api/errors.go b/api/errors.go index ef11d5522..8e09838a1 100644 --- a/api/errors.go +++ b/api/errors.go @@ -4,6 +4,7 @@ package portainer const ( ErrUnauthorized = Error("Unauthorized") ErrResourceAccessDenied = Error("Access denied to resource") + ErrAuthorizationRequired = Error("Authorization required for this operation") ErrObjectNotFound = Error("Object not found inside the database") ErrMissingSecurityContext = Error("Unable to find security details in request context") ) diff --git a/api/exec/extension.go b/api/exec/extension.go index ab7880a1b..b0b015599 100644 --- a/api/exec/extension.go +++ b/api/exec/extension.go @@ -9,10 +9,11 @@ import ( "runtime" "strconv" "strings" + "time" "github.com/orcaman/concurrent-map" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/client" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/client" ) var extensionDownloadBaseURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com/extensions/" @@ -20,6 +21,7 @@ var extensionDownloadBaseURL = "https://portainer-io-assets.sfo2.digitaloceanspa var extensionBinaryMap = map[portainer.ExtensionID]string{ portainer.RegistryManagementExtension: "extension-registry-management", portainer.OAuthAuthenticationExtension: "extension-oauth-authentication", + portainer.RBACExtension: "extension-rbac", } // ExtensionManager represents a service used to @@ -206,6 +208,8 @@ func (manager *ExtensionManager) startExtensionProcess(extension *portainer.Exte return err } + time.Sleep(3 * time.Second) + manager.processes.Set(processKey(extension.ID), extensionProcess) return nil } diff --git a/api/exec/swarm_stack.go b/api/exec/swarm_stack.go index da781b030..3f01bbb26 100644 --- a/api/exec/swarm_stack.go +++ b/api/exec/swarm_stack.go @@ -8,7 +8,7 @@ import ( "path" "runtime" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // SwarmStackManager represents a service for managing stacks. diff --git a/api/filesystem/filesystem.go b/api/filesystem/filesystem.go index dcb397eac..cf4e06977 100644 --- a/api/filesystem/filesystem.go +++ b/api/filesystem/filesystem.go @@ -6,8 +6,8 @@ import ( "encoding/pem" "io/ioutil" - "github.com/portainer/portainer" - "github.com/portainer/portainer/archive" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/archive" "io" "os" diff --git a/api/http/client/client.go b/api/http/client/client.go index 8892b6472..11b812a86 100644 --- a/api/http/client/client.go +++ b/api/http/client/client.go @@ -10,7 +10,7 @@ import ( "strings" "time" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) const ( diff --git a/api/http/handler/auth/authenticate.go b/api/http/handler/auth/authenticate.go index d5562edd3..9acfd78e3 100644 --- a/api/http/handler/auth/authenticate.go +++ b/api/http/handler/auth/authenticate.go @@ -9,7 +9,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type authenticatePayload struct { @@ -100,6 +100,23 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use user := &portainer.User{ Username: username, Role: portainer.StandardUserRole, + PortainerAuthorizations: map[portainer.Authorization]bool{ + portainer.OperationPortainerDockerHubInspect: true, + portainer.OperationPortainerEndpointGroupList: true, + portainer.OperationPortainerEndpointList: true, + portainer.OperationPortainerEndpointInspect: true, + portainer.OperationPortainerEndpointExtensionAdd: true, + portainer.OperationPortainerEndpointExtensionRemove: true, + portainer.OperationPortainerExtensionList: true, + portainer.OperationPortainerMOTD: true, + portainer.OperationPortainerRegistryList: true, + portainer.OperationPortainerRegistryInspect: true, + portainer.OperationPortainerTeamList: true, + portainer.OperationPortainerTemplateList: true, + portainer.OperationPortainerTemplateInspect: true, + portainer.OperationPortainerUserList: true, + portainer.OperationPortainerUserMemberships: true, + }, } err = handler.UserService.CreateUser(user) @@ -117,11 +134,60 @@ func (handler *Handler) authenticateLDAPAndCreateUser(w http.ResponseWriter, use func (handler *Handler) writeToken(w http.ResponseWriter, user *portainer.User) *httperror.HandlerError { tokenData := &portainer.TokenData{ - ID: user.ID, - Username: user.Username, - Role: user.Role, + ID: user.ID, + Username: user.Username, + Role: user.Role, + PortainerAuthorizations: user.PortainerAuthorizations, } + _, err := handler.ExtensionService.Extension(portainer.RBACExtension) + if err == portainer.ErrObjectNotFound { + return handler.persistAndWriteToken(w, tokenData) + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err} + } + + endpointAuthorizations, err := handler.getAuthorizations(user) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve authorizations associated to the user", err} + } + tokenData.EndpointAuthorizations = endpointAuthorizations + + return handler.persistAndWriteToken(w, tokenData) +} + +func (handler *Handler) getAuthorizations(user *portainer.User) (portainer.EndpointAuthorizations, error) { + endpointAuthorizations := portainer.EndpointAuthorizations{} + if user.Role == portainer.AdministratorRole { + return endpointAuthorizations, nil + } + + userMemberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(user.ID) + if err != nil { + return endpointAuthorizations, err + } + + endpoints, err := handler.EndpointService.Endpoints() + if err != nil { + return endpointAuthorizations, err + } + + endpointGroups, err := handler.EndpointGroupService.EndpointGroups() + if err != nil { + return endpointAuthorizations, err + } + + roles, err := handler.RoleService.Roles() + if err != nil { + return endpointAuthorizations, err + } + + endpointAuthorizations = getUserEndpointAuthorizations(user, endpoints, endpointGroups, roles, userMemberships) + + return endpointAuthorizations, nil +} + +func (handler *Handler) persistAndWriteToken(w http.ResponseWriter, tokenData *portainer.TokenData) *httperror.HandlerError { token, err := handler.JWTService.GenerateToken(tokenData) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to generate JWT token", err} diff --git a/api/http/handler/auth/authenticate_oauth.go b/api/http/handler/auth/authenticate_oauth.go index d8559e999..b87a759e0 100644 --- a/api/http/handler/auth/authenticate_oauth.go +++ b/api/http/handler/auth/authenticate_oauth.go @@ -3,13 +3,13 @@ package auth import ( "encoding/json" "io/ioutil" - "net/http" "log" + "net/http" "github.com/asaskevich/govalidator" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type oauthPayload struct { @@ -113,6 +113,23 @@ func (handler *Handler) validateOAuth(w http.ResponseWriter, r *http.Request) *h user = &portainer.User{ Username: username, Role: portainer.StandardUserRole, + PortainerAuthorizations: map[portainer.Authorization]bool{ + portainer.OperationPortainerDockerHubInspect: true, + portainer.OperationPortainerEndpointGroupList: true, + portainer.OperationPortainerEndpointList: true, + portainer.OperationPortainerEndpointInspect: true, + portainer.OperationPortainerEndpointExtensionAdd: true, + portainer.OperationPortainerEndpointExtensionRemove: true, + portainer.OperationPortainerExtensionList: true, + portainer.OperationPortainerMOTD: true, + portainer.OperationPortainerRegistryList: true, + portainer.OperationPortainerRegistryInspect: true, + portainer.OperationPortainerTeamList: true, + portainer.OperationPortainerTemplateList: true, + portainer.OperationPortainerTemplateInspect: true, + portainer.OperationPortainerUserList: true, + portainer.OperationPortainerUserMemberships: true, + }, } err = handler.UserService.CreateUser(user) diff --git a/api/http/handler/auth/authorization.go b/api/http/handler/auth/authorization.go new file mode 100644 index 000000000..64f2e5cd8 --- /dev/null +++ b/api/http/handler/auth/authorization.go @@ -0,0 +1,122 @@ +package auth + +import portainer "github.com/portainer/portainer/api" + +func getUserEndpointAuthorizations(user *portainer.User, endpoints []portainer.Endpoint, endpointGroups []portainer.EndpointGroup, roles []portainer.Role, userMemberships []portainer.TeamMembership) portainer.EndpointAuthorizations { + endpointAuthorizations := make(portainer.EndpointAuthorizations) + + groupUserAccessPolicies := map[portainer.EndpointGroupID]portainer.UserAccessPolicies{} + groupTeamAccessPolicies := map[portainer.EndpointGroupID]portainer.TeamAccessPolicies{} + for _, endpointGroup := range endpointGroups { + groupUserAccessPolicies[endpointGroup.ID] = endpointGroup.UserAccessPolicies + groupTeamAccessPolicies[endpointGroup.ID] = endpointGroup.TeamAccessPolicies + } + + for _, endpoint := range endpoints { + authorizations := getAuthorizationsFromUserEndpointPolicy(user, &endpoint, roles) + if len(authorizations) > 0 { + endpointAuthorizations[endpoint.ID] = authorizations + continue + } + + authorizations = getAuthorizationsFromUserEndpointGroupPolicy(user, &endpoint, roles, groupUserAccessPolicies) + if len(authorizations) > 0 { + endpointAuthorizations[endpoint.ID] = authorizations + continue + } + + authorizations = getAuthorizationsFromTeamEndpointPolicies(userMemberships, &endpoint, roles) + if len(authorizations) > 0 { + endpointAuthorizations[endpoint.ID] = authorizations + continue + } + + endpointAuthorizations[endpoint.ID] = getAuthorizationsFromTeamEndpointGroupPolicies(userMemberships, &endpoint, roles, groupTeamAccessPolicies) + } + + return endpointAuthorizations +} + +func getAuthorizationsFromUserEndpointPolicy(user *portainer.User, endpoint *portainer.Endpoint, roles []portainer.Role) portainer.Authorizations { + policyRoles := make([]portainer.RoleID, 0) + + policy, ok := endpoint.UserAccessPolicies[user.ID] + if ok { + policyRoles = append(policyRoles, policy.RoleID) + } + + return getAuthorizationsFromRoles(policyRoles, roles) +} + +func getAuthorizationsFromUserEndpointGroupPolicy(user *portainer.User, endpoint *portainer.Endpoint, roles []portainer.Role, groupAccessPolicies map[portainer.EndpointGroupID]portainer.UserAccessPolicies) portainer.Authorizations { + policyRoles := make([]portainer.RoleID, 0) + + policy, ok := groupAccessPolicies[endpoint.GroupID][user.ID] + if ok { + policyRoles = append(policyRoles, policy.RoleID) + } + + return getAuthorizationsFromRoles(policyRoles, roles) +} + +func getAuthorizationsFromTeamEndpointPolicies(memberships []portainer.TeamMembership, endpoint *portainer.Endpoint, roles []portainer.Role) portainer.Authorizations { + policyRoles := make([]portainer.RoleID, 0) + + for _, membership := range memberships { + policy, ok := endpoint.TeamAccessPolicies[membership.TeamID] + if ok { + policyRoles = append(policyRoles, policy.RoleID) + } + } + + return getAuthorizationsFromRoles(policyRoles, roles) +} + +func getAuthorizationsFromTeamEndpointGroupPolicies(memberships []portainer.TeamMembership, endpoint *portainer.Endpoint, roles []portainer.Role, groupAccessPolicies map[portainer.EndpointGroupID]portainer.TeamAccessPolicies) portainer.Authorizations { + policyRoles := make([]portainer.RoleID, 0) + + for _, membership := range memberships { + policy, ok := groupAccessPolicies[endpoint.GroupID][membership.TeamID] + if ok { + policyRoles = append(policyRoles, policy.RoleID) + } + } + + return getAuthorizationsFromRoles(policyRoles, roles) +} + +func getAuthorizationsFromRoles(roleIdentifiers []portainer.RoleID, roles []portainer.Role) portainer.Authorizations { + var roleAuthorizations []portainer.Authorizations + for _, id := range roleIdentifiers { + for _, role := range roles { + if role.ID == id { + roleAuthorizations = append(roleAuthorizations, role.Authorizations) + break + } + } + } + + processedAuthorizations := make(portainer.Authorizations) + if len(roleAuthorizations) > 0 { + processedAuthorizations = roleAuthorizations[0] + for idx, authorizations := range roleAuthorizations { + if idx == 0 { + continue + } + processedAuthorizations = mergeAuthorizations(processedAuthorizations, authorizations) + } + } + + return processedAuthorizations +} + +func mergeAuthorizations(a, b portainer.Authorizations) portainer.Authorizations { + c := make(map[portainer.Authorization]bool) + + for k := range b { + if _, ok := a[k]; ok { + c[k] = true + } + } + return c +} diff --git a/api/http/handler/auth/handler.go b/api/http/handler/auth/handler.go index 75e343f23..8d10abf29 100644 --- a/api/http/handler/auth/handler.go +++ b/api/http/handler/auth/handler.go @@ -5,9 +5,9 @@ import ( "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/proxy" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy" + "github.com/portainer/portainer/api/http/security" ) const ( @@ -30,6 +30,9 @@ type Handler struct { TeamService portainer.TeamService TeamMembershipService portainer.TeamMembershipService ExtensionService portainer.ExtensionService + EndpointService portainer.EndpointService + EndpointGroupService portainer.EndpointGroupService + RoleService portainer.RoleService ProxyManager *proxy.Manager } diff --git a/api/http/handler/dockerhub/dockerhub_update.go b/api/http/handler/dockerhub/dockerhub_update.go index 69d82ee50..5606677fb 100644 --- a/api/http/handler/dockerhub/dockerhub_update.go +++ b/api/http/handler/dockerhub/dockerhub_update.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type dockerhubUpdatePayload struct { diff --git a/api/http/handler/dockerhub/handler.go b/api/http/handler/dockerhub/handler.go index 9ad93232d..7e2cb4bd0 100644 --- a/api/http/handler/dockerhub/handler.go +++ b/api/http/handler/dockerhub/handler.go @@ -5,8 +5,8 @@ import ( "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) func hideFields(dockerHub *portainer.DockerHub) { @@ -25,9 +25,9 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { Router: mux.NewRouter(), } h.Handle("/dockerhub", - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.dockerhubInspect))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.dockerhubInspect))).Methods(http.MethodGet) h.Handle("/dockerhub", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.dockerhubUpdate))).Methods(http.MethodPut) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.dockerhubUpdate))).Methods(http.MethodPut) return h } diff --git a/api/http/handler/endpointgroups/endpointgroup_create.go b/api/http/handler/endpointgroups/endpointgroup_create.go index cc7a23054..6cfb72468 100644 --- a/api/http/handler/endpointgroups/endpointgroup_create.go +++ b/api/http/handler/endpointgroups/endpointgroup_create.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type endpointGroupCreatePayload struct { @@ -36,11 +36,11 @@ func (handler *Handler) endpointGroupCreate(w http.ResponseWriter, r *http.Reque } endpointGroup := &portainer.EndpointGroup{ - Name: payload.Name, - Description: payload.Description, - AuthorizedUsers: []portainer.UserID{}, - AuthorizedTeams: []portainer.TeamID{}, - Tags: payload.Tags, + Name: payload.Name, + Description: payload.Description, + UserAccessPolicies: portainer.UserAccessPolicies{}, + TeamAccessPolicies: portainer.TeamAccessPolicies{}, + Tags: payload.Tags, } err = handler.EndpointGroupService.CreateEndpointGroup(endpointGroup) diff --git a/api/http/handler/endpointgroups/endpointgroup_delete.go b/api/http/handler/endpointgroups/endpointgroup_delete.go index b436b4f2f..a7a3c0a79 100644 --- a/api/http/handler/endpointgroups/endpointgroup_delete.go +++ b/api/http/handler/endpointgroups/endpointgroup_delete.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // DELETE request on /api/endpoint_groups/:id diff --git a/api/http/handler/endpointgroups/endpointgroup_inspect.go b/api/http/handler/endpointgroups/endpointgroup_inspect.go index 46a6895e8..3ddc464e7 100644 --- a/api/http/handler/endpointgroups/endpointgroup_inspect.go +++ b/api/http/handler/endpointgroups/endpointgroup_inspect.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // GET request on /api/endpoint_groups/:id diff --git a/api/http/handler/endpointgroups/endpointgroup_list.go b/api/http/handler/endpointgroups/endpointgroup_list.go index 7acb696a8..7ea73d2d5 100644 --- a/api/http/handler/endpointgroups/endpointgroup_list.go +++ b/api/http/handler/endpointgroups/endpointgroup_list.go @@ -5,7 +5,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api/http/security" ) // GET request on /api/endpoint_groups diff --git a/api/http/handler/endpointgroups/endpointgroup_update.go b/api/http/handler/endpointgroups/endpointgroup_update.go index ec24b801f..1aa523403 100644 --- a/api/http/handler/endpointgroups/endpointgroup_update.go +++ b/api/http/handler/endpointgroups/endpointgroup_update.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type endpointGroupUpdatePayload struct { @@ -14,6 +14,8 @@ type endpointGroupUpdatePayload struct { Description string AssociatedEndpoints []portainer.EndpointID Tags []string + UserAccessPolicies portainer.UserAccessPolicies + TeamAccessPolicies portainer.TeamAccessPolicies } func (payload *endpointGroupUpdatePayload) Validate(r *http.Request) error { @@ -52,20 +54,30 @@ func (handler *Handler) endpointGroupUpdate(w http.ResponseWriter, r *http.Reque endpointGroup.Tags = payload.Tags } + if payload.UserAccessPolicies != nil { + endpointGroup.UserAccessPolicies = payload.UserAccessPolicies + } + + if payload.TeamAccessPolicies != nil { + endpointGroup.TeamAccessPolicies = payload.TeamAccessPolicies + } + err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err} } - endpoints, err := handler.EndpointService.Endpoints() - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} - } - - for _, endpoint := range endpoints { - err = handler.updateEndpointGroup(endpoint, portainer.EndpointGroupID(endpointGroupID), payload.AssociatedEndpoints) + if payload.AssociatedEndpoints != nil { + endpoints, err := handler.EndpointService.Endpoints() if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err} + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve endpoints from the database", err} + } + + for _, endpoint := range endpoints { + err = handler.updateEndpointGroup(endpoint, portainer.EndpointGroupID(endpointGroupID), payload.AssociatedEndpoints) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to update endpoint", err} + } } } diff --git a/api/http/handler/endpointgroups/endpointgroup_update_access.go b/api/http/handler/endpointgroups/endpointgroup_update_access.go deleted file mode 100644 index 6e859a4df..000000000 --- a/api/http/handler/endpointgroups/endpointgroup_update_access.go +++ /dev/null @@ -1,63 +0,0 @@ -package endpointgroups - -import ( - "net/http" - - httperror "github.com/portainer/libhttp/error" - "github.com/portainer/libhttp/request" - "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" -) - -type endpointGroupUpdateAccessPayload struct { - AuthorizedUsers []int - AuthorizedTeams []int -} - -func (payload *endpointGroupUpdateAccessPayload) Validate(r *http.Request) error { - return nil -} - -// PUT request on /api/endpoint_groups/:id/access -func (handler *Handler) endpointGroupUpdateAccess(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - endpointGroupID, err := request.RetrieveNumericRouteVariableValue(r, "id") - if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint group identifier route variable", err} - } - - var payload endpointGroupUpdateAccessPayload - err = request.DecodeAndValidateJSONPayload(r, &payload) - if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} - } - - endpointGroup, err := handler.EndpointGroupService.EndpointGroup(portainer.EndpointGroupID(endpointGroupID)) - if err == portainer.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint group with the specified identifier inside the database", err} - } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint group with the specified identifier inside the database", err} - } - - if payload.AuthorizedUsers != nil { - authorizedUserIDs := []portainer.UserID{} - for _, value := range payload.AuthorizedUsers { - authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value)) - } - endpointGroup.AuthorizedUsers = authorizedUserIDs - } - - if payload.AuthorizedTeams != nil { - authorizedTeamIDs := []portainer.TeamID{} - for _, value := range payload.AuthorizedTeams { - authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value)) - } - endpointGroup.AuthorizedTeams = authorizedTeamIDs - } - - err = handler.EndpointGroupService.UpdateEndpointGroup(endpointGroup.ID, endpointGroup) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint group changes inside the database", err} - } - - return response.JSON(w, endpointGroup) -} diff --git a/api/http/handler/endpointgroups/handler.go b/api/http/handler/endpointgroups/handler.go index 3b731e911..c210373e9 100644 --- a/api/http/handler/endpointgroups/handler.go +++ b/api/http/handler/endpointgroups/handler.go @@ -5,8 +5,8 @@ import ( "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) // Handler is the HTTP handler used to handle endpoint group operations. @@ -22,17 +22,15 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { Router: mux.NewRouter(), } h.Handle("/endpoint_groups", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupCreate))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupCreate))).Methods(http.MethodPost) h.Handle("/endpoint_groups", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointGroupList))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupList))).Methods(http.MethodGet) h.Handle("/endpoint_groups/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupInspect))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupInspect))).Methods(http.MethodGet) h.Handle("/endpoint_groups/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut) - h.Handle("/endpoint_groups/{id}/access", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupUpdateAccess))).Methods(http.MethodPut) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupUpdate))).Methods(http.MethodPut) h.Handle("/endpoint_groups/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointGroupDelete))).Methods(http.MethodDelete) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointGroupDelete))).Methods(http.MethodDelete) return h } diff --git a/api/http/handler/endpointproxy/handler.go b/api/http/handler/endpointproxy/handler.go index eaef36b1b..15a1101ba 100644 --- a/api/http/handler/endpointproxy/handler.go +++ b/api/http/handler/endpointproxy/handler.go @@ -3,9 +3,9 @@ package endpointproxy import ( "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/proxy" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy" + "github.com/portainer/portainer/api/http/security" ) // Handler is the HTTP handler used to proxy requests to external APIs. @@ -23,10 +23,10 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { requestBouncer: bouncer, } h.PathPrefix("/{id}/azure").Handler( - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI))) + bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToAzureAPI))) h.PathPrefix("/{id}/docker").Handler( - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI))) - h.PathPrefix("/{id}/extensions/storidge").Handler( - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI))) + bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToDockerAPI))) + h.PathPrefix("/{id}/storidge").Handler( + bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToStoridgeAPI))) return h } diff --git a/api/http/handler/endpointproxy/proxy_azure.go b/api/http/handler/endpointproxy/proxy_azure.go index dcbe96f4b..a756a4fb7 100644 --- a/api/http/handler/endpointproxy/proxy_azure.go +++ b/api/http/handler/endpointproxy/proxy_azure.go @@ -5,7 +5,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" "net/http" ) @@ -23,9 +23,9 @@ func (handler *Handler) proxyRequestsToAzureAPI(w http.ResponseWriter, r *http.R return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} } - err = handler.requestBouncer.EndpointAccess(r, endpoint) + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } var proxy http.Handler diff --git a/api/http/handler/endpointproxy/proxy_docker.go b/api/http/handler/endpointproxy/proxy_docker.go index 9ca932a97..b1b911452 100644 --- a/api/http/handler/endpointproxy/proxy_docker.go +++ b/api/http/handler/endpointproxy/proxy_docker.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" "net/http" ) @@ -28,9 +28,9 @@ func (handler *Handler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http. return &httperror.HandlerError{http.StatusServiceUnavailable, "Unable to query endpoint", errors.New("Endpoint is down")} } - err = handler.requestBouncer.EndpointAccess(r, endpoint) + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } var proxy http.Handler diff --git a/api/http/handler/endpointproxy/proxy_storidge.go b/api/http/handler/endpointproxy/proxy_storidge.go index 697d74ff5..f2d2aacb1 100644 --- a/api/http/handler/endpointproxy/proxy_storidge.go +++ b/api/http/handler/endpointproxy/proxy_storidge.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" "net/http" ) @@ -25,9 +25,9 @@ func (handler *Handler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *htt return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} } - err = handler.requestBouncer.EndpointAccess(r, endpoint) + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } var storidgeExtension *portainer.EndpointExtension @@ -41,7 +41,7 @@ func (handler *Handler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *htt return &httperror.HandlerError{http.StatusServiceUnavailable, "Storidge extension not supported on this endpoint", portainer.ErrEndpointExtensionNotSupported} } - proxyExtensionKey := string(endpoint.ID) + "_" + string(portainer.StoridgeEndpointExtension) + proxyExtensionKey := strconv.Itoa(endpointID) + "_" + strconv.Itoa(int(portainer.StoridgeEndpointExtension)) + "_" + storidgeExtension.URL var proxy http.Handler proxy = handler.ProxyManager.GetLegacyExtensionProxy(proxyExtensionKey) @@ -53,6 +53,6 @@ func (handler *Handler) proxyRequestsToStoridgeAPI(w http.ResponseWriter, r *htt } id := strconv.Itoa(endpointID) - http.StripPrefix("/"+id+"/extensions/storidge", proxy).ServeHTTP(w, r) + http.StripPrefix("/"+id+"/storidge", proxy).ServeHTTP(w, r) return nil } diff --git a/api/http/handler/endpoints/endpoint_create.go b/api/http/handler/endpoints/endpoint_create.go index a3eeeb114..40348899f 100644 --- a/api/http/handler/endpoints/endpoint_create.go +++ b/api/http/handler/endpoints/endpoint_create.go @@ -9,9 +9,9 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/crypto" - "github.com/portainer/portainer/http/client" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/crypto" + "github.com/portainer/portainer/api/http/client" ) type endpointCreatePayload struct { @@ -172,19 +172,19 @@ func (handler *Handler) createAzureEndpoint(payload *endpointCreatePayload) (*po endpointID := handler.EndpointService.GetNextIdentifier() endpoint := &portainer.Endpoint{ - ID: portainer.EndpointID(endpointID), - Name: payload.Name, - URL: "https://management.azure.com", - Type: portainer.AzureEnvironment, - GroupID: portainer.EndpointGroupID(payload.GroupID), - PublicURL: payload.PublicURL, - AuthorizedUsers: []portainer.UserID{}, - AuthorizedTeams: []portainer.TeamID{}, - Extensions: []portainer.EndpointExtension{}, - AzureCredentials: credentials, - Tags: payload.Tags, - Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + ID: portainer.EndpointID(endpointID), + Name: payload.Name, + URL: "https://management.azure.com", + Type: portainer.AzureEnvironment, + GroupID: portainer.EndpointGroupID(payload.GroupID), + PublicURL: payload.PublicURL, + UserAccessPolicies: portainer.UserAccessPolicies{}, + TeamAccessPolicies: portainer.TeamAccessPolicies{}, + Extensions: []portainer.EndpointExtension{}, + AzureCredentials: credentials, + Tags: payload.Tags, + Status: portainer.EndpointStatusUp, + Snapshots: []portainer.Snapshot{}, } err = handler.EndpointService.CreateEndpoint(endpoint) @@ -224,12 +224,12 @@ func (handler *Handler) createUnsecuredEndpoint(payload *endpointCreatePayload) TLSConfig: portainer.TLSConfiguration{ TLS: false, }, - AuthorizedUsers: []portainer.UserID{}, - AuthorizedTeams: []portainer.TeamID{}, - Extensions: []portainer.EndpointExtension{}, - Tags: payload.Tags, - Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + UserAccessPolicies: portainer.UserAccessPolicies{}, + TeamAccessPolicies: portainer.TeamAccessPolicies{}, + Extensions: []portainer.EndpointExtension{}, + Tags: payload.Tags, + Status: portainer.EndpointStatusUp, + Snapshots: []portainer.Snapshot{}, } err := handler.snapshotAndPersistEndpoint(endpoint) @@ -268,12 +268,12 @@ func (handler *Handler) createTLSSecuredEndpoint(payload *endpointCreatePayload) TLS: payload.TLS, TLSSkipVerify: payload.TLSSkipVerify, }, - AuthorizedUsers: []portainer.UserID{}, - AuthorizedTeams: []portainer.TeamID{}, - Extensions: []portainer.EndpointExtension{}, - Tags: payload.Tags, - Status: portainer.EndpointStatusUp, - Snapshots: []portainer.Snapshot{}, + UserAccessPolicies: portainer.UserAccessPolicies{}, + TeamAccessPolicies: portainer.TeamAccessPolicies{}, + Extensions: []portainer.EndpointExtension{}, + Tags: payload.Tags, + Status: portainer.EndpointStatusUp, + Snapshots: []portainer.Snapshot{}, } filesystemError := handler.storeTLSFiles(endpoint, payload) diff --git a/api/http/handler/endpoints/endpoint_delete.go b/api/http/handler/endpoints/endpoint_delete.go index 3b34db01e..0dd25963c 100644 --- a/api/http/handler/endpoints/endpoint_delete.go +++ b/api/http/handler/endpoints/endpoint_delete.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // DELETE request on /api/endpoints/:id diff --git a/api/http/handler/endpoints/endpoint_extension_add.go b/api/http/handler/endpoints/endpoint_extension_add.go index 7c2f69e3a..f91a714c3 100644 --- a/api/http/handler/endpoints/endpoint_extension_add.go +++ b/api/http/handler/endpoints/endpoint_extension_add.go @@ -9,7 +9,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type endpointExtensionAddPayload struct { @@ -50,9 +50,9 @@ func (handler *Handler) endpointExtensionAdd(w http.ResponseWriter, r *http.Requ extensionType := portainer.EndpointExtensionType(payload.Type) var extension *portainer.EndpointExtension - for _, ext := range endpoint.Extensions { - if ext.Type == extensionType { - extension = &ext + for idx := range endpoint.Extensions { + if endpoint.Extensions[idx].Type == extensionType { + extension = &endpoint.Extensions[idx] } } diff --git a/api/http/handler/endpoints/endpoint_extension_remove.go b/api/http/handler/endpoints/endpoint_extension_remove.go index 3f68955cc..b426071e0 100644 --- a/api/http/handler/endpoints/endpoint_extension_remove.go +++ b/api/http/handler/endpoints/endpoint_extension_remove.go @@ -8,7 +8,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // DELETE request on /api/endpoints/:id/extensions/:extensionType diff --git a/api/http/handler/endpoints/endpoint_inspect.go b/api/http/handler/endpoints/endpoint_inspect.go index dd0d3485b..10cf34ed9 100644 --- a/api/http/handler/endpoints/endpoint_inspect.go +++ b/api/http/handler/endpoints/endpoint_inspect.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // GET request on /api/endpoints/:id @@ -23,9 +23,9 @@ func (handler *Handler) endpointInspect(w http.ResponseWriter, r *http.Request) return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} } - err = handler.requestBouncer.EndpointAccess(r, endpoint) + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, false) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } hideFields(endpoint) diff --git a/api/http/handler/endpoints/endpoint_job.go b/api/http/handler/endpoints/endpoint_job.go index 565418f42..78d00bc9c 100644 --- a/api/http/handler/endpoints/endpoint_job.go +++ b/api/http/handler/endpoints/endpoint_job.go @@ -8,7 +8,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type endpointJobFromFilePayload struct { @@ -70,11 +70,6 @@ func (handler *Handler) endpointJob(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} } - err = handler.requestBouncer.EndpointAccess(r, endpoint) - if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied} - } - switch method { case "file": return handler.executeJobFromFile(w, r, endpoint, nodeName) diff --git a/api/http/handler/endpoints/endpoint_list.go b/api/http/handler/endpoints/endpoint_list.go index 5c1436ac9..d7e1ba173 100644 --- a/api/http/handler/endpoints/endpoint_list.go +++ b/api/http/handler/endpoints/endpoint_list.go @@ -5,7 +5,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api/http/security" ) // GET request on /api/endpoints diff --git a/api/http/handler/endpoints/endpoint_snapshot.go b/api/http/handler/endpoints/endpoint_snapshot.go index 559cf127c..de3eba46c 100644 --- a/api/http/handler/endpoints/endpoint_snapshot.go +++ b/api/http/handler/endpoints/endpoint_snapshot.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // POST request on /api/endpoints/:id/snapshot diff --git a/api/http/handler/endpoints/endpoint_snapshots.go b/api/http/handler/endpoints/endpoint_snapshots.go index f59b7a40d..e25be6f89 100644 --- a/api/http/handler/endpoints/endpoint_snapshots.go +++ b/api/http/handler/endpoints/endpoint_snapshots.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // POST request on /api/endpoints/snapshot diff --git a/api/http/handler/endpoints/endpoint_update.go b/api/http/handler/endpoints/endpoint_update.go index 405f6ce38..f62ee638b 100644 --- a/api/http/handler/endpoints/endpoint_update.go +++ b/api/http/handler/endpoints/endpoint_update.go @@ -7,8 +7,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/client" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/client" ) type endpointUpdatePayload struct { @@ -24,6 +24,8 @@ type endpointUpdatePayload struct { AzureTenantID *string AzureAuthenticationKey *string Tags []string + UserAccessPolicies portainer.UserAccessPolicies + TeamAccessPolicies portainer.TeamAccessPolicies } func (payload *endpointUpdatePayload) Validate(r *http.Request) error { @@ -74,6 +76,14 @@ func (handler *Handler) endpointUpdate(w http.ResponseWriter, r *http.Request) * endpoint.Tags = payload.Tags } + if payload.UserAccessPolicies != nil { + endpoint.UserAccessPolicies = payload.UserAccessPolicies + } + + if payload.TeamAccessPolicies != nil { + endpoint.TeamAccessPolicies = payload.TeamAccessPolicies + } + if payload.Status != nil { switch *payload.Status { case 1: diff --git a/api/http/handler/endpoints/endpoint_update_access.go b/api/http/handler/endpoints/endpoint_update_access.go deleted file mode 100644 index 47a7d414b..000000000 --- a/api/http/handler/endpoints/endpoint_update_access.go +++ /dev/null @@ -1,67 +0,0 @@ -package endpoints - -import ( - "net/http" - - httperror "github.com/portainer/libhttp/error" - "github.com/portainer/libhttp/request" - "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" -) - -type endpointUpdateAccessPayload struct { - AuthorizedUsers []int - AuthorizedTeams []int -} - -func (payload *endpointUpdateAccessPayload) Validate(r *http.Request) error { - return nil -} - -// PUT request on /api/endpoints/:id/access -func (handler *Handler) endpointUpdateAccess(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { - if !handler.authorizeEndpointManagement { - return &httperror.HandlerError{http.StatusServiceUnavailable, "Endpoint management is disabled", ErrEndpointManagementDisabled} - } - - endpointID, err := request.RetrieveNumericRouteVariableValue(r, "id") - if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid endpoint identifier route variable", err} - } - - var payload endpointUpdateAccessPayload - err = request.DecodeAndValidateJSONPayload(r, &payload) - if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", err} - } - - endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) - if err == portainer.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} - } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} - } - - if payload.AuthorizedUsers != nil { - authorizedUserIDs := []portainer.UserID{} - for _, value := range payload.AuthorizedUsers { - authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value)) - } - endpoint.AuthorizedUsers = authorizedUserIDs - } - - if payload.AuthorizedTeams != nil { - authorizedTeamIDs := []portainer.TeamID{} - for _, value := range payload.AuthorizedTeams { - authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value)) - } - endpoint.AuthorizedTeams = authorizedTeamIDs - } - - err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist endpoint changes inside the database", err} - } - - return response.JSON(w, endpoint) -} diff --git a/api/http/handler/endpoints/handler.go b/api/http/handler/endpoints/handler.go index d8a94d360..03192f47f 100644 --- a/api/http/handler/endpoints/handler.go +++ b/api/http/handler/endpoints/handler.go @@ -2,9 +2,9 @@ package endpoints import ( httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/proxy" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy" + "github.com/portainer/portainer/api/http/security" "net/http" @@ -37,32 +37,30 @@ type Handler struct { // NewHandler creates a handler to manage endpoint operations. func NewHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bool) *Handler { h := &Handler{ - Router: mux.NewRouter(), + Router: mux.NewRouter(), authorizeEndpointManagement: authorizeEndpointManagement, requestBouncer: bouncer, } h.Handle("/endpoints", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointCreate))).Methods(http.MethodPost) h.Handle("/endpoints/snapshot", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointSnapshots))).Methods(http.MethodPost) h.Handle("/endpoints", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointList))).Methods(http.MethodGet) h.Handle("/endpoints/{id}", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointInspect))).Methods(http.MethodGet) h.Handle("/endpoints/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointUpdate))).Methods(http.MethodPut) - h.Handle("/endpoints/{id}/access", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointUpdateAccess))).Methods(http.MethodPut) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointUpdate))).Methods(http.MethodPut) h.Handle("/endpoints/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointDelete))).Methods(http.MethodDelete) h.Handle("/endpoints/{id}/extensions", - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointExtensionAdd))).Methods(http.MethodPost) h.Handle("/endpoints/{id}/extensions/{extensionType}", - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointExtensionRemove))).Methods(http.MethodDelete) h.Handle("/endpoints/{id}/job", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointJob))).Methods(http.MethodPost) h.Handle("/endpoints/{id}/snapshot", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.endpointSnapshot))).Methods(http.MethodPost) return h } diff --git a/api/http/handler/extensions/extension_create.go b/api/http/handler/extensions/extension_create.go index 22d146f6e..7e41c9595 100644 --- a/api/http/handler/extensions/extension_create.go +++ b/api/http/handler/extensions/extension_create.go @@ -8,7 +8,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type extensionCreatePayload struct { @@ -70,6 +70,13 @@ func (handler *Handler) extensionCreate(w http.ResponseWriter, r *http.Request) 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} diff --git a/api/http/handler/extensions/extension_delete.go b/api/http/handler/extensions/extension_delete.go index be9d72bb3..6f59afc2b 100644 --- a/api/http/handler/extensions/extension_delete.go +++ b/api/http/handler/extensions/extension_delete.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // DELETE request on /api/extensions/:id diff --git a/api/http/handler/extensions/extension_inspect.go b/api/http/handler/extensions/extension_inspect.go index 712e4403e..74ac59254 100644 --- a/api/http/handler/extensions/extension_inspect.go +++ b/api/http/handler/extensions/extension_inspect.go @@ -8,8 +8,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/client" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/client" ) // GET request on /api/extensions/:id diff --git a/api/http/handler/extensions/extension_list.go b/api/http/handler/extensions/extension_list.go index 68d26a7e7..ed7325c25 100644 --- a/api/http/handler/extensions/extension_list.go +++ b/api/http/handler/extensions/extension_list.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // GET request on /api/extensions?store= diff --git a/api/http/handler/extensions/extension_update.go b/api/http/handler/extensions/extension_update.go index 4e80ea1bc..b51bf93ba 100644 --- a/api/http/handler/extensions/extension_update.go +++ b/api/http/handler/extensions/extension_update.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type extensionUpdatePayload struct { diff --git a/api/http/handler/extensions/handler.go b/api/http/handler/extensions/handler.go index bba32f389..a7ab38feb 100644 --- a/api/http/handler/extensions/handler.go +++ b/api/http/handler/extensions/handler.go @@ -5,15 +5,18 @@ import ( "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) // Handler is the HTTP handler used to handle extension operations. type Handler struct { *mux.Router - ExtensionService portainer.ExtensionService - ExtensionManager portainer.ExtensionManager + ExtensionService portainer.ExtensionService + ExtensionManager portainer.ExtensionManager + EndpointGroupService portainer.EndpointGroupService + EndpointService portainer.EndpointService + RegistryService portainer.RegistryService } // NewHandler creates a handler to manage extension operations. @@ -23,15 +26,15 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { } h.Handle("/extensions", - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionList))).Methods(http.MethodGet) h.Handle("/extensions", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionCreate))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionCreate))).Methods(http.MethodPost) h.Handle("/extensions/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionInspect))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionInspect))).Methods(http.MethodGet) h.Handle("/extensions/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionDelete))).Methods(http.MethodDelete) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionDelete))).Methods(http.MethodDelete) h.Handle("/extensions/{id}/update", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.extensionUpdate))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.extensionUpdate))).Methods(http.MethodPost) return h } diff --git a/api/http/handler/extensions/upgrade.go b/api/http/handler/extensions/upgrade.go new file mode 100644 index 000000000..7e3203412 --- /dev/null +++ b/api/http/handler/extensions/upgrade.go @@ -0,0 +1,59 @@ +package extensions + +import portainer "github.com/portainer/portainer/api" + +func updateUserAccessPolicyToReadOnlyRole(policies portainer.UserAccessPolicies, key portainer.UserID) { + tmp := policies[key] + tmp.RoleID = 4 + policies[key] = tmp +} + +func updateTeamAccessPolicyToReadOnlyRole(policies portainer.TeamAccessPolicies, key portainer.TeamID) { + tmp := policies[key] + tmp.RoleID = 4 + policies[key] = tmp +} + +func (handler *Handler) upgradeRBACData() error { + endpointGroups, err := handler.EndpointGroupService.EndpointGroups() + if err != nil { + return err + } + + for _, endpointGroup := range endpointGroups { + for key := range endpointGroup.UserAccessPolicies { + updateUserAccessPolicyToReadOnlyRole(endpointGroup.UserAccessPolicies, key) + } + + for key := range endpointGroup.TeamAccessPolicies { + updateTeamAccessPolicyToReadOnlyRole(endpointGroup.TeamAccessPolicies, key) + } + + err := handler.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 { + updateUserAccessPolicyToReadOnlyRole(endpoint.UserAccessPolicies, key) + } + + for key := range endpoint.TeamAccessPolicies { + updateTeamAccessPolicyToReadOnlyRole(endpoint.TeamAccessPolicies, key) + } + + err := handler.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint) + if err != nil { + return err + } + } + return nil +} diff --git a/api/http/handler/handler.go b/api/http/handler/handler.go index 9cb3059df..9dc541641 100644 --- a/api/http/handler/handler.go +++ b/api/http/handler/handler.go @@ -4,34 +4,36 @@ import ( "net/http" "strings" - "github.com/portainer/portainer/http/handler/auth" - "github.com/portainer/portainer/http/handler/dockerhub" - "github.com/portainer/portainer/http/handler/endpointgroups" - "github.com/portainer/portainer/http/handler/endpointproxy" - "github.com/portainer/portainer/http/handler/endpoints" - "github.com/portainer/portainer/http/handler/extensions" - "github.com/portainer/portainer/http/handler/file" - "github.com/portainer/portainer/http/handler/motd" - "github.com/portainer/portainer/http/handler/registries" - "github.com/portainer/portainer/http/handler/resourcecontrols" - "github.com/portainer/portainer/http/handler/schedules" - "github.com/portainer/portainer/http/handler/settings" - "github.com/portainer/portainer/http/handler/stacks" - "github.com/portainer/portainer/http/handler/status" - "github.com/portainer/portainer/http/handler/tags" - "github.com/portainer/portainer/http/handler/teammemberships" - "github.com/portainer/portainer/http/handler/teams" - "github.com/portainer/portainer/http/handler/templates" - "github.com/portainer/portainer/http/handler/upload" - "github.com/portainer/portainer/http/handler/users" - "github.com/portainer/portainer/http/handler/webhooks" - "github.com/portainer/portainer/http/handler/websocket" + "github.com/portainer/portainer/api/http/handler/schedules" + + "github.com/portainer/portainer/api/http/handler/roles" + + "github.com/portainer/portainer/api/http/handler/auth" + "github.com/portainer/portainer/api/http/handler/dockerhub" + "github.com/portainer/portainer/api/http/handler/endpointgroups" + "github.com/portainer/portainer/api/http/handler/endpointproxy" + "github.com/portainer/portainer/api/http/handler/endpoints" + "github.com/portainer/portainer/api/http/handler/extensions" + "github.com/portainer/portainer/api/http/handler/file" + "github.com/portainer/portainer/api/http/handler/motd" + "github.com/portainer/portainer/api/http/handler/registries" + "github.com/portainer/portainer/api/http/handler/resourcecontrols" + "github.com/portainer/portainer/api/http/handler/settings" + "github.com/portainer/portainer/api/http/handler/stacks" + "github.com/portainer/portainer/api/http/handler/status" + "github.com/portainer/portainer/api/http/handler/tags" + "github.com/portainer/portainer/api/http/handler/teammemberships" + "github.com/portainer/portainer/api/http/handler/teams" + "github.com/portainer/portainer/api/http/handler/templates" + "github.com/portainer/portainer/api/http/handler/upload" + "github.com/portainer/portainer/api/http/handler/users" + "github.com/portainer/portainer/api/http/handler/webhooks" + "github.com/portainer/portainer/api/http/handler/websocket" ) // Handler is a collection of all the service handlers. type Handler struct { - AuthHandler *auth.Handler - + AuthHandler *auth.Handler DockerHubHandler *dockerhub.Handler EndpointGroupHandler *endpointgroups.Handler EndpointHandler *endpoints.Handler @@ -41,6 +43,8 @@ type Handler struct { ExtensionHandler *extensions.Handler RegistryHandler *registries.Handler ResourceControlHandler *resourcecontrols.Handler + RoleHandler *roles.Handler + SchedulesHanlder *schedules.Handler SettingsHandler *settings.Handler StackHandler *stacks.Handler StatusHandler *status.Handler @@ -52,7 +56,6 @@ type Handler struct { UserHandler *users.Handler WebSocketHandler *websocket.Handler WebhookHandler *webhooks.Handler - SchedulesHanlder *schedules.Handler } // ServeHTTP delegates a request to the appropriate subhandler. @@ -68,21 +71,25 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { switch { case strings.Contains(r.URL.Path, "/docker/"): http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r) - case strings.Contains(r.URL.Path, "/extensions/storidge"): + case strings.Contains(r.URL.Path, "/storidge/"): http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r) case strings.Contains(r.URL.Path, "/azure/"): http.StripPrefix("/api/endpoints", h.EndpointProxyHandler).ServeHTTP(w, r) default: http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r) } - case strings.HasPrefix(r.URL.Path, "/api/motd"): - http.StripPrefix("/api", h.MOTDHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/extensions"): http.StripPrefix("/api", h.ExtensionHandler).ServeHTTP(w, r) + case strings.HasPrefix(r.URL.Path, "/api/motd"): + http.StripPrefix("/api", h.MOTDHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/registries"): http.StripPrefix("/api", h.RegistryHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/resource_controls"): http.StripPrefix("/api", h.ResourceControlHandler).ServeHTTP(w, r) + case strings.HasPrefix(r.URL.Path, "/api/roles"): + http.StripPrefix("/api", h.RoleHandler).ServeHTTP(w, r) + case strings.HasPrefix(r.URL.Path, "/api/schedules"): + http.StripPrefix("/api", h.SchedulesHanlder).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/settings"): http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/stacks"): @@ -105,8 +112,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/api/webhooks"): http.StripPrefix("/api", h.WebhookHandler).ServeHTTP(w, r) - case strings.HasPrefix(r.URL.Path, "/api/schedules"): - http.StripPrefix("/api", h.SchedulesHanlder).ServeHTTP(w, r) case strings.HasPrefix(r.URL.Path, "/"): h.FileHandler.ServeHTTP(w, r) } diff --git a/api/http/handler/motd/handler.go b/api/http/handler/motd/handler.go index 429731aa4..aa2d1d002 100644 --- a/api/http/handler/motd/handler.go +++ b/api/http/handler/motd/handler.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/gorilla/mux" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api/http/security" ) // Handler is the HTTP handler used to handle MOTD operations. @@ -18,7 +18,7 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { Router: mux.NewRouter(), } h.Handle("/motd", - bouncer.AuthenticatedAccess(http.HandlerFunc(h.motd))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(http.HandlerFunc(h.motd))).Methods(http.MethodGet) return h } diff --git a/api/http/handler/motd/motd.go b/api/http/handler/motd/motd.go index 9d4f18004..ba768becb 100644 --- a/api/http/handler/motd/motd.go +++ b/api/http/handler/motd/motd.go @@ -1,34 +1,55 @@ package motd import ( + "encoding/json" "net/http" + "strings" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/crypto" - "github.com/portainer/portainer/http/client" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/crypto" + "github.com/portainer/portainer/api/http/client" ) type motdResponse struct { - Title string `json:"Title"` - Message string `json:"Message"` - Hash []byte `json:"Hash"` + Title string `json:"Title"` + Message string `json:"Message"` + ContentLayout map[string]string `json:"ContentLayout"` + Style string `json:"Style"` + Hash []byte `json:"Hash"` +} + +type motdData struct { + Title string `json:"title"` + Message []string `json:"message"` + ContentLayout map[string]string `json:"contentLayout"` + Style string `json:"style"` } func (handler *Handler) motd(w http.ResponseWriter, r *http.Request) { - motd, err := client.Get(portainer.MessageOfTheDayURL, 0) if err != nil { response.JSON(w, &motdResponse{Message: ""}) return } - title, err := client.Get(portainer.MessageOfTheDayTitleURL, 0) + var data motdData + err = json.Unmarshal(motd, &data) if err != nil { response.JSON(w, &motdResponse{Message: ""}) return } - hash := crypto.HashFromBytes(motd) - response.JSON(w, &motdResponse{Title: string(title), Message: string(motd), Hash: hash}) + message := strings.Join(data.Message, "\n") + + hash := crypto.HashFromBytes([]byte(message)) + resp := motdResponse{ + Title: data.Title, + Message: message, + Hash: hash, + ContentLayout: data.ContentLayout, + Style: data.Style, + } + + response.JSON(w, &resp) } diff --git a/api/http/handler/registries/handler.go b/api/http/handler/registries/handler.go index 26edebd15..3c90e6e67 100644 --- a/api/http/handler/registries/handler.go +++ b/api/http/handler/registries/handler.go @@ -5,9 +5,9 @@ import ( "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/proxy" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy" + "github.com/portainer/portainer/api/http/security" ) func hideFields(registry *portainer.Registry) { @@ -33,19 +33,17 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { } h.Handle("/registries", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryCreate))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryCreate))).Methods(http.MethodPost) h.Handle("/registries", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryList))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryList))).Methods(http.MethodGet) h.Handle("/registries/{id}", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryInspect))).Methods(http.MethodGet) h.Handle("/registries/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryUpdate))).Methods(http.MethodPut) - h.Handle("/registries/{id}/access", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryUpdateAccess))).Methods(http.MethodPut) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryUpdate))).Methods(http.MethodPut) h.Handle("/registries/{id}/configure", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryConfigure))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryConfigure))).Methods(http.MethodPost) h.Handle("/registries/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.registryDelete))).Methods(http.MethodDelete) h.PathPrefix("/registries/{id}/v2").Handler( bouncer.RestrictedAccess(httperror.LoggerHandler(h.proxyRequestsToRegistryAPI))) diff --git a/api/http/handler/registries/proxy.go b/api/http/handler/registries/proxy.go index 844dc7e79..d54520508 100644 --- a/api/http/handler/registries/proxy.go +++ b/api/http/handler/registries/proxy.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // request on /api/registries/:id/v2 diff --git a/api/http/handler/registries/registry_configure.go b/api/http/handler/registries/registry_configure.go index 0f5569141..c967b6996 100644 --- a/api/http/handler/registries/registry_configure.go +++ b/api/http/handler/registries/registry_configure.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type registryConfigurePayload struct { diff --git a/api/http/handler/registries/registry_create.go b/api/http/handler/registries/registry_create.go index 082f61352..1b6dbf638 100644 --- a/api/http/handler/registries/registry_create.go +++ b/api/http/handler/registries/registry_create.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type registryCreatePayload struct { @@ -53,14 +53,14 @@ func (handler *Handler) registryCreate(w http.ResponseWriter, r *http.Request) * } registry := &portainer.Registry{ - Type: portainer.RegistryType(payload.Type), - Name: payload.Name, - URL: payload.URL, - Authentication: payload.Authentication, - Username: payload.Username, - Password: payload.Password, - AuthorizedUsers: []portainer.UserID{}, - AuthorizedTeams: []portainer.TeamID{}, + Type: portainer.RegistryType(payload.Type), + Name: payload.Name, + URL: payload.URL, + Authentication: payload.Authentication, + Username: payload.Username, + Password: payload.Password, + UserAccessPolicies: portainer.UserAccessPolicies{}, + TeamAccessPolicies: portainer.TeamAccessPolicies{}, } err = handler.RegistryService.CreateRegistry(registry) diff --git a/api/http/handler/registries/registry_delete.go b/api/http/handler/registries/registry_delete.go index ac463af5f..ebb833ab4 100644 --- a/api/http/handler/registries/registry_delete.go +++ b/api/http/handler/registries/registry_delete.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // DELETE request on /api/registries/:id diff --git a/api/http/handler/registries/registry_inspect.go b/api/http/handler/registries/registry_inspect.go index 1dd9a3ffb..d40ef693a 100644 --- a/api/http/handler/registries/registry_inspect.go +++ b/api/http/handler/registries/registry_inspect.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // GET request on /api/registries/:id diff --git a/api/http/handler/registries/registry_list.go b/api/http/handler/registries/registry_list.go index 7f75427cc..b78763375 100644 --- a/api/http/handler/registries/registry_list.go +++ b/api/http/handler/registries/registry_list.go @@ -5,7 +5,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api/http/security" ) // GET request on /api/registries diff --git a/api/http/handler/registries/registry_update.go b/api/http/handler/registries/registry_update.go index cd3a7ae67..14c11cbba 100644 --- a/api/http/handler/registries/registry_update.go +++ b/api/http/handler/registries/registry_update.go @@ -7,15 +7,17 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type registryUpdatePayload struct { - Name string - URL string - Authentication bool - Username string - Password string + Name string + URL string + Authentication bool + Username string + Password string + UserAccessPolicies portainer.UserAccessPolicies + TeamAccessPolicies portainer.TeamAccessPolicies } func (payload *registryUpdatePayload) Validate(r *http.Request) error { @@ -73,6 +75,14 @@ func (handler *Handler) registryUpdate(w http.ResponseWriter, r *http.Request) * registry.Password = "" } + if payload.UserAccessPolicies != nil { + registry.UserAccessPolicies = payload.UserAccessPolicies + } + + if payload.TeamAccessPolicies != nil { + registry.TeamAccessPolicies = payload.TeamAccessPolicies + } + err = handler.RegistryService.UpdateRegistry(registry.ID, registry) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist registry changes inside the database", err} diff --git a/api/http/handler/registries/registry_update_access.go b/api/http/handler/registries/registry_update_access.go deleted file mode 100644 index 77f6fe08e..000000000 --- a/api/http/handler/registries/registry_update_access.go +++ /dev/null @@ -1,63 +0,0 @@ -package registries - -import ( - "net/http" - - httperror "github.com/portainer/libhttp/error" - "github.com/portainer/libhttp/request" - "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" -) - -type registryUpdateAccessPayload struct { - AuthorizedUsers []int - AuthorizedTeams []int -} - -func (payload *registryUpdateAccessPayload) Validate(r *http.Request) error { - return nil -} - -// PUT request on /api/registries/:id/access -func (handler *Handler) registryUpdateAccess(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} - } - - var payload registryUpdateAccessPayload - err = request.DecodeAndValidateJSONPayload(r, &payload) - if err != nil { - return &httperror.HandlerError{http.StatusBadRequest, "Invalid request payload", 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} - } - - if payload.AuthorizedUsers != nil { - authorizedUserIDs := []portainer.UserID{} - for _, value := range payload.AuthorizedUsers { - authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value)) - } - registry.AuthorizedUsers = authorizedUserIDs - } - - if payload.AuthorizedTeams != nil { - authorizedTeamIDs := []portainer.TeamID{} - for _, value := range payload.AuthorizedTeams { - authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value)) - } - registry.AuthorizedTeams = authorizedTeamIDs - } - - err = handler.RegistryService.UpdateRegistry(registry.ID, registry) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to persist registry changes inside the database", err} - } - - return response.JSON(w, registry) -} diff --git a/api/http/handler/resourcecontrols/handler.go b/api/http/handler/resourcecontrols/handler.go index 60757ef28..d187ba6c8 100644 --- a/api/http/handler/resourcecontrols/handler.go +++ b/api/http/handler/resourcecontrols/handler.go @@ -5,8 +5,8 @@ import ( "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) // Handler is the HTTP handler used to handle resource control operations. diff --git a/api/http/handler/resourcecontrols/resourcecontrol_create.go b/api/http/handler/resourcecontrols/resourcecontrol_create.go index 97c3d5ef1..ad954805d 100644 --- a/api/http/handler/resourcecontrols/resourcecontrol_create.go +++ b/api/http/handler/resourcecontrols/resourcecontrol_create.go @@ -7,8 +7,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) type resourceControlCreatePayload struct { diff --git a/api/http/handler/resourcecontrols/resourcecontrol_delete.go b/api/http/handler/resourcecontrols/resourcecontrol_delete.go index 04ba8d5d4..1d1ea8ddf 100644 --- a/api/http/handler/resourcecontrols/resourcecontrol_delete.go +++ b/api/http/handler/resourcecontrols/resourcecontrol_delete.go @@ -6,8 +6,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) // DELETE request on /api/resource_controls/:id diff --git a/api/http/handler/resourcecontrols/resourcecontrol_update.go b/api/http/handler/resourcecontrols/resourcecontrol_update.go index c46247c5d..360e704c9 100644 --- a/api/http/handler/resourcecontrols/resourcecontrol_update.go +++ b/api/http/handler/resourcecontrols/resourcecontrol_update.go @@ -6,8 +6,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) type resourceControlUpdatePayload struct { diff --git a/api/http/handler/roles/handler.go b/api/http/handler/roles/handler.go new file mode 100644 index 000000000..e6bb7c4c7 --- /dev/null +++ b/api/http/handler/roles/handler.go @@ -0,0 +1,27 @@ +package roles + +import ( + "net/http" + + "github.com/gorilla/mux" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" +) + +// Handler is the HTTP handler used to handle role operations. +type Handler struct { + *mux.Router + RoleService portainer.RoleService +} + +// NewHandler creates a handler to manage role operations. +func NewHandler(bouncer *security.RequestBouncer) *Handler { + h := &Handler{ + Router: mux.NewRouter(), + } + h.Handle("/roles", + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.roleList))).Methods(http.MethodGet) + + return h +} diff --git a/api/http/handler/roles/role_list.go b/api/http/handler/roles/role_list.go new file mode 100644 index 000000000..e39e38595 --- /dev/null +++ b/api/http/handler/roles/role_list.go @@ -0,0 +1,18 @@ +package roles + +import ( + "net/http" + + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/response" +) + +// GET request on /api/Role +func (handler *Handler) roleList(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + roles, err := handler.RoleService.Roles() + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve authorization sets from the database", err} + } + + return response.JSON(w, roles) +} diff --git a/api/http/handler/schedules/handler.go b/api/http/handler/schedules/handler.go index c2c091209..5a160cd3b 100644 --- a/api/http/handler/schedules/handler.go +++ b/api/http/handler/schedules/handler.go @@ -5,8 +5,8 @@ import ( "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) // Handler is the HTTP handler used to handle schedule operations. @@ -27,18 +27,18 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { } h.Handle("/schedules", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleList))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleList))).Methods(http.MethodGet) h.Handle("/schedules", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleCreate))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleCreate))).Methods(http.MethodPost) h.Handle("/schedules/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleInspect))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleInspect))).Methods(http.MethodGet) h.Handle("/schedules/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleUpdate))).Methods(http.MethodPut) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleUpdate))).Methods(http.MethodPut) h.Handle("/schedules/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleDelete))).Methods(http.MethodDelete) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleDelete))).Methods(http.MethodDelete) h.Handle("/schedules/{id}/file", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleFile))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleFile))).Methods(http.MethodGet) h.Handle("/schedules/{id}/tasks", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.scheduleTasks))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.scheduleTasks))).Methods(http.MethodGet) return h } diff --git a/api/http/handler/schedules/schedule_create.go b/api/http/handler/schedules/schedule_create.go index 5c0ecbdbc..f4302bd4b 100644 --- a/api/http/handler/schedules/schedule_create.go +++ b/api/http/handler/schedules/schedule_create.go @@ -10,8 +10,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/cron" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/cron" ) type scheduleCreateFromFilePayload struct { diff --git a/api/http/handler/schedules/schedule_delete.go b/api/http/handler/schedules/schedule_delete.go index 67a02010a..a2ef81393 100644 --- a/api/http/handler/schedules/schedule_delete.go +++ b/api/http/handler/schedules/schedule_delete.go @@ -8,7 +8,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) func (handler *Handler) scheduleDelete(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { diff --git a/api/http/handler/schedules/schedule_file.go b/api/http/handler/schedules/schedule_file.go index f24e10867..c10b698dd 100644 --- a/api/http/handler/schedules/schedule_file.go +++ b/api/http/handler/schedules/schedule_file.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type scheduleFileResponse struct { diff --git a/api/http/handler/schedules/schedule_inspect.go b/api/http/handler/schedules/schedule_inspect.go index bcd74b4b9..594c29b1a 100644 --- a/api/http/handler/schedules/schedule_inspect.go +++ b/api/http/handler/schedules/schedule_inspect.go @@ -4,7 +4,7 @@ import ( "net/http" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" diff --git a/api/http/handler/schedules/schedule_list.go b/api/http/handler/schedules/schedule_list.go index f67eee452..55662dc0e 100644 --- a/api/http/handler/schedules/schedule_list.go +++ b/api/http/handler/schedules/schedule_list.go @@ -5,7 +5,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // GET request on /api/schedules diff --git a/api/http/handler/schedules/schedule_tasks.go b/api/http/handler/schedules/schedule_tasks.go index 0cbf24372..1c2810e45 100644 --- a/api/http/handler/schedules/schedule_tasks.go +++ b/api/http/handler/schedules/schedule_tasks.go @@ -9,7 +9,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type taskContainer struct { diff --git a/api/http/handler/schedules/schedule_update.go b/api/http/handler/schedules/schedule_update.go index 0edfd0dde..29a6dccbe 100644 --- a/api/http/handler/schedules/schedule_update.go +++ b/api/http/handler/schedules/schedule_update.go @@ -9,8 +9,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/cron" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/cron" ) type scheduleUpdatePayload struct { diff --git a/api/http/handler/settings/handler.go b/api/http/handler/settings/handler.go index a9033c701..2a5348b2e 100644 --- a/api/http/handler/settings/handler.go +++ b/api/http/handler/settings/handler.go @@ -5,8 +5,8 @@ import ( "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) func hideFields(settings *portainer.Settings) { @@ -30,13 +30,13 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { Router: mux.NewRouter(), } h.Handle("/settings", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.settingsInspect))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.settingsInspect))).Methods(http.MethodGet) h.Handle("/settings", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.settingsUpdate))).Methods(http.MethodPut) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.settingsUpdate))).Methods(http.MethodPut) h.Handle("/settings/public", bouncer.PublicAccess(httperror.LoggerHandler(h.settingsPublic))).Methods(http.MethodGet) h.Handle("/settings/authentication/checkLDAP", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.settingsLDAPCheck))).Methods(http.MethodPut) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.settingsLDAPCheck))).Methods(http.MethodPut) return h } diff --git a/api/http/handler/settings/settings_ldap_check.go b/api/http/handler/settings/settings_ldap_check.go index a8466e285..863612fd4 100644 --- a/api/http/handler/settings/settings_ldap_check.go +++ b/api/http/handler/settings/settings_ldap_check.go @@ -6,8 +6,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/filesystem" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/filesystem" ) type settingsLDAPCheckPayload struct { diff --git a/api/http/handler/settings/settings_public.go b/api/http/handler/settings/settings_public.go index cc1e07854..38eb8d4e3 100644 --- a/api/http/handler/settings/settings_public.go +++ b/api/http/handler/settings/settings_public.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type publicSettingsResponse struct { diff --git a/api/http/handler/settings/settings_update.go b/api/http/handler/settings/settings_update.go index 21b9daa99..cc9c901dc 100644 --- a/api/http/handler/settings/settings_update.go +++ b/api/http/handler/settings/settings_update.go @@ -7,8 +7,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/filesystem" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/filesystem" ) type settingsUpdatePayload struct { diff --git a/api/http/handler/stacks/create_compose_stack.go b/api/http/handler/stacks/create_compose_stack.go index a6a8ddcfd..66c9d91cb 100644 --- a/api/http/handler/stacks/create_compose_stack.go +++ b/api/http/handler/stacks/create_compose_stack.go @@ -9,9 +9,9 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/filesystem" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/filesystem" + "github.com/portainer/portainer/api/http/security" ) type composeStackFromFileContentPayload struct { diff --git a/api/http/handler/stacks/create_swarm_stack.go b/api/http/handler/stacks/create_swarm_stack.go index 017122026..0832111e0 100644 --- a/api/http/handler/stacks/create_swarm_stack.go +++ b/api/http/handler/stacks/create_swarm_stack.go @@ -9,9 +9,9 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/filesystem" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/filesystem" + "github.com/portainer/portainer/api/http/security" ) type swarmStackFromFileContentPayload struct { diff --git a/api/http/handler/stacks/handler.go b/api/http/handler/stacks/handler.go index 7f2fd4bc0..caf181bef 100644 --- a/api/http/handler/stacks/handler.go +++ b/api/http/handler/stacks/handler.go @@ -6,8 +6,8 @@ import ( "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) // Handler is the HTTP handler used to handle stack operations. diff --git a/api/http/handler/stacks/stack_create.go b/api/http/handler/stacks/stack_create.go index 0fe7e07ad..0d1adae53 100644 --- a/api/http/handler/stacks/stack_create.go +++ b/api/http/handler/stacks/stack_create.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) func (handler *Handler) cleanUp(stack *portainer.Stack, doCleanUp *bool) error { @@ -46,9 +46,9 @@ func (handler *Handler) stackCreate(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} } - err = handler.requestBouncer.EndpointAccess(r, endpoint) + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } switch portainer.StackType(stackType) { diff --git a/api/http/handler/stacks/stack_delete.go b/api/http/handler/stacks/stack_delete.go index 8f8fc25e8..2607fa10c 100644 --- a/api/http/handler/stacks/stack_delete.go +++ b/api/http/handler/stacks/stack_delete.go @@ -4,12 +4,13 @@ import ( "net/http" "strconv" + "github.com/portainer/portainer/api/http/proxy" + "github.com/portainer/portainer/api/http/security" + httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/proxy" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" ) // DELETE request on /api/stacks/:id?external=&endpointId= @@ -38,22 +39,6 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err} } - resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name) - if err != nil && err != portainer.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} - } - - securityContext, err := security.RetrieveRestrictedRequestContext(r) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err} - } - - if !securityContext.IsAdmin { - if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) { - 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 // The EndpointID property is not available for these stacks, this API endpoint // can use the optional EndpointID query parameter to set a valid endpoint identifier to be @@ -74,6 +59,27 @@ func (handler *Handler) stackDelete(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err} } + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true) + if err != nil { + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + } + + resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name) + if err != nil && err != portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} + } + + securityContext, err := security.RetrieveRestrictedRequestContext(r) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err} + } + + if !securityContext.IsAdmin { + if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) { + return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied} + } + } + err = handler.deleteStack(stack, endpoint) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, err.Error(), err} @@ -113,9 +119,9 @@ func (handler *Handler) deleteExternalStack(r *http.Request, w http.ResponseWrit return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err} } - err = handler.requestBouncer.EndpointAccess(r, endpoint) + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } stack = &portainer.Stack{ diff --git a/api/http/handler/stacks/stack_file.go b/api/http/handler/stacks/stack_file.go index b0b247cef..fd9767ffd 100644 --- a/api/http/handler/stacks/stack_file.go +++ b/api/http/handler/stacks/stack_file.go @@ -7,9 +7,9 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/proxy" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy" + "github.com/portainer/portainer/api/http/security" ) type stackFileResponse struct { @@ -30,6 +30,18 @@ func (handler *Handler) stackFile(w http.ResponseWriter, r *http.Request) *httpe return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err} } + endpoint, err := handler.EndpointService.Endpoint(stack.EndpointID) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + } + + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true) + if err != nil { + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + } + resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name) if err != nil && err != portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} diff --git a/api/http/handler/stacks/stack_inspect.go b/api/http/handler/stacks/stack_inspect.go index e5377f654..878a20361 100644 --- a/api/http/handler/stacks/stack_inspect.go +++ b/api/http/handler/stacks/stack_inspect.go @@ -6,9 +6,9 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/proxy" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy" + "github.com/portainer/portainer/api/http/security" ) // GET request on /api/stacks/:id @@ -25,6 +25,18 @@ func (handler *Handler) stackInspect(w http.ResponseWriter, r *http.Request) *ht return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err} } + endpoint, err := handler.EndpointService.Endpoint(stack.EndpointID) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + } + + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true) + if err != nil { + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + } + resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name) if err != nil && err != portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} diff --git a/api/http/handler/stacks/stack_list.go b/api/http/handler/stacks/stack_list.go index 337b204a7..aabd68eee 100644 --- a/api/http/handler/stacks/stack_list.go +++ b/api/http/handler/stacks/stack_list.go @@ -6,9 +6,9 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/proxy" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy" + "github.com/portainer/portainer/api/http/security" ) type stackListOperationFilters struct { diff --git a/api/http/handler/stacks/stack_migrate.go b/api/http/handler/stacks/stack_migrate.go index 704394766..340d57310 100644 --- a/api/http/handler/stacks/stack_migrate.go +++ b/api/http/handler/stacks/stack_migrate.go @@ -6,9 +6,9 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/proxy" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/proxy" + "github.com/portainer/portainer/api/http/security" ) type stackMigratePayload struct { @@ -44,6 +44,18 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err} } + endpoint, err := handler.EndpointService.Endpoint(stack.EndpointID) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} + } + + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true) + if err != nil { + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + } + resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name) if err != nil && err != portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} @@ -71,13 +83,6 @@ func (handler *Handler) stackMigrate(w http.ResponseWriter, r *http.Request) *ht stack.EndpointID = portainer.EndpointID(endpointID) } - endpoint, err := handler.EndpointService.Endpoint(stack.EndpointID) - if err == portainer.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} - } else if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err} - } - targetEndpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(payload.EndpointID)) if err == portainer.ErrObjectNotFound { return &httperror.HandlerError{http.StatusNotFound, "Unable to find an endpoint with the specified identifier inside the database", err} diff --git a/api/http/handler/stacks/stack_update.go b/api/http/handler/stacks/stack_update.go index 03999a557..63f8eb323 100644 --- a/api/http/handler/stacks/stack_update.go +++ b/api/http/handler/stacks/stack_update.go @@ -4,13 +4,14 @@ import ( "net/http" "strconv" + "github.com/portainer/portainer/api/http/proxy" + "github.com/portainer/portainer/api/http/security" + "github.com/asaskevich/govalidator" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/proxy" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" ) type updateComposeStackPayload struct { @@ -52,22 +53,6 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find a stack with the specified identifier inside the database", err} } - resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name) - if err != nil && err != portainer.ErrObjectNotFound { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} - } - - securityContext, err := security.RetrieveRestrictedRequestContext(r) - if err != nil { - return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err} - } - - if !securityContext.IsAdmin { - if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) { - 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 // The EndpointID property is not available for these stacks, this API endpoint // can use the optional EndpointID query parameter to associate a valid endpoint identifier to the stack. @@ -86,6 +71,27 @@ func (handler *Handler) stackUpdate(w http.ResponseWriter, r *http.Request) *htt return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err} } + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true) + if err != nil { + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + } + + resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name) + if err != nil && err != portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve a resource control associated to the stack", err} + } + + securityContext, err := security.RetrieveRestrictedRequestContext(r) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to retrieve info from request context", err} + } + + if !securityContext.IsAdmin { + if !proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) { + return &httperror.HandlerError{http.StatusForbidden, "Access denied to resource", portainer.ErrResourceAccessDenied} + } + } + updateError := handler.updateAndDeployStack(r, stack, endpoint) if updateError != nil { return updateError diff --git a/api/http/handler/status/handler.go b/api/http/handler/status/handler.go index 4d26fcd3a..cb30c8479 100644 --- a/api/http/handler/status/handler.go +++ b/api/http/handler/status/handler.go @@ -5,8 +5,8 @@ import ( "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) // Handler is the HTTP handler used to handle status operations. diff --git a/api/http/handler/tags/handler.go b/api/http/handler/tags/handler.go index b38bd28f3..d6461e2dc 100644 --- a/api/http/handler/tags/handler.go +++ b/api/http/handler/tags/handler.go @@ -5,8 +5,8 @@ import ( "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) // Handler is the HTTP handler used to handle tag operations. @@ -21,11 +21,11 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { Router: mux.NewRouter(), } h.Handle("/tags", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.tagCreate))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.tagCreate))).Methods(http.MethodPost) h.Handle("/tags", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.tagList))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.tagList))).Methods(http.MethodGet) h.Handle("/tags/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.tagDelete))).Methods(http.MethodDelete) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.tagDelete))).Methods(http.MethodDelete) return h } diff --git a/api/http/handler/tags/tag_create.go b/api/http/handler/tags/tag_create.go index 50b9261ba..e639cd39b 100644 --- a/api/http/handler/tags/tag_create.go +++ b/api/http/handler/tags/tag_create.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type tagCreatePayload struct { diff --git a/api/http/handler/tags/tag_delete.go b/api/http/handler/tags/tag_delete.go index e74b5ef12..9c9e9d4e3 100644 --- a/api/http/handler/tags/tag_delete.go +++ b/api/http/handler/tags/tag_delete.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // DELETE request on /api/tags/:id diff --git a/api/http/handler/teammemberships/handler.go b/api/http/handler/teammemberships/handler.go index 10cd63a83..3fd56bfc0 100644 --- a/api/http/handler/teammemberships/handler.go +++ b/api/http/handler/teammemberships/handler.go @@ -2,8 +2,8 @@ package teammemberships import ( httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" "net/http" @@ -23,13 +23,13 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { Router: mux.NewRouter(), } h.Handle("/team_memberships", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamMembershipCreate))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamMembershipCreate))).Methods(http.MethodPost) h.Handle("/team_memberships", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamMembershipList))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamMembershipList))).Methods(http.MethodGet) h.Handle("/team_memberships/{id}", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamMembershipUpdate))).Methods(http.MethodPut) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamMembershipUpdate))).Methods(http.MethodPut) h.Handle("/team_memberships/{id}", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamMembershipDelete))).Methods(http.MethodDelete) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamMembershipDelete))).Methods(http.MethodDelete) return h } diff --git a/api/http/handler/teammemberships/teammembership_create.go b/api/http/handler/teammemberships/teammembership_create.go index 37216df6a..0d8716de9 100644 --- a/api/http/handler/teammemberships/teammembership_create.go +++ b/api/http/handler/teammemberships/teammembership_create.go @@ -6,8 +6,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) type teamMembershipCreatePayload struct { diff --git a/api/http/handler/teammemberships/teammembership_delete.go b/api/http/handler/teammemberships/teammembership_delete.go index 846fb7892..dd8fe6d0b 100644 --- a/api/http/handler/teammemberships/teammembership_delete.go +++ b/api/http/handler/teammemberships/teammembership_delete.go @@ -6,8 +6,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) // DELETE request on /api/team_memberships/:id diff --git a/api/http/handler/teammemberships/teammembership_list.go b/api/http/handler/teammemberships/teammembership_list.go index 4136053d6..bb0196458 100644 --- a/api/http/handler/teammemberships/teammembership_list.go +++ b/api/http/handler/teammemberships/teammembership_list.go @@ -5,8 +5,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) // GET request on /api/team_memberships diff --git a/api/http/handler/teammemberships/teammembership_update.go b/api/http/handler/teammemberships/teammembership_update.go index 0e400e975..84ab44323 100644 --- a/api/http/handler/teammemberships/teammembership_update.go +++ b/api/http/handler/teammemberships/teammembership_update.go @@ -6,8 +6,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) type teamMembershipUpdatePayload struct { diff --git a/api/http/handler/teams/handler.go b/api/http/handler/teams/handler.go index 946a071d7..1aad28ef0 100644 --- a/api/http/handler/teams/handler.go +++ b/api/http/handler/teams/handler.go @@ -5,8 +5,8 @@ import ( "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) // Handler is the HTTP handler used to handle team operations. @@ -23,17 +23,17 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { Router: mux.NewRouter(), } h.Handle("/teams", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.teamCreate))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamCreate))).Methods(http.MethodPost) h.Handle("/teams", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamList))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamList))).Methods(http.MethodGet) h.Handle("/teams/{id}", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamInspect))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamInspect))).Methods(http.MethodGet) h.Handle("/teams/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.teamUpdate))).Methods(http.MethodPut) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamUpdate))).Methods(http.MethodPut) h.Handle("/teams/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.teamDelete))).Methods(http.MethodDelete) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamDelete))).Methods(http.MethodDelete) h.Handle("/teams/{id}/memberships", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.teamMemberships))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.teamMemberships))).Methods(http.MethodGet) return h } diff --git a/api/http/handler/teams/team_create.go b/api/http/handler/teams/team_create.go index 16b230ec4..3cfad7acb 100644 --- a/api/http/handler/teams/team_create.go +++ b/api/http/handler/teams/team_create.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type teamCreatePayload struct { diff --git a/api/http/handler/teams/team_delete.go b/api/http/handler/teams/team_delete.go index 00146d698..f38010725 100644 --- a/api/http/handler/teams/team_delete.go +++ b/api/http/handler/teams/team_delete.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // DELETE request on /api/teams/:id diff --git a/api/http/handler/teams/team_inspect.go b/api/http/handler/teams/team_inspect.go index b8aa83da1..0eeb89e8f 100644 --- a/api/http/handler/teams/team_inspect.go +++ b/api/http/handler/teams/team_inspect.go @@ -6,8 +6,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) // GET request on /api/teams/:id diff --git a/api/http/handler/teams/team_list.go b/api/http/handler/teams/team_list.go index 4acc5eabb..da67b696b 100644 --- a/api/http/handler/teams/team_list.go +++ b/api/http/handler/teams/team_list.go @@ -5,7 +5,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api/http/security" ) // GET request on /api/teams diff --git a/api/http/handler/teams/team_memberships.go b/api/http/handler/teams/team_memberships.go index 450458e35..e7c26fff6 100644 --- a/api/http/handler/teams/team_memberships.go +++ b/api/http/handler/teams/team_memberships.go @@ -6,8 +6,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) // GET request on /api/teams/:id/memberships diff --git a/api/http/handler/teams/team_update.go b/api/http/handler/teams/team_update.go index dea180b6a..740e4fc61 100644 --- a/api/http/handler/teams/team_update.go +++ b/api/http/handler/teams/team_update.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type teamUpdatePayload struct { diff --git a/api/http/handler/templates/handler.go b/api/http/handler/templates/handler.go index 660991d8d..026b137ee 100644 --- a/api/http/handler/templates/handler.go +++ b/api/http/handler/templates/handler.go @@ -5,8 +5,8 @@ import ( "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) const ( @@ -27,15 +27,15 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { } h.Handle("/templates", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.templateList))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.templateList))).Methods(http.MethodGet) h.Handle("/templates", - bouncer.AdministratorAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateCreate)))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateCreate)))).Methods(http.MethodPost) h.Handle("/templates/{id}", - bouncer.AdministratorAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateInspect)))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateInspect)))).Methods(http.MethodGet) h.Handle("/templates/{id}", - bouncer.AdministratorAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateUpdate)))).Methods(http.MethodPut) + bouncer.AuthorizedAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateUpdate)))).Methods(http.MethodPut) h.Handle("/templates/{id}", - bouncer.AdministratorAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateDelete)))).Methods(http.MethodDelete) + bouncer.AuthorizedAccess(h.templateManagementCheck(httperror.LoggerHandler(h.templateDelete)))).Methods(http.MethodDelete) return h } diff --git a/api/http/handler/templates/template_create.go b/api/http/handler/templates/template_create.go index ef9c0c58f..6c4d21a25 100644 --- a/api/http/handler/templates/template_create.go +++ b/api/http/handler/templates/template_create.go @@ -7,8 +7,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/filesystem" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/filesystem" ) type templateCreatePayload struct { diff --git a/api/http/handler/templates/template_delete.go b/api/http/handler/templates/template_delete.go index f5793bb8c..cf82e7889 100644 --- a/api/http/handler/templates/template_delete.go +++ b/api/http/handler/templates/template_delete.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // DELETE request on /api/templates/:id diff --git a/api/http/handler/templates/template_inspect.go b/api/http/handler/templates/template_inspect.go index 70b6ada02..bac836421 100644 --- a/api/http/handler/templates/template_inspect.go +++ b/api/http/handler/templates/template_inspect.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // GET request on /api/templates/:id diff --git a/api/http/handler/templates/template_list.go b/api/http/handler/templates/template_list.go index 7e802528f..41013c8ff 100644 --- a/api/http/handler/templates/template_list.go +++ b/api/http/handler/templates/template_list.go @@ -6,9 +6,9 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/client" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/client" + "github.com/portainer/portainer/api/http/security" ) // GET request on /api/templates diff --git a/api/http/handler/templates/template_update.go b/api/http/handler/templates/template_update.go index 123ea0f6a..fb0568aa3 100644 --- a/api/http/handler/templates/template_update.go +++ b/api/http/handler/templates/template_update.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type templateUpdatePayload struct { diff --git a/api/http/handler/upload/handler.go b/api/http/handler/upload/handler.go index b0068979b..fe3060dac 100644 --- a/api/http/handler/upload/handler.go +++ b/api/http/handler/upload/handler.go @@ -2,8 +2,8 @@ package upload import ( httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" "net/http" @@ -22,6 +22,6 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { Router: mux.NewRouter(), } h.Handle("/upload/tls/{certificate:(?:ca|cert|key)}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.uploadTLS))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.uploadTLS))).Methods(http.MethodPost) return h } diff --git a/api/http/handler/upload/upload_tls.go b/api/http/handler/upload/upload_tls.go index 964a7d29b..383f21fac 100644 --- a/api/http/handler/upload/upload_tls.go +++ b/api/http/handler/upload/upload_tls.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // POST request on /api/upload/tls/{certificate:(?:ca|cert|key)}?folder= diff --git a/api/http/handler/users/admin_check.go b/api/http/handler/users/admin_check.go index 5fcd1d33d..0f53e6693 100644 --- a/api/http/handler/users/admin_check.go +++ b/api/http/handler/users/admin_check.go @@ -5,7 +5,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // GET request on /api/users/admin/check diff --git a/api/http/handler/users/admin_init.go b/api/http/handler/users/admin_init.go index fb2172686..044bc2876 100644 --- a/api/http/handler/users/admin_init.go +++ b/api/http/handler/users/admin_init.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type adminInitPayload struct { @@ -45,6 +45,23 @@ func (handler *Handler) adminInit(w http.ResponseWriter, r *http.Request) *httpe user := &portainer.User{ Username: payload.Username, Role: portainer.AdministratorRole, + PortainerAuthorizations: map[portainer.Authorization]bool{ + portainer.OperationPortainerDockerHubInspect: true, + portainer.OperationPortainerEndpointGroupList: true, + portainer.OperationPortainerEndpointList: true, + portainer.OperationPortainerEndpointInspect: true, + portainer.OperationPortainerEndpointExtensionAdd: true, + portainer.OperationPortainerEndpointExtensionRemove: true, + portainer.OperationPortainerExtensionList: true, + portainer.OperationPortainerMOTD: true, + portainer.OperationPortainerRegistryList: true, + portainer.OperationPortainerRegistryInspect: true, + portainer.OperationPortainerTeamList: true, + portainer.OperationPortainerTemplateList: true, + portainer.OperationPortainerTemplateInspect: true, + portainer.OperationPortainerUserList: true, + portainer.OperationPortainerUserMemberships: true, + }, } user.Password, err = handler.CryptoService.Hash(payload.Password) diff --git a/api/http/handler/users/handler.go b/api/http/handler/users/handler.go index ae43ef521..8e7f5035b 100644 --- a/api/http/handler/users/handler.go +++ b/api/http/handler/users/handler.go @@ -2,8 +2,8 @@ package users import ( httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" "net/http" @@ -31,19 +31,19 @@ func NewHandler(bouncer *security.RequestBouncer, rateLimiter *security.RateLimi Router: mux.NewRouter(), } h.Handle("/users", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.userCreate))).Methods(http.MethodPost) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.userCreate))).Methods(http.MethodPost) h.Handle("/users", - bouncer.RestrictedAccess(httperror.LoggerHandler(h.userList))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.userList))).Methods(http.MethodGet) h.Handle("/users/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.userInspect))).Methods(http.MethodGet) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.userInspect))).Methods(http.MethodGet) h.Handle("/users/{id}", - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userUpdate))).Methods(http.MethodPut) + bouncer.RestrictedAccess(httperror.LoggerHandler(h.userUpdate))).Methods(http.MethodPut) h.Handle("/users/{id}", - bouncer.AdministratorAccess(httperror.LoggerHandler(h.userDelete))).Methods(http.MethodDelete) + bouncer.AuthorizedAccess(httperror.LoggerHandler(h.userDelete))).Methods(http.MethodDelete) h.Handle("/users/{id}/memberships", - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userMemberships))).Methods(http.MethodGet) + bouncer.RestrictedAccess(httperror.LoggerHandler(h.userMemberships))).Methods(http.MethodGet) h.Handle("/users/{id}/passwd", - rateLimiter.LimitAccess(bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.userUpdatePassword)))).Methods(http.MethodPut) + rateLimiter.LimitAccess(bouncer.RestrictedAccess(httperror.LoggerHandler(h.userUpdatePassword)))).Methods(http.MethodPut) h.Handle("/users/admin/check", bouncer.PublicAccess(httperror.LoggerHandler(h.adminCheck))).Methods(http.MethodGet) h.Handle("/users/admin/init", diff --git a/api/http/handler/users/user_create.go b/api/http/handler/users/user_create.go index 7948ec2b6..ab89cf1ff 100644 --- a/api/http/handler/users/user_create.go +++ b/api/http/handler/users/user_create.go @@ -7,8 +7,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) type userCreatePayload struct { @@ -60,6 +60,23 @@ func (handler *Handler) userCreate(w http.ResponseWriter, r *http.Request) *http user = &portainer.User{ Username: payload.Username, Role: portainer.UserRole(payload.Role), + PortainerAuthorizations: map[portainer.Authorization]bool{ + portainer.OperationPortainerDockerHubInspect: true, + portainer.OperationPortainerEndpointGroupList: true, + portainer.OperationPortainerEndpointList: true, + portainer.OperationPortainerEndpointInspect: true, + portainer.OperationPortainerEndpointExtensionAdd: true, + portainer.OperationPortainerEndpointExtensionRemove: true, + portainer.OperationPortainerExtensionList: true, + portainer.OperationPortainerMOTD: true, + portainer.OperationPortainerRegistryList: true, + portainer.OperationPortainerRegistryInspect: true, + portainer.OperationPortainerTeamList: true, + portainer.OperationPortainerTemplateList: true, + portainer.OperationPortainerTemplateInspect: true, + portainer.OperationPortainerUserList: true, + portainer.OperationPortainerUserMemberships: true, + }, } settings, err := handler.SettingsService.Settings() diff --git a/api/http/handler/users/user_delete.go b/api/http/handler/users/user_delete.go index 78f83fe57..c600e1943 100644 --- a/api/http/handler/users/user_delete.go +++ b/api/http/handler/users/user_delete.go @@ -6,8 +6,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) // DELETE request on /api/users/:id diff --git a/api/http/handler/users/user_inspect.go b/api/http/handler/users/user_inspect.go index a2e185bdf..f0723bd49 100644 --- a/api/http/handler/users/user_inspect.go +++ b/api/http/handler/users/user_inspect.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // GET request on /api/users/:id diff --git a/api/http/handler/users/user_list.go b/api/http/handler/users/user_list.go index 9b46ff5eb..5792689a0 100644 --- a/api/http/handler/users/user_list.go +++ b/api/http/handler/users/user_list.go @@ -5,7 +5,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api/http/security" ) // GET request on /api/users diff --git a/api/http/handler/users/user_memberships.go b/api/http/handler/users/user_memberships.go index 2ba837aec..090880e6b 100644 --- a/api/http/handler/users/user_memberships.go +++ b/api/http/handler/users/user_memberships.go @@ -6,8 +6,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) // GET request on /api/users/:id/memberships diff --git a/api/http/handler/users/user_update.go b/api/http/handler/users/user_update.go index 4ae5e50ab..62ba24f46 100644 --- a/api/http/handler/users/user_update.go +++ b/api/http/handler/users/user_update.go @@ -6,8 +6,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) type userUpdatePayload struct { diff --git a/api/http/handler/users/user_update_password.go b/api/http/handler/users/user_update_password.go index e5a65c8e3..77ce99b49 100644 --- a/api/http/handler/users/user_update_password.go +++ b/api/http/handler/users/user_update_password.go @@ -7,8 +7,8 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) type userUpdatePasswordPayload struct { diff --git a/api/http/handler/webhooks/handler.go b/api/http/handler/webhooks/handler.go index d9b583535..f2deb2e5c 100644 --- a/api/http/handler/webhooks/handler.go +++ b/api/http/handler/webhooks/handler.go @@ -5,9 +5,9 @@ import ( "github.com/gorilla/mux" httperror "github.com/portainer/libhttp/error" - portainer "github.com/portainer/portainer" - "github.com/portainer/portainer/docker" - "github.com/portainer/portainer/http/security" + portainer "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/docker" + "github.com/portainer/portainer/api/http/security" ) // Handler is the HTTP handler used to handle webhook operations. @@ -24,11 +24,11 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { Router: mux.NewRouter(), } h.Handle("/webhooks", - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookCreate))).Methods(http.MethodPost) + bouncer.RestrictedAccess(httperror.LoggerHandler(h.webhookCreate))).Methods(http.MethodPost) h.Handle("/webhooks", - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookList))).Methods(http.MethodGet) + bouncer.RestrictedAccess(httperror.LoggerHandler(h.webhookList))).Methods(http.MethodGet) h.Handle("/webhooks/{id}", - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.webhookDelete))).Methods(http.MethodDelete) + bouncer.RestrictedAccess(httperror.LoggerHandler(h.webhookDelete))).Methods(http.MethodDelete) h.Handle("/webhooks/{token}", bouncer.PublicAccess(httperror.LoggerHandler(h.webhookExecute))).Methods(http.MethodPost) return h diff --git a/api/http/handler/webhooks/webhook_create.go b/api/http/handler/webhooks/webhook_create.go index 288435889..5a6867591 100644 --- a/api/http/handler/webhooks/webhook_create.go +++ b/api/http/handler/webhooks/webhook_create.go @@ -7,7 +7,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" "github.com/satori/go.uuid" ) diff --git a/api/http/handler/webhooks/webhook_delete.go b/api/http/handler/webhooks/webhook_delete.go index 6df0e4156..ee36f822c 100644 --- a/api/http/handler/webhooks/webhook_delete.go +++ b/api/http/handler/webhooks/webhook_delete.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // DELETE request on /api/webhook/:serviceID diff --git a/api/http/handler/webhooks/webhook_execute.go b/api/http/handler/webhooks/webhook_execute.go index 9126942e7..2e82f0e1a 100644 --- a/api/http/handler/webhooks/webhook_execute.go +++ b/api/http/handler/webhooks/webhook_execute.go @@ -9,7 +9,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // Acts on a passed in token UUID to restart the docker service @@ -39,16 +39,18 @@ func (handler *Handler) webhookExecute(w http.ResponseWriter, r *http.Request) * } else if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find an endpoint with the specified identifier inside the database", err} } + + imageTag, _ := request.RetrieveQueryParameter(r, "tag", true) + switch webhookType { case portainer.ServiceWebhook: - return handler.executeServiceWebhook(w, endpoint, resourceID) + return handler.executeServiceWebhook(w, endpoint, resourceID, imageTag) default: return &httperror.HandlerError{http.StatusInternalServerError, "Unsupported webhook type", portainer.ErrUnsupportedWebhookType} } - } -func (handler *Handler) executeServiceWebhook(w http.ResponseWriter, endpoint *portainer.Endpoint, resourceID string) *httperror.HandlerError { +func (handler *Handler) executeServiceWebhook(w http.ResponseWriter, endpoint *portainer.Endpoint, resourceID string, imageTag string) *httperror.HandlerError { dockerClient, err := handler.DockerClientFactory.CreateClient(endpoint, "") if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Error creating docker client", err} @@ -62,8 +64,14 @@ func (handler *Handler) executeServiceWebhook(w http.ResponseWriter, endpoint *p service.Spec.TaskTemplate.ForceUpdate++ - service.Spec.TaskTemplate.ContainerSpec.Image = strings.Split(service.Spec.TaskTemplate.ContainerSpec.Image, "@sha")[0] + if imageTag != "" { + service.Spec.TaskTemplate.ContainerSpec.Image = strings.Split(service.Spec.TaskTemplate.ContainerSpec.Image, ":")[0] + ":" + imageTag + } else { + service.Spec.TaskTemplate.ContainerSpec.Image = strings.Split(service.Spec.TaskTemplate.ContainerSpec.Image, "@sha")[0] + } + _, err = dockerClient.ServiceUpdate(context.Background(), resourceID, service.Version, service.Spec, dockertypes.ServiceUpdateOptions{QueryRegistry: true}) + if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "Error updating service", err} } diff --git a/api/http/handler/webhooks/webhook_list.go b/api/http/handler/webhooks/webhook_list.go index 7ed0df213..a45051884 100644 --- a/api/http/handler/webhooks/webhook_list.go +++ b/api/http/handler/webhooks/webhook_list.go @@ -6,7 +6,7 @@ import ( httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" "github.com/portainer/libhttp/response" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type webhookListOperationFilters struct { diff --git a/api/http/handler/websocket/attach.go b/api/http/handler/websocket/attach.go new file mode 100644 index 000000000..95702179c --- /dev/null +++ b/api/http/handler/websocket/attach.go @@ -0,0 +1,122 @@ +package websocket + +import ( + "net" + "net/http" + "net/http/httputil" + "time" + + "github.com/asaskevich/govalidator" + "github.com/gorilla/websocket" + httperror "github.com/portainer/libhttp/error" + "github.com/portainer/libhttp/request" + "github.com/portainer/portainer/api" +) + +// websocketAttach handles GET requests on /websocket/attach?id=&endpointId=&nodeName=&token= +// If the nodeName query parameter is present, the request will be proxied to the underlying agent endpoint. +// If the nodeName query parameter is not specified, the request will be upgraded to the websocket protocol and +// an AttachStart operation HTTP request will be created and hijacked. +// Authentication and access is controled via the mandatory token query parameter. +func (handler *Handler) websocketAttach(w http.ResponseWriter, r *http.Request) *httperror.HandlerError { + attachID, err := request.RetrieveQueryParameter(r, "id", false) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: id", err} + } + if !govalidator.IsHexadecimal(attachID) { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: id (must be hexadecimal identifier)", err} + } + + endpointID, err := request.RetrieveNumericQueryParameter(r, "endpointId", false) + if err != nil { + return &httperror.HandlerError{http.StatusBadRequest, "Invalid query parameter: endpointId", err} + } + + endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID)) + if err == portainer.ErrObjectNotFound { + return &httperror.HandlerError{http.StatusNotFound, "Unable to find the endpoint associated to the stack inside the database", err} + } else if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err} + } + + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true) + if err != nil { + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} + } + + params := &webSocketRequestParams{ + endpoint: endpoint, + ID: attachID, + nodeName: r.FormValue("nodeName"), + } + + err = handler.handleAttachRequest(w, r, params) + if err != nil { + return &httperror.HandlerError{http.StatusInternalServerError, "An error occured during websocket attach operation", err} + } + + return nil +} + +func (handler *Handler) handleAttachRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error { + + r.Header.Del("Origin") + + if params.nodeName != "" || params.endpoint.Type == portainer.AgentOnDockerEnvironment { + return handler.proxyWebsocketRequest(w, r, params) + } + + websocketConn, err := handler.connectionUpgrader.Upgrade(w, r, nil) + if err != nil { + return err + } + defer websocketConn.Close() + + return hijackAttachStartOperation(websocketConn, params.endpoint, params.ID) +} + +func hijackAttachStartOperation(websocketConn *websocket.Conn, endpoint *portainer.Endpoint, attachID string) error { + dial, err := initDial(endpoint) + if err != nil { + return err + } + + // When we set up a TCP connection for hijack, there could be long periods + // of inactivity (a long running command with no output) that in certain + // network setups may cause ECONNTIMEOUT, leaving the client in an unknown + // state. Setting TCP KeepAlive on the socket connection will prohibit + // ECONNTIMEOUT unless the socket connection truly is broken + if tcpConn, ok := dial.(*net.TCPConn); ok { + tcpConn.SetKeepAlive(true) + tcpConn.SetKeepAlivePeriod(30 * time.Second) + } + + httpConn := httputil.NewClientConn(dial, nil) + defer httpConn.Close() + + attachStartRequest, err := createAttachStartRequest(attachID) + if err != nil { + return err + } + + err = hijackRequest(websocketConn, httpConn, attachStartRequest) + if err != nil { + return err + } + + return nil +} + +func createAttachStartRequest(attachID string) (*http.Request, error) { + + request, err := http.NewRequest("POST", "/containers/"+attachID+"/attach?stdin=1&stdout=1&stderr=1&stream=1", nil) + if err != nil { + return nil, err + } + + request.Header.Set("Content-Type", "application/json") + request.Header.Set("Connection", "Upgrade") + request.Header.Set("Upgrade", "tcp") + + return request, nil +} diff --git a/api/http/handler/websocket/websocket_dial.go b/api/http/handler/websocket/dial.go similarity index 100% rename from api/http/handler/websocket/websocket_dial.go rename to api/http/handler/websocket/dial.go diff --git a/api/http/handler/websocket/websocket_dial_windows.go b/api/http/handler/websocket/dial_windows.go similarity index 99% rename from api/http/handler/websocket/websocket_dial_windows.go rename to api/http/handler/websocket/dial_windows.go index 49a9afd28..5a3cf0d0f 100644 --- a/api/http/handler/websocket/websocket_dial_windows.go +++ b/api/http/handler/websocket/dial_windows.go @@ -3,9 +3,8 @@ package websocket import ( - "net" - "github.com/Microsoft/go-winio" + "net" ) func createDial(scheme, host string) (net.Conn, error) { diff --git a/api/http/handler/websocket/websocket_exec.go b/api/http/handler/websocket/exec.go similarity index 52% rename from api/http/handler/websocket/websocket_exec.go rename to api/http/handler/websocket/exec.go index ba4a9d7e6..3b3fad969 100644 --- a/api/http/handler/websocket/websocket_exec.go +++ b/api/http/handler/websocket/exec.go @@ -1,32 +1,20 @@ package websocket import ( - "bufio" "bytes" - "crypto/tls" "encoding/json" - "fmt" "net" "net/http" "net/http/httputil" - "net/url" "time" "github.com/asaskevich/govalidator" "github.com/gorilla/websocket" - "github.com/koding/websocketproxy" httperror "github.com/portainer/libhttp/error" "github.com/portainer/libhttp/request" - "github.com/portainer/portainer" - "github.com/portainer/portainer/crypto" + "github.com/portainer/portainer/api" ) -type webSocketExecRequestParams struct { - execID string - nodeName string - endpoint *portainer.Endpoint -} - type execStartOperationPayload struct { Tty bool Detach bool @@ -58,18 +46,18 @@ func (handler *Handler) websocketExec(w http.ResponseWriter, r *http.Request) *h return &httperror.HandlerError{http.StatusInternalServerError, "Unable to find the endpoint associated to the stack inside the database", err} } - err = handler.requestBouncer.EndpointAccess(r, endpoint) + err = handler.requestBouncer.AuthorizedEndpointOperation(r, endpoint, true) if err != nil { - return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", portainer.ErrEndpointAccessDenied} + return &httperror.HandlerError{http.StatusForbidden, "Permission denied to access endpoint", err} } - params := &webSocketExecRequestParams{ + params := &webSocketRequestParams{ endpoint: endpoint, - execID: execID, + ID: execID, nodeName: r.FormValue("nodeName"), } - err = handler.handleRequest(w, r, params) + err = handler.handleExecRequest(w, r, params) if err != nil { return &httperror.HandlerError{http.StatusInternalServerError, "An error occured during websocket exec operation", err} } @@ -77,7 +65,7 @@ func (handler *Handler) websocketExec(w http.ResponseWriter, r *http.Request) *h return nil } -func (handler *Handler) handleRequest(w http.ResponseWriter, r *http.Request, params *webSocketExecRequestParams) error { +func (handler *Handler) handleExecRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error { r.Header.Del("Origin") if params.nodeName != "" || params.endpoint.Type == portainer.AgentOnDockerEnvironment { @@ -90,41 +78,7 @@ func (handler *Handler) handleRequest(w http.ResponseWriter, r *http.Request, pa } defer websocketConn.Close() - return hijackExecStartOperation(websocketConn, params.endpoint, params.execID) -} - -func (handler *Handler) proxyWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketExecRequestParams) error { - agentURL, err := url.Parse(params.endpoint.URL) - if err != nil { - return err - } - - agentURL.Scheme = "ws" - proxy := websocketproxy.NewProxy(agentURL) - - if params.endpoint.TLSConfig.TLS || params.endpoint.TLSConfig.TLSSkipVerify { - agentURL.Scheme = "wss" - proxy.Dialer = &websocket.Dialer{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: params.endpoint.TLSConfig.TLSSkipVerify, - }, - } - } - - signature, err := handler.SignatureService.CreateSignature(portainer.PortainerAgentSignatureMessage) - if err != nil { - return err - } - - proxy.Director = func(incoming *http.Request, out http.Header) { - out.Set(portainer.PortainerAgentPublicKeyHeader, handler.SignatureService.EncodedPublicKey()) - out.Set(portainer.PortainerAgentSignatureHeader, signature) - out.Set(portainer.PortainerAgentTargetHeader, params.nodeName) - } - - proxy.ServeHTTP(w, r) - - return nil + return hijackExecStartOperation(websocketConn, params.endpoint, params.ID) } func hijackExecStartOperation(websocketConn *websocket.Conn, endpoint *portainer.Endpoint, execID string) error { @@ -159,30 +113,6 @@ func hijackExecStartOperation(websocketConn *websocket.Conn, endpoint *portainer return nil } -func initDial(endpoint *portainer.Endpoint) (net.Conn, error) { - url, err := url.Parse(endpoint.URL) - if err != nil { - return nil, err - } - - host := url.Host - - if url.Scheme == "unix" || url.Scheme == "npipe" { - host = url.Path - } - - if endpoint.TLSConfig.TLS { - tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify) - if err != nil { - return nil, err - } - - return tls.Dial(url.Scheme, host, tlsConfig) - } - - return createDial(url.Scheme, host) -} - func createExecStartRequest(execID string) (*http.Request, error) { execStartOperationPayload := &execStartOperationPayload{ Tty: true, @@ -206,64 +136,3 @@ func createExecStartRequest(execID string) (*http.Request, error) { return request, nil } - -func hijackRequest(websocketConn *websocket.Conn, httpConn *httputil.ClientConn, request *http.Request) error { - // Server hijacks the connection, error 'connection closed' expected - resp, err := httpConn.Do(request) - if err != httputil.ErrPersistEOF { - if err != nil { - return err - } - if resp.StatusCode != http.StatusSwitchingProtocols { - resp.Body.Close() - return fmt.Errorf("unable to upgrade to tcp, received %d", resp.StatusCode) - } - } - - tcpConn, brw := httpConn.Hijack() - defer tcpConn.Close() - - errorChan := make(chan error, 1) - go streamFromTCPConnToWebsocketConn(websocketConn, brw, errorChan) - go streamFromWebsocketConnToTCPConn(websocketConn, tcpConn, errorChan) - - err = <-errorChan - if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) { - return err - } - - return nil -} - -func streamFromWebsocketConnToTCPConn(websocketConn *websocket.Conn, tcpConn net.Conn, errorChan chan error) { - for { - _, in, err := websocketConn.ReadMessage() - if err != nil { - errorChan <- err - break - } - - _, err = tcpConn.Write(in) - if err != nil { - errorChan <- err - break - } - } -} - -func streamFromTCPConnToWebsocketConn(websocketConn *websocket.Conn, br *bufio.Reader, errorChan chan error) { - for { - out := make([]byte, 2048) - _, err := br.Read(out) - if err != nil { - errorChan <- err - break - } - - err = websocketConn.WriteMessage(websocket.TextMessage, out) - if err != nil { - errorChan <- err - break - } - } -} diff --git a/api/http/handler/websocket/handler.go b/api/http/handler/websocket/handler.go index 3de1cbc60..853dea038 100644 --- a/api/http/handler/websocket/handler.go +++ b/api/http/handler/websocket/handler.go @@ -4,8 +4,8 @@ import ( "github.com/gorilla/mux" "github.com/gorilla/websocket" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) // Handler is the HTTP handler used to handle websocket operations. @@ -25,6 +25,8 @@ func NewHandler(bouncer *security.RequestBouncer) *Handler { requestBouncer: bouncer, } h.PathPrefix("/websocket/exec").Handler( - bouncer.AuthenticatedAccess(httperror.LoggerHandler(h.websocketExec))) + bouncer.RestrictedAccess(httperror.LoggerHandler(h.websocketExec))) + h.PathPrefix("/websocket/attach").Handler( + bouncer.RestrictedAccess(httperror.LoggerHandler(h.websocketAttach))) return h } diff --git a/api/http/handler/websocket/hijack.go b/api/http/handler/websocket/hijack.go new file mode 100644 index 000000000..f8a7b6624 --- /dev/null +++ b/api/http/handler/websocket/hijack.go @@ -0,0 +1,36 @@ +package websocket + +import ( + "fmt" + "github.com/gorilla/websocket" + "net/http" + "net/http/httputil" +) + +func hijackRequest(websocketConn *websocket.Conn, httpConn *httputil.ClientConn, request *http.Request) error { + // Server hijacks the connection, error 'connection closed' expected + resp, err := httpConn.Do(request) + if err != httputil.ErrPersistEOF { + if err != nil { + return err + } + if resp.StatusCode != http.StatusSwitchingProtocols { + resp.Body.Close() + return fmt.Errorf("unable to upgrade to tcp, received %d", resp.StatusCode) + } + } + + tcpConn, brw := httpConn.Hijack() + defer tcpConn.Close() + + errorChan := make(chan error, 1) + go streamFromTCPConnToWebsocketConn(websocketConn, brw, errorChan) + go streamFromWebsocketConnToTCPConn(websocketConn, tcpConn, errorChan) + + err = <-errorChan + if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseNoStatusReceived) { + return err + } + + return nil +} diff --git a/api/http/handler/websocket/initdial.go b/api/http/handler/websocket/initdial.go new file mode 100644 index 000000000..27663734a --- /dev/null +++ b/api/http/handler/websocket/initdial.go @@ -0,0 +1,35 @@ +package websocket + +import ( + "crypto/tls" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/crypto" + "net" + "net/url" +) + +func initDial(endpoint *portainer.Endpoint) (net.Conn, error) { + url, err := url.Parse(endpoint.URL) + if err != nil { + return nil, err + } + + host := url.Host + + if url.Scheme == "unix" || url.Scheme == "npipe" { + host = url.Path + } + + if endpoint.TLSConfig.TLS { + tlsConfig, err := crypto.CreateTLSConfigurationFromDisk(endpoint.TLSConfig.TLSCACertPath, endpoint.TLSConfig.TLSCertPath, endpoint.TLSConfig.TLSKeyPath, endpoint.TLSConfig.TLSSkipVerify) + if err != nil { + return nil, err + } + + return tls.Dial(url.Scheme, host, tlsConfig) + } + + con, err := createDial(url.Scheme, host) + + return con, err +} diff --git a/api/http/handler/websocket/proxy.go b/api/http/handler/websocket/proxy.go new file mode 100644 index 000000000..7eba78b3a --- /dev/null +++ b/api/http/handler/websocket/proxy.go @@ -0,0 +1,44 @@ +package websocket + +import ( + "crypto/tls" + "github.com/gorilla/websocket" + "github.com/koding/websocketproxy" + "github.com/portainer/portainer/api" + "net/http" + "net/url" +) + +func (handler *Handler) proxyWebsocketRequest(w http.ResponseWriter, r *http.Request, params *webSocketRequestParams) error { + agentURL, err := url.Parse(params.endpoint.URL) + if err != nil { + return err + } + + agentURL.Scheme = "ws" + proxy := websocketproxy.NewProxy(agentURL) + + if params.endpoint.TLSConfig.TLS || params.endpoint.TLSConfig.TLSSkipVerify { + agentURL.Scheme = "wss" + proxy.Dialer = &websocket.Dialer{ + TLSClientConfig: &tls.Config{ + InsecureSkipVerify: params.endpoint.TLSConfig.TLSSkipVerify, + }, + } + } + + signature, err := handler.SignatureService.CreateSignature(portainer.PortainerAgentSignatureMessage) + if err != nil { + return err + } + + proxy.Director = func(incoming *http.Request, out http.Header) { + out.Set(portainer.PortainerAgentPublicKeyHeader, handler.SignatureService.EncodedPublicKey()) + out.Set(portainer.PortainerAgentSignatureHeader, signature) + out.Set(portainer.PortainerAgentTargetHeader, params.nodeName) + } + + proxy.ServeHTTP(w, r) + + return nil +} diff --git a/api/http/handler/websocket/stream.go b/api/http/handler/websocket/stream.go new file mode 100644 index 000000000..a598b7cb7 --- /dev/null +++ b/api/http/handler/websocket/stream.go @@ -0,0 +1,59 @@ +package websocket + +import ( + "bufio" + "github.com/gorilla/websocket" + "net" + "unicode/utf8" +) + +func streamFromWebsocketConnToTCPConn(websocketConn *websocket.Conn, tcpConn net.Conn, errorChan chan error) { + for { + _, in, err := websocketConn.ReadMessage() + if err != nil { + errorChan <- err + break + } + + _, err = tcpConn.Write(in) + if err != nil { + errorChan <- err + break + } + } +} + +func streamFromTCPConnToWebsocketConn(websocketConn *websocket.Conn, br *bufio.Reader, errorChan chan error) { + for { + out := make([]byte, 2048) + _, err := br.Read(out) + if err != nil { + errorChan <- err + break + } + + processedOutput := validString(string(out[:])) + err = websocketConn.WriteMessage(websocket.TextMessage, []byte(processedOutput)) + if err != nil { + errorChan <- err + break + } + } +} + +func validString(s string) string { + if !utf8.ValidString(s) { + v := make([]rune, 0, len(s)) + for i, r := range s { + if r == utf8.RuneError { + _, size := utf8.DecodeRuneInString(s[i:]) + if size == 1 { + continue + } + } + v = append(v, r) + } + s = string(v) + } + return s +} diff --git a/api/http/handler/websocket/types.go b/api/http/handler/websocket/types.go new file mode 100644 index 000000000..abb86c7db --- /dev/null +++ b/api/http/handler/websocket/types.go @@ -0,0 +1,11 @@ +package websocket + +import ( + "github.com/portainer/portainer/api" +) + +type webSocketRequestParams struct { + ID string + nodeName string + endpoint *portainer.Endpoint +} diff --git a/api/http/proxy/access_control.go b/api/http/proxy/access_control.go index e63dc6346..fea0014ec 100644 --- a/api/http/proxy/access_control.go +++ b/api/http/proxy/access_control.go @@ -1,7 +1,7 @@ package proxy import ( - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type ( @@ -20,7 +20,7 @@ type ( // 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 *restrictedOperationContext) (map[string]interface{}, bool) { + context *restrictedDockerOperationContext) (map[string]interface{}, bool) { if labelsObject != nil && labelsObject[labelIdentifier] != nil { resourceIdentifier := labelsObject[labelIdentifier].(string) @@ -38,14 +38,14 @@ func applyResourceAccessControlFromLabel(labelsObject, resourceObject map[string // 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 *restrictedOperationContext) (map[string]interface{}, bool) { + context *restrictedDockerOperationContext) (map[string]interface{}, bool) { resourceControl := getResourceControlByResourceID(resourceIdentifier, context.resourceControls) if resourceControl == nil { - return resourceObject, context.isAdmin + return resourceObject, context.isAdmin || context.endpointResourceAccess } - if context.isAdmin || resourceControl.Public || canUserAccessResource(context.userID, context.userTeamIDs, resourceControl) { + if context.isAdmin || context.endpointResourceAccess || resourceControl.Public || canUserAccessResource(context.userID, context.userTeamIDs, resourceControl) { resourceObject = decorateObject(resourceObject, resourceControl) return resourceObject, true } diff --git a/api/http/proxy/azure_transport.go b/api/http/proxy/azure_transport.go index dccf451b5..f0752b925 100644 --- a/api/http/proxy/azure_transport.go +++ b/api/http/proxy/azure_transport.go @@ -6,8 +6,8 @@ import ( "sync" "time" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/client" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/client" ) type ( diff --git a/api/http/proxy/build.go b/api/http/proxy/build.go index aaa486f07..93f0d4a68 100644 --- a/api/http/proxy/build.go +++ b/api/http/proxy/build.go @@ -7,7 +7,7 @@ import ( "net/http" "strings" - "github.com/portainer/portainer/archive" + "github.com/portainer/portainer/api/archive" ) type postDockerfileRequest struct { diff --git a/api/http/proxy/configs.go b/api/http/proxy/configs.go index fc05dd74c..6863b5971 100644 --- a/api/http/proxy/configs.go +++ b/api/http/proxy/configs.go @@ -3,7 +3,7 @@ package proxy import ( "net/http" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) const ( @@ -24,7 +24,7 @@ func configListOperation(response *http.Response, executor *operationExecutor) e return err } - if executor.operationContext.isAdmin { + if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess { responseArray, err = decorateConfigList(responseArray, executor.operationContext.resourceControls) } else { responseArray, err = filterConfigList(responseArray, executor.operationContext) @@ -87,7 +87,7 @@ func decorateConfigList(configData []interface{}, resourceControls []portainer.R // 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 *restrictedOperationContext) ([]interface{}, error) { +func filterConfigList(configData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) { filteredConfigData := make([]interface{}, 0) for _, config := range configData { diff --git a/api/http/proxy/containers.go b/api/http/proxy/containers.go index 4e6c835ff..81bde4c4f 100644 --- a/api/http/proxy/containers.go +++ b/api/http/proxy/containers.go @@ -3,7 +3,7 @@ package proxy import ( "net/http" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) const ( @@ -26,7 +26,7 @@ func containerListOperation(response *http.Response, executor *operationExecutor return err } - if executor.operationContext.isAdmin { + if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess { responseArray, err = decorateContainerList(responseArray, executor.operationContext.resourceControls) } else { responseArray, err = filterContainerList(responseArray, executor.operationContext) @@ -137,7 +137,7 @@ func decorateContainerList(containerData []interface{}, resourceControls []porta // 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 *restrictedOperationContext) ([]interface{}, error) { +func filterContainerList(containerData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) { filteredContainerData := make([]interface{}, 0) for _, container := range containerData { diff --git a/api/http/proxy/docker_transport.go b/api/http/proxy/docker_transport.go index 7521ee337..873b099cf 100644 --- a/api/http/proxy/docker_transport.go +++ b/api/http/proxy/docker_transport.go @@ -8,8 +8,8 @@ import ( "regexp" "strings" - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) var apiVersionRe = regexp.MustCompile(`(/v[0-9]\.[0-9]*)?`) @@ -24,12 +24,14 @@ type ( DockerHubService portainer.DockerHubService SettingsService portainer.SettingsService SignatureService portainer.DigitalSignatureService + endpointIdentifier portainer.EndpointID } - restrictedOperationContext struct { - isAdmin bool - userID portainer.UserID - userTeamIDs []portainer.TeamID - resourceControls []portainer.ResourceControl + restrictedDockerOperationContext struct { + isAdmin bool + endpointResourceAccess bool + userID portainer.UserID + userTeamIDs []portainer.TeamID + resourceControls []portainer.ResourceControl } registryAccessContext struct { isAdmin bool @@ -44,7 +46,7 @@ type ( Serveraddress string `json:"serveraddress"` } operationExecutor struct { - operationContext *restrictedOperationContext + operationContext *restrictedDockerOperationContext labelBlackList []portainer.Pair } restrictedOperationRequest func(*http.Response, *operationExecutor) error @@ -460,7 +462,7 @@ func (p *proxyTransport) createRegistryAccessContext(request *http.Request) (*re return accessContext, nil } -func (p *proxyTransport) createOperationContext(request *http.Request) (*restrictedOperationContext, error) { +func (p *proxyTransport) createOperationContext(request *http.Request) (*restrictedDockerOperationContext, error) { var err error tokenData, err := security.RetrieveTokenData(request) if err != nil { @@ -472,15 +474,21 @@ func (p *proxyTransport) createOperationContext(request *http.Request) (*restric return nil, err } - operationContext := &restrictedOperationContext{ - isAdmin: true, - userID: tokenData.ID, - resourceControls: resourceControls, + operationContext := &restrictedDockerOperationContext{ + isAdmin: true, + userID: tokenData.ID, + resourceControls: resourceControls, + endpointResourceAccess: false, } if tokenData.Role != portainer.AdministratorRole { operationContext.isAdmin = false + _, ok := tokenData.EndpointAuthorizations[p.endpointIdentifier][portainer.EndpointResourcesAccess] + if ok { + operationContext.endpointResourceAccess = true + } + teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID) if err != nil { return nil, err diff --git a/api/http/proxy/factory.go b/api/http/proxy/factory.go index 90b5ad444..7308b7f34 100644 --- a/api/http/proxy/factory.go +++ b/api/http/proxy/factory.go @@ -6,8 +6,8 @@ import ( "net/http/httputil" "net/url" - "github.com/portainer/portainer" - "github.com/portainer/portainer/crypto" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/crypto" ) // AzureAPIBaseURL is the URL where Azure API requests will be proxied. @@ -40,10 +40,10 @@ func newAzureProxy(credentials *portainer.AzureCredentials) (http.Handler, error return proxy, nil } -func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portainer.TLSConfiguration, enableSignature bool) (http.Handler, error) { +func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portainer.TLSConfiguration, enableSignature bool, endpointID portainer.EndpointID) (http.Handler, error) { u.Scheme = "https" - proxy := factory.createDockerReverseProxy(u, enableSignature) + proxy := factory.createDockerReverseProxy(u, enableSignature, endpointID) config, err := crypto.CreateTLSConfigurationFromDisk(tlsConfig.TLSCACertPath, tlsConfig.TLSCertPath, tlsConfig.TLSKeyPath, tlsConfig.TLSSkipVerify) if err != nil { return nil, err @@ -53,12 +53,12 @@ func (factory *proxyFactory) newDockerHTTPSProxy(u *url.URL, tlsConfig *portaine return proxy, nil } -func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL, enableSignature bool) http.Handler { +func (factory *proxyFactory) newDockerHTTPProxy(u *url.URL, enableSignature bool, endpointID portainer.EndpointID) http.Handler { u.Scheme = "http" - return factory.createDockerReverseProxy(u, enableSignature) + return factory.createDockerReverseProxy(u, enableSignature, endpointID) } -func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, enableSignature bool) *httputil.ReverseProxy { +func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, enableSignature bool, endpointID portainer.EndpointID) *httputil.ReverseProxy { proxy := newSingleHostReverseProxyWithHostHeader(u) transport := &proxyTransport{ enableSignature: enableSignature, @@ -68,6 +68,7 @@ func (factory *proxyFactory) createDockerReverseProxy(u *url.URL, enableSignatur RegistryService: factory.RegistryService, DockerHubService: factory.DockerHubService, dockerTransport: &http.Transport{}, + endpointIdentifier: endpointID, } if enableSignature { diff --git a/api/http/proxy/factory_local.go b/api/http/proxy/factory_local.go index da154e03c..431fd5604 100644 --- a/api/http/proxy/factory_local.go +++ b/api/http/proxy/factory_local.go @@ -4,9 +4,11 @@ package proxy import ( "net/http" + + portainer "github.com/portainer/portainer/api" ) -func (factory *proxyFactory) newLocalProxy(path string) http.Handler { +func (factory *proxyFactory) newLocalProxy(path string, endpointID portainer.EndpointID) http.Handler { proxy := &localProxy{} transport := &proxyTransport{ enableSignature: false, @@ -16,6 +18,7 @@ func (factory *proxyFactory) newLocalProxy(path string) http.Handler { RegistryService: factory.RegistryService, DockerHubService: factory.DockerHubService, dockerTransport: newSocketTransport(path), + endpointIdentifier: endpointID, } proxy.Transport = transport return proxy diff --git a/api/http/proxy/factory_local_windows.go b/api/http/proxy/factory_local_windows.go index 5e98e47ac..a2105f886 100644 --- a/api/http/proxy/factory_local_windows.go +++ b/api/http/proxy/factory_local_windows.go @@ -5,11 +5,12 @@ package proxy import ( "net" "net/http" - "github.com/Microsoft/go-winio" + + portainer "github.com/portainer/portainer/api" ) -func (factory *proxyFactory) newLocalProxy(path string) http.Handler { +func (factory *proxyFactory) newLocalProxy(path string, endpointID portainer.EndpointID) http.Handler { proxy := &localProxy{} transport := &proxyTransport{ enableSignature: false, @@ -19,6 +20,7 @@ func (factory *proxyFactory) newLocalProxy(path string) http.Handler { RegistryService: factory.RegistryService, DockerHubService: factory.DockerHubService, dockerTransport: newNamedPipeTransport(path), + endpointIdentifier: endpointID, } proxy.Transport = transport return proxy diff --git a/api/http/proxy/manager.go b/api/http/proxy/manager.go index f7cbbd5dd..8d1efb404 100644 --- a/api/http/proxy/manager.go +++ b/api/http/proxy/manager.go @@ -6,7 +6,7 @@ import ( "strconv" "github.com/orcaman/concurrent-map" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // TODO: contain code related to legacy extension management @@ -14,6 +14,7 @@ import ( var extensionPorts = map[portainer.ExtensionID]string{ portainer.RegistryManagementExtension: "7001", portainer.OAuthAuthenticationExtension: "7002", + portainer.RBACExtension: "7003", } type ( @@ -104,6 +105,7 @@ func (manager *Manager) CreateExtensionProxy(extensionID portainer.ExtensionID) return proxy, nil } +// GetExtensionURL retrieves the URL of an extension running locally based on the extension port table func (manager *Manager) GetExtensionURL(extensionID portainer.ExtensionID) string { return "http://localhost:" + extensionPorts[extensionID] } @@ -130,18 +132,18 @@ func (manager *Manager) CreateLegacyExtensionProxy(key, extensionAPIURL string) } proxy := manager.proxyFactory.newHTTPProxy(extensionURL) - manager.extensionProxies.Set(key, proxy) + manager.legacyExtensionProxies.Set(key, proxy) return proxy, nil } -func (manager *Manager) createDockerProxy(endpointURL *url.URL, tlsConfig *portainer.TLSConfiguration) (http.Handler, error) { +func (manager *Manager) createDockerProxy(endpointURL *url.URL, tlsConfig *portainer.TLSConfiguration, endpointID portainer.EndpointID) (http.Handler, error) { if endpointURL.Scheme == "tcp" { if tlsConfig.TLS || tlsConfig.TLSSkipVerify { - return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, tlsConfig, false) + return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, tlsConfig, false, endpointID) } - return manager.proxyFactory.newDockerHTTPProxy(endpointURL, false), nil + return manager.proxyFactory.newDockerHTTPProxy(endpointURL, false, endpointID), nil } - return manager.proxyFactory.newLocalProxy(endpointURL.Path), nil + return manager.proxyFactory.newLocalProxy(endpointURL.Path, endpointID), nil } func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler, error) { @@ -152,10 +154,10 @@ func (manager *Manager) createProxy(endpoint *portainer.Endpoint) (http.Handler, switch endpoint.Type { case portainer.AgentOnDockerEnvironment: - return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, true) + return manager.proxyFactory.newDockerHTTPSProxy(endpointURL, &endpoint.TLSConfig, true, endpoint.ID) case portainer.AzureEnvironment: return newAzureProxy(&endpoint.AzureCredentials) default: - return manager.createDockerProxy(endpointURL, &endpoint.TLSConfig) + return manager.createDockerProxy(endpointURL, &endpoint.TLSConfig, endpoint.ID) } } diff --git a/api/http/proxy/networks.go b/api/http/proxy/networks.go index a131ab2ab..084f8604a 100644 --- a/api/http/proxy/networks.go +++ b/api/http/proxy/networks.go @@ -3,7 +3,7 @@ package proxy import ( "net/http" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) const ( @@ -24,7 +24,7 @@ func networkListOperation(response *http.Response, executor *operationExecutor) return err } - if executor.operationContext.isAdmin { + if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess { responseArray, err = decorateNetworkList(responseArray, executor.operationContext.resourceControls) } else { responseArray, err = filterNetworkList(responseArray, executor.operationContext) @@ -110,7 +110,7 @@ func decorateNetworkList(networkData []interface{}, resourceControls []portainer // 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 *restrictedOperationContext) ([]interface{}, error) { +func filterNetworkList(networkData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) { filteredNetworkData := make([]interface{}, 0) for _, network := range networkData { diff --git a/api/http/proxy/registry.go b/api/http/proxy/registry.go index 5edeb73b7..5f614f29d 100644 --- a/api/http/proxy/registry.go +++ b/api/http/proxy/registry.go @@ -1,8 +1,8 @@ package proxy import ( - "github.com/portainer/portainer" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/http/security" ) func createRegistryAuthenticationHeader(serverAddress string, accessContext *registryAccessContext) *registryAuthenticationHeader { diff --git a/api/http/proxy/response.go b/api/http/proxy/response.go index 3be65bbc6..a974d8f25 100644 --- a/api/http/proxy/response.go +++ b/api/http/proxy/response.go @@ -8,7 +8,7 @@ import ( "net/http" "strconv" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) const ( diff --git a/api/http/proxy/secrets.go b/api/http/proxy/secrets.go index 9c4d8b093..67af61936 100644 --- a/api/http/proxy/secrets.go +++ b/api/http/proxy/secrets.go @@ -3,7 +3,7 @@ package proxy import ( "net/http" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) const ( @@ -24,7 +24,7 @@ func secretListOperation(response *http.Response, executor *operationExecutor) e return err } - if executor.operationContext.isAdmin { + if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess { responseArray, err = decorateSecretList(responseArray, executor.operationContext.resourceControls) } else { responseArray, err = filterSecretList(responseArray, executor.operationContext) @@ -87,7 +87,7 @@ func decorateSecretList(secretData []interface{}, resourceControls []portainer.R // Authorized secrets are decorated during the process. // Resource controls checks are based on: resource identifier. // Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList -func filterSecretList(secretData []interface{}, context *restrictedOperationContext) ([]interface{}, error) { +func filterSecretList(secretData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) { filteredSecretData := make([]interface{}, 0) for _, secret := range secretData { diff --git a/api/http/proxy/services.go b/api/http/proxy/services.go index b11208b58..b14b50a47 100644 --- a/api/http/proxy/services.go +++ b/api/http/proxy/services.go @@ -3,7 +3,7 @@ package proxy import ( "net/http" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) const ( @@ -24,7 +24,7 @@ func serviceListOperation(response *http.Response, executor *operationExecutor) return err } - if executor.operationContext.isAdmin { + if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess { responseArray, err = decorateServiceList(responseArray, executor.operationContext.resourceControls) } else { responseArray, err = filterServiceList(responseArray, executor.operationContext) @@ -118,7 +118,7 @@ func decorateServiceList(serviceData []interface{}, resourceControls []portainer // 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 *restrictedOperationContext) ([]interface{}, error) { +func filterServiceList(serviceData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) { filteredServiceData := make([]interface{}, 0) for _, service := range serviceData { diff --git a/api/http/proxy/tasks.go b/api/http/proxy/tasks.go index d75ce54ec..1da630ff7 100644 --- a/api/http/proxy/tasks.go +++ b/api/http/proxy/tasks.go @@ -3,7 +3,7 @@ package proxy import ( "net/http" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) const ( @@ -25,7 +25,7 @@ func taskListOperation(response *http.Response, executor *operationExecutor) err return err } - if !executor.operationContext.isAdmin { + if !executor.operationContext.isAdmin && !executor.operationContext.endpointResourceAccess { responseArray, err = filterTaskList(responseArray, executor.operationContext) if err != nil { return err @@ -54,7 +54,7 @@ func extractTaskLabelsFromTaskListObject(responseObject map[string]interface{}) // 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 *restrictedOperationContext) ([]interface{}, error) { +func filterTaskList(taskData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) { filteredTaskData := make([]interface{}, 0) for _, task := range taskData { diff --git a/api/http/proxy/volumes.go b/api/http/proxy/volumes.go index 82f1b5cf6..59099d9ed 100644 --- a/api/http/proxy/volumes.go +++ b/api/http/proxy/volumes.go @@ -3,7 +3,7 @@ package proxy import ( "net/http" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) const ( @@ -29,7 +29,7 @@ func volumeListOperation(response *http.Response, executor *operationExecutor) e if responseObject["Volumes"] != nil { volumeData := responseObject["Volumes"].([]interface{}) - if executor.operationContext.isAdmin { + if executor.operationContext.isAdmin || executor.operationContext.endpointResourceAccess { volumeData, err = decorateVolumeList(volumeData, executor.operationContext.resourceControls) } else { volumeData, err = filterVolumeList(volumeData, executor.operationContext) @@ -119,7 +119,7 @@ func decorateVolumeList(volumeData []interface{}, resourceControls []portainer.R // 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 *restrictedOperationContext) ([]interface{}, error) { +func filterVolumeList(volumeData []interface{}, context *restrictedDockerOperationContext) ([]interface{}, error) { filteredVolumeData := make([]interface{}, 0) for _, volume := range volumeData { diff --git a/api/http/security/authorization.go b/api/http/security/authorization.go index 4a0bac587..d2a0550c6 100644 --- a/api/http/security/authorization.go +++ b/api/http/security/authorization.go @@ -1,7 +1,7 @@ package security import ( - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // AuthorizedResourceControlDeletion ensure that the user can delete a resource control object. @@ -79,7 +79,7 @@ func AuthorizedResourceControlUpdate(resourceControl *portainer.ResourceControl, // * the Public flag is set false // * 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 tp 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 func AuthorizedResourceControlCreation(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool { if context.IsAdmin || resourceControl.Public { @@ -146,9 +146,9 @@ func AuthorizedUserManagement(userID portainer.UserID, context *RestrictedReques // 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. func authorizedEndpointAccess(endpoint *portainer.Endpoint, endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool { - groupAccess := authorizedAccess(userID, memberships, endpointGroup.AuthorizedUsers, endpointGroup.AuthorizedTeams) + groupAccess := authorizedAccess(userID, memberships, endpointGroup.UserAccessPolicies, endpointGroup.TeamAccessPolicies) if !groupAccess { - return authorizedAccess(userID, memberships, endpoint.AuthorizedUsers, endpoint.AuthorizedTeams) + return authorizedAccess(userID, memberships, endpoint.UserAccessPolicies, endpoint.TeamAccessPolicies) } return true } @@ -157,28 +157,28 @@ func authorizedEndpointAccess(endpoint *portainer.Endpoint, endpointGroup *porta // It will check if the user is part of the authorized users or part of a team that is // listed in the authorized teams. func authorizedEndpointGroupAccess(endpointGroup *portainer.EndpointGroup, userID portainer.UserID, memberships []portainer.TeamMembership) bool { - return authorizedAccess(userID, memberships, endpointGroup.AuthorizedUsers, endpointGroup.AuthorizedTeams) + return authorizedAccess(userID, memberships, endpointGroup.UserAccessPolicies, endpointGroup.TeamAccessPolicies) } // AuthorizedRegistryAccess ensure that the user can access the specified registry. // It will check if the user is part of the authorized users or part of a team that is // listed in the authorized teams. func AuthorizedRegistryAccess(registry *portainer.Registry, userID portainer.UserID, memberships []portainer.TeamMembership) bool { - return authorizedAccess(userID, memberships, registry.AuthorizedUsers, registry.AuthorizedTeams) + return authorizedAccess(userID, memberships, registry.UserAccessPolicies, registry.TeamAccessPolicies) } -func authorizedAccess(userID portainer.UserID, memberships []portainer.TeamMembership, authorizedUsers []portainer.UserID, authorizedTeams []portainer.TeamID) bool { - for _, authorizedUserID := range authorizedUsers { - if authorizedUserID == userID { +func authorizedAccess(userID portainer.UserID, memberships []portainer.TeamMembership, userAccessPolicies portainer.UserAccessPolicies, teamAccessPolicies portainer.TeamAccessPolicies) bool { + _, userAccess := userAccessPolicies[userID] + if userAccess { + return true + } + + for _, membership := range memberships { + _, teamAccess := teamAccessPolicies[membership.TeamID] + if teamAccess { return true } } - for _, membership := range memberships { - for _, authorizedTeamID := range authorizedTeams { - if membership.TeamID == authorizedTeamID { - return true - } - } - } + return false } diff --git a/api/http/security/bouncer.go b/api/http/security/bouncer.go index fa88612a8..ad2152c77 100644 --- a/api/http/security/bouncer.go +++ b/api/http/security/bouncer.go @@ -2,7 +2,7 @@ package security import ( httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" "net/http" "strings" @@ -14,7 +14,10 @@ type ( jwtService portainer.JWTService userService portainer.UserService teamMembershipService portainer.TeamMembershipService + endpointService portainer.EndpointService endpointGroupService portainer.EndpointGroupService + extensionService portainer.ExtensionService + rbacExtensionClient *rbacExtensionClient authDisabled bool } @@ -23,7 +26,10 @@ type ( JWTService portainer.JWTService UserService portainer.UserService TeamMembershipService portainer.TeamMembershipService + EndpointService portainer.EndpointService EndpointGroupService portainer.EndpointGroupService + ExtensionService portainer.ExtensionService + RBACExtensionURL string AuthDisabled bool } @@ -43,48 +49,49 @@ func NewRequestBouncer(parameters *RequestBouncerParams) *RequestBouncer { jwtService: parameters.JWTService, userService: parameters.UserService, teamMembershipService: parameters.TeamMembershipService, + endpointService: parameters.EndpointService, endpointGroupService: parameters.EndpointGroupService, + extensionService: parameters.ExtensionService, + rbacExtensionClient: newRBACExtensionClient(parameters.RBACExtensionURL), authDisabled: parameters.AuthDisabled, } } -// PublicAccess defines a security check for public endpoints. +// PublicAccess defines a security check for public API endpoints. // No authentication is required to access these endpoints. func (bouncer *RequestBouncer) PublicAccess(h http.Handler) http.Handler { h = mwSecureHeaders(h) return h } -// AuthenticatedAccess defines a security check for private endpoints. +// AuthorizedAccess defines a security check for API endpoints that require an authorization check. // Authentication is required to access these endpoints. -func (bouncer *RequestBouncer) AuthenticatedAccess(h http.Handler) http.Handler { - h = bouncer.mwCheckAuthentication(h) - h = mwSecureHeaders(h) +// If the RBAC extension is enabled, authorizations are required to use these endpoints. +// If the RBAC extension is not enabled, the administrator role is required to use these endpoints. +func (bouncer *RequestBouncer) AuthorizedAccess(h http.Handler) http.Handler { + h = bouncer.mwUpgradeToRestrictedRequest(h) + h = bouncer.mwCheckPortainerAuthorizations(h) + h = bouncer.mwAuthenticatedUser(h) return h } -// RestrictedAccess defines a security check for restricted endpoints. +// RestrictedAccess defines a security check for restricted API endpoints. // Authentication is required to access these endpoints. // The request context will be enhanced with a RestrictedRequestContext object -// that might be used later to authorize/filter access to resources. +// that might be used later to authorize/filter access to resources inside an endpoint. func (bouncer *RequestBouncer) RestrictedAccess(h http.Handler) http.Handler { h = bouncer.mwUpgradeToRestrictedRequest(h) - h = bouncer.AuthenticatedAccess(h) + h = bouncer.mwAuthenticatedUser(h) return h } -// AdministratorAccess defines a chain of middleware for restricted endpoints. -// Authentication as well as administrator role are required to access these endpoints. -func (bouncer *RequestBouncer) AdministratorAccess(h http.Handler) http.Handler { - h = mwCheckAdministratorRole(h) - h = bouncer.AuthenticatedAccess(h) - return h -} - -// EndpointAccess retrieves the JWT token from the request context and verifies +// AuthorizedEndpointOperation retrieves the JWT token from the request context and verifies // that the user can access the specified endpoint. -// An error is returned when access is denied. -func (bouncer *RequestBouncer) EndpointAccess(r *http.Request, endpoint *portainer.Endpoint) error { +// If the RBAC extension is enabled and the authorizationCheck flag is set, +// it will also validate that the user can execute the specified operation. +// An error is returned when access to the endpoint is denied or if the user do not have the required +// authorization to execute the operation. +func (bouncer *RequestBouncer) AuthorizedEndpointOperation(r *http.Request, endpoint *portainer.Endpoint, authorizationCheck bool) error { tokenData, err := RetrieveTokenData(r) if err != nil { return err @@ -108,9 +115,43 @@ func (bouncer *RequestBouncer) EndpointAccess(r *http.Request, endpoint *portain return portainer.ErrEndpointAccessDenied } + if authorizationCheck { + err = bouncer.checkEndpointOperationAuthorization(r, endpoint) + if err != nil { + return portainer.ErrAuthorizationRequired + } + } + return nil } +func (bouncer *RequestBouncer) checkEndpointOperationAuthorization(r *http.Request, endpoint *portainer.Endpoint) error { + tokenData, err := RetrieveTokenData(r) + if err != nil { + return err + } + + if tokenData.Role == portainer.AdministratorRole { + return nil + } + + extension, err := bouncer.extensionService.Extension(portainer.RBACExtension) + if err == portainer.ErrObjectNotFound { + return nil + } else if err != nil { + return err + } + + apiOperation := &portainer.APIOperationAuthorizationRequest{ + Path: r.URL.String(), + Method: r.Method, + Authorizations: tokenData.EndpointAuthorizations[endpoint.ID], + } + + bouncer.rbacExtensionClient.setLicenseKey(extension.License.LicenseKey) + return bouncer.rbacExtensionClient.checkAuthorization(apiOperation) +} + // RegistryAccess retrieves the JWT token from the request context and verifies // that the user can access the specified registry. // An error is returned when access is denied. @@ -136,11 +177,50 @@ func (bouncer *RequestBouncer) RegistryAccess(r *http.Request, registry *portain return nil } -// mwSecureHeaders provides secure headers middleware for handlers. -func mwSecureHeaders(next http.Handler) http.Handler { +func (bouncer *RequestBouncer) mwAuthenticatedUser(h http.Handler) http.Handler { + h = bouncer.mwCheckAuthentication(h) + h = mwSecureHeaders(h) + return h +} + +// mwCheckPortainerAuthorizations will verify that the user has the required authorization to access +// a specific API endpoint. It will leverage the RBAC extension authorization validation if the extension +// is enabled. +func (bouncer *RequestBouncer) mwCheckPortainerAuthorizations(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Add("X-XSS-Protection", "1; mode=block") - w.Header().Add("X-Content-Type-Options", "nosniff") + tokenData, err := RetrieveTokenData(r) + if err != nil { + httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrResourceAccessDenied) + return + } + + if tokenData.Role == portainer.AdministratorRole { + next.ServeHTTP(w, r) + return + } + + extension, err := bouncer.extensionService.Extension(portainer.RBACExtension) + if err == portainer.ErrObjectNotFound { + next.ServeHTTP(w, r) + return + } else if err != nil { + httperror.WriteError(w, http.StatusInternalServerError, "Unable to find a extension with the specified identifier inside the database", err) + return + } + + apiOperation := &portainer.APIOperationAuthorizationRequest{ + Path: r.URL.String(), + Method: r.Method, + Authorizations: tokenData.PortainerAuthorizations, + } + + bouncer.rbacExtensionClient.setLicenseKey(extension.License.LicenseKey) + err = bouncer.rbacExtensionClient.checkAuthorization(apiOperation) + if err != nil { + httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrAuthorizationRequired) + return + } + next.ServeHTTP(w, r) }) } @@ -166,19 +246,6 @@ func (bouncer *RequestBouncer) mwUpgradeToRestrictedRequest(next http.Handler) h }) } -// mwCheckAdministratorRole check the role of the user associated to the request -func mwCheckAdministratorRole(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - tokenData, err := RetrieveTokenData(r) - if err != nil || tokenData.Role != portainer.AdministratorRole { - httperror.WriteError(w, http.StatusForbidden, "Access denied", portainer.ErrResourceAccessDenied) - return - } - - next.ServeHTTP(w, r) - }) -} - // mwCheckAuthentication provides Authentication middleware for handlers func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -229,6 +296,15 @@ func (bouncer *RequestBouncer) mwCheckAuthentication(next http.Handler) http.Han }) } +// mwSecureHeaders provides secure headers middleware for handlers. +func mwSecureHeaders(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Add("X-XSS-Protection", "1; mode=block") + w.Header().Add("X-Content-Type-Options", "nosniff") + next.ServeHTTP(w, r) + }) +} + func (bouncer *RequestBouncer) newRestrictedContextRequest(userID portainer.UserID, userRole portainer.UserRole) (*RestrictedRequestContext, error) { requestContext := &RestrictedRequestContext{ IsAdmin: true, diff --git a/api/http/security/context.go b/api/http/security/context.go index 4f6141768..58daaa9e3 100644 --- a/api/http/security/context.go +++ b/api/http/security/context.go @@ -4,7 +4,7 @@ import ( "context" "net/http" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) type ( diff --git a/api/http/security/filter.go b/api/http/security/filter.go index 4d44043b3..ba7872c39 100644 --- a/api/http/security/filter.go +++ b/api/http/security/filter.go @@ -1,6 +1,8 @@ package security -import "github.com/portainer/portainer" +import ( + "github.com/portainer/portainer/api" +) // FilterUserTeams filters teams based on user role. // non-administrator users only have access to team they are member of. @@ -78,7 +80,7 @@ func FilterRegistries(registries []portainer.Registry, context *RestrictedReques } // FilterTemplates filters templates based on the user role. -// Non-administrato template do not have access to templates where the AdministratorOnly flag is set to true. +// Non-administrator template do not have access to templates where the AdministratorOnly flag is set to true. func FilterTemplates(templates []portainer.Template, context *RestrictedRequestContext) []portainer.Template { filteredTemplates := templates diff --git a/api/http/security/rate_limiter.go b/api/http/security/rate_limiter.go index 307329723..e8cc7ae5c 100644 --- a/api/http/security/rate_limiter.go +++ b/api/http/security/rate_limiter.go @@ -7,7 +7,7 @@ import ( "github.com/g07cha/defender" httperror "github.com/portainer/libhttp/error" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // RateLimiter represents an entity that manages request rate limiting diff --git a/api/http/security/rbac.go b/api/http/security/rbac.go new file mode 100644 index 000000000..08366cb72 --- /dev/null +++ b/api/http/security/rbac.go @@ -0,0 +1,59 @@ +package security + +import ( + "encoding/json" + "net/http" + "time" + + portainer "github.com/portainer/portainer/api" +) + +const ( + defaultHTTPTimeout = 5 +) + +type rbacExtensionClient struct { + httpClient *http.Client + extensionURL string + licenseKey string +} + +func newRBACExtensionClient(extensionURL string) *rbacExtensionClient { + return &rbacExtensionClient{ + extensionURL: extensionURL, + httpClient: &http.Client{ + Timeout: time.Second * time.Duration(defaultHTTPTimeout), + }, + } +} + +func (client *rbacExtensionClient) setLicenseKey(licenseKey string) { + client.licenseKey = licenseKey +} + +func (client *rbacExtensionClient) checkAuthorization(authRequest *portainer.APIOperationAuthorizationRequest) error { + encodedAuthRequest, err := json.Marshal(authRequest) + if err != nil { + return err + } + + req, err := http.NewRequest("GET", client.extensionURL+"/authorized_operation", nil) + if err != nil { + return err + } + + req.Header.Set("X-RBAC-AuthorizationRequest", string(encodedAuthRequest)) + req.Header.Set("X-PortainerExtension-License", client.licenseKey) + + resp, err := client.httpClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusNoContent { + return portainer.ErrAuthorizationRequired + } + + return nil +} diff --git a/api/http/server.go b/api/http/server.go index 7f3368f25..b232adcdc 100644 --- a/api/http/server.go +++ b/api/http/server.go @@ -3,33 +3,35 @@ package http import ( "time" - "github.com/portainer/portainer" - "github.com/portainer/portainer/docker" - "github.com/portainer/portainer/http/handler" - "github.com/portainer/portainer/http/handler/auth" - "github.com/portainer/portainer/http/handler/dockerhub" - "github.com/portainer/portainer/http/handler/endpointgroups" - "github.com/portainer/portainer/http/handler/endpointproxy" - "github.com/portainer/portainer/http/handler/endpoints" - "github.com/portainer/portainer/http/handler/extensions" - "github.com/portainer/portainer/http/handler/file" - "github.com/portainer/portainer/http/handler/motd" - "github.com/portainer/portainer/http/handler/registries" - "github.com/portainer/portainer/http/handler/resourcecontrols" - "github.com/portainer/portainer/http/handler/schedules" - "github.com/portainer/portainer/http/handler/settings" - "github.com/portainer/portainer/http/handler/stacks" - "github.com/portainer/portainer/http/handler/status" - "github.com/portainer/portainer/http/handler/tags" - "github.com/portainer/portainer/http/handler/teammemberships" - "github.com/portainer/portainer/http/handler/teams" - "github.com/portainer/portainer/http/handler/templates" - "github.com/portainer/portainer/http/handler/upload" - "github.com/portainer/portainer/http/handler/users" - "github.com/portainer/portainer/http/handler/webhooks" - "github.com/portainer/portainer/http/handler/websocket" - "github.com/portainer/portainer/http/proxy" - "github.com/portainer/portainer/http/security" + "github.com/portainer/portainer/api/http/handler/roles" + + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/docker" + "github.com/portainer/portainer/api/http/handler" + "github.com/portainer/portainer/api/http/handler/auth" + "github.com/portainer/portainer/api/http/handler/dockerhub" + "github.com/portainer/portainer/api/http/handler/endpointgroups" + "github.com/portainer/portainer/api/http/handler/endpointproxy" + "github.com/portainer/portainer/api/http/handler/endpoints" + "github.com/portainer/portainer/api/http/handler/extensions" + "github.com/portainer/portainer/api/http/handler/file" + "github.com/portainer/portainer/api/http/handler/motd" + "github.com/portainer/portainer/api/http/handler/registries" + "github.com/portainer/portainer/api/http/handler/resourcecontrols" + "github.com/portainer/portainer/api/http/handler/schedules" + "github.com/portainer/portainer/api/http/handler/settings" + "github.com/portainer/portainer/api/http/handler/stacks" + "github.com/portainer/portainer/api/http/handler/status" + "github.com/portainer/portainer/api/http/handler/tags" + "github.com/portainer/portainer/api/http/handler/teammemberships" + "github.com/portainer/portainer/api/http/handler/teams" + "github.com/portainer/portainer/api/http/handler/templates" + "github.com/portainer/portainer/api/http/handler/upload" + "github.com/portainer/portainer/api/http/handler/users" + "github.com/portainer/portainer/api/http/handler/webhooks" + "github.com/portainer/portainer/api/http/handler/websocket" + "github.com/portainer/portainer/api/http/proxy" + "github.com/portainer/portainer/api/http/security" "net/http" "path/filepath" @@ -48,6 +50,7 @@ type Server struct { SignatureService portainer.DigitalSignatureService JobScheduler portainer.JobScheduler Snapshotter portainer.Snapshotter + RoleService portainer.RoleService DockerHubService portainer.DockerHubService EndpointService portainer.EndpointService EndpointGroupService portainer.EndpointGroupService @@ -78,15 +81,6 @@ type Server struct { // Start starts the HTTP server func (server *Server) Start() error { - requestBouncerParameters := &security.RequestBouncerParams{ - JWTService: server.JWTService, - UserService: server.UserService, - TeamMembershipService: server.TeamMembershipService, - EndpointGroupService: server.EndpointGroupService, - AuthDisabled: server.AuthDisabled, - } - requestBouncer := security.NewRequestBouncer(requestBouncerParameters) - proxyManagerParameters := &proxy.ManagerParams{ ResourceControlService: server.ResourceControlService, TeamMembershipService: server.TeamMembershipService, @@ -97,6 +91,18 @@ func (server *Server) Start() error { } proxyManager := proxy.NewManager(proxyManagerParameters) + requestBouncerParameters := &security.RequestBouncerParams{ + JWTService: server.JWTService, + UserService: server.UserService, + TeamMembershipService: server.TeamMembershipService, + EndpointService: server.EndpointService, + EndpointGroupService: server.EndpointGroupService, + ExtensionService: server.ExtensionService, + RBACExtensionURL: proxyManager.GetExtensionURL(portainer.RBACExtension), + AuthDisabled: server.AuthDisabled, + } + requestBouncer := security.NewRequestBouncer(requestBouncerParameters) + rateLimiter := security.NewRateLimiter(10, 1*time.Second, 1*time.Hour) var authHandler = auth.NewHandler(requestBouncer, rateLimiter, server.AuthDisabled) @@ -108,8 +114,14 @@ func (server *Server) Start() error { authHandler.TeamService = server.TeamService authHandler.TeamMembershipService = server.TeamMembershipService authHandler.ExtensionService = server.ExtensionService + authHandler.EndpointService = server.EndpointService + authHandler.EndpointGroupService = server.EndpointGroupService + authHandler.RoleService = server.RoleService authHandler.ProxyManager = proxyManager + var roleHandler = roles.NewHandler(requestBouncer) + roleHandler.RoleService = server.RoleService + var dockerHubHandler = dockerhub.NewHandler(requestBouncer) dockerHubHandler.DockerHubService = server.DockerHubService @@ -136,6 +148,9 @@ func (server *Server) Start() error { var extensionHandler = extensions.NewHandler(requestBouncer) extensionHandler.ExtensionService = server.ExtensionService extensionHandler.ExtensionManager = server.ExtensionManager + extensionHandler.EndpointGroupService = server.EndpointGroupService + extensionHandler.EndpointService = server.EndpointService + extensionHandler.RegistryService = server.RegistryService var registryHandler = registries.NewHandler(requestBouncer) registryHandler.RegistryService = server.RegistryService @@ -208,6 +223,7 @@ func (server *Server) Start() error { webhookHandler.DockerClientFactory = server.DockerClientFactory server.Handler = &handler.Handler{ + RoleHandler: roleHandler, AuthHandler: authHandler, DockerHubHandler: dockerHubHandler, EndpointGroupHandler: endpointGroupHandler, diff --git a/api/jwt/jwt.go b/api/jwt/jwt.go index 9d14be4bc..9e18a4bbb 100644 --- a/api/jwt/jwt.go +++ b/api/jwt/jwt.go @@ -1,7 +1,7 @@ package jwt import ( - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" "fmt" "time" @@ -16,9 +16,11 @@ type Service struct { } type claims struct { - UserID int `json:"id"` - Username string `json:"username"` - Role int `json:"role"` + UserID int `json:"id"` + Username string `json:"username"` + Role int `json:"role"` + EndpointAuthorizations portainer.EndpointAuthorizations `json:"endpointAuthorizations"` + PortainerAuthorizations portainer.Authorizations `json:"portainerAuthorizations"` jwt.StandardClaims } @@ -41,6 +43,8 @@ func (service *Service) GenerateToken(data *portainer.TokenData) (string, error) int(data.ID), data.Username, int(data.Role), + data.EndpointAuthorizations, + data.PortainerAuthorizations, jwt.StandardClaims{ ExpiresAt: expireToken, }, @@ -67,9 +71,11 @@ func (service *Service) ParseAndVerifyToken(token string) (*portainer.TokenData, if err == nil && parsedToken != nil { if cl, ok := parsedToken.Claims.(*claims); ok && parsedToken.Valid { tokenData := &portainer.TokenData{ - ID: portainer.UserID(cl.UserID), - Username: cl.Username, - Role: portainer.UserRole(cl.Role), + ID: portainer.UserID(cl.UserID), + Username: cl.Username, + Role: portainer.UserRole(cl.Role), + EndpointAuthorizations: cl.EndpointAuthorizations, + PortainerAuthorizations: cl.PortainerAuthorizations, } return tokenData, nil } diff --git a/api/ldap/ldap.go b/api/ldap/ldap.go index 05c9d55d7..c86f75508 100644 --- a/api/ldap/ldap.go +++ b/api/ldap/ldap.go @@ -4,8 +4,8 @@ import ( "fmt" "strings" - "github.com/portainer/portainer" - "github.com/portainer/portainer/crypto" + "github.com/portainer/portainer/api" + "github.com/portainer/portainer/api/crypto" "gopkg.in/ldap.v2" ) diff --git a/api/libcompose/compose_stack.go b/api/libcompose/compose_stack.go index 2759f01cf..bcd211a6b 100644 --- a/api/libcompose/compose_stack.go +++ b/api/libcompose/compose_stack.go @@ -12,7 +12,7 @@ import ( "github.com/portainer/libcompose/lookup" "github.com/portainer/libcompose/project" "github.com/portainer/libcompose/project/options" - "github.com/portainer/portainer" + "github.com/portainer/portainer/api" ) // ComposeStackManager represents a service for managing compose stacks. diff --git a/api/portainer.go b/api/portainer.go index 0b2eff4f4..3d6e73739 100644 --- a/api/portainer.go +++ b/api/portainer.go @@ -113,10 +113,11 @@ type ( // User represents a user account User struct { - ID UserID `json:"Id"` - Username string `json:"Username"` - Password string `json:"Password,omitempty"` - Role UserRole `json:"Role"` + ID UserID `json:"Id"` + Username string `json:"Username"` + Password string `json:"Password,omitempty"` + Role UserRole `json:"Role"` + PortainerAuthorizations Authorizations `json:"PortainerAuthorizations"` } // UserID represents a user identifier @@ -154,9 +155,11 @@ type ( // TokenData represents the data embedded in a JWT token TokenData struct { - ID UserID - Username string - Role UserRole + ID UserID + Username string + Role UserRole + EndpointAuthorizations EndpointAuthorizations + PortainerAuthorizations Authorizations } // StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier) @@ -193,9 +196,14 @@ type ( Authentication bool `json:"Authentication"` Username string `json:"Username"` Password string `json:"Password,omitempty"` - AuthorizedUsers []UserID `json:"AuthorizedUsers"` - AuthorizedTeams []TeamID `json:"AuthorizedTeams"` ManagementConfiguration *RegistryManagementConfiguration `json:"ManagementConfiguration"` + UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` + TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"` + + // Deprecated fields + // Deprecated in DBVersion == 18 + AuthorizedUsers []UserID `json:"AuthorizedUsers"` + AuthorizedTeams []TeamID `json:"AuthorizedTeams"` } // RegistryManagementConfiguration represents a configuration that can be used to query @@ -228,20 +236,20 @@ type ( // Endpoint represents a Docker endpoint with all the info required // to connect to it Endpoint struct { - ID EndpointID `json:"Id"` - Name string `json:"Name"` - Type EndpointType `json:"Type"` - URL string `json:"URL"` - GroupID EndpointGroupID `json:"GroupId"` - PublicURL string `json:"PublicURL"` - TLSConfig TLSConfiguration `json:"TLSConfig"` - AuthorizedUsers []UserID `json:"AuthorizedUsers"` - AuthorizedTeams []TeamID `json:"AuthorizedTeams"` - Extensions []EndpointExtension `json:"Extensions"` - AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"` - Tags []string `json:"Tags"` - Status EndpointStatus `json:"Status"` - Snapshots []Snapshot `json:"Snapshots"` + ID EndpointID `json:"Id"` + Name string `json:"Name"` + Type EndpointType `json:"Type"` + URL string `json:"URL"` + GroupID EndpointGroupID `json:"GroupId"` + PublicURL string `json:"PublicURL"` + TLSConfig TLSConfiguration `json:"TLSConfig"` + Extensions []EndpointExtension `json:"Extensions"` + AzureCredentials AzureCredentials `json:"AzureCredentials,omitempty"` + Tags []string `json:"Tags"` + Status EndpointStatus `json:"Status"` + Snapshots []Snapshot `json:"Snapshots"` + UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` + TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"` // Deprecated fields // Deprecated in DBVersion == 4 @@ -249,8 +257,50 @@ type ( TLSCACertPath string `json:"TLSCACert,omitempty"` TLSCertPath string `json:"TLSCert,omitempty"` TLSKeyPath string `json:"TLSKey,omitempty"` + + // Deprecated in DBVersion == 18 + AuthorizedUsers []UserID `json:"AuthorizedUsers"` + AuthorizedTeams []TeamID `json:"AuthorizedTeams"` } + // Authorization represents an authorization associated to an operation + Authorization string + + // Authorizations represents a set of authorizations associated to a role + Authorizations map[Authorization]bool + + // EndpointAuthorizations represents the authorizations associated to a set of endpoints + EndpointAuthorizations map[EndpointID]Authorizations + + // APIOperationAuthorizationRequest represent an request for the authorization to execute an API operation + APIOperationAuthorizationRequest struct { + Path string + Method string + Authorizations Authorizations + } + + // RoleID represents a role identifier + RoleID int + + // Role represents a set of authorizations that can be associated to a user or + // to a team. + Role struct { + ID RoleID `json:"Id"` + Name string `json:"Name"` + Description string `json:"Description"` + Authorizations Authorizations `json:"Authorizations"` + } + + // AccessPolicy represent a policy that can be associated to a user or team + AccessPolicy struct { + RoleID RoleID `json:"RoleId"` + } + + // UserAccessPolicies represent the association of an access policy and a user + UserAccessPolicies map[UserID]AccessPolicy + // TeamAccessPolicies represent the association of an access policy and a team + TeamAccessPolicies map[TeamID]AccessPolicy + // ScheduleID represents a schedule identifier. ScheduleID int @@ -342,15 +392,19 @@ type ( // EndpointGroup represents a group of endpoints EndpointGroup struct { - ID EndpointGroupID `json:"Id"` - Name string `json:"Name"` - Description string `json:"Description"` - AuthorizedUsers []UserID `json:"AuthorizedUsers"` - AuthorizedTeams []TeamID `json:"AuthorizedTeams"` - Tags []string `json:"Tags"` + ID EndpointGroupID `json:"Id"` + Name string `json:"Name"` + Description string `json:"Description"` + UserAccessPolicies UserAccessPolicies `json:"UserAccessPolicies"` + TeamAccessPolicies TeamAccessPolicies `json:"TeamAccessPolicies"` + Tags []string `json:"Tags"` // Deprecated fields Labels []Pair `json:"Labels"` + + // Deprecated in DBVersion == 18 + AuthorizedUsers []UserID `json:"AuthorizedUsers"` + AuthorizedTeams []TeamID `json:"AuthorizedTeams"` } // EndpointExtension represents a deprecated form of Portainer extension @@ -551,6 +605,12 @@ type ( DeleteUser(ID UserID) error } + RoleService interface { + Role(ID RoleID) (*Role, error) + Roles() ([]Role, error) + CreateRole(set *Role) error + } + // TeamService represents a service for managing user data TeamService interface { Team(ID TeamID) (*Team, error) @@ -794,17 +854,15 @@ type ( const ( // APIVersion is the version number of the Portainer API - APIVersion = "1.20.2" + APIVersion = "1.21.0" // DBVersion is the version number of the Portainer database - DBVersion = 17 + DBVersion = 18 // AssetsServerURL represents the URL of the Portainer asset server AssetsServerURL = "https://portainer-io-assets.sfo2.digitaloceanspaces.com" // MessageOfTheDayURL represents the URL where Portainer MOTD message can be retrieved - MessageOfTheDayURL = AssetsServerURL + "/motd.html" - // MessageOfTheDayTitleURL represents the URL where Portainer MOTD title can be retrieved - MessageOfTheDayTitleURL = AssetsServerURL + "/motd-title.txt" + MessageOfTheDayURL = AssetsServerURL + "/motd.json" // ExtensionDefinitionsURL represents the URL where Portainer extension definitions can be retrieved - ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.20.2.json" + ExtensionDefinitionsURL = AssetsServerURL + "/extensions-1.21.0.json" // PortainerAgentHeader represents the name of the header available in any agent response PortainerAgentHeader = "Portainer-Agent" // PortainerAgentTargetHeader represent the name of the header containing the target node name @@ -933,6 +991,8 @@ const ( RegistryManagementExtension // OAuthAuthenticationExtension represents the OAuth authentication extension OAuthAuthenticationExtension + // RBACExtension represents the RBAC extension + RBACExtension ) const ( @@ -956,3 +1016,216 @@ const ( // CustomRegistry represents a custom registry CustomRegistry ) + +const ( + OperationDockerContainerArchiveInfo Authorization = "DockerContainerArchiveInfo" + OperationDockerContainerList Authorization = "DockerContainerList" + OperationDockerContainerExport Authorization = "DockerContainerExport" + OperationDockerContainerChanges Authorization = "DockerContainerChanges" + OperationDockerContainerInspect Authorization = "DockerContainerInspect" + OperationDockerContainerTop Authorization = "DockerContainerTop" + OperationDockerContainerLogs Authorization = "DockerContainerLogs" + OperationDockerContainerStats Authorization = "DockerContainerStats" + OperationDockerContainerAttachWebsocket Authorization = "DockerContainerAttachWebsocket" + OperationDockerContainerArchive Authorization = "DockerContainerArchive" + OperationDockerContainerCreate Authorization = "DockerContainerCreate" + OperationDockerContainerPrune Authorization = "DockerContainerPrune" + OperationDockerContainerKill Authorization = "DockerContainerKill" + OperationDockerContainerPause Authorization = "DockerContainerPause" + OperationDockerContainerUnpause Authorization = "DockerContainerUnpause" + OperationDockerContainerRestart Authorization = "DockerContainerRestart" + OperationDockerContainerStart Authorization = "DockerContainerStart" + OperationDockerContainerStop Authorization = "DockerContainerStop" + OperationDockerContainerWait Authorization = "DockerContainerWait" + OperationDockerContainerResize Authorization = "DockerContainerResize" + OperationDockerContainerAttach Authorization = "DockerContainerAttach" + OperationDockerContainerExec Authorization = "DockerContainerExec" + OperationDockerContainerRename Authorization = "DockerContainerRename" + OperationDockerContainerUpdate Authorization = "DockerContainerUpdate" + OperationDockerContainerPutContainerArchive Authorization = "DockerContainerPutContainerArchive" + OperationDockerContainerDelete Authorization = "DockerContainerDelete" + OperationDockerImageList Authorization = "DockerImageList" + OperationDockerImageSearch Authorization = "DockerImageSearch" + OperationDockerImageGetAll Authorization = "DockerImageGetAll" + OperationDockerImageGet Authorization = "DockerImageGet" + OperationDockerImageHistory Authorization = "DockerImageHistory" + OperationDockerImageInspect Authorization = "DockerImageInspect" + OperationDockerImageLoad Authorization = "DockerImageLoad" + OperationDockerImageCreate Authorization = "DockerImageCreate" + OperationDockerImagePrune Authorization = "DockerImagePrune" + OperationDockerImagePush Authorization = "DockerImagePush" + OperationDockerImageTag Authorization = "DockerImageTag" + OperationDockerImageDelete Authorization = "DockerImageDelete" + OperationDockerImageCommit Authorization = "DockerImageCommit" + OperationDockerImageBuild Authorization = "DockerImageBuild" + OperationDockerNetworkList Authorization = "DockerNetworkList" + OperationDockerNetworkInspect Authorization = "DockerNetworkInspect" + OperationDockerNetworkCreate Authorization = "DockerNetworkCreate" + OperationDockerNetworkConnect Authorization = "DockerNetworkConnect" + OperationDockerNetworkDisconnect Authorization = "DockerNetworkDisconnect" + OperationDockerNetworkPrune Authorization = "DockerNetworkPrune" + OperationDockerNetworkDelete Authorization = "DockerNetworkDelete" + OperationDockerVolumeList Authorization = "DockerVolumeList" + OperationDockerVolumeInspect Authorization = "DockerVolumeInspect" + OperationDockerVolumeCreate Authorization = "DockerVolumeCreate" + OperationDockerVolumePrune Authorization = "DockerVolumePrune" + OperationDockerVolumeDelete Authorization = "DockerVolumeDelete" + OperationDockerExecInspect Authorization = "DockerExecInspect" + OperationDockerExecStart Authorization = "DockerExecStart" + OperationDockerExecResize Authorization = "DockerExecResize" + OperationDockerSwarmInspect Authorization = "DockerSwarmInspect" + OperationDockerSwarmUnlockKey Authorization = "DockerSwarmUnlockKey" + OperationDockerSwarmInit Authorization = "DockerSwarmInit" + OperationDockerSwarmJoin Authorization = "DockerSwarmJoin" + OperationDockerSwarmLeave Authorization = "DockerSwarmLeave" + OperationDockerSwarmUpdate Authorization = "DockerSwarmUpdate" + OperationDockerSwarmUnlock Authorization = "DockerSwarmUnlock" + OperationDockerNodeList Authorization = "DockerNodeList" + OperationDockerNodeInspect Authorization = "DockerNodeInspect" + OperationDockerNodeUpdate Authorization = "DockerNodeUpdate" + OperationDockerNodeDelete Authorization = "DockerNodeDelete" + OperationDockerServiceList Authorization = "DockerServiceList" + OperationDockerServiceInspect Authorization = "DockerServiceInspect" + OperationDockerServiceLogs Authorization = "DockerServiceLogs" + OperationDockerServiceCreate Authorization = "DockerServiceCreate" + OperationDockerServiceUpdate Authorization = "DockerServiceUpdate" + OperationDockerServiceDelete Authorization = "DockerServiceDelete" + OperationDockerSecretList Authorization = "DockerSecretList" + OperationDockerSecretInspect Authorization = "DockerSecretInspect" + OperationDockerSecretCreate Authorization = "DockerSecretCreate" + OperationDockerSecretUpdate Authorization = "DockerSecretUpdate" + OperationDockerSecretDelete Authorization = "DockerSecretDelete" + OperationDockerConfigList Authorization = "DockerConfigList" + OperationDockerConfigInspect Authorization = "DockerConfigInspect" + OperationDockerConfigCreate Authorization = "DockerConfigCreate" + OperationDockerConfigUpdate Authorization = "DockerConfigUpdate" + OperationDockerConfigDelete Authorization = "DockerConfigDelete" + OperationDockerTaskList Authorization = "DockerTaskList" + OperationDockerTaskInspect Authorization = "DockerTaskInspect" + OperationDockerTaskLogs Authorization = "DockerTaskLogs" + OperationDockerPluginList Authorization = "DockerPluginList" + OperationDockerPluginPrivileges Authorization = "DockerPluginPrivileges" + OperationDockerPluginInspect Authorization = "DockerPluginInspect" + OperationDockerPluginPull Authorization = "DockerPluginPull" + OperationDockerPluginCreate Authorization = "DockerPluginCreate" + OperationDockerPluginEnable Authorization = "DockerPluginEnable" + OperationDockerPluginDisable Authorization = "DockerPluginDisable" + OperationDockerPluginPush Authorization = "DockerPluginPush" + OperationDockerPluginUpgrade Authorization = "DockerPluginUpgrade" + OperationDockerPluginSet Authorization = "DockerPluginSet" + OperationDockerPluginDelete Authorization = "DockerPluginDelete" + OperationDockerSessionStart Authorization = "DockerSessionStart" + OperationDockerDistributionInspect Authorization = "DockerDistributionInspect" + OperationDockerBuildPrune Authorization = "DockerBuildPrune" + OperationDockerBuildCancel Authorization = "DockerBuildCancel" + OperationDockerPing Authorization = "DockerPing" + OperationDockerInfo Authorization = "DockerInfo" + OperationDockerEvents Authorization = "DockerEvents" + OperationDockerSystem Authorization = "DockerSystem" + OperationDockerVersion Authorization = "DockerVersion" + + OperationDockerAgentPing Authorization = "DockerAgentPing" + OperationDockerAgentList Authorization = "DockerAgentList" + OperationDockerAgentHostInfo Authorization = "DockerAgentHostInfo" + OperationDockerAgentBrowseDelete Authorization = "DockerAgentBrowseDelete" + OperationDockerAgentBrowseGet Authorization = "DockerAgentBrowseGet" + OperationDockerAgentBrowseList Authorization = "DockerAgentBrowseList" + OperationDockerAgentBrowsePut Authorization = "DockerAgentBrowsePut" + OperationDockerAgentBrowseRename Authorization = "DockerAgentBrowseRename" + + OperationPortainerDockerHubInspect Authorization = "PortainerDockerHubInspect" + OperationPortainerDockerHubUpdate Authorization = "PortainerDockerHubUpdate" + OperationPortainerEndpointGroupCreate Authorization = "PortainerEndpointGroupCreate" + OperationPortainerEndpointGroupList Authorization = "PortainerEndpointGroupList" + OperationPortainerEndpointGroupDelete Authorization = "PortainerEndpointGroupDelete" + OperationPortainerEndpointGroupInspect Authorization = "PortainerEndpointGroupInspect" + OperationPortainerEndpointGroupUpdate Authorization = "PortainerEndpointGroupEdit" + OperationPortainerEndpointGroupAccess Authorization = "PortainerEndpointGroupAccess " + OperationPortainerEndpointList Authorization = "PortainerEndpointList" + OperationPortainerEndpointInspect Authorization = "PortainerEndpointInspect" + OperationPortainerEndpointCreate Authorization = "PortainerEndpointCreate" + OperationPortainerEndpointExtensionAdd Authorization = "PortainerEndpointExtensionAdd" + OperationPortainerEndpointJob Authorization = "PortainerEndpointJob" + OperationPortainerEndpointSnapshots Authorization = "PortainerEndpointSnapshots" + OperationPortainerEndpointSnapshot Authorization = "PortainerEndpointSnapshot" + OperationPortainerEndpointUpdate Authorization = "PortainerEndpointUpdate" + OperationPortainerEndpointUpdateAccess Authorization = "PortainerEndpointUpdateAccess" + OperationPortainerEndpointDelete Authorization = "PortainerEndpointDelete" + OperationPortainerEndpointExtensionRemove Authorization = "PortainerEndpointExtensionRemove" + OperationPortainerExtensionList Authorization = "PortainerExtensionList" + OperationPortainerExtensionInspect Authorization = "PortainerExtensionInspect" + OperationPortainerExtensionCreate Authorization = "PortainerExtensionCreate" + OperationPortainerExtensionUpdate Authorization = "PortainerExtensionUpdate" + OperationPortainerExtensionDelete Authorization = "PortainerExtensionDelete" + OperationPortainerMOTD Authorization = "PortainerMOTD" + OperationPortainerRegistryList Authorization = "PortainerRegistryList" + OperationPortainerRegistryInspect Authorization = "PortainerRegistryInspect" + OperationPortainerRegistryCreate Authorization = "PortainerRegistryCreate" + OperationPortainerRegistryConfigure Authorization = "PortainerRegistryConfigure" + OperationPortainerRegistryUpdate Authorization = "PortainerRegistryUpdate" + OperationPortainerRegistryUpdateAccess Authorization = "PortainerRegistryUpdateAccess" + OperationPortainerRegistryDelete Authorization = "PortainerRegistryDelete" + OperationPortainerResourceControlCreate Authorization = "PortainerResourceControlCreate" + OperationPortainerResourceControlUpdate Authorization = "PortainerResourceControlUpdate" + OperationPortainerResourceControlDelete Authorization = "PortainerResourceControlDelete" + OperationPortainerRoleList Authorization = "PortainerRoleList" + OperationPortainerRoleInspect Authorization = "PortainerRoleInspect" + OperationPortainerRoleCreate Authorization = "PortainerRoleCreate" + OperationPortainerRoleUpdate Authorization = "PortainerRoleUpdate" + OperationPortainerRoleDelete Authorization = "PortainerRoleDelete" + OperationPortainerScheduleList Authorization = "PortainerScheduleList" + OperationPortainerScheduleInspect Authorization = "PortainerScheduleInspect" + OperationPortainerScheduleFile Authorization = "PortainerScheduleFile" + OperationPortainerScheduleTasks Authorization = "PortainerScheduleTasks" + OperationPortainerScheduleCreate Authorization = "PortainerScheduleCreate" + OperationPortainerScheduleUpdate Authorization = "PortainerScheduleUpdate" + OperationPortainerScheduleDelete Authorization = "PortainerScheduleDelete" + OperationPortainerSettingsInspect Authorization = "PortainerSettingsInspect" + OperationPortainerSettingsUpdate Authorization = "PortainerSettingsUpdate" + OperationPortainerSettingsLDAPCheck Authorization = "PortainerSettingsLDAPCheck" + OperationPortainerStackList Authorization = "PortainerStackList" + OperationPortainerStackInspect Authorization = "PortainerStackInspect" + OperationPortainerStackFile Authorization = "PortainerStackFile" + OperationPortainerStackCreate Authorization = "PortainerStackCreate" + OperationPortainerStackMigrate Authorization = "PortainerStackMigrate" + OperationPortainerStackUpdate Authorization = "PortainerStackUpdate" + OperationPortainerStackDelete Authorization = "PortainerStackDelete" + OperationPortainerTagList Authorization = "PortainerTagList" + OperationPortainerTagCreate Authorization = "PortainerTagCreate" + OperationPortainerTagDelete Authorization = "PortainerTagDelete" + OperationPortainerTeamMembershipList Authorization = "PortainerTeamMembershipList" + OperationPortainerTeamMembershipCreate Authorization = "PortainerTeamMembershipCreate" + OperationPortainerTeamMembershipUpdate Authorization = "PortainerTeamMembershipUpdate" + OperationPortainerTeamMembershipDelete Authorization = "PortainerTeamMembershipDelete" + OperationPortainerTeamList Authorization = "PortainerTeamList" + OperationPortainerTeamInspect Authorization = "PortainerTeamInspect" + OperationPortainerTeamMemberships Authorization = "PortainerTeamMemberships" + OperationPortainerTeamCreate Authorization = "PortainerTeamCreate" + OperationPortainerTeamUpdate Authorization = "PortainerTeamUpdate" + OperationPortainerTeamDelete Authorization = "PortainerTeamDelete" + OperationPortainerTemplateList Authorization = "PortainerTemplateList" + OperationPortainerTemplateInspect Authorization = "PortainerTemplateInspect" + OperationPortainerTemplateCreate Authorization = "PortainerTemplateCreate" + OperationPortainerTemplateUpdate Authorization = "PortainerTemplateUpdate" + OperationPortainerTemplateDelete Authorization = "PortainerTemplateDelete" + OperationPortainerUploadTLS Authorization = "PortainerUploadTLS" + OperationPortainerUserList Authorization = "PortainerUserList" + OperationPortainerUserInspect Authorization = "PortainerUserInspect" + OperationPortainerUserMemberships Authorization = "PortainerUserMemberships" + OperationPortainerUserCreate Authorization = "PortainerUserCreate" + OperationPortainerUserUpdate Authorization = "PortainerUserUpdate" + OperationPortainerUserUpdatePassword Authorization = "PortainerUserUpdatePassword" + OperationPortainerUserDelete Authorization = "PortainerUserDelete" + OperationPortainerWebsocketExec Authorization = "PortainerWebsocketExec" + OperationPortainerWebhookList Authorization = "PortainerWebhookList" + OperationPortainerWebhookCreate Authorization = "PortainerWebhookCreate" + OperationPortainerWebhookDelete Authorization = "PortainerWebhookDelete" + + OperationIntegrationStoridgeAdmin Authorization = "IntegrationStoridgeAdmin" + + OperationDockerUndefined Authorization = "DockerUndefined" + OperationDockerAgentUndefined Authorization = "DockerAgentUndefined" + OperationPortainerUndefined Authorization = "PortainerUndefined" + + EndpointResourcesAccess Authorization = "EndpointResourcesAccess" +) diff --git a/api/swagger.yaml b/api/swagger.yaml index a6d50a8b5..45df32348 100644 --- a/api/swagger.yaml +++ b/api/swagger.yaml @@ -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). - version: "1.20.2" + version: "1.21.0" title: "Portainer API" contact: email: "info@portainer.io" @@ -465,56 +465,6 @@ paths: examples: application/json: err: "Endpoint management is disabled" - /endpoints/{id}/access: - put: - tags: - - "endpoints" - summary: "Manage accesses to an endpoint" - description: | - Manage user and team accesses to an endpoint. - **Access policy**: administrator - operationId: "EndpointAccessUpdate" - consumes: - - "application/json" - produces: - - "application/json" - security: - - jwt: [] - parameters: - - name: "id" - in: "path" - description: "Endpoint identifier" - required: true - type: "integer" - - in: "body" - name: "body" - description: "Authorizations details" - required: true - schema: - $ref: "#/definitions/EndpointAccessUpdateRequest" - responses: - 200: - description: "Success" - schema: - $ref: "#/definitions/Endpoint" - 400: - description: "Invalid request" - schema: - $ref: "#/definitions/GenericError" - examples: - application/json: - err: "Invalid request data format" - 404: - description: "Endpoint not found" - schema: - $ref: "#/definitions/GenericError" - examples: - application/json: - err: "Endpoint not found" - 500: - description: "Server error" - schema: - $ref: "#/definitions/GenericError" /endpoints/{id}/job: post: tags: @@ -791,56 +741,6 @@ paths: examples: application/json: err: "EndpointGroup management is disabled" - /endpoint_groups/{id}/access: - put: - tags: - - "endpoint_groups" - summary: "Manage accesses to an endpoint group" - description: | - Manage user and team accesses to an endpoint group. - **Access policy**: administrator - operationId: "EndpointGroupAccessUpdate" - consumes: - - "application/json" - produces: - - "application/json" - security: - - jwt: [] - parameters: - - name: "id" - in: "path" - description: "EndpointGroup identifier" - required: true - type: "integer" - - in: "body" - name: "body" - description: "Authorizations details" - required: true - schema: - $ref: "#/definitions/EndpointGroupAccessUpdateRequest" - responses: - 200: - description: "Success" - schema: - $ref: "#/definitions/EndpointGroup" - 400: - description: "Invalid request" - schema: - $ref: "#/definitions/GenericError" - examples: - application/json: - err: "Invalid request data format" - 404: - description: "EndpointGroup not found" - schema: - $ref: "#/definitions/GenericError" - examples: - application/json: - err: "EndpointGroup not found" - 500: - description: "Server error" - schema: - $ref: "#/definitions/GenericError" /registries: get: tags: @@ -1045,56 +945,6 @@ paths: description: "Server error" schema: $ref: "#/definitions/GenericError" - /registries/{id}/access: - put: - tags: - - "registries" - summary: "Manage accesses to a registry" - description: | - Manage user and team accesses to a registry. - **Access policy**: administrator - operationId: "RegistryAccessUpdate" - consumes: - - "application/json" - produces: - - "application/json" - security: - - jwt: [] - parameters: - - name: "id" - in: "path" - description: "Registry identifier" - required: true - type: "integer" - - in: "body" - name: "body" - description: "Authorizations details" - required: true - schema: - $ref: "#/definitions/RegistryAccessUpdateRequest" - responses: - 200: - description: "Success" - schema: - $ref: "#/definitions/Registry" - 400: - description: "Invalid request" - schema: - $ref: "#/definitions/GenericError" - examples: - application/json: - err: "Invalid request data format" - 404: - description: "Registry not found" - schema: - $ref: "#/definitions/GenericError" - examples: - application/json: - err: "Registry not found" - 500: - description: "Server error" - schema: - $ref: "#/definitions/GenericError" /resource_controls: post: tags: @@ -3018,7 +2868,7 @@ definitions: description: "Is analytics enabled" Version: type: "string" - example: "1.20.2" + example: "1.21.0" description: "Portainer API version" PublicSettingsInspectResponse: type: "object" @@ -3114,7 +2964,29 @@ definitions: type: "string" example: "member" description: "LDAP attribute which denotes the group membership." - + UserAccessPolicies: + type: "object" + description: "User access policies associated to a registry/endpoint/endpoint group. RoleID is not required for registry access policies and can be set to 0." + additionalProperties: + $ref: "#/definitions/AccessPolicy" + example: + 1: { RoleID: 1 } + 2: { RoleID: 3 } + TeamAccessPolicies: + type: "object" + description: "Team access policies associated to a registry/endpoint/endpoint group. RoleID is not required for registry access policies and can be set to 0." + additionalProperties: + $ref: "#/definitions/AccessPolicy" + example: + 1: { RoleID: 1 } + 2: { RoleID: 3 } + AccessPolicy: + type: "object" + properties: + RoleID: + type: "integer" + example: "1" + description: "Role identifier. Reference the role that will be associated to this access policy" LDAPSettings: type: "object" properties: @@ -3566,40 +3438,10 @@ definitions: type: "string" example: "cOrXoK/1D35w8YQ8nH1/8ZGwzz45JIYD5jxHKXEQknk=" description: "Azure authentication key" - EndpointAccessUpdateRequest: - type: "object" - properties: - AuthorizedUsers: - type: "array" - description: "List of user identifiers authorized to connect to this endpoint" - items: - type: "integer" - example: 1 - description: "User identifier" - AuthorizedTeams: - type: "array" - description: "List of team identifiers authorized to connect to this endpoint" - items: - type: "integer" - example: 1 - description: "Team identifier" - EndpointGroupAccessUpdateRequest: - type: "object" - properties: - AuthorizedUsers: - type: "array" - description: "List of user identifiers authorized to connect to this endpoint" - items: - type: "integer" - example: 1 - description: "User identifier" - AuthorizedTeams: - type: "array" - description: "List of team identifiers authorized to connect to this endpoint" - items: - type: "integer" - example: 1 - description: "Team identifier" + UserAccessPolicies: + $ref: "#/definitions/UserAccessPolicies" + TeamAccessPolicies: + $ref: "#/definitions/TeamAccessPolicies" RegistryCreateRequest: type: "object" required: @@ -3664,23 +3506,10 @@ definitions: type: "string" example: "registry_password" description: "Password used to authenticate against this registry" - RegistryAccessUpdateRequest: - type: "object" - properties: - AuthorizedUsers: - type: "array" - description: "List of user identifiers authorized to use thi registry" - items: - type: "integer" - example: 1 - description: "User identifier" - AuthorizedTeams: - type: "array" - description: "List of team identifiers authorized to use thi registry" - items: - type: "integer" - example: 1 - description: "Team identifier" + UserAccessPolicies: + $ref: "#/definitions/UserAccessPolicies" + TeamAccessPolicies: + $ref: "#/definitions/TeamAccessPolicies" ResourceControlCreateRequest: type: "object" required: @@ -3831,6 +3660,10 @@ definitions: type: "integer" example: 1 description: "Endpoint identifier" + UserAccessPolicies: + $ref: "#/definitions/UserAccessPolicies" + TeamAccessPolicies: + $ref: "#/definitions/TeamAccessPolicies" UserCreateRequest: type: "object" required: diff --git a/api/swagger_config.json b/api/swagger_config.json index 1da6f8728..c37dd5843 100644 --- a/api/swagger_config.json +++ b/api/swagger_config.json @@ -1,5 +1,5 @@ { "packageName": "portainer", - "packageVersion": "1.20.2", + "packageVersion": "1.21.0", "projectName": "portainer" } diff --git a/app/__module.js b/app/__module.js index 0ffc3c7c6..64252819d 100644 --- a/app/__module.js +++ b/app/__module.js @@ -1,3 +1,12 @@ +import '../assets/css/app.css'; +import angular from 'angular'; + +import './agent/_module'; +import './azure/_module'; +import './docker/__module'; +import './extensions/storidge/__module'; +import './portainer/__module'; + angular.module('portainer', [ 'ui.bootstrap', 'ui.router', @@ -17,7 +26,6 @@ angular.module('portainer', [ 'angular-clipboard', 'ngFileSaver', 'luegg.directives', - 'portainer.templates', 'portainer.app', 'portainer.agent', 'portainer.azure', @@ -26,4 +34,11 @@ angular.module('portainer', [ 'extension.storidge', 'rzModule', 'moment-picker' - ]); +]); + +if (require) { + var req = require.context('./', true, /^(.*\.(js$))[^.]*$/im); + req.keys().forEach(function(key) { + req(key); + }); +} diff --git a/app/agent/components/file-uploader/file-uploader.js b/app/agent/components/file-uploader/file-uploader.js index 58ba7b04b..6427bae4a 100644 --- a/app/agent/components/file-uploader/file-uploader.js +++ b/app/agent/components/file-uploader/file-uploader.js @@ -1,5 +1,5 @@ angular.module('portainer.agent').component('fileUploader', { - templateUrl: 'app/agent/components/file-uploader/file-uploader.html', + templateUrl: './file-uploader.html', controller: 'FileUploaderController', bindings: { uploadFile: ' - + @@ -71,14 +71,14 @@ {{ item.ModTime | getisodatefromtimestamp }} - Download - + Rename - + Delete diff --git a/app/agent/components/files-datatable/files-datatable.js b/app/agent/components/files-datatable/files-datatable.js index 4e90e1d00..d1735cf48 100644 --- a/app/agent/components/files-datatable/files-datatable.js +++ b/app/agent/components/files-datatable/files-datatable.js @@ -1,5 +1,5 @@ angular.module('portainer.agent').component('filesDatatable', { - templateUrl: 'app/agent/components/files-datatable/files-datatable.html', + templateUrl: './files-datatable.html', controller: 'GenericDatatableController', bindings: { titleText: '@', diff --git a/app/agent/components/host-browser/host-browser-controller.js b/app/agent/components/host-browser/host-browser-controller.js index a50cb6b6e..797f5c6ac 100644 --- a/app/agent/components/host-browser/host-browser-controller.js +++ b/app/agent/components/host-browser/host-browser-controller.js @@ -1,3 +1,5 @@ +import _ from 'lodash-es'; + angular.module('portainer.agent').controller('HostBrowserController', [ 'HostBrowserService', 'Notifications', 'FileSaver', 'ModalService', function HostBrowserController(HostBrowserService, Notifications, FileSaver, ModalService) { diff --git a/app/agent/components/host-browser/host-browser.js b/app/agent/components/host-browser/host-browser.js index a136730ee..d702255eb 100644 --- a/app/agent/components/host-browser/host-browser.js +++ b/app/agent/components/host-browser/host-browser.js @@ -1,5 +1,5 @@ angular.module('portainer.agent').component('hostBrowser', { controller: 'HostBrowserController', - templateUrl: 'app/agent/components/host-browser/host-browser.html', + templateUrl: './host-browser.html', bindings: {} }); diff --git a/app/agent/components/node-selector/node-selector.js b/app/agent/components/node-selector/node-selector.js index 8dfb46ce6..0d9c93ed3 100644 --- a/app/agent/components/node-selector/node-selector.js +++ b/app/agent/components/node-selector/node-selector.js @@ -1,5 +1,5 @@ angular.module('portainer.agent').component('nodeSelector', { - templateUrl: 'app/agent/components/node-selector/nodeSelector.html', + templateUrl: './nodeSelector.html', controller: 'NodeSelectorController', bindings: { model: '=' diff --git a/app/agent/components/volume-browser/volume-browser.js b/app/agent/components/volume-browser/volume-browser.js index 78e963604..bea11e61a 100644 --- a/app/agent/components/volume-browser/volume-browser.js +++ b/app/agent/components/volume-browser/volume-browser.js @@ -1,5 +1,5 @@ angular.module('portainer.agent').component('volumeBrowser', { - templateUrl: 'app/agent/components/volume-browser/volumeBrowser.html', + templateUrl: './volumeBrowser.html', controller: 'VolumeBrowserController', bindings: { volumeId: '<', diff --git a/app/agent/components/volume-browser/volumeBrowserController.js b/app/agent/components/volume-browser/volumeBrowserController.js index 8db735a3b..20bd82891 100644 --- a/app/agent/components/volume-browser/volumeBrowserController.js +++ b/app/agent/components/volume-browser/volumeBrowserController.js @@ -1,3 +1,5 @@ +import _ from 'lodash-es'; + angular.module('portainer.agent') .controller('VolumeBrowserController', ['HttpRequestHelper', 'VolumeBrowserService', 'FileSaver', 'Blob', 'ModalService', 'Notifications', function (HttpRequestHelper, VolumeBrowserService, FileSaver, Blob, ModalService, Notifications) { diff --git a/app/agent/models/agent.js b/app/agent/models/agent.js index 9f7ed8b43..3e171ff07 100644 --- a/app/agent/models/agent.js +++ b/app/agent/models/agent.js @@ -1,4 +1,4 @@ -function AgentViewModel(data) { +export function AgentViewModel(data) { this.IPAddress = data.IPAddress; this.NodeName = data.NodeName; this.NodeRole = data.NodeRole; diff --git a/app/agent/rest/browse.js b/app/agent/rest/browse.js index e78cd3206..c17028ecf 100644 --- a/app/agent/rest/browse.js +++ b/app/agent/rest/browse.js @@ -1,3 +1,5 @@ +import { browseGetResponse } from './response/browse'; + angular.module('portainer.agent') .factory('Browse', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', 'StateManager', function BrowseFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider, StateManager) { diff --git a/app/agent/rest/response/browse.js b/app/agent/rest/response/browse.js index 7047777a6..32e454305 100644 --- a/app/agent/rest/response/browse.js +++ b/app/agent/rest/response/browse.js @@ -2,7 +2,7 @@ // ngResource will transform it as an array of chars. // This functions simply creates a response object and assign // the data to a field. -function browseGetResponse(data) { +export function browseGetResponse(data) { var response = {}; response.file = data; return response; diff --git a/app/agent/rest/v1/browse.js b/app/agent/rest/v1/browse.js index d576433fa..c175f2369 100644 --- a/app/agent/rest/v1/browse.js +++ b/app/agent/rest/v1/browse.js @@ -1,3 +1,5 @@ +import { browseGetResponse } from '../response/browse'; + angular.module('portainer.agent') .factory('BrowseVersion1', ['$resource', 'API_ENDPOINT_ENDPOINTS', 'EndpointProvider', function BrowseFactory($resource, API_ENDPOINT_ENDPOINTS, EndpointProvider) { 'use strict'; diff --git a/app/agent/services/agentService.js b/app/agent/services/agentService.js index 17a2c3166..af471da09 100644 --- a/app/agent/services/agentService.js +++ b/app/agent/services/agentService.js @@ -1,3 +1,5 @@ +import { AgentViewModel } from '../models/agent'; + angular.module('portainer.agent').factory('AgentService', [ '$q', 'Agent', 'AgentVersion1', 'HttpRequestHelper', 'Host', 'StateManager', function AgentServiceFactory($q, Agent, AgentVersion1, HttpRequestHelper, Host, StateManager) { diff --git a/app/app.js b/app/app.js index f1d2c14ca..38f0151e7 100644 --- a/app/app.js +++ b/app/app.js @@ -1,3 +1,5 @@ +import _ from 'lodash-es'; + angular.module('portainer') .run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', 'cfpLoadingBar', '$transitions', 'HttpRequestHelper', function ($rootScope, $state, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics, cfpLoadingBar, $transitions, HttpRequestHelper) { diff --git a/app/azure/_module.js b/app/azure/_module.js index 23693574c..663f2c511 100644 --- a/app/azure/_module.js +++ b/app/azure/_module.js @@ -14,7 +14,7 @@ angular.module('portainer.azure', ['portainer.app']) url: '/containerinstances', views: { 'content@': { - templateUrl: 'app/azure/views/containerinstances/containerinstances.html', + templateUrl: './views/containerinstances/containerinstances.html', controller: 'AzureContainerInstancesController' } } @@ -25,7 +25,7 @@ angular.module('portainer.azure', ['portainer.app']) url: '/new/', views: { 'content@': { - templateUrl: 'app/azure/views/containerinstances/create/createcontainerinstance.html', + templateUrl: './views/containerinstances/create/createcontainerinstance.html', controller: 'AzureCreateContainerInstanceController' } } @@ -36,7 +36,7 @@ angular.module('portainer.azure', ['portainer.app']) url: '/dashboard', views: { 'content@': { - templateUrl: 'app/azure/views/dashboard/dashboard.html', + templateUrl: './views/dashboard/dashboard.html', controller: 'AzureDashboardController' } } diff --git a/app/azure/components/azure-endpoint-config/azure-endpoint-config.js b/app/azure/components/azure-endpoint-config/azure-endpoint-config.js index b5af18fb7..2909d3853 100644 --- a/app/azure/components/azure-endpoint-config/azure-endpoint-config.js +++ b/app/azure/components/azure-endpoint-config/azure-endpoint-config.js @@ -4,5 +4,5 @@ angular.module('portainer.azure').component('azureEndpointConfig', { tenantId: '=', authenticationKey: '=' }, - templateUrl: 'app/azure/components/azure-endpoint-config/azureEndpointConfig.html' + templateUrl: './azureEndpointConfig.html' }); diff --git a/app/azure/components/azure-sidebar-content/azure-sidebar-content.js b/app/azure/components/azure-sidebar-content/azure-sidebar-content.js index d1f9230f4..68401cc1e 100644 --- a/app/azure/components/azure-sidebar-content/azure-sidebar-content.js +++ b/app/azure/components/azure-sidebar-content/azure-sidebar-content.js @@ -1,3 +1,3 @@ angular.module('portainer.azure').component('azureSidebarContent', { - templateUrl: 'app/azure/components/azure-sidebar-content/azureSidebarContent.html' + templateUrl: './azureSidebarContent.html' }); diff --git a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.js b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.js index b30626be5..eb7c68601 100644 --- a/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.js +++ b/app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.js @@ -1,5 +1,5 @@ angular.module('portainer.azure').component('containergroupsDatatable', { - templateUrl: 'app/azure/components/datatables/containergroups-datatable/containerGroupsDatatable.html', + templateUrl: './containerGroupsDatatable.html', controller: 'GenericDatatableController', bindings: { title: '@', diff --git a/app/azure/models/container_group.js b/app/azure/models/container_group.js index 9a855129f..940be248d 100644 --- a/app/azure/models/container_group.js +++ b/app/azure/models/container_group.js @@ -1,4 +1,4 @@ -function ContainerGroupDefaultModel() { +export function ContainerGroupDefaultModel() { this.Location = ''; this.OSType = 'Linux'; this.Name = ''; @@ -15,7 +15,7 @@ function ContainerGroupDefaultModel() { this.Memory = 1; } -function ContainerGroupViewModel(data) { +export function ContainerGroupViewModel(data) { this.Id = data.id; this.Name = data.name; this.Location = data.location; @@ -23,7 +23,7 @@ function ContainerGroupViewModel(data) { this.Ports = data.properties.ipAddress.ports; } -function CreateContainerGroupRequest(model) { +export function CreateContainerGroupRequest(model) { this.location = model.Location; var containerPorts = []; diff --git a/app/azure/models/location.js b/app/azure/models/location.js index a010776ba..6d4031331 100644 --- a/app/azure/models/location.js +++ b/app/azure/models/location.js @@ -1,4 +1,4 @@ -function LocationViewModel(data) { +export function LocationViewModel(data) { this.Id = data.id; this.SubscriptionId = data.subscriptionId; this.DisplayName = data.displayName; diff --git a/app/azure/models/provider.js b/app/azure/models/provider.js index 48cca79e9..069deb77d 100644 --- a/app/azure/models/provider.js +++ b/app/azure/models/provider.js @@ -1,4 +1,6 @@ -function ContainerInstanceProviderViewModel(data) { +import _ from 'lodash-es'; + +export function ContainerInstanceProviderViewModel(data) { this.Id = data.id; this.Namespace = data.namespace; diff --git a/app/azure/models/resource_group.js b/app/azure/models/resource_group.js index aa04f1809..894ce326d 100644 --- a/app/azure/models/resource_group.js +++ b/app/azure/models/resource_group.js @@ -1,4 +1,4 @@ -function ResourceGroupViewModel(data, subscriptionId) { +export function ResourceGroupViewModel(data, subscriptionId) { this.Id = data.id; this.SubscriptionId = subscriptionId; this.Name = data.name; diff --git a/app/azure/models/subscription.js b/app/azure/models/subscription.js index 2baa0da4d..eb9bfaf52 100644 --- a/app/azure/models/subscription.js +++ b/app/azure/models/subscription.js @@ -1,4 +1,4 @@ -function SubscriptionViewModel(data) { +export function SubscriptionViewModel(data) { this.Id = data.subscriptionId; this.Name = data.displayName; } diff --git a/app/azure/services/containerGroupService.js b/app/azure/services/containerGroupService.js index 96031a587..9a039eba2 100644 --- a/app/azure/services/containerGroupService.js +++ b/app/azure/services/containerGroupService.js @@ -1,3 +1,5 @@ +import { ContainerGroupViewModel, CreateContainerGroupRequest } from '../models/container_group'; + angular.module('portainer.azure') .factory('ContainerGroupService', ['$q', 'ContainerGroup', function ContainerGroupServiceFactory($q, ContainerGroup) { 'use strict'; diff --git a/app/azure/services/locationService.js b/app/azure/services/locationService.js index 547ed93b3..83e4837de 100644 --- a/app/azure/services/locationService.js +++ b/app/azure/services/locationService.js @@ -1,3 +1,5 @@ +import { LocationViewModel } from '../models/location'; + angular.module('portainer.azure') .factory('LocationService', ['$q', 'Location', function LocationServiceFactory($q, Location) { 'use strict'; diff --git a/app/azure/services/providerService.js b/app/azure/services/providerService.js index 88451d4f5..80f1365c4 100644 --- a/app/azure/services/providerService.js +++ b/app/azure/services/providerService.js @@ -1,3 +1,5 @@ +import { ContainerInstanceProviderViewModel } from '../models/provider'; + angular.module('portainer.azure') .factory('ProviderService', ['$q', 'Provider', function ProviderServiceFactory($q, Provider) { 'use strict'; diff --git a/app/azure/services/resourceGroupService.js b/app/azure/services/resourceGroupService.js index 1777edea8..4f530cfab 100644 --- a/app/azure/services/resourceGroupService.js +++ b/app/azure/services/resourceGroupService.js @@ -1,3 +1,5 @@ +import { ResourceGroupViewModel } from '../models/resource_group'; + angular.module('portainer.azure') .factory('ResourceGroupService', ['$q', 'ResourceGroup', function ResourceGroupServiceFactory($q, ResourceGroup) { 'use strict'; diff --git a/app/azure/services/subscriptionService.js b/app/azure/services/subscriptionService.js index f468e1c8e..c85a7359e 100644 --- a/app/azure/services/subscriptionService.js +++ b/app/azure/services/subscriptionService.js @@ -1,3 +1,5 @@ +import { SubscriptionViewModel } from '../models/subscription'; + angular.module('portainer.azure') .factory('SubscriptionService', ['$q', 'Subscription', function SubscriptionServiceFactory($q, Subscription) { 'use strict'; diff --git a/app/azure/views/containerinstances/create/createContainerInstanceController.js b/app/azure/views/containerinstances/create/createContainerInstanceController.js index a7ecab9e8..87e4bdbc9 100644 --- a/app/azure/views/containerinstances/create/createContainerInstanceController.js +++ b/app/azure/views/containerinstances/create/createContainerInstanceController.js @@ -1,3 +1,5 @@ +import { ContainerGroupDefaultModel } from '../../../models/container_group'; + angular.module('portainer.azure') .controller('AzureCreateContainerInstanceController', ['$q', '$scope', '$state', 'AzureService', 'Notifications', function ($q, $scope, $state, AzureService, Notifications) { diff --git a/app/config.js b/app/config.js index 268b39288..0ab6b60c2 100644 --- a/app/config.js +++ b/app/config.js @@ -1,3 +1,7 @@ +import toastr from 'toastr'; +import { Terminal } from 'xterm'; +import * as fit from 'xterm/lib/addons/fit/fit'; + angular.module('portainer') .config(['$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', '$uibTooltipProvider', '$compileProvider', 'cfpLoadingBarProvider', function ($urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider, $uibTooltipProvider, $compileProvider, cfpLoadingBarProvider) { @@ -36,7 +40,7 @@ angular.module('portainer') }; }]); - AnalyticsProvider.setAccount('@@CONFIG_GA_ID'); + AnalyticsProvider.setAccount({ tracker: __CONFIG_GA_ID, set: { anonymizeIp: true } }); AnalyticsProvider.startOffline(true); toastr.options.timeOut = 3000; diff --git a/app/docker/__module.js b/app/docker/__module.js index d36f0df8c..a3b15cc8f 100644 --- a/app/docker/__module.js +++ b/app/docker/__module.js @@ -23,8 +23,9 @@ angular.module('portainer.docker', ['portainer.app']) url: '/configs', views: { 'content@': { - templateUrl: 'app/docker/views/configs/configs.html', - controller: 'ConfigsController' + templateUrl: './views/configs/configs.html', + controller: 'ConfigsController', + controllerAs: 'ctrl' } } }; @@ -34,7 +35,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/:id', views: { 'content@': { - templateUrl: 'app/docker/views/configs/edit/config.html', + templateUrl: './views/configs/edit/config.html', controller: 'ConfigController' } } @@ -45,8 +46,9 @@ angular.module('portainer.docker', ['portainer.app']) url: '/new?id', views: { 'content@': { - templateUrl: 'app/docker/views/configs/create/createconfig.html', - controller: 'CreateConfigController' + templateUrl: './views/configs/create/createconfig.html', + controller: 'CreateConfigController', + controllerAs: 'ctrl' } } }; @@ -56,7 +58,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/containers', views: { 'content@': { - templateUrl: 'app/docker/views/containers/containers.html', + templateUrl: './views/containers/containers.html', controller: 'ContainersController' } } @@ -67,18 +69,29 @@ angular.module('portainer.docker', ['portainer.app']) url: '/:id?nodeName', views: { 'content@': { - templateUrl: 'app/docker/views/containers/edit/container.html', + templateUrl: './views/containers/edit/container.html', controller: 'ContainerController' } } }; - var containerConsole = { - name: 'docker.containers.container.console', - url: '/console', + var containerAttachConsole = { + name: 'docker.containers.container.attach', + url: '/attach', views: { 'content@': { - templateUrl: 'app/docker/views/containers/console/containerconsole.html', + templateUrl: './views/containers/console/attach.html', + controller: 'ContainerConsoleController' + } + } + }; + + var containerExecConsole = { + name: 'docker.containers.container.exec', + url: '/exec', + views: { + 'content@': { + templateUrl: './views/containers/console/exec.html', controller: 'ContainerConsoleController' } } @@ -89,7 +102,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/new?nodeName&from', views: { 'content@': { - templateUrl: 'app/docker/views/containers/create/createcontainer.html', + templateUrl: './views/containers/create/createcontainer.html', controller: 'CreateContainerController' } } @@ -100,7 +113,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/inspect', views: { 'content@': { - templateUrl: 'app/docker/views/containers/inspect/containerinspect.html', + templateUrl: './views/containers/inspect/containerinspect.html', controller: 'ContainerInspectController' } } @@ -111,7 +124,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/logs', views: { 'content@': { - templateUrl: 'app/docker/views/containers/logs/containerlogs.html', + templateUrl: './views/containers/logs/containerlogs.html', controller: 'ContainerLogsController' } } @@ -122,7 +135,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/stats', views: { 'content@': { - templateUrl: 'app/docker/views/containers/stats/containerstats.html', + templateUrl: './views/containers/stats/containerstats.html', controller: 'ContainerStatsController' } } @@ -133,7 +146,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/dashboard', views: { 'content@': { - templateUrl: 'app/docker/views/dashboard/dashboard.html', + templateUrl: './views/dashboard/dashboard.html', controller: 'DashboardController' } } @@ -174,7 +187,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/events', views: { 'content@': { - templateUrl: 'app/docker/views/events/events.html', + templateUrl: './views/events/events.html', controller: 'EventsController' } } @@ -185,7 +198,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/images', views: { 'content@': { - templateUrl: 'app/docker/views/images/images.html', + templateUrl: './views/images/images.html', controller: 'ImagesController' } } @@ -196,7 +209,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/:id?nodeName', views: { 'content@': { - templateUrl: 'app/docker/views/images/edit/image.html', + templateUrl: './views/images/edit/image.html', controller: 'ImageController' } } @@ -207,7 +220,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/build', views: { 'content@': { - templateUrl: 'app/docker/views/images/build/buildimage.html', + templateUrl: './views/images/build/buildimage.html', controller: 'BuildImageController' } } @@ -218,7 +231,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/import', views: { 'content@': { - templateUrl: 'app/docker/views/images/import/importimage.html', + templateUrl: './views/images/import/importimage.html', controller: 'ImportImageController' } } @@ -229,7 +242,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/networks', views: { 'content@': { - templateUrl: 'app/docker/views/networks/networks.html', + templateUrl: './views/networks/networks.html', controller: 'NetworksController' } } @@ -240,7 +253,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/:id?nodeName', views: { 'content@': { - templateUrl: 'app/docker/views/networks/edit/network.html', + templateUrl: './views/networks/edit/network.html', controller: 'NetworkController' } } @@ -251,7 +264,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/new', views: { 'content@': { - templateUrl: 'app/docker/views/networks/create/createnetwork.html', + templateUrl: './views/networks/create/createnetwork.html', controller: 'CreateNetworkController' } } @@ -298,7 +311,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/secrets', views: { 'content@': { - templateUrl: 'app/docker/views/secrets/secrets.html', + templateUrl: './views/secrets/secrets.html', controller: 'SecretsController' } } @@ -309,7 +322,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/:id', views: { 'content@': { - templateUrl: 'app/docker/views/secrets/edit/secret.html', + templateUrl: './views/secrets/edit/secret.html', controller: 'SecretController' } } @@ -320,7 +333,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/new', views: { 'content@': { - templateUrl: 'app/docker/views/secrets/create/createsecret.html', + templateUrl: './views/secrets/create/createsecret.html', controller: 'CreateSecretController' } } @@ -331,7 +344,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/services', views: { 'content@': { - templateUrl: 'app/docker/views/services/services.html', + templateUrl: './views/services/services.html', controller: 'ServicesController' } } @@ -342,7 +355,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/:id', views: { 'content@': { - templateUrl: 'app/docker/views/services/edit/service.html', + templateUrl: './views/services/edit/service.html', controller: 'ServiceController' } } @@ -353,7 +366,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/new', views: { 'content@': { - templateUrl: 'app/docker/views/services/create/createservice.html', + templateUrl: './views/services/create/createservice.html', controller: 'CreateServiceController' } } @@ -364,7 +377,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/logs', views: { 'content@': { - templateUrl: 'app/docker/views/services/logs/servicelogs.html', + templateUrl: './views/services/logs/servicelogs.html', controller: 'ServiceLogsController' } } @@ -375,7 +388,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/swarm', views: { 'content@': { - templateUrl: 'app/docker/views/swarm/swarm.html', + templateUrl: './views/swarm/swarm.html', controller: 'SwarmController' } } @@ -386,7 +399,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/visualizer', views: { 'content@': { - templateUrl: 'app/docker/views/swarm/visualizer/swarmvisualizer.html', + templateUrl: './views/swarm/visualizer/swarmvisualizer.html', controller: 'SwarmVisualizerController' } } @@ -403,7 +416,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/:id', views: { 'content@': { - templateUrl: 'app/docker/views/tasks/edit/task.html', + templateUrl: './views/tasks/edit/task.html', controller: 'TaskController' } } @@ -414,7 +427,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/logs', views: { 'content@': { - templateUrl: 'app/docker/views/tasks/logs/tasklogs.html', + templateUrl: './views/tasks/logs/tasklogs.html', controller: 'TaskLogsController' } } @@ -425,7 +438,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/volumes', views: { 'content@': { - templateUrl: 'app/docker/views/volumes/volumes.html', + templateUrl: './views/volumes/volumes.html', controller: 'VolumesController' } } @@ -436,7 +449,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/:id?nodeName', views: { 'content@': { - templateUrl: 'app/docker/views/volumes/edit/volume.html', + templateUrl: './views/volumes/edit/volume.html', controller: 'VolumeController' } } @@ -447,7 +460,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/browse', views: { 'content@': { - templateUrl: 'app/docker/views/volumes/browse/browsevolume.html', + templateUrl: './views/volumes/browse/browsevolume.html', controller: 'BrowseVolumeController' } } @@ -458,7 +471,7 @@ angular.module('portainer.docker', ['portainer.app']) url: '/new', views: { 'content@': { - templateUrl: 'app/docker/views/volumes/create/createvolume.html', + templateUrl: './views/volumes/create/createvolume.html', controller: 'CreateVolumeController' } } @@ -471,7 +484,8 @@ angular.module('portainer.docker', ['portainer.app']) $stateRegistryProvider.register(configCreation); $stateRegistryProvider.register(containers); $stateRegistryProvider.register(container); - $stateRegistryProvider.register(containerConsole); + $stateRegistryProvider.register(containerExecConsole); + $stateRegistryProvider.register(containerAttachConsole); $stateRegistryProvider.register(containerCreation); $stateRegistryProvider.register(containerInspect); $stateRegistryProvider.register(containerLogs); diff --git a/app/docker/components/container-capabilities/container-capabilities.js b/app/docker/components/container-capabilities/container-capabilities.js index 6d572a464..4bf04ba6f 100644 --- a/app/docker/components/container-capabilities/container-capabilities.js +++ b/app/docker/components/container-capabilities/container-capabilities.js @@ -1,5 +1,5 @@ angular.module('portainer.docker').component('containerCapabilities', { - templateUrl: 'app/docker/components/container-capabilities/containerCapabilities.html', + templateUrl: './containerCapabilities.html', bindings: { capabilities: '=' } diff --git a/app/docker/components/container-quick-actions/containerQuickActions.html b/app/docker/components/container-quick-actions/containerQuickActions.html index 0befd7e8d..65ee13949 100644 --- a/app/docker/components/container-quick-actions/containerQuickActions.html +++ b/app/docker/components/container-quick-actions/containerQuickActions.html @@ -1,44 +1,58 @@ \ No newline at end of file diff --git a/app/docker/components/container-quick-actions/containerQuickActions.js b/app/docker/components/container-quick-actions/containerQuickActions.js index 3fb616d7c..619cf7efa 100644 --- a/app/docker/components/container-quick-actions/containerQuickActions.js +++ b/app/docker/components/container-quick-actions/containerQuickActions.js @@ -1,5 +1,5 @@ angular.module('portainer.docker').component('containerQuickActions', { - templateUrl: 'app/docker/components/container-quick-actions/containerQuickActions.html', + templateUrl: './containerQuickActions.html', bindings: { containerId: '<', nodeName: '<', diff --git a/app/docker/components/container-restart-policy/container-restart-policy.html b/app/docker/components/container-restart-policy/container-restart-policy.html index 41b87c2fc..844282c25 100644 --- a/app/docker/components/container-restart-policy/container-restart-policy.html +++ b/app/docker/components/container-restart-policy/container-restart-policy.html @@ -5,14 +5,14 @@ Name - - + diff --git a/app/docker/components/container-restart-policy/container-restart-policy.js b/app/docker/components/container-restart-policy/container-restart-policy.js index 0a8d7edf2..60e4f0c4b 100644 --- a/app/docker/components/container-restart-policy/container-restart-policy.js +++ b/app/docker/components/container-restart-policy/container-restart-policy.js @@ -1,6 +1,6 @@ angular.module('portainer.docker') .component('containerRestartPolicy', { - templateUrl: 'app/docker/components/container-restart-policy/container-restart-policy.html', + templateUrl: './container-restart-policy.html', controller: 'ContainerRestartPolicyController', bindings: { 'name': '<', diff --git a/app/docker/components/dashboard-cluster-agent-info/dashboard-cluster-agent-info.js b/app/docker/components/dashboard-cluster-agent-info/dashboard-cluster-agent-info.js index 54dc0caf6..88aae5cec 100644 --- a/app/docker/components/dashboard-cluster-agent-info/dashboard-cluster-agent-info.js +++ b/app/docker/components/dashboard-cluster-agent-info/dashboard-cluster-agent-info.js @@ -1,4 +1,4 @@ angular.module('portainer.docker').component('dashboardClusterAgentInfo', { - templateUrl: 'app/docker/components/dashboard-cluster-agent-info/dashboardClusterAgentInfo.html', + templateUrl: './dashboardClusterAgentInfo.html', controller: 'DashboardClusterAgentInfoController' }); diff --git a/app/docker/components/datatables/configs-datatable/configsDatatable.html b/app/docker/components/datatables/configs-datatable/configsDatatable.html index 5e8f075a6..623359a4b 100644 --- a/app/docker/components/datatables/configs-datatable/configsDatatable.html +++ b/app/docker/components/datatables/configs-datatable/configsDatatable.html @@ -6,12 +6,12 @@ {{ $ctrl.titleText }} -
- -
@@ -24,7 +24,7 @@ - + @@ -53,7 +53,7 @@ - + diff --git a/app/docker/components/datatables/configs-datatable/configsDatatable.js b/app/docker/components/datatables/configs-datatable/configsDatatable.js index b6735f28b..b7294eb2a 100644 --- a/app/docker/components/datatables/configs-datatable/configsDatatable.js +++ b/app/docker/components/datatables/configs-datatable/configsDatatable.js @@ -1,5 +1,5 @@ angular.module('portainer.docker').component('configsDatatable', { - templateUrl: 'app/docker/components/datatables/configs-datatable/configsDatatable.html', + templateUrl: './configsDatatable.html', controller: 'GenericDatatableController', bindings: { titleText: '@', diff --git a/app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.html b/app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.html index bb8a5d4c6..faf668ba0 100644 --- a/app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.html +++ b/app/docker/components/datatables/container-networks-datatable/containerNetworksDatatable.html @@ -8,7 +8,7 @@
-
+
@@ -70,25 +70,31 @@
- - -
-
+
+
-
-
-
- -
diff --git a/app/docker/components/datatables/networks-datatable/networksDatatable.js b/app/docker/components/datatables/networks-datatable/networksDatatable.js index ca9112f98..448a8b5a8 100644 --- a/app/docker/components/datatables/networks-datatable/networksDatatable.js +++ b/app/docker/components/datatables/networks-datatable/networksDatatable.js @@ -1,5 +1,5 @@ angular.module('portainer.docker').component('networksDatatable', { - templateUrl: 'app/docker/components/datatables/networks-datatable/networksDatatable.html', + templateUrl: './networksDatatable.html', controller: 'NetworksDatatableController', bindings: { titleText: '@', diff --git a/app/docker/components/datatables/node-tasks-datatable/nodeTasksDatatable.js b/app/docker/components/datatables/node-tasks-datatable/nodeTasksDatatable.js index f638f4625..e76a209c0 100644 --- a/app/docker/components/datatables/node-tasks-datatable/nodeTasksDatatable.js +++ b/app/docker/components/datatables/node-tasks-datatable/nodeTasksDatatable.js @@ -1,5 +1,5 @@ angular.module('portainer.docker').component('nodeTasksDatatable', { - templateUrl: 'app/docker/components/datatables/node-tasks-datatable/nodeTasksDatatable.html', + templateUrl: './nodeTasksDatatable.html', controller: 'GenericDatatableController', bindings: { titleText: '@', diff --git a/app/docker/components/datatables/nodes-datatable/nodesDatatable.html b/app/docker/components/datatables/nodes-datatable/nodesDatatable.html index dac5c644b..5ca2ba136 100644 --- a/app/docker/components/datatables/nodes-datatable/nodesDatatable.html +++ b/app/docker/components/datatables/nodes-datatable/nodesDatatable.html @@ -63,6 +63,13 @@ + + + Availability + + + + @@ -77,6 +84,7 @@ {{ item.EngineVersion }} {{ item.Addr }} {{ item.Status }} + {{ item.Availability }} Loading... diff --git a/app/docker/components/datatables/nodes-datatable/nodesDatatable.js b/app/docker/components/datatables/nodes-datatable/nodesDatatable.js index 7a461e5a6..a36fb2e40 100644 --- a/app/docker/components/datatables/nodes-datatable/nodesDatatable.js +++ b/app/docker/components/datatables/nodes-datatable/nodesDatatable.js @@ -1,5 +1,5 @@ angular.module('portainer.docker').component('nodesDatatable', { - templateUrl: 'app/docker/components/datatables/nodes-datatable/nodesDatatable.html', + templateUrl: './nodesDatatable.html', controller: 'GenericDatatableController', bindings: { titleText: '@', diff --git a/app/docker/components/datatables/secrets-datatable/secretsDatatable.html b/app/docker/components/datatables/secrets-datatable/secretsDatatable.html index bb5df46d6..8db6b2e5c 100644 --- a/app/docker/components/datatables/secrets-datatable/secretsDatatable.html +++ b/app/docker/components/datatables/secrets-datatable/secretsDatatable.html @@ -6,12 +6,12 @@ {{ $ctrl.titleText }}
-
- -
@@ -24,7 +24,7 @@ - + @@ -53,7 +53,7 @@ - + diff --git a/app/docker/components/datatables/secrets-datatable/secretsDatatable.js b/app/docker/components/datatables/secrets-datatable/secretsDatatable.js index 181473aa6..091cafe49 100644 --- a/app/docker/components/datatables/secrets-datatable/secretsDatatable.js +++ b/app/docker/components/datatables/secrets-datatable/secretsDatatable.js @@ -1,5 +1,5 @@ angular.module('portainer.docker').component('secretsDatatable', { - templateUrl: 'app/docker/components/datatables/secrets-datatable/secretsDatatable.html', + templateUrl: './secretsDatatable.html', controller: 'GenericDatatableController', bindings: { titleText: '@', diff --git a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.html b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.html index 40f255ae0..ebabb9b4c 100644 --- a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.html +++ b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.html @@ -60,8 +60,8 @@ {{ item.Status.State }} - {{ item.Id }}Roz - {{ item.Id }}Doz + {{ item.Id }} + {{ item.Id }} diff --git a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.js b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.js index ce15b64db..b93bbb957 100644 --- a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.js +++ b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.js @@ -1,5 +1,5 @@ angular.module('portainer.docker').component('serviceTasksDatatable', { - templateUrl: 'app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatable.html', + templateUrl: './serviceTasksDatatable.html', controller: 'ServiceTasksDatatableController', bindings: { dataset: '<', diff --git a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js index 46c12d67c..ddd9a0b34 100644 --- a/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js +++ b/app/docker/components/datatables/service-tasks-datatable/serviceTasksDatatableController.js @@ -1,3 +1,5 @@ +import _ from 'lodash-es'; + angular.module('portainer.docker') .controller('ServiceTasksDatatableController', ['DatatableService', function (DatatableService) { @@ -7,8 +9,9 @@ function (DatatableService) { orderBy: this.orderBy, showQuickActionStats: true, showQuickActionLogs: true, - showQuickActionConsole: true, - showQuickActionInspect: true + showQuickActionExec: true, + showQuickActionInspect: true, + showQuickActionAttach: false }; this.filters = { diff --git a/app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.html b/app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.html index 4ca83adcb..aaf97d95c 100644 --- a/app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.html +++ b/app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.html @@ -1,15 +1,15 @@ -
+
- -
-
diff --git a/app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.js b/app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.js index 32c16717f..310b63751 100644 --- a/app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.js +++ b/app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.js @@ -1,5 +1,5 @@ angular.module('portainer.docker').component('servicesDatatableActions', { - templateUrl: 'app/docker/components/datatables/services-datatable/actions/servicesDatatableActions.html', + templateUrl: './servicesDatatableActions.html', controller: 'ServicesDatatableActionsController', bindings: { selectedItems: '=', diff --git a/app/docker/components/datatables/services-datatable/servicesDatatable.html b/app/docker/components/datatables/services-datatable/servicesDatatable.html index c982365a3..e941aee41 100644 --- a/app/docker/components/datatables/services-datatable/servicesDatatable.html +++ b/app/docker/components/datatables/services-datatable/servicesDatatable.html @@ -21,7 +21,7 @@ - + @@ -83,7 +83,7 @@ - + @@ -97,7 +97,7 @@ {{ item.Mode }} {{ item.Tasks | runningtaskscount }} / {{ item.Mode === 'replicated' ? item.Replicas : ($ctrl.nodes | availablenodecount:item) }} - + Scale diff --git a/app/docker/components/datatables/services-datatable/servicesDatatable.js b/app/docker/components/datatables/services-datatable/servicesDatatable.js index ebd81487e..a7491a861 100644 --- a/app/docker/components/datatables/services-datatable/servicesDatatable.js +++ b/app/docker/components/datatables/services-datatable/servicesDatatable.js @@ -1,5 +1,5 @@ angular.module('portainer.docker').component('servicesDatatable', { - templateUrl: 'app/docker/components/datatables/services-datatable/servicesDatatable.html', + templateUrl: './servicesDatatable.html', controller: 'ServicesDatatableController', bindings: { titleText: '@', diff --git a/app/docker/components/datatables/services-datatable/servicesDatatableController.js b/app/docker/components/datatables/services-datatable/servicesDatatableController.js index b265863b4..adee53500 100644 --- a/app/docker/components/datatables/services-datatable/servicesDatatableController.js +++ b/app/docker/components/datatables/services-datatable/servicesDatatableController.js @@ -1,3 +1,5 @@ +import _ from 'lodash-es'; + angular.module('portainer.docker') .controller('ServicesDatatableController', ['PaginationService', 'DatatableService', 'EndpointProvider', function (PaginationService, DatatableService, EndpointProvider) { diff --git a/app/docker/components/datatables/tasks-datatable/tasksDatatable.js b/app/docker/components/datatables/tasks-datatable/tasksDatatable.js index f24e14446..d5c1318f3 100644 --- a/app/docker/components/datatables/tasks-datatable/tasksDatatable.js +++ b/app/docker/components/datatables/tasks-datatable/tasksDatatable.js @@ -1,5 +1,5 @@ angular.module('portainer.docker').component('tasksDatatable', { - templateUrl: 'app/docker/components/datatables/tasks-datatable/tasksDatatable.html', + templateUrl: './tasksDatatable.html', controller: 'TasksDatatableController', bindings: { titleText: '@', diff --git a/app/docker/components/datatables/tasks-datatable/tasksDatatableController.js b/app/docker/components/datatables/tasks-datatable/tasksDatatableController.js index 368fa76d2..eabf6202b 100644 --- a/app/docker/components/datatables/tasks-datatable/tasksDatatableController.js +++ b/app/docker/components/datatables/tasks-datatable/tasksDatatableController.js @@ -4,8 +4,9 @@ function (PaginationService, DatatableService) { this.state = { showQuickActionStats: true, showQuickActionLogs: true, - showQuickActionConsole: true, + showQuickActionExec: true, showQuickActionInspect: true, + showQuickActionAttach: false, selectAll: false, orderBy: this.orderBy, paginatedItemLimit: PaginationService.getPaginationLimit(this.tableKey), diff --git a/app/docker/components/datatables/volumes-datatable/volumesDatatable.html b/app/docker/components/datatables/volumes-datatable/volumesDatatable.html index 475290f84..211759c46 100644 --- a/app/docker/components/datatables/volumes-datatable/volumesDatatable.html +++ b/app/docker/components/datatables/volumes-datatable/volumesDatatable.html @@ -6,12 +6,12 @@ {{ $ctrl.titleText }}
-
- -
@@ -24,7 +24,7 @@ - + @@ -105,13 +105,13 @@ - + {{ item.Id | truncate:40 }} {{ item.Id | truncate:40 }} - + browse Unused diff --git a/app/docker/components/datatables/volumes-datatable/volumesDatatable.js b/app/docker/components/datatables/volumes-datatable/volumesDatatable.js index 4c878677a..2a71ef19b 100644 --- a/app/docker/components/datatables/volumes-datatable/volumesDatatable.js +++ b/app/docker/components/datatables/volumes-datatable/volumesDatatable.js @@ -1,5 +1,5 @@ angular.module('portainer.docker').component('volumesDatatable', { - templateUrl: 'app/docker/components/datatables/volumes-datatable/volumesDatatable.html', + templateUrl: './volumesDatatable.html', controller: 'VolumesDatatableController', bindings: { titleText: '@', diff --git a/app/docker/components/dockerSidebarContent/docker-sidebar-content.js b/app/docker/components/dockerSidebarContent/docker-sidebar-content.js index 2a33aabbc..c57b8fd92 100644 --- a/app/docker/components/dockerSidebarContent/docker-sidebar-content.js +++ b/app/docker/components/dockerSidebarContent/docker-sidebar-content.js @@ -1,5 +1,5 @@ angular.module('portainer.docker').component('dockerSidebarContent', { - templateUrl: 'app/docker/components/dockerSidebarContent/dockerSidebarContent.html', + templateUrl: './dockerSidebarContent.html', bindings: { endpointApiVersion: '<', swarmManagement: '<', diff --git a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html index 12151542a..55113a2d8 100644 --- a/app/docker/components/dockerSidebarContent/dockerSidebarContent.html +++ b/app/docker/components/dockerSidebarContent/dockerSidebarContent.html @@ -1,7 +1,7 @@ - - - - diff --git a/app/portainer/views/sidebar/sidebarController.js b/app/portainer/views/sidebar/sidebarController.js index fe24ec5b3..9c374a730 100644 --- a/app/portainer/views/sidebar/sidebarController.js +++ b/app/portainer/views/sidebar/sidebarController.js @@ -18,8 +18,8 @@ angular.module('portainer.app') var authenticationEnabled = $scope.applicationState.application.authentication; if (authenticationEnabled) { - var userDetails = Authentication.getUserDetails(); - var isAdmin = userDetails.role === 1; + let userDetails = Authentication.getUserDetails(); + let isAdmin = Authentication.isAdmin(); $scope.isAdmin = isAdmin; $q.when(!isAdmin ? UserService.userMemberships(userDetails.ID) : []) diff --git a/app/portainer/views/stacks/create/createStackController.js b/app/portainer/views/stacks/create/createStackController.js index 5f7d1a892..6ecd6519a 100644 --- a/app/portainer/views/stacks/create/createStackController.js +++ b/app/portainer/views/stacks/create/createStackController.js @@ -1,3 +1,5 @@ +import { AccessControlFormData } from '../../../components/accessControlForm/porAccessControlFormModel'; + angular.module('portainer.app') .controller('CreateStackController', ['$scope', '$state', 'StackService', 'Authentication', 'Notifications', 'FormValidator', 'ResourceControlService', 'FormHelper', 'EndpointProvider', function ($scope, $state, StackService, Authentication, Notifications, FormValidator, ResourceControlService, FormHelper, EndpointProvider) { @@ -95,7 +97,7 @@ function ($scope, $state, StackService, Authentication, Notifications, FormValid var accessControlData = $scope.formValues.AccessControlData; var userDetails = Authentication.getUserDetails(); - var isAdmin = userDetails.role === 1; + var isAdmin = Authentication.isAdmin(); var userId = userDetails.ID; if (method === 'editor' && $scope.formValues.StackFileContent === '') { diff --git a/app/portainer/views/stacks/edit/stack.html b/app/portainer/views/stacks/edit/stack.html index 7a0c1427e..bd2f198d1 100644 --- a/app/portainer/views/stacks/edit/stack.html +++ b/app/portainer/views/stacks/edit/stack.html @@ -42,11 +42,11 @@
{{ stackName }} - +
-
-
+
add environment variable @@ -103,7 +103,7 @@ value
-
@@ -113,7 +113,7 @@
-
+
Options
@@ -130,15 +130,17 @@
-
- Actions -
-
-
- +
+
+ Actions +
+
+
+ +
diff --git a/app/portainer/views/stacks/edit/stackController.js b/app/portainer/views/stacks/edit/stackController.js index 55eba59c4..3185fcdda 100644 --- a/app/portainer/views/stacks/edit/stackController.js +++ b/app/portainer/views/stacks/edit/stackController.js @@ -121,7 +121,7 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe var stack = $scope.stack; // TODO: this is a work-around for stacks created with Portainer version >= 1.17.1 - // The EndpointID property is not available for these stacks, we can pass + // The EndpointID property is not available for these stacks, we can pass // the current endpoint identifier as a part of the update request. It will be used if // the EndpointID property is not defined on the stack. var endpointId = EndpointProvider.endpointID(); @@ -239,7 +239,7 @@ function ($q, $scope, $state, $transition$, StackService, NodeService, ServiceSe function loadExternalStack(name) { var stackType = $transition$.params().type; if (!stackType || (stackType !== '1' && stackType !== '2')) { - Notifications.error('Failure', err, 'Invalid type URL parameter.'); + Notifications.error('Failure', null, 'Invalid type URL parameter.'); return; } diff --git a/app/portainer/views/support/product/product.html b/app/portainer/views/support/product/product.html index 554fd3fb7..6aea4bd80 100644 --- a/app/portainer/views/support/product/product.html +++ b/app/portainer/views/support/product/product.html @@ -51,9 +51,9 @@
-
- - Buy + diff --git a/app/portainer/views/teams/edit/teamController.js b/app/portainer/views/teams/edit/teamController.js index f07054607..84753793c 100644 --- a/app/portainer/views/teams/edit/teamController.js +++ b/app/portainer/views/teams/edit/teamController.js @@ -6,6 +6,7 @@ function ($q, $scope, $state, $transition$, TeamService, UserService, TeamMember pagination_count_users: PaginationService.getPaginationLimit('team_available_users'), pagination_count_members: PaginationService.getPaginationLimit('team_members') }; + $scope.sortTypeUsers = 'Username'; $scope.sortReverseUsers = true; $scope.users = []; @@ -112,6 +113,7 @@ function ($q, $scope, $state, $transition$, TeamService, UserService, TeamMember .then(function success() { $scope.users = $scope.users.concat($scope.teamMembers); $scope.teamMembers = []; + $scope.leaderCount = 0; Notifications.success('All users successfully removed'); }) .catch(function error(err) { @@ -123,6 +125,9 @@ function ($q, $scope, $state, $transition$, TeamService, UserService, TeamMember TeamMembershipService.deleteMembership(user.MembershipId) .then(function success() { removeUserFromArray(user.Id, $scope.teamMembers); + if (user.TeamRole === 'Leader') { + $scope.leaderCount--; + } $scope.users.push(user); Notifications.success('User removed from team', user.Username); }) @@ -177,7 +182,7 @@ function ($q, $scope, $state, $transition$, TeamService, UserService, TeamMember } function initView() { - $scope.isAdmin = Authentication.getUserDetails().role === 1 ? true: false; + $scope.isAdmin = Authentication.isAdmin(); $q.all({ team: TeamService.team($transition$.params().id), users: UserService.users(false), diff --git a/app/portainer/views/teams/teamsController.js b/app/portainer/views/teams/teamsController.js index 9b63b234e..7f2e4eee0 100644 --- a/app/portainer/views/teams/teamsController.js +++ b/app/portainer/views/teams/teamsController.js @@ -75,14 +75,15 @@ function ($q, $scope, $state, TeamService, UserService, ModalService, Notificati function initView() { var userDetails = Authentication.getUserDetails(); - var isAdmin = userDetails.role === 1 ? true: false; + var isAdmin = Authentication.isAdmin(); $scope.isAdmin = isAdmin; $q.all({ users: UserService.users(false), teams: isAdmin ? TeamService.teams() : UserService.userLeadingTeams(userDetails.ID) }) .then(function success(data) { - $scope.teams = data.teams; + var teams = data.teams; + $scope.teams = teams; $scope.users = data.users; }) .catch(function error(err) { diff --git a/app/portainer/views/templates/create/createTemplateController.js b/app/portainer/views/templates/create/createTemplateController.js index 57f6c577e..8d28f5c27 100644 --- a/app/portainer/views/templates/create/createTemplateController.js +++ b/app/portainer/views/templates/create/createTemplateController.js @@ -1,3 +1,5 @@ +import { TemplateDefaultModel } from "../../../models/template"; + angular.module('portainer.app') .controller('CreateTemplateController', ['$q', '$scope', '$state', 'TemplateService', 'TemplateHelper', 'NetworkService', 'Notifications', function ($q, $scope, $state, TemplateService, TemplateHelper, NetworkService, Notifications) { diff --git a/app/portainer/views/templates/edit/templateController.js b/app/portainer/views/templates/edit/templateController.js index 352764e43..227507a3b 100644 --- a/app/portainer/views/templates/edit/templateController.js +++ b/app/portainer/views/templates/edit/templateController.js @@ -1,3 +1,5 @@ +import _ from 'lodash-es'; + angular.module('portainer.app') .controller('TemplateController', ['$q', '$scope', '$state', '$transition$', 'TemplateService', 'TemplateHelper', 'NetworkService', 'Notifications', function ($q, $scope, $state, $transition$, TemplateService, TemplateHelper, NetworkService, Notifications) { diff --git a/app/portainer/views/templates/templatesController.js b/app/portainer/views/templates/templatesController.js index 5a4533fee..31f3ad859 100644 --- a/app/portainer/views/templates/templatesController.js +++ b/app/portainer/views/templates/templatesController.js @@ -1,3 +1,6 @@ +import _ from 'lodash-es'; +import { AccessControlFormData } from '../../components/accessControlForm/porAccessControlFormModel'; + angular.module('portainer.app') .controller('TemplatesController', ['$scope', '$q', '$state', '$transition$', '$anchorScroll', 'ContainerService', 'ImageService', 'NetworkService', 'TemplateService', 'TemplateHelper', 'VolumeService', 'Notifications', 'ResourceControlService', 'Authentication', 'FormValidator', 'SettingsService', 'StackService', 'EndpointProvider', 'ModalService', function ($scope, $q, $state, $transition$, $anchorScroll, ContainerService, ImageService, NetworkService, TemplateService, TemplateHelper, VolumeService, Notifications, ResourceControlService, Authentication, FormValidator, SettingsService, StackService, EndpointProvider, ModalService) { @@ -164,9 +167,8 @@ function ($scope, $q, $state, $transition$, $anchorScroll, ContainerService, Ima var userDetails = Authentication.getUserDetails(); var userId = userDetails.ID; var accessControlData = $scope.formValues.AccessControlData; - var isAdmin = userDetails.role === 1; - if (!validateForm(accessControlData, isAdmin)) { + if (!validateForm(accessControlData, $scope.isAdmin)) { return; } @@ -233,8 +235,7 @@ function ($scope, $q, $state, $transition$, $anchorScroll, ContainerService, Ima } function initView() { - var userDetails = Authentication.getUserDetails(); - $scope.isAdmin = userDetails.role === 1; + $scope.isAdmin = Authentication.isAdmin(); var endpointMode = $scope.applicationState.endpoint.mode; var apiVersion = $scope.applicationState.endpoint.apiVersion; diff --git a/app/portainer/views/users/edit/user.html b/app/portainer/views/users/edit/user.html index 4c82b4b83..ea5f18805 100644 --- a/app/portainer/views/users/edit/user.html +++ b/app/portainer/views/users/edit/user.html @@ -19,7 +19,7 @@ - +