Compare commits
269 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5785ba5f4a | ||
|
|
e110986728 | ||
|
|
587e2fa673 | ||
|
|
80827935da | ||
|
|
f3a1250b27 | ||
|
|
79121f9977 | ||
|
|
f678d05088 | ||
|
|
c6341eead0 | ||
|
|
3e99fae070 | ||
|
|
249bcf5bac | ||
|
|
9c10a1def2 | ||
|
|
93120d23c6 | ||
|
|
b59dd03b43 | ||
|
|
1263866548 | ||
|
|
0bdcff09f8 | ||
|
|
ca9d9b9a77 | ||
|
|
6cfffb38f9 | ||
|
|
e2979a631a | ||
|
|
7b924bde83 | ||
|
|
6bf7c90634 | ||
|
|
f5749f82d8 | ||
|
|
8413b79fa9 | ||
|
|
dffcdcc148 | ||
|
|
4b53c3422f | ||
|
|
3fb668474d | ||
|
|
ff628bb438 | ||
|
|
819d0f6a16 | ||
|
|
601ae9daf2 | ||
|
|
09409804af | ||
|
|
1bccd521f8 | ||
|
|
5e2b3c1d07 | ||
|
|
210bdc8022 | ||
|
|
3cb96235b7 | ||
|
|
d695657711 | ||
|
|
5131c4c10b | ||
|
|
912ebf4672 | ||
|
|
dd0fc6fab8 | ||
|
|
910136ee9b | ||
|
|
61f652da04 | ||
|
|
a2b4cd8050 | ||
|
|
774738110b | ||
|
|
851a1ac64c | ||
|
|
d653391cdd | ||
|
|
f96b70841f | ||
|
|
8d4807c9e7 | ||
|
|
87825f7ebb | ||
|
|
be4f3ec81d | ||
|
|
56604a5445 | ||
|
|
c0d282e85b | ||
|
|
b9b32f0526 | ||
|
|
be4beacdf7 | ||
|
|
bf6b398a27 | ||
|
|
9a0f0a9701 | ||
|
|
ef8edfb67b | ||
|
|
0e8da2db18 | ||
|
|
e65d132b3d | ||
|
|
13b2fcffd2 | ||
|
|
c1e486bf43 | ||
|
|
8c68e92e74 | ||
|
|
a6ef27164c | ||
|
|
d50a650686 | ||
|
|
35dd3916dd | ||
|
|
1a28e1091c | ||
|
|
124458c3d6 | ||
|
|
8e2dbd1775 | ||
|
|
27188f4dff | ||
|
|
ef13f6fb3b | ||
|
|
92391254bc | ||
|
|
d3e87b2435 | ||
|
|
e5666dfdf2 | ||
|
|
e96e615761 | ||
|
|
c85aa0739d | ||
|
|
d814f3aaa4 | ||
|
|
3d5f9a76e4 | ||
|
|
d27528a771 | ||
|
|
04ea81e7cd | ||
|
|
d7769dec33 | ||
|
|
12adeadc94 | ||
|
|
b5429f7504 | ||
|
|
cf5c3ee536 | ||
|
|
86c450bd91 | ||
|
|
0d6ab099ac | ||
|
|
5110f83fae | ||
|
|
252e05e963 | ||
|
|
635ecdef72 | ||
|
|
b08d2b07bc | ||
|
|
3919ad3ccf | ||
|
|
aca4f5c286 | ||
|
|
387b4c66d9 | ||
|
|
7c40d2caa9 | ||
|
|
02203e7ce5 | ||
|
|
53583741ba | ||
|
|
12eb9671de | ||
|
|
29d66bfd97 | ||
|
|
57fde5ae7c | ||
|
|
471f902171 | ||
|
|
2e2aba1bbb | ||
|
|
f2347b2f77 | ||
|
|
a39645a297 | ||
|
|
806a0b92a0 | ||
|
|
a438357b45 | ||
|
|
206eb0513d | ||
|
|
5ad6837547 | ||
|
|
272a040c91 | ||
|
|
c04b9e5340 | ||
|
|
3f085a977c | ||
|
|
a1dd12a947 | ||
|
|
a7df43bd45 | ||
|
|
5d749c2ebf | ||
|
|
536ca15e90 | ||
|
|
703e423e04 | ||
|
|
780fec8e36 | ||
|
|
0a436600f4 | ||
|
|
32c2ce90e2 | ||
|
|
a864641692 | ||
|
|
344eee098d | ||
|
|
bc4b0a0b35 | ||
|
|
b23943e30b | ||
|
|
25ed6a71fb | ||
|
|
8dc6d05ed6 | ||
|
|
fe5a993fc9 | ||
|
|
6df5eb3787 | ||
|
|
bc3d5e97ea | ||
|
|
9909b6d481 | ||
|
|
90a32d1b67 | ||
|
|
472834ac42 | ||
|
|
b3f4c6f751 | ||
|
|
317303fc43 | ||
|
|
b6b579d55d | ||
|
|
6d6f4f092d | ||
|
|
7473681c5b | ||
|
|
54c8872d25 | ||
|
|
c5ce45f588 | ||
|
|
07a0c4dfe3 | ||
|
|
80bb94e745 | ||
|
|
6c89412f39 | ||
|
|
034e29cd74 | ||
|
|
0e0764eff8 | ||
|
|
e47db0b8c9 | ||
|
|
6d401dcd59 | ||
|
|
6609c2e928 | ||
|
|
a161d25d48 | ||
|
|
4adedf9436 | ||
|
|
1168e94534 | ||
|
|
b57bfe3eee | ||
|
|
3592e88e4f | ||
|
|
219cde4733 | ||
|
|
c82cd50d87 | ||
|
|
dae4893fe1 | ||
|
|
1e686f0428 | ||
|
|
08c5a5a4f6 | ||
|
|
9360f24d89 | ||
|
|
d0477b216f | ||
|
|
a812f4729c | ||
|
|
db324998e3 | ||
|
|
4ec65a80df | ||
|
|
f2b9700345 | ||
|
|
d8f8ab785c | ||
|
|
b316efe80b | ||
|
|
14a4587f5e | ||
|
|
afd99d2d68 | ||
|
|
7bba1c9c5e | ||
|
|
fd79afb429 | ||
|
|
d5f00597a5 | ||
|
|
1c4ccfe294 | ||
|
|
f48423d5aa | ||
|
|
5d98d9b54b | ||
|
|
132dd4acc4 | ||
|
|
c7e306841a | ||
|
|
5e74a3993b | ||
|
|
5bf10b89b1 | ||
|
|
bde9dd8b88 | ||
|
|
42d28db47a | ||
|
|
128601bb58 | ||
|
|
86addbdc9a | ||
|
|
de9be4bbe0 | ||
|
|
49b79aadfd | ||
|
|
6dab3eddea | ||
|
|
949f14b119 | ||
|
|
de2818de4c | ||
|
|
0f3fcb2917 | ||
|
|
3356fd9815 | ||
|
|
7bef930d0c | ||
|
|
db1a754b39 | ||
|
|
9b9b2731ba | ||
|
|
5523fc9023 | ||
|
|
a380fd9adc | ||
|
|
d3ecf1d7a8 | ||
|
|
6834c20b5d | ||
|
|
b9035659d2 | ||
|
|
5b47427484 | ||
|
|
6e95e1279a | ||
|
|
a2e781fb3f | ||
|
|
69c7f116b1 | ||
|
|
2ef1c90248 | ||
|
|
782df54570 | ||
|
|
0ba6645df0 | ||
|
|
0579251c70 | ||
|
|
c3363604ac | ||
|
|
09aa67ba61 | ||
|
|
4ff7ee4e60 | ||
|
|
5b81b35bf8 | ||
|
|
df3a529f0a | ||
|
|
43e1f25f89 | ||
|
|
7c6c9284f2 | ||
|
|
3d8eec2557 | ||
|
|
5a07638f4d | ||
|
|
87250d13d7 | ||
|
|
90d13684e5 | ||
|
|
25206e71cf | ||
|
|
6fa6dde637 | ||
|
|
e70817f776 | ||
|
|
ca5c606dfc | ||
|
|
ac872b577a | ||
|
|
2761959f93 | ||
|
|
7bf708faab | ||
|
|
c526209925 | ||
|
|
8215cf7857 | ||
|
|
5745606fe7 | ||
|
|
f15cf3e8be | ||
|
|
8e8b0578b2 | ||
|
|
abc929824c | ||
|
|
44e48423ed | ||
|
|
3883cc8b67 | ||
|
|
8e6272920b | ||
|
|
0cde215259 | ||
|
|
3fc54c095e | ||
|
|
80a0a15490 | ||
|
|
af49c78498 | ||
|
|
4839c5f313 | ||
|
|
e9c6feb3c4 | ||
|
|
b8803f380b | ||
|
|
16166c3367 | ||
|
|
db4b153ce1 | ||
|
|
50305e0eee | ||
|
|
53f31ba3b8 | ||
|
|
ffca440135 | ||
|
|
9fda8f9c92 | ||
|
|
a48503d821 | ||
|
|
f9c1941384 | ||
|
|
9520380388 | ||
|
|
a88d02b0b4 | ||
|
|
0a8501fcbb | ||
|
|
c9d50641c8 | ||
|
|
9e06cfbdf0 | ||
|
|
135a92feb4 | ||
|
|
cd4b5e0c80 | ||
|
|
3cd0506810 | ||
|
|
ffa2cf62f5 | ||
|
|
0e439d7ae6 | ||
|
|
a99c6c4cbe | ||
|
|
9e818c2882 | ||
|
|
c243a02e7a | ||
|
|
967286f45d | ||
|
|
8e794be13f | ||
|
|
a8f70d7f59 | ||
|
|
ab91ffe12c | ||
|
|
24b51a7e87 | ||
|
|
c2e63070e6 | ||
|
|
b6627098c2 | ||
|
|
097955e587 | ||
|
|
497a8392f6 | ||
|
|
dcce211676 | ||
|
|
631b29eddc | ||
|
|
9f12cbd43d | ||
|
|
b24825d453 | ||
|
|
3861e964f4 | ||
|
|
ca4428cff2 | ||
|
|
6b09c4f9b7 |
29
.codeclimate.yml
Normal file
29
.codeclimate.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
engines:
|
||||
gofmt:
|
||||
enabled: true
|
||||
golint:
|
||||
enabled: true
|
||||
govet:
|
||||
enabled: true
|
||||
csslint:
|
||||
enabled: true
|
||||
duplication:
|
||||
enabled: true
|
||||
config:
|
||||
languages:
|
||||
javascript:
|
||||
mass_threshold: 80
|
||||
eslint:
|
||||
enabled: true
|
||||
config:
|
||||
config: .eslintrc.yml
|
||||
fixme:
|
||||
enabled: true
|
||||
ratings:
|
||||
paths:
|
||||
- "**.css"
|
||||
- "**.js"
|
||||
- "**.go"
|
||||
exclude_paths:
|
||||
- test/
|
||||
@@ -1,2 +1,3 @@
|
||||
*
|
||||
!dist
|
||||
!build
|
||||
|
||||
284
.eslintrc.yml
Normal file
284
.eslintrc.yml
Normal file
@@ -0,0 +1,284 @@
|
||||
env:
|
||||
browser: true
|
||||
jquery: true
|
||||
|
||||
# globals:
|
||||
# angular: true
|
||||
# $: true
|
||||
# _: true
|
||||
# moment: true
|
||||
# filesize: true
|
||||
# splitargs: true
|
||||
extends:
|
||||
- 'eslint:recommended'
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
||||
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
|
||||
|
||||
# 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: off
|
||||
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
|
||||
|
||||
# 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
|
||||
6
.github/ISSUE_TEMPLATE.md
vendored
6
.github/ISSUE_TEMPLATE.md
vendored
@@ -2,7 +2,7 @@
|
||||
|
||||
Thanks for opening an issue on Portainer !
|
||||
|
||||
Do you need help or have a question? Come chat with us on gitter: https://gitter.im/portainer/Lobby.
|
||||
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.
|
||||
|
||||
If you are reporting 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
|
||||
@@ -37,8 +37,8 @@ Any other info e.g. Why do you consider this to be a bug? What did you expect to
|
||||
**Technical details:**
|
||||
|
||||
* Portainer version:
|
||||
* Portainer Docker image tag (latest/arm/windows...):
|
||||
* Target Docker version (the host/cluster you manage):
|
||||
* Target Swarm version (if applicable):
|
||||
* Platform (windows/linux):
|
||||
* Command used to start Portainer (`docker run -p 9000:9000 portainer/portainer`):
|
||||
* Target Swarm version (if applicable):
|
||||
* Browser:
|
||||
|
||||
46
CODE_OF_CONDUCT.md
Normal file
46
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at anthony.lapenna@portainer.io. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
||||
@@ -22,11 +22,13 @@ Some of the open issues are labeled with prefix `exp/`, this is used to mark the
|
||||
* **beginner**: a task that should be accessible with users not familiar with the codebase
|
||||
* **intermediate**: a task that require some understanding of the project codebase or some experience in
|
||||
either AngularJS or Golang
|
||||
* **advanced**: a task that require a deep understanding of the project codebase
|
||||
|
||||
You can have a use Github filters to list these issues:
|
||||
|
||||
* beginner labeled issues: https://github.com/portainer/portainer/labels/exp%2Fbeginner
|
||||
* intermediate labeled issues: https://github.com/portainer/portainer/labels/exp%2Fintermediate
|
||||
* advanced labeled issues: https://github.com/portainer/portainer/labels/exp%2Fadvanced
|
||||
|
||||
### Linting
|
||||
|
||||
|
||||
22
README.md
22
README.md
@@ -1,24 +1,28 @@
|
||||
|
||||
<p align="center">
|
||||
<img title="portainer" src='http://portainer.io/images/logo_alt.png' />
|
||||
<img title="portainer" src='https://portainer.io/images/logo_alt.png' />
|
||||
</p>
|
||||
|
||||
[](https://hub.docker.com/r/portainer/portainer/)
|
||||
[](http://microbadger.com/images/portainer/portainer "Image size")
|
||||
[](http://portainer.readthedocs.io/en/latest/?badge=stable)
|
||||
[](http://portainer.readthedocs.io/en/stable/?badge=stable)
|
||||
[]( https://g.codefresh.io/repositories/portainer/portainer/builds?filter=trigger:build;branch:develop;service:5922a08a3a1aab000116fcc6~portainer-ci)
|
||||
[](https://codeclimate.com/github/portainer/portainer)
|
||||
[](https://portainer.io/slack/)
|
||||
[](https://gitter.im/portainer/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
|
||||
[](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 Docker host or Swarm cluster.
|
||||
**_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 (Docker for Linux and Docker for Windows are supported).
|
||||
**_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*.
|
||||
**_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*.
|
||||
|
||||
## Demo
|
||||
|
||||
<img src="http://portainer.io/images/screenshots/portainer.gif" width="77%"/>
|
||||
<img src="https://portainer.io/images/screenshots/portainer.gif" width="77%"/>
|
||||
|
||||
You can try out the public demo instance: http://demo.portainer.io/ (login with the username **demo** and the password **tryportainer**).
|
||||
You can try out the public demo instance: http://demo.portainer.io/ (login with the username **admin** and the password **tryportainer**).
|
||||
|
||||
Please note that the public demo cluster is **reset every 15min**.
|
||||
|
||||
@@ -31,8 +35,8 @@ Please note that the public demo cluster is **reset every 15min**.
|
||||
|
||||
* Issues: https://github.com/portainer/portainer/issues
|
||||
* FAQ: https://portainer.readthedocs.io/en/latest/faq.html
|
||||
* Slack (chat): https://portainer.io/slack/
|
||||
* Gitter (chat): https://gitter.im/portainer/Lobby
|
||||
* Slack: http://portainer.io/slack/
|
||||
|
||||
## Reporting bugs and contributing
|
||||
|
||||
@@ -43,7 +47,7 @@ Please note that the public demo cluster is **reset every 15min**.
|
||||
|
||||
**_Portainer_** has full support for the following Docker versions:
|
||||
|
||||
* Docker 1.10 to Docker 1.12 (including `swarm-mode`)
|
||||
* Docker 1.10 to the latest version
|
||||
* Docker Swarm >= 1.2.3
|
||||
|
||||
Partial support for the following Docker versions (some features may not be available):
|
||||
|
||||
@@ -1,76 +0,0 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
type Migrator struct {
|
||||
UserService *UserService
|
||||
EndpointService *EndpointService
|
||||
ResourceControlService *ResourceControlService
|
||||
VersionService *VersionService
|
||||
CurrentDBVersion int
|
||||
store *Store
|
||||
}
|
||||
|
||||
func NewMigrator(store *Store, version int) *Migrator {
|
||||
return &Migrator{
|
||||
UserService: store.UserService,
|
||||
EndpointService: store.EndpointService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
VersionService: store.VersionService,
|
||||
CurrentDBVersion: version,
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Migrator) Migrate() error {
|
||||
|
||||
// Portainer < 1.12
|
||||
if m.CurrentDBVersion == 0 {
|
||||
err := m.updateAdminUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err := m.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateAdminUser() error {
|
||||
u, err := m.UserService.UserByUsername("admin")
|
||||
if err == nil {
|
||||
admin := &portainer.User{
|
||||
Username: "admin",
|
||||
Password: u.Password,
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
err = m.UserService.CreateUser(admin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = m.removeLegacyAdminUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil && err != portainer.ErrUserNotFound {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) removeLegacyAdminUser() error {
|
||||
return m.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(userBucketName))
|
||||
err := bucket.Delete([]byte("admin"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -17,22 +17,32 @@ type Store struct {
|
||||
|
||||
// Services
|
||||
UserService *UserService
|
||||
TeamService *TeamService
|
||||
TeamMembershipService *TeamMembershipService
|
||||
EndpointService *EndpointService
|
||||
ResourceControlService *ResourceControlService
|
||||
VersionService *VersionService
|
||||
SettingsService *SettingsService
|
||||
RegistryService *RegistryService
|
||||
DockerHubService *DockerHubService
|
||||
StackService *StackService
|
||||
|
||||
db *bolt.DB
|
||||
checkForDataMigration bool
|
||||
}
|
||||
|
||||
const (
|
||||
databaseFileName = "portainer.db"
|
||||
versionBucketName = "version"
|
||||
userBucketName = "users"
|
||||
endpointBucketName = "endpoints"
|
||||
containerResourceControlBucketName = "containerResourceControl"
|
||||
serviceResourceControlBucketName = "serviceResourceControl"
|
||||
volumeResourceControlBucketName = "volumeResourceControl"
|
||||
databaseFileName = "portainer.db"
|
||||
versionBucketName = "version"
|
||||
userBucketName = "users"
|
||||
teamBucketName = "teams"
|
||||
teamMembershipBucketName = "team_membership"
|
||||
endpointBucketName = "endpoints"
|
||||
resourceControlBucketName = "resource_control"
|
||||
settingsBucketName = "settings"
|
||||
registryBucketName = "registries"
|
||||
dockerhubBucketName = "dockerhub"
|
||||
stackBucketName = "stacks"
|
||||
)
|
||||
|
||||
// NewStore initializes a new Store and the associated services
|
||||
@@ -40,16 +50,28 @@ func NewStore(storePath string) (*Store, error) {
|
||||
store := &Store{
|
||||
Path: storePath,
|
||||
UserService: &UserService{},
|
||||
TeamService: &TeamService{},
|
||||
TeamMembershipService: &TeamMembershipService{},
|
||||
EndpointService: &EndpointService{},
|
||||
ResourceControlService: &ResourceControlService{},
|
||||
VersionService: &VersionService{},
|
||||
SettingsService: &SettingsService{},
|
||||
RegistryService: &RegistryService{},
|
||||
DockerHubService: &DockerHubService{},
|
||||
StackService: &StackService{},
|
||||
}
|
||||
store.UserService.store = store
|
||||
store.TeamService.store = store
|
||||
store.TeamMembershipService.store = store
|
||||
store.EndpointService.store = store
|
||||
store.ResourceControlService.store = store
|
||||
store.VersionService.store = store
|
||||
store.SettingsService.store = store
|
||||
store.RegistryService.store = store
|
||||
store.DockerHubService.store = store
|
||||
store.StackService.store = store
|
||||
|
||||
_, err := os.Stat(storePath)
|
||||
_, err := os.Stat(storePath + "/" + databaseFileName)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
store.checkForDataMigration = false
|
||||
} else if err != nil {
|
||||
@@ -64,36 +86,26 @@ func NewStore(storePath string) (*Store, error) {
|
||||
// Open opens and initializes the BoltDB database.
|
||||
func (store *Store) Open() error {
|
||||
path := store.Path + "/" + databaseFileName
|
||||
|
||||
db, err := bolt.Open(path, 0600, &bolt.Options{Timeout: 1 * time.Second})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
store.db = db
|
||||
|
||||
bucketsToCreate := []string{versionBucketName, userBucketName, teamBucketName, endpointBucketName,
|
||||
resourceControlBucketName, teamMembershipBucketName, settingsBucketName,
|
||||
registryBucketName, dockerhubBucketName, stackBucketName}
|
||||
|
||||
return db.Update(func(tx *bolt.Tx) error {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(versionBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(userBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(endpointBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(containerResourceControlBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(serviceResourceControlBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(volumeResourceControlBucketName))
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
for _, bucket := range bucketsToCreate {
|
||||
_, err := tx.CreateBucketIfNotExists([]byte(bucket))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
61
api/bolt/dockerhub_service.go
Normal file
61
api/bolt/dockerhub_service.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
// DockerHubService represents a service for managing registries.
|
||||
type DockerHubService struct {
|
||||
store *Store
|
||||
}
|
||||
|
||||
const (
|
||||
dbDockerHubKey = "DOCKERHUB"
|
||||
)
|
||||
|
||||
// DockerHub returns the DockerHub object.
|
||||
func (service *DockerHubService) DockerHub() (*portainer.DockerHub, error) {
|
||||
var data []byte
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(dockerhubBucketName))
|
||||
value := bucket.Get([]byte(dbDockerHubKey))
|
||||
if value == nil {
|
||||
return portainer.ErrDockerHubNotFound
|
||||
}
|
||||
|
||||
data = make([]byte, len(value))
|
||||
copy(data, value)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dockerhub portainer.DockerHub
|
||||
err = internal.UnmarshalDockerHub(data, &dockerhub)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dockerhub, nil
|
||||
}
|
||||
|
||||
// StoreDockerHub persists a DockerHub object.
|
||||
func (service *DockerHubService) StoreDockerHub(dockerhub *portainer.DockerHub) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(dockerhubBucketName))
|
||||
|
||||
data, err := internal.MarshalDockerHub(dockerhub)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = bucket.Put([]byte(dbDockerHubKey), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
// EndpointService represents a service for managing users.
|
||||
// EndpointService represents a service for managing endpoints.
|
||||
type EndpointService struct {
|
||||
store *Store
|
||||
}
|
||||
|
||||
@@ -17,6 +17,26 @@ func UnmarshalUser(data []byte, user *portainer.User) error {
|
||||
return json.Unmarshal(data, user)
|
||||
}
|
||||
|
||||
// MarshalTeam encodes a team to binary format.
|
||||
func MarshalTeam(team *portainer.Team) ([]byte, error) {
|
||||
return json.Marshal(team)
|
||||
}
|
||||
|
||||
// UnmarshalTeam decodes a team from a binary data.
|
||||
func UnmarshalTeam(data []byte, team *portainer.Team) error {
|
||||
return json.Unmarshal(data, team)
|
||||
}
|
||||
|
||||
// MarshalTeamMembership encodes a team membership to binary format.
|
||||
func MarshalTeamMembership(membership *portainer.TeamMembership) ([]byte, error) {
|
||||
return json.Marshal(membership)
|
||||
}
|
||||
|
||||
// UnmarshalTeamMembership decodes a team membership from a binary data.
|
||||
func UnmarshalTeamMembership(data []byte, membership *portainer.TeamMembership) error {
|
||||
return json.Unmarshal(data, membership)
|
||||
}
|
||||
|
||||
// MarshalEndpoint encodes an endpoint to binary format.
|
||||
func MarshalEndpoint(endpoint *portainer.Endpoint) ([]byte, error) {
|
||||
return json.Marshal(endpoint)
|
||||
@@ -27,6 +47,26 @@ func UnmarshalEndpoint(data []byte, endpoint *portainer.Endpoint) error {
|
||||
return json.Unmarshal(data, endpoint)
|
||||
}
|
||||
|
||||
// MarshalStack encodes a stack to binary format.
|
||||
func MarshalStack(stack *portainer.Stack) ([]byte, error) {
|
||||
return json.Marshal(stack)
|
||||
}
|
||||
|
||||
// UnmarshalStack decodes a stack from a binary data.
|
||||
func UnmarshalStack(data []byte, stack *portainer.Stack) error {
|
||||
return json.Unmarshal(data, stack)
|
||||
}
|
||||
|
||||
// MarshalRegistry encodes a registry to binary format.
|
||||
func MarshalRegistry(registry *portainer.Registry) ([]byte, error) {
|
||||
return json.Marshal(registry)
|
||||
}
|
||||
|
||||
// UnmarshalRegistry decodes a registry from a binary data.
|
||||
func UnmarshalRegistry(data []byte, registry *portainer.Registry) error {
|
||||
return json.Unmarshal(data, registry)
|
||||
}
|
||||
|
||||
// MarshalResourceControl encodes a resource control object to binary format.
|
||||
func MarshalResourceControl(rc *portainer.ResourceControl) ([]byte, error) {
|
||||
return json.Marshal(rc)
|
||||
@@ -37,6 +77,26 @@ func UnmarshalResourceControl(data []byte, rc *portainer.ResourceControl) error
|
||||
return json.Unmarshal(data, rc)
|
||||
}
|
||||
|
||||
// MarshalSettings encodes a settings object to binary format.
|
||||
func MarshalSettings(settings *portainer.Settings) ([]byte, error) {
|
||||
return json.Marshal(settings)
|
||||
}
|
||||
|
||||
// UnmarshalSettings decodes a settings object from a binary data.
|
||||
func UnmarshalSettings(data []byte, settings *portainer.Settings) error {
|
||||
return json.Unmarshal(data, settings)
|
||||
}
|
||||
|
||||
// MarshalDockerHub encodes a Dockerhub object to binary format.
|
||||
func MarshalDockerHub(settings *portainer.DockerHub) ([]byte, error) {
|
||||
return json.Marshal(settings)
|
||||
}
|
||||
|
||||
// UnmarshalDockerHub decodes a Dockerhub object from a binary data.
|
||||
func UnmarshalDockerHub(data []byte, settings *portainer.DockerHub) error {
|
||||
return json.Unmarshal(data, settings)
|
||||
}
|
||||
|
||||
// Itob returns an 8-byte big endian representation of v.
|
||||
// This function is typically used for encoding integer IDs to byte slices
|
||||
// so that they can be used as BoltDB keys.
|
||||
|
||||
39
api/bolt/migrate_dbversion0.go
Normal file
39
api/bolt/migrate_dbversion0.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateAdminUserToDBVersion1() error {
|
||||
u, err := m.UserService.UserByUsername("admin")
|
||||
if err == nil {
|
||||
admin := &portainer.User{
|
||||
Username: "admin",
|
||||
Password: u.Password,
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
err = m.UserService.CreateUser(admin)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = m.removeLegacyAdminUser()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil && err != portainer.ErrUserNotFound {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) removeLegacyAdminUser() error {
|
||||
return m.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(userBucketName))
|
||||
err := bucket.Delete([]byte("admin"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
103
api/bolt/migrate_dbversion1.go
Normal file
103
api/bolt/migrate_dbversion1.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/boltdb/bolt"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/internal"
|
||||
)
|
||||
|
||||
func (m *Migrator) updateResourceControlsToDBVersion2() error {
|
||||
legacyResourceControls, err := m.retrieveLegacyResourceControls()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, resourceControl := range legacyResourceControls {
|
||||
resourceControl.SubResourceIDs = []string{}
|
||||
resourceControl.TeamAccesses = []portainer.TeamResourceAccess{}
|
||||
|
||||
owner, err := m.UserService.User(resourceControl.OwnerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if owner.Role == portainer.AdministratorRole {
|
||||
resourceControl.AdministratorsOnly = true
|
||||
resourceControl.UserAccesses = []portainer.UserResourceAccess{}
|
||||
} else {
|
||||
resourceControl.AdministratorsOnly = false
|
||||
userAccess := portainer.UserResourceAccess{
|
||||
UserID: resourceControl.OwnerID,
|
||||
AccessLevel: portainer.ReadWriteAccessLevel,
|
||||
}
|
||||
resourceControl.UserAccesses = []portainer.UserResourceAccess{userAccess}
|
||||
}
|
||||
|
||||
err = m.ResourceControlService.CreateResourceControl(&resourceControl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) updateEndpointsToDBVersion2() error {
|
||||
legacyEndpoints, err := m.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range legacyEndpoints {
|
||||
endpoint.AuthorizedTeams = []portainer.TeamID{}
|
||||
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Migrator) retrieveLegacyResourceControls() ([]portainer.ResourceControl, error) {
|
||||
legacyResourceControls := make([]portainer.ResourceControl, 0)
|
||||
err := m.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte("containerResourceControl"))
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var resourceControl portainer.ResourceControl
|
||||
err := internal.UnmarshalResourceControl(v, &resourceControl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resourceControl.Type = portainer.ContainerResourceControl
|
||||
legacyResourceControls = append(legacyResourceControls, resourceControl)
|
||||
}
|
||||
|
||||
bucket = tx.Bucket([]byte("serviceResourceControl"))
|
||||
cursor = bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var resourceControl portainer.ResourceControl
|
||||
err := internal.UnmarshalResourceControl(v, &resourceControl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resourceControl.Type = portainer.ServiceResourceControl
|
||||
legacyResourceControls = append(legacyResourceControls, resourceControl)
|
||||
}
|
||||
|
||||
bucket = tx.Bucket([]byte("volumeResourceControl"))
|
||||
cursor = bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var resourceControl portainer.ResourceControl
|
||||
err := internal.UnmarshalResourceControl(v, &resourceControl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resourceControl.Type = portainer.VolumeResourceControl
|
||||
legacyResourceControls = append(legacyResourceControls, resourceControl)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return legacyResourceControls, err
|
||||
}
|
||||
25
api/bolt/migrate_dbversion2.go
Normal file
25
api/bolt/migrate_dbversion2.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package bolt
|
||||
|
||||
import "github.com/portainer/portainer"
|
||||
|
||||
func (m *Migrator) updateSettingsToDBVersion3() error {
|
||||
legacySettings, err := m.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
legacySettings.AuthenticationMethod = portainer.AuthenticationInternal
|
||||
legacySettings.LDAPSettings = portainer.LDAPSettings{
|
||||
TLSConfig: portainer.TLSConfiguration{},
|
||||
SearchSettings: []portainer.LDAPSearchSettings{
|
||||
portainer.LDAPSearchSettings{},
|
||||
},
|
||||
}
|
||||
|
||||
err = m.SettingsService.StoreSettings(legacySettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
27
api/bolt/migrate_dbversion3.go
Normal file
27
api/bolt/migrate_dbversion3.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package bolt
|
||||
|
||||
import "github.com/portainer/portainer"
|
||||
|
||||
func (m *Migrator) updateEndpointsToDBVersion4() error {
|
||||
legacyEndpoints, err := m.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, endpoint := range legacyEndpoints {
|
||||
endpoint.TLSConfig = portainer.TLSConfiguration{}
|
||||
if endpoint.TLS {
|
||||
endpoint.TLSConfig.TLS = true
|
||||
endpoint.TLSConfig.TLSSkipVerify = false
|
||||
endpoint.TLSConfig.TLSCACertPath = endpoint.TLSCACertPath
|
||||
endpoint.TLSConfig.TLSCertPath = endpoint.TLSCertPath
|
||||
endpoint.TLSConfig.TLSKeyPath = endpoint.TLSKeyPath
|
||||
}
|
||||
err = m.EndpointService.UpdateEndpoint(endpoint.ID, &endpoint)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
16
api/bolt/migrate_dbversion4.go
Normal file
16
api/bolt/migrate_dbversion4.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package bolt
|
||||
|
||||
func (m *Migrator) updateSettingsToVersion5() error {
|
||||
legacySettings, err := m.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
legacySettings.AllowBindMountsForRegularUsers = true
|
||||
|
||||
err = m.SettingsService.StoreSettings(legacySettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
16
api/bolt/migrate_dbversion5.go
Normal file
16
api/bolt/migrate_dbversion5.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package bolt
|
||||
|
||||
func (m *Migrator) updateSettingsToVersion6() error {
|
||||
legacySettings, err := m.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
legacySettings.AllowPrivilegedModeForRegularUsers = true
|
||||
|
||||
err = m.SettingsService.StoreSettings(legacySettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
89
api/bolt/migrator.go
Normal file
89
api/bolt/migrator.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package bolt
|
||||
|
||||
import "github.com/portainer/portainer"
|
||||
|
||||
// Migrator defines a service to migrate data after a Portainer version update.
|
||||
type Migrator struct {
|
||||
UserService *UserService
|
||||
EndpointService *EndpointService
|
||||
ResourceControlService *ResourceControlService
|
||||
SettingsService *SettingsService
|
||||
VersionService *VersionService
|
||||
CurrentDBVersion int
|
||||
store *Store
|
||||
}
|
||||
|
||||
// NewMigrator creates a new Migrator.
|
||||
func NewMigrator(store *Store, version int) *Migrator {
|
||||
return &Migrator{
|
||||
UserService: store.UserService,
|
||||
EndpointService: store.EndpointService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
SettingsService: store.SettingsService,
|
||||
VersionService: store.VersionService,
|
||||
CurrentDBVersion: version,
|
||||
store: store,
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate checks the database version and migrate the existing data to the most recent data model.
|
||||
func (m *Migrator) Migrate() error {
|
||||
|
||||
// Portainer < 1.12
|
||||
if m.CurrentDBVersion < 1 {
|
||||
err := m.updateAdminUserToDBVersion1()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.12.x
|
||||
if m.CurrentDBVersion < 2 {
|
||||
err := m.updateResourceControlsToDBVersion2()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = m.updateEndpointsToDBVersion2()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.13.x
|
||||
if m.CurrentDBVersion < 3 {
|
||||
err := m.updateSettingsToDBVersion3()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Portainer 1.14.0
|
||||
if m.CurrentDBVersion < 4 {
|
||||
err := m.updateEndpointsToDBVersion4()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1235
|
||||
if m.CurrentDBVersion < 5 {
|
||||
err := m.updateSettingsToVersion5()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/portainer/portainer/issues/1236
|
||||
if m.CurrentDBVersion < 6 {
|
||||
err := m.updateSettingsToVersion6()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err := m.VersionService.StoreDBVersion(portainer.DBVersion)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
114
api/bolt/registry_service.go
Normal file
114
api/bolt/registry_service.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
// RegistryService represents a service for managing registries.
|
||||
type RegistryService struct {
|
||||
store *Store
|
||||
}
|
||||
|
||||
// Registry returns an registry by ID.
|
||||
func (service *RegistryService) Registry(ID portainer.RegistryID) (*portainer.Registry, error) {
|
||||
var data []byte
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(registryBucketName))
|
||||
value := bucket.Get(internal.Itob(int(ID)))
|
||||
if value == nil {
|
||||
return portainer.ErrRegistryNotFound
|
||||
}
|
||||
|
||||
data = make([]byte, len(value))
|
||||
copy(data, value)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var registry portainer.Registry
|
||||
err = internal.UnmarshalRegistry(data, ®istry)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ®istry, nil
|
||||
}
|
||||
|
||||
// Registries returns an array containing all the registries.
|
||||
func (service *RegistryService) Registries() ([]portainer.Registry, error) {
|
||||
var registries = make([]portainer.Registry, 0)
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(registryBucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var registry portainer.Registry
|
||||
err := internal.UnmarshalRegistry(v, ®istry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
registries = append(registries, registry)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return registries, nil
|
||||
}
|
||||
|
||||
// CreateRegistry creates a new registry.
|
||||
func (service *RegistryService) CreateRegistry(registry *portainer.Registry) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(registryBucketName))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
registry.ID = portainer.RegistryID(id)
|
||||
|
||||
data, err := internal.MarshalRegistry(registry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = bucket.Put(internal.Itob(int(registry.ID)), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateRegistry updates an registry.
|
||||
func (service *RegistryService) UpdateRegistry(ID portainer.RegistryID, registry *portainer.Registry) error {
|
||||
data, err := internal.MarshalRegistry(registry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(registryBucketName))
|
||||
err = bucket.Put(internal.Itob(int(ID)), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteRegistry deletes an registry.
|
||||
func (service *RegistryService) DeleteRegistry(ID portainer.RegistryID) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(registryBucketName))
|
||||
err := bucket.Delete(internal.Itob(int(ID)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
148
api/bolt/resource_control_service.go
Normal file
148
api/bolt/resource_control_service.go
Normal file
@@ -0,0 +1,148 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
// ResourceControlService represents a service for managing resource controls.
|
||||
type ResourceControlService struct {
|
||||
store *Store
|
||||
}
|
||||
|
||||
// ResourceControl returns a ResourceControl object by ID
|
||||
func (service *ResourceControlService) ResourceControl(ID portainer.ResourceControlID) (*portainer.ResourceControl, error) {
|
||||
var data []byte
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(resourceControlBucketName))
|
||||
value := bucket.Get(internal.Itob(int(ID)))
|
||||
if value == nil {
|
||||
return portainer.ErrResourceControlNotFound
|
||||
}
|
||||
|
||||
data = make([]byte, len(value))
|
||||
copy(data, value)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var resourceControl portainer.ResourceControl
|
||||
err = internal.UnmarshalResourceControl(data, &resourceControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &resourceControl, nil
|
||||
}
|
||||
|
||||
// ResourceControlByResourceID returns a ResourceControl object by checking if the resourceID is equal
|
||||
// to the main ResourceID or in SubResourceIDs
|
||||
func (service *ResourceControlService) ResourceControlByResourceID(resourceID string) (*portainer.ResourceControl, error) {
|
||||
var resourceControl *portainer.ResourceControl
|
||||
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(resourceControlBucketName))
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var rc portainer.ResourceControl
|
||||
err := internal.UnmarshalResourceControl(v, &rc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rc.ResourceID == resourceID {
|
||||
resourceControl = &rc
|
||||
}
|
||||
for _, subResourceID := range rc.SubResourceIDs {
|
||||
if subResourceID == resourceID {
|
||||
resourceControl = &rc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if resourceControl == nil {
|
||||
return portainer.ErrResourceControlNotFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resourceControl, nil
|
||||
}
|
||||
|
||||
// ResourceControls returns all the ResourceControl objects
|
||||
func (service *ResourceControlService) ResourceControls() ([]portainer.ResourceControl, error) {
|
||||
var rcs = make([]portainer.ResourceControl, 0)
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(resourceControlBucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var resourceControl portainer.ResourceControl
|
||||
err := internal.UnmarshalResourceControl(v, &resourceControl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rcs = append(rcs, resourceControl)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rcs, nil
|
||||
}
|
||||
|
||||
// CreateResourceControl creates a new ResourceControl object
|
||||
func (service *ResourceControlService) CreateResourceControl(resourceControl *portainer.ResourceControl) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(resourceControlBucketName))
|
||||
id, _ := bucket.NextSequence()
|
||||
resourceControl.ID = portainer.ResourceControlID(id)
|
||||
data, err := internal.MarshalResourceControl(resourceControl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = bucket.Put(internal.Itob(int(resourceControl.ID)), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateResourceControl saves a ResourceControl object.
|
||||
func (service *ResourceControlService) UpdateResourceControl(ID portainer.ResourceControlID, resourceControl *portainer.ResourceControl) error {
|
||||
data, err := internal.MarshalResourceControl(resourceControl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(resourceControlBucketName))
|
||||
err = bucket.Put(internal.Itob(int(ID)), data)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteResourceControl deletes a ResourceControl object by ID
|
||||
func (service *ResourceControlService) DeleteResourceControl(ID portainer.ResourceControlID) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(resourceControlBucketName))
|
||||
err := bucket.Delete(internal.Itob(int(ID)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
// ResourceControlService represents a service for managing resource controls.
|
||||
type ResourceControlService struct {
|
||||
store *Store
|
||||
}
|
||||
|
||||
func getBucketNameByResourceControlType(rcType portainer.ResourceControlType) string {
|
||||
bucketName := containerResourceControlBucketName
|
||||
if rcType == portainer.ServiceResourceControl {
|
||||
bucketName = serviceResourceControlBucketName
|
||||
} else if rcType == portainer.VolumeResourceControl {
|
||||
bucketName = volumeResourceControlBucketName
|
||||
}
|
||||
return bucketName
|
||||
}
|
||||
|
||||
// ResourceControl returns a resource control object by resource ID
|
||||
func (service *ResourceControlService) ResourceControl(resourceID string, rcType portainer.ResourceControlType) (*portainer.ResourceControl, error) {
|
||||
var data []byte
|
||||
bucketName := getBucketNameByResourceControlType(rcType)
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
value := bucket.Get([]byte(resourceID))
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
data = make([]byte, len(value))
|
||||
copy(data, value)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if data == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var rc portainer.ResourceControl
|
||||
err = internal.UnmarshalResourceControl(data, &rc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &rc, nil
|
||||
}
|
||||
|
||||
// ResourceControls returns all resource control objects
|
||||
func (service *ResourceControlService) ResourceControls(rcType portainer.ResourceControlType) ([]portainer.ResourceControl, error) {
|
||||
var rcs = make([]portainer.ResourceControl, 0)
|
||||
bucketName := getBucketNameByResourceControlType(rcType)
|
||||
err := service.store.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 rc portainer.ResourceControl
|
||||
err := internal.UnmarshalResourceControl(v, &rc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rcs = append(rcs, rc)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rcs, nil
|
||||
}
|
||||
|
||||
// CreateResourceControl creates a new resource control
|
||||
func (service *ResourceControlService) CreateResourceControl(resourceID string, rc *portainer.ResourceControl, rcType portainer.ResourceControlType) error {
|
||||
bucketName := getBucketNameByResourceControlType(rcType)
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
data, err := internal.MarshalResourceControl(rc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = bucket.Put([]byte(resourceID), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteResourceControl deletes a resource control object by resource ID
|
||||
func (service *ResourceControlService) DeleteResourceControl(resourceID string, rcType portainer.ResourceControlType) error {
|
||||
bucketName := getBucketNameByResourceControlType(rcType)
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(bucketName))
|
||||
err := bucket.Delete([]byte(resourceID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
61
api/bolt/settings_service.go
Normal file
61
api/bolt/settings_service.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
// SettingsService represents a service to manage application settings.
|
||||
type SettingsService struct {
|
||||
store *Store
|
||||
}
|
||||
|
||||
const (
|
||||
dbSettingsKey = "SETTINGS"
|
||||
)
|
||||
|
||||
// Settings retrieve the settings object.
|
||||
func (service *SettingsService) Settings() (*portainer.Settings, error) {
|
||||
var data []byte
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(settingsBucketName))
|
||||
value := bucket.Get([]byte(dbSettingsKey))
|
||||
if value == nil {
|
||||
return portainer.ErrSettingsNotFound
|
||||
}
|
||||
|
||||
data = make([]byte, len(value))
|
||||
copy(data, value)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var settings portainer.Settings
|
||||
err = internal.UnmarshalSettings(data, &settings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &settings, nil
|
||||
}
|
||||
|
||||
// StoreSettings persists a Settings object.
|
||||
func (service *SettingsService) StoreSettings(settings *portainer.Settings) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(settingsBucketName))
|
||||
|
||||
data, err := internal.MarshalSettings(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = bucket.Put([]byte(dbSettingsKey), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
138
api/bolt/stack_service.go
Normal file
138
api/bolt/stack_service.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
// StackService represents a service for managing stacks.
|
||||
type StackService struct {
|
||||
store *Store
|
||||
}
|
||||
|
||||
// Stack returns a stack object by ID.
|
||||
func (service *StackService) Stack(ID portainer.StackID) (*portainer.Stack, error) {
|
||||
var data []byte
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(stackBucketName))
|
||||
value := bucket.Get([]byte(ID))
|
||||
if value == nil {
|
||||
return portainer.ErrStackNotFound
|
||||
}
|
||||
|
||||
data = make([]byte, len(value))
|
||||
copy(data, value)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var stack portainer.Stack
|
||||
err = internal.UnmarshalStack(data, &stack)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &stack, nil
|
||||
}
|
||||
|
||||
// Stacks returns an array containing all the stacks.
|
||||
func (service *StackService) Stacks() ([]portainer.Stack, error) {
|
||||
var stacks = make([]portainer.Stack, 0)
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(stackBucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var stack portainer.Stack
|
||||
err := internal.UnmarshalStack(v, &stack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stacks = append(stacks, stack)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stacks, nil
|
||||
}
|
||||
|
||||
// StacksBySwarmID return an array containing all the stacks related to the specified Swarm ID.
|
||||
func (service *StackService) StacksBySwarmID(id string) ([]portainer.Stack, error) {
|
||||
var stacks = make([]portainer.Stack, 0)
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(stackBucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var stack portainer.Stack
|
||||
err := internal.UnmarshalStack(v, &stack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if stack.SwarmID == id {
|
||||
stacks = append(stacks, stack)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stacks, nil
|
||||
}
|
||||
|
||||
// CreateStack creates a new stack.
|
||||
func (service *StackService) CreateStack(stack *portainer.Stack) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(stackBucketName))
|
||||
|
||||
data, err := internal.MarshalStack(stack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = bucket.Put([]byte(stack.ID), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateStack updates an stack.
|
||||
func (service *StackService) UpdateStack(ID portainer.StackID, stack *portainer.Stack) error {
|
||||
data, err := internal.MarshalStack(stack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(stackBucketName))
|
||||
err = bucket.Put([]byte(ID), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteStack deletes an stack.
|
||||
func (service *StackService) DeleteStack(ID portainer.StackID) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(stackBucketName))
|
||||
err := bucket.Delete([]byte(ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
217
api/bolt/team_membership_service.go
Normal file
217
api/bolt/team_membership_service.go
Normal file
@@ -0,0 +1,217 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
// TeamMembershipService represents a service for managing TeamMembership objects.
|
||||
type TeamMembershipService struct {
|
||||
store *Store
|
||||
}
|
||||
|
||||
// TeamMembership returns a TeamMembership object by ID
|
||||
func (service *TeamMembershipService) TeamMembership(ID portainer.TeamMembershipID) (*portainer.TeamMembership, error) {
|
||||
var data []byte
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(teamMembershipBucketName))
|
||||
value := bucket.Get(internal.Itob(int(ID)))
|
||||
if value == nil {
|
||||
return portainer.ErrTeamMembershipNotFound
|
||||
}
|
||||
|
||||
data = make([]byte, len(value))
|
||||
copy(data, value)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var membership portainer.TeamMembership
|
||||
err = internal.UnmarshalTeamMembership(data, &membership)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &membership, nil
|
||||
}
|
||||
|
||||
// TeamMemberships return an array containing all the TeamMembership objects.
|
||||
func (service *TeamMembershipService) TeamMemberships() ([]portainer.TeamMembership, error) {
|
||||
var memberships = make([]portainer.TeamMembership, 0)
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(teamMembershipBucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var membership portainer.TeamMembership
|
||||
err := internal.UnmarshalTeamMembership(v, &membership)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
memberships = append(memberships, membership)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return memberships, nil
|
||||
}
|
||||
|
||||
// TeamMembershipsByUserID return an array containing all the TeamMembership objects where the specified userID is present.
|
||||
func (service *TeamMembershipService) TeamMembershipsByUserID(userID portainer.UserID) ([]portainer.TeamMembership, error) {
|
||||
var memberships = make([]portainer.TeamMembership, 0)
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(teamMembershipBucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var membership portainer.TeamMembership
|
||||
err := internal.UnmarshalTeamMembership(v, &membership)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if membership.UserID == userID {
|
||||
memberships = append(memberships, membership)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return memberships, nil
|
||||
}
|
||||
|
||||
// TeamMembershipsByTeamID return an array containing all the TeamMembership objects where the specified teamID is present.
|
||||
func (service *TeamMembershipService) TeamMembershipsByTeamID(teamID portainer.TeamID) ([]portainer.TeamMembership, error) {
|
||||
var memberships = make([]portainer.TeamMembership, 0)
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(teamMembershipBucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var membership portainer.TeamMembership
|
||||
err := internal.UnmarshalTeamMembership(v, &membership)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if membership.TeamID == teamID {
|
||||
memberships = append(memberships, membership)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return memberships, nil
|
||||
}
|
||||
|
||||
// UpdateTeamMembership saves a TeamMembership object.
|
||||
func (service *TeamMembershipService) UpdateTeamMembership(ID portainer.TeamMembershipID, membership *portainer.TeamMembership) error {
|
||||
data, err := internal.MarshalTeamMembership(membership)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(teamMembershipBucketName))
|
||||
err = bucket.Put(internal.Itob(int(ID)), data)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// CreateTeamMembership creates a new TeamMembership object.
|
||||
func (service *TeamMembershipService) CreateTeamMembership(membership *portainer.TeamMembership) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(teamMembershipBucketName))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
membership.ID = portainer.TeamMembershipID(id)
|
||||
|
||||
data, err := internal.MarshalTeamMembership(membership)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = bucket.Put(internal.Itob(int(membership.ID)), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteTeamMembership deletes a TeamMembership object.
|
||||
func (service *TeamMembershipService) DeleteTeamMembership(ID portainer.TeamMembershipID) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(teamMembershipBucketName))
|
||||
err := bucket.Delete(internal.Itob(int(ID)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteTeamMembershipByUserID deletes all the TeamMembership object associated to a UserID.
|
||||
func (service *TeamMembershipService) DeleteTeamMembershipByUserID(userID portainer.UserID) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(teamMembershipBucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var membership portainer.TeamMembership
|
||||
err := internal.UnmarshalTeamMembership(v, &membership)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if membership.UserID == userID {
|
||||
err := bucket.Delete(internal.Itob(int(membership.ID)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteTeamMembershipByTeamID deletes all the TeamMembership object associated to a TeamID.
|
||||
func (service *TeamMembershipService) DeleteTeamMembershipByTeamID(teamID portainer.TeamID) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(teamMembershipBucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var membership portainer.TeamMembership
|
||||
err := internal.UnmarshalTeamMembership(v, &membership)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if membership.TeamID == teamID {
|
||||
err := bucket.Delete(internal.Itob(int(membership.ID)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
144
api/bolt/team_service.go
Normal file
144
api/bolt/team_service.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package bolt
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/bolt/internal"
|
||||
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
// TeamService represents a service for managing teams.
|
||||
type TeamService struct {
|
||||
store *Store
|
||||
}
|
||||
|
||||
// Team returns a Team by ID
|
||||
func (service *TeamService) Team(ID portainer.TeamID) (*portainer.Team, error) {
|
||||
var data []byte
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(teamBucketName))
|
||||
value := bucket.Get(internal.Itob(int(ID)))
|
||||
if value == nil {
|
||||
return portainer.ErrTeamNotFound
|
||||
}
|
||||
|
||||
data = make([]byte, len(value))
|
||||
copy(data, value)
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var team portainer.Team
|
||||
err = internal.UnmarshalTeam(data, &team)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &team, nil
|
||||
}
|
||||
|
||||
// TeamByName returns a team by name.
|
||||
func (service *TeamService) TeamByName(name string) (*portainer.Team, error) {
|
||||
var team *portainer.Team
|
||||
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(teamBucketName))
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var t portainer.Team
|
||||
err := internal.UnmarshalTeam(v, &t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if t.Name == name {
|
||||
team = &t
|
||||
}
|
||||
}
|
||||
|
||||
if team == nil {
|
||||
return portainer.ErrTeamNotFound
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return team, nil
|
||||
}
|
||||
|
||||
// Teams return an array containing all the teams.
|
||||
func (service *TeamService) Teams() ([]portainer.Team, error) {
|
||||
var teams = make([]portainer.Team, 0)
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(teamBucketName))
|
||||
|
||||
cursor := bucket.Cursor()
|
||||
for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
|
||||
var team portainer.Team
|
||||
err := internal.UnmarshalTeam(v, &team)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
teams = append(teams, team)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return teams, nil
|
||||
}
|
||||
|
||||
// UpdateTeam saves a Team.
|
||||
func (service *TeamService) UpdateTeam(ID portainer.TeamID, team *portainer.Team) error {
|
||||
data, err := internal.MarshalTeam(team)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(teamBucketName))
|
||||
err = bucket.Put(internal.Itob(int(ID)), data)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// CreateTeam creates a new Team.
|
||||
func (service *TeamService) CreateTeam(team *portainer.Team) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(teamBucketName))
|
||||
|
||||
id, _ := bucket.NextSequence()
|
||||
team.ID = portainer.TeamID(id)
|
||||
|
||||
data, err := internal.MarshalTeam(team)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = bucket.Put(internal.Itob(int(team.ID)), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteTeam deletes a Team.
|
||||
func (service *TeamService) DeleteTeam(ID portainer.TeamID) error {
|
||||
return service.store.db.Update(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(teamBucketName))
|
||||
err := bucket.Delete(internal.Itob(int(ID)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
@@ -8,21 +8,21 @@ import (
|
||||
"github.com/boltdb/bolt"
|
||||
)
|
||||
|
||||
// EndpointService represents a service for managing users.
|
||||
// VersionService represents a service to manage stored versions.
|
||||
type VersionService struct {
|
||||
store *Store
|
||||
}
|
||||
|
||||
const (
|
||||
DBVersionKey = "DB_VERSION"
|
||||
dBVersionKey = "DB_VERSION"
|
||||
)
|
||||
|
||||
// DBVersion the stored database version.
|
||||
// DBVersion retrieves the stored database version.
|
||||
func (service *VersionService) DBVersion() (int, error) {
|
||||
var data []byte
|
||||
err := service.store.db.View(func(tx *bolt.Tx) error {
|
||||
bucket := tx.Bucket([]byte(versionBucketName))
|
||||
value := bucket.Get([]byte(DBVersionKey))
|
||||
value := bucket.Get([]byte(dBVersionKey))
|
||||
if value == nil {
|
||||
return portainer.ErrDBVersionNotFound
|
||||
}
|
||||
@@ -49,7 +49,7 @@ func (service *VersionService) StoreDBVersion(version int) error {
|
||||
bucket := tx.Bucket([]byte(versionBucketName))
|
||||
|
||||
data := []byte(strconv.Itoa(version))
|
||||
err := bucket.Put([]byte(DBVersionKey), data)
|
||||
err := bucket.Put([]byte(dBVersionKey), data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
@@ -15,11 +16,13 @@ import (
|
||||
type Service struct{}
|
||||
|
||||
const (
|
||||
errInvalidEnpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix:// or tcp://")
|
||||
errSocketNotFound = portainer.Error("Unable to locate Unix socket")
|
||||
errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file")
|
||||
errInvalidSyncInterval = portainer.Error("Invalid synchronization interval")
|
||||
errEndpointExcludeExternal = portainer.Error("Cannot use the -H flag mutually with --external-endpoints")
|
||||
errInvalidEndpointProtocol = portainer.Error("Invalid endpoint protocol: Portainer only supports unix:// or tcp://")
|
||||
errSocketNotFound = portainer.Error("Unable to locate Unix socket")
|
||||
errEndpointsFileNotFound = portainer.Error("Unable to locate external endpoints file")
|
||||
errInvalidSyncInterval = portainer.Error("Invalid synchronization interval")
|
||||
errEndpointExcludeExternal = portainer.Error("Cannot use the -H flag mutually with --external-endpoints")
|
||||
errNoAuthExcludeAdminPassword = portainer.Error("Cannot use --no-auth with --admin-password or --admin-password-file")
|
||||
errAdminPassExcludeAdminPassFile = portainer.Error("Cannot use --admin-password with --admin-password-file")
|
||||
)
|
||||
|
||||
// ParseFlags parse the CLI flags and return a portainer.Flags struct
|
||||
@@ -28,20 +31,26 @@ func (*Service) ParseFlags(version string) (*portainer.CLIFlags, error) {
|
||||
|
||||
flags := &portainer.CLIFlags{
|
||||
Endpoint: kingpin.Flag("host", "Dockerd endpoint").Short('H').String(),
|
||||
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
|
||||
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
||||
ExternalEndpoints: kingpin.Flag("external-endpoints", "Path to a file defining available endpoints").String(),
|
||||
SyncInterval: kingpin.Flag("sync-interval", "Duration between each synchronization via the external endpoints source").Default(defaultSyncInterval).String(),
|
||||
Addr: kingpin.Flag("bind", "Address and port to serve Portainer").Default(defaultBindAddress).Short('p').String(),
|
||||
Assets: kingpin.Flag("assets", "Path to the assets").Default(defaultAssetsDirectory).Short('a').String(),
|
||||
Data: kingpin.Flag("data", "Path to the folder where the data is stored").Default(defaultDataDirectory).Short('d').String(),
|
||||
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Default(defaultTemplatesURL).Short('t').String(),
|
||||
NoAuth: kingpin.Flag("no-auth", "Disable authentication").Default(defaultNoAuth).Bool(),
|
||||
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAuth).Bool(),
|
||||
NoAnalytics: kingpin.Flag("no-analytics", "Disable Analytics in app").Default(defaultNoAnalytics).Bool(),
|
||||
TLSVerify: kingpin.Flag("tlsverify", "TLS support").Default(defaultTLSVerify).Bool(),
|
||||
TLSCacert: kingpin.Flag("tlscacert", "Path to the CA").Default(defaultTLSCACertPath).String(),
|
||||
TLSCert: kingpin.Flag("tlscert", "Path to the TLS certificate file").Default(defaultTLSCertPath).String(),
|
||||
TLSKey: kingpin.Flag("tlskey", "Path to the TLS key").Default(defaultTLSKeyPath).String(),
|
||||
SSL: kingpin.Flag("ssl", "Secure Portainer instance using SSL").Default(defaultSSL).Bool(),
|
||||
SSLCert: kingpin.Flag("sslcert", "Path to the SSL certificate used to secure the Portainer instance").Default(defaultSSLCertPath).String(),
|
||||
SSLKey: kingpin.Flag("sslkey", "Path to the SSL key used to secure the Portainer instance").Default(defaultSSLKeyPath).String(),
|
||||
AdminPassword: kingpin.Flag("admin-password", "Hashed admin password").String(),
|
||||
AdminPasswordFile: kingpin.Flag("admin-password-file", "Path to the file containing the password for the admin user").String(),
|
||||
// Deprecated flags
|
||||
Labels: pairs(kingpin.Flag("hide-label", "Hide containers with a specific label in the UI").Short('l')),
|
||||
Logo: kingpin.Flag("logo", "URL for the logo displayed in the UI").String(),
|
||||
Templates: kingpin.Flag("templates", "URL to the templates (apps) definitions").Short('t').String(),
|
||||
}
|
||||
|
||||
kingpin.Parse()
|
||||
@@ -70,13 +79,23 @@ func (*Service) ValidateFlags(flags *portainer.CLIFlags) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if *flags.NoAuth && (*flags.AdminPassword != "" || *flags.AdminPasswordFile != "") {
|
||||
return errNoAuthExcludeAdminPassword
|
||||
}
|
||||
|
||||
if *flags.AdminPassword != "" && *flags.AdminPasswordFile != "" {
|
||||
return errAdminPassExcludeAdminPassFile
|
||||
}
|
||||
|
||||
displayDeprecationWarnings(*flags.Templates, *flags.Logo, *flags.Labels)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateEndpoint(endpoint string) error {
|
||||
if endpoint != "" {
|
||||
if !strings.HasPrefix(endpoint, "unix://") && !strings.HasPrefix(endpoint, "tcp://") {
|
||||
return errInvalidEnpointProtocol
|
||||
return errInvalidEndpointProtocol
|
||||
}
|
||||
|
||||
if strings.HasPrefix(endpoint, "unix://") {
|
||||
@@ -113,3 +132,15 @@ func validateSyncInterval(syncInterval string) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func displayDeprecationWarnings(templates, logo string, labels []portainer.Pair) {
|
||||
if templates != "" {
|
||||
log.Println("Warning: the --templates / -t flag is deprecated and will be removed in future versions.")
|
||||
}
|
||||
if logo != "" {
|
||||
log.Println("Warning: the --logo flag is deprecated and will be removed in future versions.")
|
||||
}
|
||||
if labels != nil {
|
||||
log.Println("Warning: the --hide-label / -l flag is deprecated and will be removed in future versions.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,12 +6,14 @@ const (
|
||||
defaultBindAddress = ":9000"
|
||||
defaultDataDirectory = "/data"
|
||||
defaultAssetsDirectory = "."
|
||||
defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLSVerify = "false"
|
||||
defaultTLSCACertPath = "/certs/ca.pem"
|
||||
defaultTLSCertPath = "/certs/cert.pem"
|
||||
defaultTLSKeyPath = "/certs/key.pem"
|
||||
defaultSSL = "false"
|
||||
defaultSSLCertPath = "/certs/portainer.crt"
|
||||
defaultSSLKeyPath = "/certs/portainer.key"
|
||||
defaultSyncInterval = "60s"
|
||||
)
|
||||
|
||||
@@ -4,12 +4,14 @@ const (
|
||||
defaultBindAddress = ":9000"
|
||||
defaultDataDirectory = "C:\\data"
|
||||
defaultAssetsDirectory = "."
|
||||
defaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||
defaultNoAuth = "false"
|
||||
defaultNoAnalytics = "false"
|
||||
defaultTLSVerify = "false"
|
||||
defaultTLSCACertPath = "C:\\certs\\ca.pem"
|
||||
defaultTLSCertPath = "C:\\certs\\cert.pem"
|
||||
defaultTLSKeyPath = "C:\\certs\\key.pem"
|
||||
defaultSSL = "false"
|
||||
defaultSSLCertPath = "C:\\certs\\portainer.crt"
|
||||
defaultSSLKeyPath = "C:\\certs\\portainer.key"
|
||||
defaultSyncInterval = "60s"
|
||||
)
|
||||
|
||||
@@ -6,9 +6,12 @@ import (
|
||||
"github.com/portainer/portainer/cli"
|
||||
"github.com/portainer/portainer/cron"
|
||||
"github.com/portainer/portainer/crypto"
|
||||
"github.com/portainer/portainer/exec"
|
||||
"github.com/portainer/portainer/file"
|
||||
"github.com/portainer/portainer/git"
|
||||
"github.com/portainer/portainer/http"
|
||||
"github.com/portainer/portainer/jwt"
|
||||
"github.com/portainer/portainer/ldap"
|
||||
|
||||
"log"
|
||||
)
|
||||
@@ -53,6 +56,10 @@ func initStore(dataStorePath string) *bolt.Store {
|
||||
return store
|
||||
}
|
||||
|
||||
func initStackManager(assetsPath string) portainer.StackManager {
|
||||
return exec.NewStackManager(assetsPath)
|
||||
}
|
||||
|
||||
func initJWTService(authenticationEnabled bool) portainer.JWTService {
|
||||
if authenticationEnabled {
|
||||
jwtService, err := jwt.NewService()
|
||||
@@ -68,6 +75,14 @@ func initCryptoService() portainer.CryptoService {
|
||||
return &crypto.Service{}
|
||||
}
|
||||
|
||||
func initLDAPService() portainer.LDAPService {
|
||||
return &ldap.Service{}
|
||||
}
|
||||
|
||||
func initGitService() portainer.GitService {
|
||||
return &git.Service{}
|
||||
}
|
||||
|
||||
func initEndpointWatcher(endpointService portainer.EndpointService, externalEnpointFile string, syncInterval string) bool {
|
||||
authorizeEndpointMgmt := true
|
||||
if externalEnpointFile != "" {
|
||||
@@ -82,16 +97,68 @@ func initEndpointWatcher(endpointService portainer.EndpointService, externalEnpo
|
||||
return authorizeEndpointMgmt
|
||||
}
|
||||
|
||||
func initSettings(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portainer.Settings {
|
||||
return &portainer.Settings{
|
||||
HiddenLabels: *flags.Labels,
|
||||
Logo: *flags.Logo,
|
||||
func initStatus(authorizeEndpointMgmt bool, flags *portainer.CLIFlags) *portainer.Status {
|
||||
return &portainer.Status{
|
||||
Analytics: !*flags.NoAnalytics,
|
||||
Authentication: !*flags.NoAuth,
|
||||
EndpointManagement: authorizeEndpointMgmt,
|
||||
Version: portainer.APIVersion,
|
||||
}
|
||||
}
|
||||
|
||||
func initDockerHub(dockerHubService portainer.DockerHubService) error {
|
||||
_, err := dockerHubService.DockerHub()
|
||||
if err == portainer.ErrDockerHubNotFound {
|
||||
dockerhub := &portainer.DockerHub{
|
||||
Authentication: false,
|
||||
Username: "",
|
||||
Password: "",
|
||||
}
|
||||
return dockerHubService.StoreDockerHub(dockerhub)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func initSettings(settingsService portainer.SettingsService, flags *portainer.CLIFlags) error {
|
||||
_, err := settingsService.Settings()
|
||||
if err == portainer.ErrSettingsNotFound {
|
||||
settings := &portainer.Settings{
|
||||
LogoURL: *flags.Logo,
|
||||
DisplayExternalContributors: true,
|
||||
AuthenticationMethod: portainer.AuthenticationInternal,
|
||||
LDAPSettings: portainer.LDAPSettings{
|
||||
TLSConfig: portainer.TLSConfiguration{},
|
||||
SearchSettings: []portainer.LDAPSearchSettings{
|
||||
portainer.LDAPSearchSettings{},
|
||||
},
|
||||
},
|
||||
AllowBindMountsForRegularUsers: true,
|
||||
AllowPrivilegedModeForRegularUsers: true,
|
||||
}
|
||||
|
||||
if *flags.Templates != "" {
|
||||
settings.TemplatesURL = *flags.Templates
|
||||
} else {
|
||||
settings.TemplatesURL = portainer.DefaultTemplatesURL
|
||||
}
|
||||
|
||||
if *flags.Labels != nil {
|
||||
settings.BlackListedLabels = *flags.Labels
|
||||
} else {
|
||||
settings.BlackListedLabels = make([]portainer.Pair, 0)
|
||||
}
|
||||
|
||||
return settingsService.StoreSettings(settings)
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func retrieveFirstEndpointFromDatabase(endpointService portainer.EndpointService) *portainer.Endpoint {
|
||||
endpoints, err := endpointService.Endpoints()
|
||||
if err != nil {
|
||||
@@ -108,28 +175,48 @@ func main() {
|
||||
store := initStore(*flags.Data)
|
||||
defer store.Close()
|
||||
|
||||
stackManager := initStackManager(*flags.Assets)
|
||||
|
||||
jwtService := initJWTService(!*flags.NoAuth)
|
||||
|
||||
cryptoService := initCryptoService()
|
||||
|
||||
ldapService := initLDAPService()
|
||||
|
||||
gitService := initGitService()
|
||||
|
||||
authorizeEndpointMgmt := initEndpointWatcher(store.EndpointService, *flags.ExternalEndpoints, *flags.SyncInterval)
|
||||
|
||||
settings := initSettings(authorizeEndpointMgmt, flags)
|
||||
err := initSettings(store.SettingsService, flags)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
err = initDockerHub(store.DockerHubService)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
applicationStatus := initStatus(authorizeEndpointMgmt, flags)
|
||||
|
||||
if *flags.Endpoint != "" {
|
||||
var endpoints []portainer.Endpoint
|
||||
endpoints, err := store.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if len(endpoints) == 0 {
|
||||
endpoint := &portainer.Endpoint{
|
||||
Name: "primary",
|
||||
URL: *flags.Endpoint,
|
||||
TLS: *flags.TLSVerify,
|
||||
TLSCACertPath: *flags.TLSCacert,
|
||||
TLSCertPath: *flags.TLSCert,
|
||||
TLSKeyPath: *flags.TLSKey,
|
||||
Name: "primary",
|
||||
URL: *flags.Endpoint,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: *flags.TLSVerify,
|
||||
TLSSkipVerify: false,
|
||||
TLSCACertPath: *flags.TLSCacert,
|
||||
TLSCertPath: *flags.TLSCert,
|
||||
TLSKeyPath: *flags.TLSKey,
|
||||
},
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
}
|
||||
err = store.EndpointService.CreateEndpoint(endpoint)
|
||||
if err != nil {
|
||||
@@ -140,23 +227,70 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
adminPasswordHash := ""
|
||||
if *flags.AdminPasswordFile != "" {
|
||||
content, err := fileService.GetFileContent(*flags.AdminPasswordFile)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
adminPasswordHash, err = cryptoService.Hash(content)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else if *flags.AdminPassword != "" {
|
||||
adminPasswordHash = *flags.AdminPassword
|
||||
}
|
||||
|
||||
if adminPasswordHash != "" {
|
||||
users, err := store.UserService.UsersByRole(portainer.AdministratorRole)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if len(users) == 0 {
|
||||
log.Printf("Creating admin user with password hash %s", adminPasswordHash)
|
||||
user := &portainer.User{
|
||||
Username: "admin",
|
||||
Role: portainer.AdministratorRole,
|
||||
Password: adminPasswordHash,
|
||||
}
|
||||
err := store.UserService.CreateUser(user)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
} else {
|
||||
log.Println("Instance already has an administrator user defined. Skipping admin password related flags.")
|
||||
}
|
||||
}
|
||||
|
||||
var server portainer.Server = &http.Server{
|
||||
Status: applicationStatus,
|
||||
BindAddress: *flags.Addr,
|
||||
AssetsPath: *flags.Assets,
|
||||
Settings: settings,
|
||||
TemplatesURL: *flags.Templates,
|
||||
AuthDisabled: *flags.NoAuth,
|
||||
EndpointManagement: authorizeEndpointMgmt,
|
||||
UserService: store.UserService,
|
||||
TeamService: store.TeamService,
|
||||
TeamMembershipService: store.TeamMembershipService,
|
||||
EndpointService: store.EndpointService,
|
||||
ResourceControlService: store.ResourceControlService,
|
||||
SettingsService: store.SettingsService,
|
||||
RegistryService: store.RegistryService,
|
||||
DockerHubService: store.DockerHubService,
|
||||
StackService: store.StackService,
|
||||
StackManager: stackManager,
|
||||
CryptoService: cryptoService,
|
||||
JWTService: jwtService,
|
||||
FileService: fileService,
|
||||
LDAPService: ldapService,
|
||||
GitService: gitService,
|
||||
SSL: *flags.SSL,
|
||||
SSLCert: *flags.SSLCert,
|
||||
SSLKey: *flags.SSLKey,
|
||||
}
|
||||
|
||||
log.Printf("Starting Portainer on %s", *flags.Addr)
|
||||
err := server.Start()
|
||||
log.Printf("Starting Portainer %s on %s", portainer.APIVersion, *flags.Addr)
|
||||
err = server.Start()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,16 @@ type (
|
||||
endpointsToUpdate []*portainer.Endpoint
|
||||
endpointsToDelete []*portainer.Endpoint
|
||||
}
|
||||
|
||||
fileEndpoint struct {
|
||||
Name string `json:"Name"`
|
||||
URL string `json:"URL"`
|
||||
TLS bool `json:"TLS,omitempty"`
|
||||
TLSSkipVerify bool `json:"TLSSkipVerify,omitempty"`
|
||||
TLSCACert string `json:"TLSCACert,omitempty"`
|
||||
TLSCert string `json:"TLSCert,omitempty"`
|
||||
TLSKey string `json:"TLSKey,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -55,6 +65,28 @@ func isValidEndpoint(endpoint *portainer.Endpoint) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func convertFileEndpoints(fileEndpoints []fileEndpoint) []portainer.Endpoint {
|
||||
convertedEndpoints := make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, e := range fileEndpoints {
|
||||
endpoint := portainer.Endpoint{
|
||||
Name: e.Name,
|
||||
URL: e.URL,
|
||||
TLSConfig: portainer.TLSConfiguration{},
|
||||
}
|
||||
if e.TLS {
|
||||
endpoint.TLSConfig.TLS = true
|
||||
endpoint.TLSConfig.TLSSkipVerify = e.TLSSkipVerify
|
||||
endpoint.TLSConfig.TLSCACertPath = e.TLSCACert
|
||||
endpoint.TLSConfig.TLSCertPath = e.TLSCert
|
||||
endpoint.TLSConfig.TLSKeyPath = e.TLSKey
|
||||
}
|
||||
convertedEndpoints = append(convertedEndpoints, endpoint)
|
||||
}
|
||||
|
||||
return convertedEndpoints
|
||||
}
|
||||
|
||||
func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint) int {
|
||||
for idx, v := range endpoints {
|
||||
if endpoint.Name == v.Name && isValidEndpoint(&v) {
|
||||
@@ -66,22 +98,25 @@ func endpointExists(endpoint *portainer.Endpoint, endpoints []portainer.Endpoint
|
||||
|
||||
func mergeEndpointIfRequired(original, updated *portainer.Endpoint) *portainer.Endpoint {
|
||||
var endpoint *portainer.Endpoint
|
||||
if original.URL != updated.URL || original.TLS != updated.TLS ||
|
||||
(updated.TLS && original.TLSCACertPath != updated.TLSCACertPath) ||
|
||||
(updated.TLS && original.TLSCertPath != updated.TLSCertPath) ||
|
||||
(updated.TLS && original.TLSKeyPath != updated.TLSKeyPath) {
|
||||
if original.URL != updated.URL || original.TLSConfig.TLS != updated.TLSConfig.TLS ||
|
||||
(updated.TLSConfig.TLS && original.TLSConfig.TLSSkipVerify != updated.TLSConfig.TLSSkipVerify) ||
|
||||
(updated.TLSConfig.TLS && original.TLSConfig.TLSCACertPath != updated.TLSConfig.TLSCACertPath) ||
|
||||
(updated.TLSConfig.TLS && original.TLSConfig.TLSCertPath != updated.TLSConfig.TLSCertPath) ||
|
||||
(updated.TLSConfig.TLS && original.TLSConfig.TLSKeyPath != updated.TLSConfig.TLSKeyPath) {
|
||||
endpoint = original
|
||||
endpoint.URL = updated.URL
|
||||
if updated.TLS {
|
||||
endpoint.TLS = true
|
||||
endpoint.TLSCACertPath = updated.TLSCACertPath
|
||||
endpoint.TLSCertPath = updated.TLSCertPath
|
||||
endpoint.TLSKeyPath = updated.TLSKeyPath
|
||||
if updated.TLSConfig.TLS {
|
||||
endpoint.TLSConfig.TLS = true
|
||||
endpoint.TLSConfig.TLSSkipVerify = updated.TLSConfig.TLSSkipVerify
|
||||
endpoint.TLSConfig.TLSCACertPath = updated.TLSConfig.TLSCACertPath
|
||||
endpoint.TLSConfig.TLSCertPath = updated.TLSConfig.TLSCertPath
|
||||
endpoint.TLSConfig.TLSKeyPath = updated.TLSConfig.TLSKeyPath
|
||||
} else {
|
||||
endpoint.TLS = false
|
||||
endpoint.TLSCACertPath = ""
|
||||
endpoint.TLSCertPath = ""
|
||||
endpoint.TLSKeyPath = ""
|
||||
endpoint.TLSConfig.TLS = false
|
||||
endpoint.TLSConfig.TLSSkipVerify = false
|
||||
endpoint.TLSConfig.TLSCACertPath = ""
|
||||
endpoint.TLSConfig.TLSCertPath = ""
|
||||
endpoint.TLSConfig.TLSKeyPath = ""
|
||||
}
|
||||
}
|
||||
return endpoint
|
||||
@@ -117,7 +152,7 @@ func (job endpointSyncJob) prepareSyncData(storedEndpoints, fileEndpoints []port
|
||||
}
|
||||
|
||||
for idx, endpoint := range fileEndpoints {
|
||||
if endpoint.Name == "" || endpoint.URL == "" {
|
||||
if !isValidEndpoint(&endpoint) {
|
||||
job.logger.Printf("Invalid file endpoint definition, skipping. [name: %v] [url: %v]", endpoint.Name, endpoint.URL)
|
||||
continue
|
||||
}
|
||||
@@ -141,7 +176,7 @@ func (job endpointSyncJob) Sync() error {
|
||||
return err
|
||||
}
|
||||
|
||||
var fileEndpoints []portainer.Endpoint
|
||||
var fileEndpoints []fileEndpoint
|
||||
err = json.Unmarshal(data, &fileEndpoints)
|
||||
if endpointSyncError(err, job.logger) {
|
||||
return err
|
||||
@@ -156,7 +191,9 @@ func (job endpointSyncJob) Sync() error {
|
||||
return err
|
||||
}
|
||||
|
||||
sync := job.prepareSyncData(storedEndpoints, fileEndpoints)
|
||||
convertedFileEndpoints := convertFileEndpoints(fileEndpoints)
|
||||
|
||||
sync := job.prepareSyncData(storedEndpoints, convertedFileEndpoints)
|
||||
if sync.requireSync() {
|
||||
err = job.endpointService.Synchronize(sync.endpointsToCreate, sync.endpointsToUpdate, sync.endpointsToDelete)
|
||||
if endpointSyncError(err, job.logger) {
|
||||
|
||||
39
api/crypto/tls.go
Normal file
39
api/crypto/tls.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// CreateTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key
|
||||
func CreateTLSConfiguration(config *portainer.TLSConfiguration) (*tls.Config, error) {
|
||||
TLSConfig := &tls.Config{}
|
||||
|
||||
if config.TLSCertPath != "" && config.TLSKeyPath != "" {
|
||||
cert, err := tls.LoadX509KeyPair(config.TLSCertPath, config.TLSKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
TLSConfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
|
||||
if !config.TLSSkipVerify {
|
||||
caCert, err := ioutil.ReadFile(config.TLSCACertPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
|
||||
TLSConfig.RootCAs = caCertPool
|
||||
}
|
||||
|
||||
TLSConfig.InsecureSkipVerify = config.TLSSkipVerify
|
||||
|
||||
return TLSConfig, nil
|
||||
}
|
||||
@@ -2,15 +2,40 @@ package portainer
|
||||
|
||||
// General errors.
|
||||
const (
|
||||
ErrUnauthorized = Error("Unauthorized")
|
||||
ErrResourceAccessDenied = Error("Access denied to resource")
|
||||
ErrUnauthorized = Error("Unauthorized")
|
||||
ErrResourceAccessDenied = Error("Access denied to resource")
|
||||
ErrResourceNotFound = Error("Unable to find resource")
|
||||
ErrUnsupportedDockerAPI = Error("Unsupported Docker API response")
|
||||
ErrMissingSecurityContext = Error("Unable to find security details in request context")
|
||||
)
|
||||
|
||||
// User errors.
|
||||
const (
|
||||
ErrUserNotFound = Error("User not found")
|
||||
ErrUserAlreadyExists = Error("User already exists")
|
||||
ErrAdminAlreadyInitialized = Error("Admin user already initialized")
|
||||
ErrInvalidUsername = Error("Invalid username. White spaces are not allowed")
|
||||
ErrAdminAlreadyInitialized = Error("An administrator user already exists")
|
||||
ErrCannotRemoveAdmin = Error("Cannot remove the default administrator account")
|
||||
ErrAdminCannotRemoveSelf = Error("Cannot remove your own user account. Contact another administrator")
|
||||
)
|
||||
|
||||
// Team errors.
|
||||
const (
|
||||
ErrTeamNotFound = Error("Team not found")
|
||||
ErrTeamAlreadyExists = Error("Team already exists")
|
||||
)
|
||||
|
||||
// TeamMembership errors.
|
||||
const (
|
||||
ErrTeamMembershipNotFound = Error("Team membership not found")
|
||||
ErrTeamMembershipAlreadyExists = Error("Team membership already exists for this user and team.")
|
||||
)
|
||||
|
||||
// ResourceControl errors.
|
||||
const (
|
||||
ErrResourceControlNotFound = Error("Resource control not found")
|
||||
ErrResourceControlAlreadyExists = Error("A resource control is already applied on this resource")
|
||||
ErrInvalidResourceControlType = Error("Unsupported resource control type")
|
||||
)
|
||||
|
||||
// Endpoint errors.
|
||||
@@ -19,11 +44,34 @@ const (
|
||||
ErrEndpointAccessDenied = Error("Access denied to endpoint")
|
||||
)
|
||||
|
||||
// Registry errors.
|
||||
const (
|
||||
ErrRegistryNotFound = Error("Registry not found")
|
||||
ErrRegistryAlreadyExists = Error("A registry is already defined for this URL")
|
||||
)
|
||||
|
||||
// Stack errors
|
||||
const (
|
||||
ErrStackNotFound = Error("Stack not found")
|
||||
ErrStackAlreadyExists = Error("A stack already exists with this name")
|
||||
ErrComposeFileNotFoundInRepository = Error("Unable to find a Compose file in the repository")
|
||||
)
|
||||
|
||||
// Version errors.
|
||||
const (
|
||||
ErrDBVersionNotFound = Error("DB version not found")
|
||||
)
|
||||
|
||||
// Settings errors.
|
||||
const (
|
||||
ErrSettingsNotFound = Error("Settings not found")
|
||||
)
|
||||
|
||||
// DockerHub errors.
|
||||
const (
|
||||
ErrDockerHubNotFound = Error("Dockerhub not found")
|
||||
)
|
||||
|
||||
// Crypto errors.
|
||||
const (
|
||||
ErrCryptoHashFailure = Error("Unable to hash data")
|
||||
|
||||
76
api/exec/stack_manager.go
Normal file
76
api/exec/stack_manager.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package exec
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
"path"
|
||||
"runtime"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// StackManager represents a service for managing stacks.
|
||||
type StackManager struct {
|
||||
binaryPath string
|
||||
}
|
||||
|
||||
// NewStackManager initializes a new StackManager service.
|
||||
func NewStackManager(binaryPath string) *StackManager {
|
||||
return &StackManager{
|
||||
binaryPath: binaryPath,
|
||||
}
|
||||
}
|
||||
|
||||
// Deploy will execute the Docker stack deploy command
|
||||
func (manager *StackManager) Deploy(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
stackFilePath := path.Join(stack.ProjectPath, stack.EntryPoint)
|
||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
|
||||
args = append(args, "stack", "deploy", "--with-registry-auth", "--compose-file", stackFilePath, stack.Name)
|
||||
return runCommandAndCaptureStdErr(command, args)
|
||||
}
|
||||
|
||||
// Remove will execute the Docker stack rm command
|
||||
func (manager *StackManager) Remove(stack *portainer.Stack, endpoint *portainer.Endpoint) error {
|
||||
command, args := prepareDockerCommandAndArgs(manager.binaryPath, endpoint)
|
||||
args = append(args, "stack", "rm", stack.Name)
|
||||
return runCommandAndCaptureStdErr(command, args)
|
||||
}
|
||||
|
||||
func runCommandAndCaptureStdErr(command string, args []string) error {
|
||||
var stderr bytes.Buffer
|
||||
cmd := exec.Command(command, args...)
|
||||
cmd.Stderr = &stderr
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return portainer.Error(stderr.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareDockerCommandAndArgs(binaryPath string, endpoint *portainer.Endpoint) (string, []string) {
|
||||
// Assume Linux as a default
|
||||
command := path.Join(binaryPath, "docker")
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
command = path.Join(binaryPath, "docker.exe")
|
||||
}
|
||||
|
||||
args := make([]string, 0)
|
||||
args = append(args, "-H", endpoint.URL)
|
||||
|
||||
if endpoint.TLSConfig.TLS {
|
||||
args = append(args, "--tls")
|
||||
|
||||
if !endpoint.TLSConfig.TLSSkipVerify {
|
||||
args = append(args, "--tlsverify", "--tlscacert", endpoint.TLSConfig.TLSCACertPath)
|
||||
}
|
||||
|
||||
if endpoint.TLSConfig.TLSCertPath != "" && endpoint.TLSConfig.TLSKeyPath != "" {
|
||||
args = append(args, "--tlscert", endpoint.TLSConfig.TLSCertPath, "--tlskey", endpoint.TLSConfig.TLSKeyPath)
|
||||
}
|
||||
}
|
||||
|
||||
return command, args
|
||||
}
|
||||
128
api/file/file.go
128
api/file/file.go
@@ -1,23 +1,31 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
// TLSStorePath represents the subfolder where TLS files are stored in the file store folder.
|
||||
TLSStorePath = "tls"
|
||||
// LDAPStorePath represents the subfolder where LDAP TLS files are stored in the TLSStorePath.
|
||||
LDAPStorePath = "ldap"
|
||||
// TLSCACertFile represents the name on disk for a TLS CA file.
|
||||
TLSCACertFile = "ca.pem"
|
||||
// TLSCertFile represents the name on disk for a TLS certificate file.
|
||||
TLSCertFile = "cert.pem"
|
||||
// TLSKeyFile represents the name on disk for a TLS key file.
|
||||
TLSKeyFile = "key.pem"
|
||||
// ComposeStorePath represents the subfolder where compose files are stored in the file store folder.
|
||||
ComposeStorePath = "compose"
|
||||
// ComposeFileDefaultName represents the default name of a compose file.
|
||||
ComposeFileDefaultName = "docker-compose.yml"
|
||||
)
|
||||
|
||||
// Service represents a service for managing files and directories.
|
||||
@@ -36,6 +44,7 @@ func NewService(dataStorePath, fileStorePath string) (*Service, error) {
|
||||
|
||||
// Checking if a mount directory exists is broken with Go on Windows.
|
||||
// This will need to be reviewed after the issue has been fixed in Go.
|
||||
// See: https://github.com/portainer/portainer/issues/474
|
||||
// err := createDirectoryIfNotExist(dataStorePath, 0755)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
@@ -46,14 +55,69 @@ func NewService(dataStorePath, fileStorePath string) (*Service, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = service.createDirectoryInStoreIfNotExist(ComposeStorePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// StoreTLSFile creates a subfolder in the TLSStorePath and stores a new file with the content from r.
|
||||
func (service *Service) StoreTLSFile(endpointID portainer.EndpointID, fileType portainer.TLSFileType, r io.Reader) error {
|
||||
ID := strconv.Itoa(int(endpointID))
|
||||
endpointStorePath := path.Join(TLSStorePath, ID)
|
||||
err := service.createDirectoryInStoreIfNotExist(endpointStorePath)
|
||||
// RemoveDirectory removes a directory on the filesystem.
|
||||
func (service *Service) RemoveDirectory(directoryPath string) error {
|
||||
return os.RemoveAll(directoryPath)
|
||||
}
|
||||
|
||||
// GetStackProjectPath returns the absolute path on the FS for a stack based
|
||||
// on its identifier.
|
||||
func (service *Service) GetStackProjectPath(stackIdentifier string) string {
|
||||
return path.Join(service.fileStorePath, ComposeStorePath, stackIdentifier)
|
||||
}
|
||||
|
||||
// StoreStackFileFromString creates a subfolder in the ComposeStorePath and stores a new file using the content from a string.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreStackFileFromString(stackIdentifier, stackFileContent string) (string, error) {
|
||||
stackStorePath := path.Join(ComposeStorePath, stackIdentifier)
|
||||
err := service.createDirectoryInStoreIfNotExist(stackStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
composeFilePath := path.Join(stackStorePath, ComposeFileDefaultName)
|
||||
data := []byte(stackFileContent)
|
||||
r := bytes.NewReader(data)
|
||||
|
||||
err = service.createFileInStore(composeFilePath, r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path.Join(service.fileStorePath, stackStorePath), nil
|
||||
}
|
||||
|
||||
// StoreStackFileFromReader creates a subfolder in the ComposeStorePath and stores a new file using the content from an io.Reader.
|
||||
// It returns the path to the folder where the file is stored.
|
||||
func (service *Service) StoreStackFileFromReader(stackIdentifier string, r io.Reader) (string, error) {
|
||||
stackStorePath := path.Join(ComposeStorePath, stackIdentifier)
|
||||
err := service.createDirectoryInStoreIfNotExist(stackStorePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
composeFilePath := path.Join(stackStorePath, ComposeFileDefaultName)
|
||||
|
||||
err = service.createFileInStore(composeFilePath, r)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return path.Join(service.fileStorePath, stackStorePath), nil
|
||||
}
|
||||
|
||||
// StoreTLSFile creates a folder in the TLSStorePath and stores a new file with the content from r.
|
||||
func (service *Service) StoreTLSFile(folder string, fileType portainer.TLSFileType, r io.Reader) error {
|
||||
storePath := path.Join(TLSStorePath, folder)
|
||||
err := service.createDirectoryInStoreIfNotExist(storePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -70,7 +134,7 @@ func (service *Service) StoreTLSFile(endpointID portainer.EndpointID, fileType p
|
||||
return portainer.ErrUndefinedTLSFileType
|
||||
}
|
||||
|
||||
tlsFilePath := path.Join(endpointStorePath, fileName)
|
||||
tlsFilePath := path.Join(storePath, fileName)
|
||||
err = service.createFileInStore(tlsFilePath, r)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -79,7 +143,7 @@ func (service *Service) StoreTLSFile(endpointID portainer.EndpointID, fileType p
|
||||
}
|
||||
|
||||
// GetPathForTLSFile returns the absolute path to a specific TLS file for an endpoint.
|
||||
func (service *Service) GetPathForTLSFile(endpointID portainer.EndpointID, fileType portainer.TLSFileType) (string, error) {
|
||||
func (service *Service) GetPathForTLSFile(folder string, fileType portainer.TLSFileType) (string, error) {
|
||||
var fileName string
|
||||
switch fileType {
|
||||
case portainer.TLSFileCA:
|
||||
@@ -91,21 +155,52 @@ func (service *Service) GetPathForTLSFile(endpointID portainer.EndpointID, fileT
|
||||
default:
|
||||
return "", portainer.ErrUndefinedTLSFileType
|
||||
}
|
||||
ID := strconv.Itoa(int(endpointID))
|
||||
return path.Join(service.fileStorePath, TLSStorePath, ID, fileName), nil
|
||||
return path.Join(service.fileStorePath, TLSStorePath, folder, fileName), nil
|
||||
}
|
||||
|
||||
// DeleteTLSFiles deletes a folder containing the TLS files for an endpoint.
|
||||
func (service *Service) DeleteTLSFiles(endpointID portainer.EndpointID) error {
|
||||
ID := strconv.Itoa(int(endpointID))
|
||||
endpointPath := path.Join(service.fileStorePath, TLSStorePath, ID)
|
||||
err := os.RemoveAll(endpointPath)
|
||||
// DeleteTLSFiles deletes a folder in the TLS store path.
|
||||
func (service *Service) DeleteTLSFiles(folder string) error {
|
||||
storePath := path.Join(service.fileStorePath, TLSStorePath, folder)
|
||||
err := os.RemoveAll(storePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteTLSFile deletes a specific TLS file from a folder.
|
||||
func (service *Service) DeleteTLSFile(folder string, fileType portainer.TLSFileType) error {
|
||||
var fileName string
|
||||
switch fileType {
|
||||
case portainer.TLSFileCA:
|
||||
fileName = TLSCACertFile
|
||||
case portainer.TLSFileCert:
|
||||
fileName = TLSCertFile
|
||||
case portainer.TLSFileKey:
|
||||
fileName = TLSKeyFile
|
||||
default:
|
||||
return portainer.ErrUndefinedTLSFileType
|
||||
}
|
||||
|
||||
filePath := path.Join(service.fileStorePath, TLSStorePath, folder, fileName)
|
||||
|
||||
err := os.Remove(filePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetFileContent returns a string content from file.
|
||||
func (service *Service) GetFileContent(filePath string) (string, error) {
|
||||
content, err := ioutil.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
// createDirectoryInStoreIfNotExist creates a new directory in the file store if it doesn't exists on the file system.
|
||||
func (service *Service) createDirectoryInStoreIfNotExist(name string) error {
|
||||
path := path.Join(service.fileStorePath, name)
|
||||
@@ -129,14 +224,17 @@ func createDirectoryIfNotExist(path string, mode uint32) error {
|
||||
// createFile creates a new file in the file store with the content from r.
|
||||
func (service *Service) createFileInStore(filePath string, r io.Reader) error {
|
||||
path := path.Join(service.fileStorePath, filePath)
|
||||
|
||||
out, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
25
api/git/git.go
Normal file
25
api/git/git.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package git
|
||||
|
||||
import (
|
||||
"gopkg.in/src-d/go-git.v4"
|
||||
)
|
||||
|
||||
// Service represents a service for managing Git.
|
||||
type Service struct{}
|
||||
|
||||
// NewService initializes a new service.
|
||||
func NewService(dataStorePath string) (*Service, error) {
|
||||
service := &Service{}
|
||||
|
||||
return service, nil
|
||||
}
|
||||
|
||||
// CloneRepository clones a git repository using the specified URL in the specified
|
||||
// destination folder.
|
||||
func (service *Service) CloneRepository(url, destination string) error {
|
||||
_, err := git.PlainClone(destination, false, &git.CloneOptions{
|
||||
URL: url,
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// AuthHandler represents an HTTP API handler for managing authentication.
|
||||
type AuthHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
authDisabled bool
|
||||
UserService portainer.UserService
|
||||
CryptoService portainer.CryptoService
|
||||
JWTService portainer.JWTService
|
||||
}
|
||||
|
||||
const (
|
||||
// ErrInvalidCredentialsFormat is an error raised when credentials format is not valid
|
||||
ErrInvalidCredentialsFormat = portainer.Error("Invalid credentials format")
|
||||
// ErrInvalidCredentials is an error raised when credentials for a user are invalid
|
||||
ErrInvalidCredentials = portainer.Error("Invalid credentials")
|
||||
// ErrAuthDisabled is an error raised when trying to access the authentication endpoints
|
||||
// when the server has been started with the --no-auth flag
|
||||
ErrAuthDisabled = portainer.Error("Authentication is disabled")
|
||||
)
|
||||
|
||||
// NewAuthHandler returns a new instance of AuthHandler.
|
||||
func NewAuthHandler(mw *middleWareService) *AuthHandler {
|
||||
h := &AuthHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/auth",
|
||||
mw.public(http.HandlerFunc(h.handlePostAuth)))
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
handleNotAllowed(w, []string{http.MethodPost})
|
||||
return
|
||||
}
|
||||
|
||||
if handler.authDisabled {
|
||||
Error(w, ErrAuthDisabled, http.StatusServiceUnavailable, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req postAuthRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidCredentialsFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var username = req.Username
|
||||
var password = req.Password
|
||||
|
||||
u, err := handler.UserService.UserByUsername(username)
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.CryptoService.CompareHashAndData(u.Password, password)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidCredentials, http.StatusUnprocessableEntity, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData := &portainer.TokenData{
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
Role: u.Role,
|
||||
}
|
||||
token, err := handler.JWTService.GenerateToken(tokenData)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &postAuthResponse{JWT: token}, handler.Logger)
|
||||
}
|
||||
|
||||
type postAuthRequest struct {
|
||||
Username string `valid:"alphanum,required"`
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
|
||||
type postAuthResponse struct {
|
||||
JWT string `json:"jwt"`
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// DockerHandler represents an HTTP API handler for proxying requests to the Docker API.
|
||||
type DockerHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
EndpointService portainer.EndpointService
|
||||
ProxyFactory ProxyFactory
|
||||
proxies map[portainer.EndpointID]http.Handler
|
||||
}
|
||||
|
||||
// NewDockerHandler returns a new instance of DockerHandler.
|
||||
func NewDockerHandler(mw *middleWareService, resourceControlService portainer.ResourceControlService) *DockerHandler {
|
||||
h := &DockerHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
ProxyFactory: ProxyFactory{
|
||||
ResourceControlService: resourceControlService,
|
||||
},
|
||||
proxies: make(map[portainer.EndpointID]http.Handler),
|
||||
}
|
||||
h.PathPrefix("/{id}/").Handler(
|
||||
mw.authenticated(http.HandlerFunc(h.proxyRequestsToDockerAPI)))
|
||||
return h
|
||||
}
|
||||
|
||||
func checkEndpointAccessControl(endpoint *portainer.Endpoint, userID portainer.UserID) bool {
|
||||
for _, authorizedUserID := range endpoint.AuthorizedUsers {
|
||||
if authorizedUserID == userID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
parsedID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(parsedID)
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := extractTokenDataFromRequestContext(r)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
if tokenData.Role != portainer.AdministratorRole && !checkEndpointAccessControl(endpoint, tokenData.ID) {
|
||||
Error(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
proxy := handler.proxies[endpointID]
|
||||
if proxy == nil {
|
||||
proxy, err = handler.createAndRegisterEndpointProxy(endpoint)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
http.StripPrefix("/"+id, proxy).ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
func (handler *DockerHandler) createAndRegisterEndpointProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
var proxy http.Handler
|
||||
|
||||
endpointURL, err := url.Parse(endpoint.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if endpointURL.Scheme == "tcp" {
|
||||
if endpoint.TLS {
|
||||
proxy, err = handler.ProxyFactory.newHTTPSProxy(endpointURL, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
proxy = handler.ProxyFactory.newHTTPProxy(endpointURL)
|
||||
}
|
||||
} else {
|
||||
// Assume unix:// scheme
|
||||
proxy = handler.ProxyFactory.newSocketProxy(endpointURL.Path)
|
||||
}
|
||||
|
||||
handler.proxies[endpoint.ID] = proxy
|
||||
return proxy, nil
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// ProxyFactory is a factory to create reverse proxies to Docker endpoints
|
||||
type ProxyFactory struct {
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
}
|
||||
|
||||
// singleJoiningSlash from golang.org/src/net/http/httputil/reverseproxy.go
|
||||
// included here for use in NewSingleHostReverseProxyWithHostHeader
|
||||
// because its used in NewSingleHostReverseProxy from golang.org/src/net/http/httputil/reverseproxy.go
|
||||
func singleJoiningSlash(a, b string) string {
|
||||
aslash := strings.HasSuffix(a, "/")
|
||||
bslash := strings.HasPrefix(b, "/")
|
||||
switch {
|
||||
case aslash && bslash:
|
||||
return a + b[1:]
|
||||
case !aslash && !bslash:
|
||||
return a + "/" + b
|
||||
}
|
||||
return a + b
|
||||
}
|
||||
|
||||
// NewSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
|
||||
// from golang.org/src/net/http/httputil/reverseproxy.go and merely sets the Host
|
||||
// HTTP header, which NewSingleHostReverseProxy deliberately preserves.
|
||||
// It also adds an extra Transport to the proxy to allow Portainer to rewrite the responses.
|
||||
func (factory *ProxyFactory) newSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy {
|
||||
targetQuery := target.RawQuery
|
||||
director := func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
|
||||
req.Host = req.URL.Host
|
||||
if targetQuery == "" || req.URL.RawQuery == "" {
|
||||
req.URL.RawQuery = targetQuery + req.URL.RawQuery
|
||||
} else {
|
||||
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
|
||||
}
|
||||
if _, ok := req.Header["User-Agent"]; !ok {
|
||||
// explicitly disable User-Agent so it's not set to default value
|
||||
req.Header.Set("User-Agent", "")
|
||||
}
|
||||
}
|
||||
transport := &proxyTransport{
|
||||
ResourceControlService: factory.ResourceControlService,
|
||||
transport: &http.Transport{},
|
||||
}
|
||||
return &httputil.ReverseProxy{Director: director, Transport: transport}
|
||||
}
|
||||
|
||||
func (factory *ProxyFactory) newHTTPProxy(u *url.URL) http.Handler {
|
||||
u.Scheme = "http"
|
||||
return factory.newSingleHostReverseProxyWithHostHeader(u)
|
||||
}
|
||||
|
||||
func (factory *ProxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
u.Scheme = "https"
|
||||
proxy := factory.newSingleHostReverseProxyWithHostHeader(u)
|
||||
config, err := createTLSConfiguration(endpoint.TLSCACertPath, endpoint.TLSCertPath, endpoint.TLSKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy.Transport.(*proxyTransport).transport.TLSClientConfig = config
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
func (factory *ProxyFactory) newSocketProxy(path string) http.Handler {
|
||||
return &unixSocketHandler{path, &proxyTransport{
|
||||
ResourceControlService: factory.ResourceControlService,
|
||||
}}
|
||||
}
|
||||
|
||||
// unixSocketHandler represents a handler to proxy HTTP requests via a unix:// socket
|
||||
type unixSocketHandler struct {
|
||||
path string
|
||||
transport *proxyTransport
|
||||
}
|
||||
|
||||
func (h *unixSocketHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := net.Dial("unix", h.path)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, nil)
|
||||
return
|
||||
}
|
||||
c := httputil.NewClientConn(conn, nil)
|
||||
defer c.Close()
|
||||
|
||||
res, err := c.Do(r)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, nil)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
err = h.transport.proxyDockerRequests(r, res)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
for k, vv := range res.Header {
|
||||
for _, v := range vv {
|
||||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
if _, err := io.Copy(w, res.Body); err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, nil)
|
||||
}
|
||||
}
|
||||
@@ -1,336 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// EndpointHandler represents an HTTP API handler for managing Docker endpoints.
|
||||
type EndpointHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
authorizeEndpointManagement bool
|
||||
EndpointService portainer.EndpointService
|
||||
FileService portainer.FileService
|
||||
// server *Server
|
||||
}
|
||||
|
||||
const (
|
||||
// ErrEndpointManagementDisabled is an error raised when trying to access the endpoints management endpoints
|
||||
// when the server has been started with the --external-endpoints flag
|
||||
ErrEndpointManagementDisabled = portainer.Error("Endpoint management is disabled")
|
||||
)
|
||||
|
||||
// NewEndpointHandler returns a new instance of EndpointHandler.
|
||||
func NewEndpointHandler(mw *middleWareService) *EndpointHandler {
|
||||
h := &EndpointHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/endpoints",
|
||||
mw.administrator(http.HandlerFunc(h.handlePostEndpoints))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints",
|
||||
mw.authenticated(http.HandlerFunc(h.handleGetEndpoints))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}",
|
||||
mw.administrator(http.HandlerFunc(h.handleGetEndpoint))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}",
|
||||
mw.administrator(http.HandlerFunc(h.handlePutEndpoint))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoints/{id}/access",
|
||||
mw.administrator(http.HandlerFunc(h.handlePutEndpointAccess))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoints/{id}",
|
||||
mw.administrator(http.HandlerFunc(h.handleDeleteEndpoint))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// handleGetEndpoints handles GET requests on /endpoints
|
||||
func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *http.Request) {
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := extractTokenDataFromRequestContext(r)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
if tokenData == nil {
|
||||
Error(w, portainer.ErrInvalidJWTToken, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var allowedEndpoints []portainer.Endpoint
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
allowedEndpoints = make([]portainer.Endpoint, 0)
|
||||
for _, endpoint := range endpoints {
|
||||
for _, authorizedUserID := range endpoint.AuthorizedUsers {
|
||||
if authorizedUserID == tokenData.ID {
|
||||
allowedEndpoints = append(allowedEndpoints, endpoint)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
allowedEndpoints = endpoints
|
||||
}
|
||||
|
||||
encodeJSON(w, allowedEndpoints, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePostEndpoints handles POST requests on /endpoints
|
||||
func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req postEndpointsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
TLS: req.TLS,
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
}
|
||||
|
||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.TLS {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA)
|
||||
endpoint.TLSCACertPath = caCertPath
|
||||
certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert)
|
||||
endpoint.TLSCertPath = certPath
|
||||
keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey)
|
||||
endpoint.TLSKeyPath = keyPath
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
encodeJSON(w, &postEndpointsResponse{ID: int(endpoint.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
type postEndpointsRequest struct {
|
||||
Name string `valid:"required"`
|
||||
URL string `valid:"required"`
|
||||
TLS bool
|
||||
}
|
||||
|
||||
type postEndpointsResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
// handleGetEndpoint handles GET requests on /endpoints/:id
|
||||
func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, endpoint, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePutEndpointAccess handles PUT requests on /endpoints/:id/access
|
||||
func (handler *EndpointHandler) handlePutEndpointAccess(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putEndpointAccessRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
authorizedUserIDs := []portainer.UserID{}
|
||||
for _, value := range req.AuthorizedUsers {
|
||||
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
|
||||
}
|
||||
endpoint.AuthorizedUsers = authorizedUserIDs
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type putEndpointAccessRequest struct {
|
||||
AuthorizedUsers []int `valid:"required"`
|
||||
}
|
||||
|
||||
// handlePutEndpoint handles PUT requests on /endpoints/:id
|
||||
func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putEndpointsRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
endpoint.Name = req.Name
|
||||
}
|
||||
|
||||
if req.URL != "" {
|
||||
endpoint.URL = req.URL
|
||||
}
|
||||
|
||||
if req.TLS {
|
||||
endpoint.TLS = true
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCA)
|
||||
endpoint.TLSCACertPath = caCertPath
|
||||
certPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileCert)
|
||||
endpoint.TLSCertPath = certPath
|
||||
keyPath, _ := handler.FileService.GetPathForTLSFile(endpoint.ID, portainer.TLSFileKey)
|
||||
endpoint.TLSKeyPath = keyPath
|
||||
} else {
|
||||
endpoint.TLS = false
|
||||
endpoint.TLSCACertPath = ""
|
||||
endpoint.TLSCertPath = ""
|
||||
endpoint.TLSKeyPath = ""
|
||||
err = handler.FileService.DeleteTLSFiles(endpoint.ID)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type putEndpointsRequest struct {
|
||||
Name string `valid:"-"`
|
||||
URL string `valid:"-"`
|
||||
TLS bool `valid:"-"`
|
||||
}
|
||||
|
||||
// handleDeleteEndpoint handles DELETE requests on /endpoints/:id
|
||||
func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
Error(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID))
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if endpoint.TLS {
|
||||
err = handler.FileService.DeleteTLSFiles(portainer.EndpointID(endpointID))
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
}
|
||||
}
|
||||
22
api/http/error/error.go
Normal file
22
api/http/error/error.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package error
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// errorResponse is a generic response for sending a error.
|
||||
type errorResponse struct {
|
||||
Err string `json:"err,omitempty"`
|
||||
}
|
||||
|
||||
// WriteErrorResponse writes an error message to the response and logger.
|
||||
func WriteErrorResponse(w http.ResponseWriter, err error, code int, logger *log.Logger) {
|
||||
if logger != nil {
|
||||
logger.Printf("http error: %s (code=%d)", err, code)
|
||||
}
|
||||
|
||||
w.WriteHeader(code)
|
||||
json.NewEncoder(w).Encode(&errorResponse{Err: err.Error()})
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileHandler represents an HTTP API handler for managing static files.
|
||||
type FileHandler struct {
|
||||
http.Handler
|
||||
}
|
||||
|
||||
func newFileHandler(assetPath string) *FileHandler {
|
||||
h := &FileHandler{
|
||||
Handler: http.FileServer(http.Dir(assetPath)),
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func isHTML(acceptContent []string) bool {
|
||||
for _, accept := range acceptContent {
|
||||
if strings.Contains(accept, "text/html") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (fileHandler *FileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if !isHTML(r.Header["Accept"]) {
|
||||
w.Header().Set("Cache-Control", "max-age=31536000")
|
||||
} else {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
}
|
||||
fileHandler.Handler.ServeHTTP(w, r)
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Handler is a collection of all the service handlers.
|
||||
type Handler struct {
|
||||
AuthHandler *AuthHandler
|
||||
UserHandler *UserHandler
|
||||
EndpointHandler *EndpointHandler
|
||||
SettingsHandler *SettingsHandler
|
||||
TemplatesHandler *TemplatesHandler
|
||||
DockerHandler *DockerHandler
|
||||
WebSocketHandler *WebSocketHandler
|
||||
UploadHandler *UploadHandler
|
||||
FileHandler *FileHandler
|
||||
}
|
||||
|
||||
const (
|
||||
// ErrInvalidJSON defines an error raised the app is unable to parse request data
|
||||
ErrInvalidJSON = portainer.Error("Invalid JSON")
|
||||
// ErrInvalidRequestFormat defines an error raised when the format of the data sent in a request is not valid
|
||||
ErrInvalidRequestFormat = portainer.Error("Invalid request data format")
|
||||
// ErrInvalidQueryFormat defines an error raised when the data sent in the query or the URL is invalid
|
||||
ErrInvalidQueryFormat = portainer.Error("Invalid query format")
|
||||
// ErrEmptyResponseBody defines an error raised when portainer excepts to parse the body of a HTTP response and there is nothing to parse
|
||||
ErrEmptyResponseBody = portainer.Error("Empty response body")
|
||||
)
|
||||
|
||||
// ServeHTTP delegates a request to the appropriate subhandler.
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
if strings.HasPrefix(r.URL.Path, "/api/auth") {
|
||||
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/users") {
|
||||
http.StripPrefix("/api", h.UserHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/endpoints") {
|
||||
http.StripPrefix("/api", h.EndpointHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/settings") {
|
||||
http.StripPrefix("/api", h.SettingsHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/templates") {
|
||||
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/upload") {
|
||||
http.StripPrefix("/api", h.UploadHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/websocket") {
|
||||
http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/api/docker") {
|
||||
http.StripPrefix("/api/docker", h.DockerHandler).ServeHTTP(w, r)
|
||||
} else if strings.HasPrefix(r.URL.Path, "/") {
|
||||
h.FileHandler.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// Error writes an API error message to the response and logger.
|
||||
func Error(w http.ResponseWriter, err error, code int, logger *log.Logger) {
|
||||
// Log error.
|
||||
if logger != nil {
|
||||
logger.Printf("http error: %s (code=%d)", err, code)
|
||||
}
|
||||
|
||||
// Write generic error response.
|
||||
w.WriteHeader(code)
|
||||
json.NewEncoder(w).Encode(&errorResponse{Err: err.Error()})
|
||||
}
|
||||
|
||||
// errorResponse is a generic response for sending a error.
|
||||
type errorResponse struct {
|
||||
Err string `json:"err,omitempty"`
|
||||
}
|
||||
|
||||
// handleNotAllowed writes an API error message to the response and sets the Allow header.
|
||||
func handleNotAllowed(w http.ResponseWriter, allowedMethods []string) {
|
||||
w.Header().Set("Allow", strings.Join(allowedMethods, ", "))
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
json.NewEncoder(w).Encode(&errorResponse{Err: http.StatusText(http.StatusMethodNotAllowed)})
|
||||
}
|
||||
|
||||
// encodeJSON encodes v to w in JSON format. Error() is called if encoding fails.
|
||||
func encodeJSON(w http.ResponseWriter, v interface{}, logger *log.Logger) {
|
||||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, logger)
|
||||
}
|
||||
}
|
||||
126
api/http/handler/auth.go
Normal file
126
api/http/handler/auth.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// AuthHandler represents an HTTP API handler for managing authentication.
|
||||
type AuthHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
authDisabled bool
|
||||
UserService portainer.UserService
|
||||
CryptoService portainer.CryptoService
|
||||
JWTService portainer.JWTService
|
||||
LDAPService portainer.LDAPService
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
const (
|
||||
// ErrInvalidCredentialsFormat is an error raised when credentials format is not valid
|
||||
ErrInvalidCredentialsFormat = portainer.Error("Invalid credentials format")
|
||||
// ErrInvalidCredentials is an error raised when credentials for a user are invalid
|
||||
ErrInvalidCredentials = portainer.Error("Invalid credentials")
|
||||
// ErrAuthDisabled is an error raised when trying to access the authentication endpoints
|
||||
// when the server has been started with the --no-auth flag
|
||||
ErrAuthDisabled = portainer.Error("Authentication is disabled")
|
||||
)
|
||||
|
||||
// NewAuthHandler returns a new instance of AuthHandler.
|
||||
func NewAuthHandler(bouncer *security.RequestBouncer, authDisabled bool) *AuthHandler {
|
||||
h := &AuthHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
authDisabled: authDisabled,
|
||||
}
|
||||
h.Handle("/auth",
|
||||
bouncer.PublicAccess(http.HandlerFunc(h.handlePostAuth))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postAuthRequest struct {
|
||||
Username string `valid:"required"`
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
|
||||
postAuthResponse struct {
|
||||
JWT string `json:"jwt"`
|
||||
}
|
||||
)
|
||||
|
||||
func (handler *AuthHandler) handlePostAuth(w http.ResponseWriter, r *http.Request) {
|
||||
if handler.authDisabled {
|
||||
httperror.WriteErrorResponse(w, ErrAuthDisabled, http.StatusServiceUnavailable, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req postAuthRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidCredentialsFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var username = req.Username
|
||||
var password = req.Password
|
||||
|
||||
u, err := handler.UserService.UserByUsername(username)
|
||||
if err == portainer.ErrUserNotFound {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidCredentials, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if settings.AuthenticationMethod == portainer.AuthenticationLDAP && u.ID != 1 {
|
||||
err = handler.LDAPService.AuthenticateUser(username, password, &settings.LDAPSettings)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
err = handler.CryptoService.CompareHashAndData(u.Password, password)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidCredentials, http.StatusUnprocessableEntity, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tokenData := &portainer.TokenData{
|
||||
ID: u.ID,
|
||||
Username: u.Username,
|
||||
Role: u.Role,
|
||||
}
|
||||
|
||||
token, err := handler.JWTService.GenerateToken(tokenData)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &postAuthResponse{JWT: token}, handler.Logger)
|
||||
}
|
||||
94
api/http/handler/docker.go
Normal file
94
api/http/handler/docker.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// DockerHandler represents an HTTP API handler for proxying requests to the Docker API.
|
||||
type DockerHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
EndpointService portainer.EndpointService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ProxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
// NewDockerHandler returns a new instance of DockerHandler.
|
||||
func NewDockerHandler(bouncer *security.RequestBouncer) *DockerHandler {
|
||||
h := &DockerHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.PathPrefix("/{id}/docker").Handler(
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.proxyRequestsToDockerAPI)))
|
||||
return h
|
||||
}
|
||||
|
||||
func (handler *DockerHandler) checkEndpointAccessControl(endpoint *portainer.Endpoint, userID portainer.UserID) bool {
|
||||
for _, authorizedUserID := range endpoint.AuthorizedUsers {
|
||||
if authorizedUserID == userID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
memberships, _ := handler.TeamMembershipService.TeamMembershipsByUserID(userID)
|
||||
for _, authorizedTeamID := range endpoint.AuthorizedTeams {
|
||||
for _, membership := range memberships {
|
||||
if membership.TeamID == authorizedTeamID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (handler *DockerHandler) proxyRequestsToDockerAPI(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
parsedID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpointID := portainer.EndpointID(parsedID)
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if tokenData.Role != portainer.AdministratorRole && !handler.checkEndpointAccessControl(endpoint, tokenData.ID) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrEndpointAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var proxy http.Handler
|
||||
proxy = handler.ProxyManager.GetProxy(string(endpointID))
|
||||
if proxy == nil {
|
||||
proxy, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
http.StripPrefix("/"+id+"/docker", proxy).ServeHTTP(w, r)
|
||||
}
|
||||
89
api/http/handler/dockerhub.go
Normal file
89
api/http/handler/dockerhub.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// DockerHubHandler represents an HTTP API handler for managing DockerHub.
|
||||
type DockerHubHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
DockerHubService portainer.DockerHubService
|
||||
}
|
||||
|
||||
// NewDockerHubHandler returns a new instance of DockerHubHandler.
|
||||
func NewDockerHubHandler(bouncer *security.RequestBouncer) *DockerHubHandler {
|
||||
h := &DockerHubHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/dockerhub",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetDockerHub))).Methods(http.MethodGet)
|
||||
h.Handle("/dockerhub",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutDockerHub))).Methods(http.MethodPut)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
putDockerHubRequest struct {
|
||||
Authentication bool `valid:""`
|
||||
Username string `valid:""`
|
||||
Password string `valid:""`
|
||||
}
|
||||
)
|
||||
|
||||
// handleGetDockerHub handles GET requests on /dockerhub
|
||||
func (handler *DockerHubHandler) handleGetDockerHub(w http.ResponseWriter, r *http.Request) {
|
||||
dockerhub, err := handler.DockerHubService.DockerHub()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, dockerhub, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
// handlePutDockerHub handles PUT requests on /dockerhub
|
||||
func (handler *DockerHubHandler) handlePutDockerHub(w http.ResponseWriter, r *http.Request) {
|
||||
var req putDockerHubRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
dockerhub := &portainer.DockerHub{
|
||||
Authentication: false,
|
||||
Username: "",
|
||||
Password: "",
|
||||
}
|
||||
|
||||
if req.Authentication {
|
||||
dockerhub.Authentication = true
|
||||
dockerhub.Username = req.Username
|
||||
dockerhub.Password = req.Password
|
||||
}
|
||||
|
||||
err = handler.DockerHubService.StoreDockerHub(dockerhub)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
}
|
||||
389
api/http/handler/endpoint.go
Normal file
389
api/http/handler/endpoint.go
Normal file
@@ -0,0 +1,389 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// EndpointHandler represents an HTTP API handler for managing Docker endpoints.
|
||||
type EndpointHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
authorizeEndpointManagement bool
|
||||
EndpointService portainer.EndpointService
|
||||
FileService portainer.FileService
|
||||
ProxyManager *proxy.Manager
|
||||
}
|
||||
|
||||
const (
|
||||
// ErrEndpointManagementDisabled is an error raised when trying to access the endpoints management endpoints
|
||||
// when the server has been started with the --external-endpoints flag
|
||||
ErrEndpointManagementDisabled = portainer.Error("Endpoint management is disabled")
|
||||
)
|
||||
|
||||
// NewEndpointHandler returns a new instance of EndpointHandler.
|
||||
func NewEndpointHandler(bouncer *security.RequestBouncer, authorizeEndpointManagement bool) *EndpointHandler {
|
||||
h := &EndpointHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
authorizeEndpointManagement: authorizeEndpointManagement,
|
||||
}
|
||||
h.Handle("/endpoints",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostEndpoints))).Methods(http.MethodPost)
|
||||
h.Handle("/endpoints",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetEndpoints))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetEndpoint))).Methods(http.MethodGet)
|
||||
h.Handle("/endpoints/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutEndpoint))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoints/{id}/access",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutEndpointAccess))).Methods(http.MethodPut)
|
||||
h.Handle("/endpoints/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteEndpoint))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postEndpointsRequest struct {
|
||||
Name string `valid:"required"`
|
||||
URL string `valid:"required"`
|
||||
PublicURL string `valid:"-"`
|
||||
TLS bool
|
||||
TLSSkipVerify bool
|
||||
TLSSkipClientVerify bool
|
||||
}
|
||||
|
||||
postEndpointsResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
putEndpointAccessRequest struct {
|
||||
AuthorizedUsers []int `valid:"-"`
|
||||
AuthorizedTeams []int `valid:"-"`
|
||||
}
|
||||
|
||||
putEndpointsRequest struct {
|
||||
Name string `valid:"-"`
|
||||
URL string `valid:"-"`
|
||||
PublicURL string `valid:"-"`
|
||||
TLS bool `valid:"-"`
|
||||
TLSSkipVerify bool `valid:"-"`
|
||||
TLSSkipClientVerify bool `valid:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
// handleGetEndpoints handles GET requests on /endpoints
|
||||
func (handler *EndpointHandler) handleGetEndpoints(w http.ResponseWriter, r *http.Request) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoints, err := handler.EndpointService.Endpoints()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
filteredEndpoints, err := security.FilterEndpoints(endpoints, securityContext)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, filteredEndpoints, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePostEndpoints handles POST requests on /endpoints
|
||||
func (handler *EndpointHandler) handlePostEndpoints(w http.ResponseWriter, r *http.Request) {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req postEndpointsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint := &portainer.Endpoint{
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
PublicURL: req.PublicURL,
|
||||
TLSConfig: portainer.TLSConfiguration{
|
||||
TLS: req.TLS,
|
||||
TLSSkipVerify: req.TLSSkipVerify,
|
||||
},
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
}
|
||||
|
||||
err = handler.EndpointService.CreateEndpoint(endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.TLS {
|
||||
folder := strconv.Itoa(int(endpoint.ID))
|
||||
|
||||
if !req.TLSSkipVerify {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
|
||||
endpoint.TLSConfig.TLSCACertPath = caCertPath
|
||||
}
|
||||
|
||||
if !req.TLSSkipClientVerify {
|
||||
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
|
||||
endpoint.TLSConfig.TLSCertPath = certPath
|
||||
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
|
||||
endpoint.TLSConfig.TLSKeyPath = keyPath
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
encodeJSON(w, &postEndpointsResponse{ID: int(endpoint.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetEndpoint handles GET requests on /endpoints/:id
|
||||
func (handler *EndpointHandler) handleGetEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, endpoint, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePutEndpointAccess handles PUT requests on /endpoints/:id/access
|
||||
func (handler *EndpointHandler) handlePutEndpointAccess(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putEndpointAccessRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.AuthorizedUsers != nil {
|
||||
authorizedUserIDs := []portainer.UserID{}
|
||||
for _, value := range req.AuthorizedUsers {
|
||||
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
|
||||
}
|
||||
endpoint.AuthorizedUsers = authorizedUserIDs
|
||||
}
|
||||
|
||||
if req.AuthorizedTeams != nil {
|
||||
authorizedTeamIDs := []portainer.TeamID{}
|
||||
for _, value := range req.AuthorizedTeams {
|
||||
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
|
||||
}
|
||||
endpoint.AuthorizedTeams = authorizedTeamIDs
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handlePutEndpoint handles PUT requests on /endpoints/:id
|
||||
func (handler *EndpointHandler) handlePutEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putEndpointsRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
endpoint.Name = req.Name
|
||||
}
|
||||
|
||||
if req.URL != "" {
|
||||
endpoint.URL = req.URL
|
||||
}
|
||||
|
||||
if req.PublicURL != "" {
|
||||
endpoint.PublicURL = req.PublicURL
|
||||
}
|
||||
|
||||
folder := strconv.Itoa(int(endpoint.ID))
|
||||
if req.TLS {
|
||||
endpoint.TLSConfig.TLS = true
|
||||
endpoint.TLSConfig.TLSSkipVerify = req.TLSSkipVerify
|
||||
if !req.TLSSkipVerify {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCA)
|
||||
endpoint.TLSConfig.TLSCACertPath = caCertPath
|
||||
} else {
|
||||
endpoint.TLSConfig.TLSCACertPath = ""
|
||||
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCA)
|
||||
}
|
||||
|
||||
if !req.TLSSkipClientVerify {
|
||||
certPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileCert)
|
||||
endpoint.TLSConfig.TLSCertPath = certPath
|
||||
keyPath, _ := handler.FileService.GetPathForTLSFile(folder, portainer.TLSFileKey)
|
||||
endpoint.TLSConfig.TLSKeyPath = keyPath
|
||||
} else {
|
||||
endpoint.TLSConfig.TLSCertPath = ""
|
||||
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileCert)
|
||||
endpoint.TLSConfig.TLSKeyPath = ""
|
||||
handler.FileService.DeleteTLSFile(folder, portainer.TLSFileKey)
|
||||
}
|
||||
} else {
|
||||
endpoint.TLSConfig.TLS = false
|
||||
endpoint.TLSConfig.TLSSkipVerify = true
|
||||
endpoint.TLSConfig.TLSCACertPath = ""
|
||||
endpoint.TLSConfig.TLSCertPath = ""
|
||||
endpoint.TLSConfig.TLSKeyPath = ""
|
||||
err = handler.FileService.DeleteTLSFiles(folder)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, err = handler.ProxyManager.CreateAndRegisterProxy(endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.EndpointService.UpdateEndpoint(endpoint.ID, endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleDeleteEndpoint handles DELETE requests on /endpoints/:id
|
||||
func (handler *EndpointHandler) handleDeleteEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
if !handler.authorizeEndpointManagement {
|
||||
httperror.WriteErrorResponse(w, ErrEndpointManagementDisabled, http.StatusServiceUnavailable, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
handler.ProxyManager.DeleteProxy(string(endpointID))
|
||||
|
||||
err = handler.EndpointService.DeleteEndpoint(portainer.EndpointID(endpointID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if endpoint.TLSConfig.TLS {
|
||||
err = handler.FileService.DeleteTLSFiles(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
62
api/http/handler/file.go
Normal file
62
api/http/handler/file.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// FileHandler represents an HTTP API handler for managing static files.
|
||||
type FileHandler struct {
|
||||
http.Handler
|
||||
Logger *log.Logger
|
||||
allowedDirectories map[string]bool
|
||||
}
|
||||
|
||||
// NewFileHandler returns a new instance of FileHandler.
|
||||
func NewFileHandler(assetPath string) *FileHandler {
|
||||
h := &FileHandler{
|
||||
Handler: http.FileServer(http.Dir(assetPath)),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
allowedDirectories: map[string]bool{
|
||||
"/": true,
|
||||
"/css": true,
|
||||
"/js": true,
|
||||
"/images": true,
|
||||
"/fonts": true,
|
||||
"/ico": true,
|
||||
},
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func isHTML(acceptContent []string) bool {
|
||||
for _, accept := range acceptContent {
|
||||
if strings.Contains(accept, "text/html") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (handler *FileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
requestDirectory := path.Dir(r.URL.Path)
|
||||
if !handler.allowedDirectories[requestDirectory] {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceNotFound, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !isHTML(r.Header["Accept"]) {
|
||||
w.Header().Set("Cache-Control", "max-age=31536000")
|
||||
} else {
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
}
|
||||
|
||||
handler.Handler.ServeHTTP(w, r)
|
||||
}
|
||||
88
api/http/handler/handler.go
Normal file
88
api/http/handler/handler.go
Normal file
@@ -0,0 +1,88 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
)
|
||||
|
||||
// Handler is a collection of all the service handlers.
|
||||
type Handler struct {
|
||||
AuthHandler *AuthHandler
|
||||
UserHandler *UserHandler
|
||||
TeamHandler *TeamHandler
|
||||
TeamMembershipHandler *TeamMembershipHandler
|
||||
EndpointHandler *EndpointHandler
|
||||
RegistryHandler *RegistryHandler
|
||||
DockerHubHandler *DockerHubHandler
|
||||
ResourceHandler *ResourceHandler
|
||||
StackHandler *StackHandler
|
||||
StatusHandler *StatusHandler
|
||||
SettingsHandler *SettingsHandler
|
||||
TemplatesHandler *TemplatesHandler
|
||||
DockerHandler *DockerHandler
|
||||
WebSocketHandler *WebSocketHandler
|
||||
UploadHandler *UploadHandler
|
||||
FileHandler *FileHandler
|
||||
}
|
||||
|
||||
const (
|
||||
// ErrInvalidJSON defines an error raised the app is unable to parse request data
|
||||
ErrInvalidJSON = portainer.Error("Invalid JSON")
|
||||
// ErrInvalidRequestFormat defines an error raised when the format of the data sent in a request is not valid
|
||||
ErrInvalidRequestFormat = portainer.Error("Invalid request data format")
|
||||
// ErrInvalidQueryFormat defines an error raised when the data sent in the query or the URL is invalid
|
||||
ErrInvalidQueryFormat = portainer.Error("Invalid query format")
|
||||
)
|
||||
|
||||
// ServeHTTP delegates a request to the appropriate subhandler.
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(r.URL.Path, "/api/auth"):
|
||||
http.StripPrefix("/api", h.AuthHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/dockerhub"):
|
||||
http.StripPrefix("/api", h.DockerHubHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/endpoints"):
|
||||
if strings.Contains(r.URL.Path, "/docker") {
|
||||
http.StripPrefix("/api/endpoints", h.DockerHandler).ServeHTTP(w, r)
|
||||
} else if strings.Contains(r.URL.Path, "/stacks") {
|
||||
http.StripPrefix("/api/endpoints", h.StackHandler).ServeHTTP(w, r)
|
||||
} else {
|
||||
http.StripPrefix("/api", h.EndpointHandler).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.ResourceHandler).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/status"):
|
||||
http.StripPrefix("/api", h.StatusHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/templates"):
|
||||
http.StripPrefix("/api", h.TemplatesHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/upload"):
|
||||
http.StripPrefix("/api", h.UploadHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/users"):
|
||||
http.StripPrefix("/api", h.UserHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/teams"):
|
||||
http.StripPrefix("/api", h.TeamHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/team_memberships"):
|
||||
http.StripPrefix("/api", h.TeamMembershipHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/api/websocket"):
|
||||
http.StripPrefix("/api", h.WebSocketHandler).ServeHTTP(w, r)
|
||||
case strings.HasPrefix(r.URL.Path, "/"):
|
||||
h.FileHandler.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
// encodeJSON encodes v to w in JSON format. WriteErrorResponse() is called if encoding fails.
|
||||
func encodeJSON(w http.ResponseWriter, v interface{}, logger *log.Logger) {
|
||||
if err := json.NewEncoder(w).Encode(v); err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, logger)
|
||||
}
|
||||
}
|
||||
314
api/http/handler/registry.go
Normal file
314
api/http/handler/registry.go
Normal file
@@ -0,0 +1,314 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// RegistryHandler represents an HTTP API handler for managing Docker registries.
|
||||
type RegistryHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
RegistryService portainer.RegistryService
|
||||
}
|
||||
|
||||
// NewRegistryHandler returns a new instance of RegistryHandler.
|
||||
func NewRegistryHandler(bouncer *security.RequestBouncer) *RegistryHandler {
|
||||
h := &RegistryHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/registries",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostRegistries))).Methods(http.MethodPost)
|
||||
h.Handle("/registries",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetRegistries))).Methods(http.MethodGet)
|
||||
h.Handle("/registries/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetRegistry))).Methods(http.MethodGet)
|
||||
h.Handle("/registries/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutRegistry))).Methods(http.MethodPut)
|
||||
h.Handle("/registries/{id}/access",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutRegistryAccess))).Methods(http.MethodPut)
|
||||
h.Handle("/registries/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteRegistry))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postRegistriesRequest struct {
|
||||
Name string `valid:"required"`
|
||||
URL string `valid:"required"`
|
||||
Authentication bool `valid:""`
|
||||
Username string `valid:""`
|
||||
Password string `valid:""`
|
||||
}
|
||||
|
||||
postRegistriesResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
putRegistryAccessRequest struct {
|
||||
AuthorizedUsers []int `valid:"-"`
|
||||
AuthorizedTeams []int `valid:"-"`
|
||||
}
|
||||
|
||||
putRegistriesRequest struct {
|
||||
Name string `valid:"required"`
|
||||
URL string `valid:"required"`
|
||||
Authentication bool `valid:""`
|
||||
Username string `valid:""`
|
||||
Password string `valid:""`
|
||||
}
|
||||
)
|
||||
|
||||
// handleGetRegistries handles GET requests on /registries
|
||||
func (handler *RegistryHandler) handleGetRegistries(w http.ResponseWriter, r *http.Request) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
registries, err := handler.RegistryService.Registries()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
filteredRegistries, err := security.FilterRegistries(registries, securityContext)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, filteredRegistries, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePostRegistries handles POST requests on /registries
|
||||
func (handler *RegistryHandler) handlePostRegistries(w http.ResponseWriter, r *http.Request) {
|
||||
var req postRegistriesRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
registries, err := handler.RegistryService.Registries()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
for _, r := range registries {
|
||||
if r.URL == req.URL {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrRegistryAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
registry := &portainer.Registry{
|
||||
Name: req.Name,
|
||||
URL: req.URL,
|
||||
Authentication: req.Authentication,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
AuthorizedUsers: []portainer.UserID{},
|
||||
AuthorizedTeams: []portainer.TeamID{},
|
||||
}
|
||||
|
||||
err = handler.RegistryService.CreateRegistry(registry)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &postRegistriesResponse{ID: int(registry.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetRegistry handles GET requests on /registries/:id
|
||||
func (handler *RegistryHandler) handleGetRegistry(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
registryID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID))
|
||||
if err == portainer.ErrRegistryNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, registry, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePutRegistryAccess handles PUT requests on /registries/:id/access
|
||||
func (handler *RegistryHandler) handlePutRegistryAccess(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
registryID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putRegistryAccessRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID))
|
||||
if err == portainer.ErrRegistryNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.AuthorizedUsers != nil {
|
||||
authorizedUserIDs := []portainer.UserID{}
|
||||
for _, value := range req.AuthorizedUsers {
|
||||
authorizedUserIDs = append(authorizedUserIDs, portainer.UserID(value))
|
||||
}
|
||||
registry.AuthorizedUsers = authorizedUserIDs
|
||||
}
|
||||
|
||||
if req.AuthorizedTeams != nil {
|
||||
authorizedTeamIDs := []portainer.TeamID{}
|
||||
for _, value := range req.AuthorizedTeams {
|
||||
authorizedTeamIDs = append(authorizedTeamIDs, portainer.TeamID(value))
|
||||
}
|
||||
registry.AuthorizedTeams = authorizedTeamIDs
|
||||
}
|
||||
|
||||
err = handler.RegistryService.UpdateRegistry(registry.ID, registry)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handlePutRegistry handles PUT requests on /registries/:id
|
||||
func (handler *RegistryHandler) handlePutRegistry(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
registryID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putRegistriesRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
registry, err := handler.RegistryService.Registry(portainer.RegistryID(registryID))
|
||||
if err == portainer.ErrRegistryNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
registries, err := handler.RegistryService.Registries()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
for _, r := range registries {
|
||||
if r.URL == req.URL && r.ID != registry.ID {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrRegistryAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
registry.Name = req.Name
|
||||
}
|
||||
|
||||
if req.URL != "" {
|
||||
registry.URL = req.URL
|
||||
}
|
||||
|
||||
if req.Authentication {
|
||||
registry.Authentication = true
|
||||
registry.Username = req.Username
|
||||
registry.Password = req.Password
|
||||
} else {
|
||||
registry.Authentication = false
|
||||
registry.Username = ""
|
||||
registry.Password = ""
|
||||
}
|
||||
|
||||
err = handler.RegistryService.UpdateRegistry(registry.ID, registry)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleDeleteRegistry handles DELETE requests on /registries/:id
|
||||
func (handler *RegistryHandler) handleDeleteRegistry(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
registryID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.RegistryService.Registry(portainer.RegistryID(registryID))
|
||||
if err == portainer.ErrRegistryNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.RegistryService.DeleteRegistry(portainer.RegistryID(registryID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
264
api/http/handler/resource_control.go
Normal file
264
api/http/handler/resource_control.go
Normal file
@@ -0,0 +1,264 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// ResourceHandler represents an HTTP API handler for managing resource controls.
|
||||
type ResourceHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
}
|
||||
|
||||
// NewResourceHandler returns a new instance of ResourceHandler.
|
||||
func NewResourceHandler(bouncer *security.RequestBouncer) *ResourceHandler {
|
||||
h := &ResourceHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/resource_controls",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handlePostResources))).Methods(http.MethodPost)
|
||||
h.Handle("/resource_controls/{id}",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handlePutResources))).Methods(http.MethodPut)
|
||||
h.Handle("/resource_controls/{id}",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleDeleteResources))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postResourcesRequest struct {
|
||||
ResourceID string `valid:"required"`
|
||||
Type string `valid:"required"`
|
||||
AdministratorsOnly bool `valid:"-"`
|
||||
Users []int `valid:"-"`
|
||||
Teams []int `valid:"-"`
|
||||
SubResourceIDs []string `valid:"-"`
|
||||
}
|
||||
|
||||
putResourcesRequest struct {
|
||||
AdministratorsOnly bool `valid:"-"`
|
||||
Users []int `valid:"-"`
|
||||
Teams []int `valid:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
// handlePostResources handles POST requests on /resources
|
||||
func (handler *ResourceHandler) handlePostResources(w http.ResponseWriter, r *http.Request) {
|
||||
var req postResourcesRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var resourceControlType portainer.ResourceControlType
|
||||
switch req.Type {
|
||||
case "container":
|
||||
resourceControlType = portainer.ContainerResourceControl
|
||||
case "service":
|
||||
resourceControlType = portainer.ServiceResourceControl
|
||||
case "volume":
|
||||
resourceControlType = portainer.VolumeResourceControl
|
||||
case "network":
|
||||
resourceControlType = portainer.NetworkResourceControl
|
||||
case "secret":
|
||||
resourceControlType = portainer.SecretResourceControl
|
||||
case "stack":
|
||||
resourceControlType = portainer.StackResourceControl
|
||||
default:
|
||||
httperror.WriteErrorResponse(w, portainer.ErrInvalidResourceControlType, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Users) == 0 && len(req.Teams) == 0 && !req.AdministratorsOnly {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
rc, err := handler.ResourceControlService.ResourceControlByResourceID(req.ResourceID)
|
||||
if err != nil && err != portainer.ErrResourceControlNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if rc != nil {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceControlAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var userAccesses = make([]portainer.UserResourceAccess, 0)
|
||||
for _, v := range req.Users {
|
||||
userAccess := portainer.UserResourceAccess{
|
||||
UserID: portainer.UserID(v),
|
||||
AccessLevel: portainer.ReadWriteAccessLevel,
|
||||
}
|
||||
userAccesses = append(userAccesses, userAccess)
|
||||
}
|
||||
|
||||
var teamAccesses = make([]portainer.TeamResourceAccess, 0)
|
||||
for _, v := range req.Teams {
|
||||
teamAccess := portainer.TeamResourceAccess{
|
||||
TeamID: portainer.TeamID(v),
|
||||
AccessLevel: portainer.ReadWriteAccessLevel,
|
||||
}
|
||||
teamAccesses = append(teamAccesses, teamAccess)
|
||||
}
|
||||
|
||||
resourceControl := portainer.ResourceControl{
|
||||
ResourceID: req.ResourceID,
|
||||
SubResourceIDs: req.SubResourceIDs,
|
||||
Type: resourceControlType,
|
||||
AdministratorsOnly: req.AdministratorsOnly,
|
||||
UserAccesses: userAccesses,
|
||||
TeamAccesses: teamAccesses,
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !security.AuthorizedResourceControlCreation(&resourceControl, securityContext) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.ResourceControlService.CreateResourceControl(&resourceControl)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// handlePutResources handles PUT requests on /resources/:id
|
||||
func (handler *ResourceHandler) handlePutResources(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
resourceControlID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putResourcesRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControl(portainer.ResourceControlID(resourceControlID))
|
||||
|
||||
if err == portainer.ErrResourceControlNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
resourceControl.AdministratorsOnly = req.AdministratorsOnly
|
||||
|
||||
var userAccesses = make([]portainer.UserResourceAccess, 0)
|
||||
for _, v := range req.Users {
|
||||
userAccess := portainer.UserResourceAccess{
|
||||
UserID: portainer.UserID(v),
|
||||
AccessLevel: portainer.ReadWriteAccessLevel,
|
||||
}
|
||||
userAccesses = append(userAccesses, userAccess)
|
||||
}
|
||||
resourceControl.UserAccesses = userAccesses
|
||||
|
||||
var teamAccesses = make([]portainer.TeamResourceAccess, 0)
|
||||
for _, v := range req.Teams {
|
||||
teamAccess := portainer.TeamResourceAccess{
|
||||
TeamID: portainer.TeamID(v),
|
||||
AccessLevel: portainer.ReadWriteAccessLevel,
|
||||
}
|
||||
teamAccesses = append(teamAccesses, teamAccess)
|
||||
}
|
||||
resourceControl.TeamAccesses = teamAccesses
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !security.AuthorizedResourceControlUpdate(resourceControl, securityContext) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.ResourceControlService.UpdateResourceControl(resourceControl.ID, resourceControl)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleDeleteResources handles DELETE requests on /resources/:id
|
||||
func (handler *ResourceHandler) handleDeleteResources(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
resourceControlID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControl(portainer.ResourceControlID(resourceControlID))
|
||||
|
||||
if err == portainer.ErrResourceControlNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !security.AuthorizedResourceControlDeletion(resourceControl, securityContext) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.ResourceControlService.DeleteResourceControl(portainer.ResourceControlID(resourceControlID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
177
api/http/handler/settings.go
Normal file
177
api/http/handler/settings.go
Normal file
@@ -0,0 +1,177 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/file"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// SettingsHandler represents an HTTP API handler for managing Settings.
|
||||
type SettingsHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
SettingsService portainer.SettingsService
|
||||
LDAPService portainer.LDAPService
|
||||
FileService portainer.FileService
|
||||
}
|
||||
|
||||
// NewSettingsHandler returns a new instance of OldSettingsHandler.
|
||||
func NewSettingsHandler(bouncer *security.RequestBouncer) *SettingsHandler {
|
||||
h := &SettingsHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/settings",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetSettings))).Methods(http.MethodGet)
|
||||
h.Handle("/settings",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutSettings))).Methods(http.MethodPut)
|
||||
h.Handle("/settings/public",
|
||||
bouncer.PublicAccess(http.HandlerFunc(h.handleGetPublicSettings))).Methods(http.MethodGet)
|
||||
h.Handle("/settings/authentication/checkLDAP",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutSettingsLDAPCheck))).Methods(http.MethodPut)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
publicSettingsResponse struct {
|
||||
LogoURL string `json:"LogoURL"`
|
||||
DisplayExternalContributors bool `json:"DisplayExternalContributors"`
|
||||
AuthenticationMethod portainer.AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||
}
|
||||
|
||||
putSettingsRequest struct {
|
||||
TemplatesURL string `valid:"required"`
|
||||
LogoURL string `valid:""`
|
||||
BlackListedLabels []portainer.Pair `valid:""`
|
||||
DisplayExternalContributors bool `valid:""`
|
||||
AuthenticationMethod int `valid:"required"`
|
||||
LDAPSettings portainer.LDAPSettings `valid:""`
|
||||
AllowBindMountsForRegularUsers bool `valid:""`
|
||||
AllowPrivilegedModeForRegularUsers bool `valid:""`
|
||||
}
|
||||
|
||||
putSettingsLDAPCheckRequest struct {
|
||||
LDAPSettings portainer.LDAPSettings `valid:""`
|
||||
}
|
||||
)
|
||||
|
||||
// handleGetSettings handles GET requests on /settings
|
||||
func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http.Request) {
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, settings, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
// handleGetPublicSettings handles GET requests on /settings/public
|
||||
func (handler *SettingsHandler) handleGetPublicSettings(w http.ResponseWriter, r *http.Request) {
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
publicSettings := &publicSettingsResponse{
|
||||
LogoURL: settings.LogoURL,
|
||||
DisplayExternalContributors: settings.DisplayExternalContributors,
|
||||
AuthenticationMethod: settings.AuthenticationMethod,
|
||||
AllowBindMountsForRegularUsers: settings.AllowBindMountsForRegularUsers,
|
||||
AllowPrivilegedModeForRegularUsers: settings.AllowPrivilegedModeForRegularUsers,
|
||||
}
|
||||
|
||||
encodeJSON(w, publicSettings, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
// handlePutSettings handles PUT requests on /settings
|
||||
func (handler *SettingsHandler) handlePutSettings(w http.ResponseWriter, r *http.Request) {
|
||||
var req putSettingsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
settings := &portainer.Settings{
|
||||
TemplatesURL: req.TemplatesURL,
|
||||
LogoURL: req.LogoURL,
|
||||
BlackListedLabels: req.BlackListedLabels,
|
||||
DisplayExternalContributors: req.DisplayExternalContributors,
|
||||
LDAPSettings: req.LDAPSettings,
|
||||
AllowBindMountsForRegularUsers: req.AllowBindMountsForRegularUsers,
|
||||
AllowPrivilegedModeForRegularUsers: req.AllowPrivilegedModeForRegularUsers,
|
||||
}
|
||||
|
||||
if req.AuthenticationMethod == 1 {
|
||||
settings.AuthenticationMethod = portainer.AuthenticationInternal
|
||||
} else if req.AuthenticationMethod == 2 {
|
||||
settings.AuthenticationMethod = portainer.AuthenticationLDAP
|
||||
} else {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if (settings.LDAPSettings.TLSConfig.TLS || settings.LDAPSettings.StartTLS) && !settings.LDAPSettings.TLSConfig.TLSSkipVerify {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(file.LDAPStorePath, portainer.TLSFileCA)
|
||||
settings.LDAPSettings.TLSConfig.TLSCACertPath = caCertPath
|
||||
} else {
|
||||
settings.LDAPSettings.TLSConfig.TLSCACertPath = ""
|
||||
err := handler.FileService.DeleteTLSFiles(file.LDAPStorePath)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.SettingsService.StoreSettings(settings)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
}
|
||||
|
||||
// handlePutSettingsLDAPCheck handles PUT requests on /settings/ldap/check
|
||||
func (handler *SettingsHandler) handlePutSettingsLDAPCheck(w http.ResponseWriter, r *http.Request) {
|
||||
var req putSettingsLDAPCheckRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if (req.LDAPSettings.TLSConfig.TLS || req.LDAPSettings.StartTLS) && !req.LDAPSettings.TLSConfig.TLSSkipVerify {
|
||||
caCertPath, _ := handler.FileService.GetPathForTLSFile(file.LDAPStorePath, portainer.TLSFileCA)
|
||||
req.LDAPSettings.TLSConfig.TLSCACertPath = caCertPath
|
||||
}
|
||||
|
||||
err = handler.LDAPService.TestConnectivity(&req.LDAPSettings)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
609
api/http/handler/stack.go
Normal file
609
api/http/handler/stack.go
Normal file
@@ -0,0 +1,609 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/file"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// StackHandler represents an HTTP API handler for managing Stack.
|
||||
type StackHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
FileService portainer.FileService
|
||||
GitService portainer.GitService
|
||||
StackService portainer.StackService
|
||||
EndpointService portainer.EndpointService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
StackManager portainer.StackManager
|
||||
}
|
||||
|
||||
// NewStackHandler returns a new instance of StackHandler.
|
||||
func NewStackHandler(bouncer *security.RequestBouncer) *StackHandler {
|
||||
h := &StackHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/{endpointId}/stacks",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handlePostStacks))).Methods(http.MethodPost)
|
||||
h.Handle("/{endpointId}/stacks",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetStacks))).Methods(http.MethodGet)
|
||||
h.Handle("/{endpointId}/stacks/{id}",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetStack))).Methods(http.MethodGet)
|
||||
h.Handle("/{endpointId}/stacks/{id}",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleDeleteStack))).Methods(http.MethodDelete)
|
||||
h.Handle("/{endpointId}/stacks/{id}",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handlePutStack))).Methods(http.MethodPut)
|
||||
h.Handle("/{endpointId}/stacks/{id}/stackfile",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetStackFile))).Methods(http.MethodGet)
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postStacksRequest struct {
|
||||
Name string `valid:"required"`
|
||||
SwarmID string `valid:"required"`
|
||||
StackFileContent string `valid:""`
|
||||
GitRepository string `valid:""`
|
||||
PathInRepository string `valid:""`
|
||||
}
|
||||
postStacksResponse struct {
|
||||
ID string `json:"Id"`
|
||||
}
|
||||
getStackFileResponse struct {
|
||||
StackFileContent string `json:"StackFileContent"`
|
||||
}
|
||||
putStackRequest struct {
|
||||
StackFileContent string `valid:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
// handlePostStacks handles POST requests on /:endpointId/stacks?method=<method>
|
||||
func (handler *StackHandler) handlePostStacks(w http.ResponseWriter, r *http.Request) {
|
||||
method := r.FormValue("method")
|
||||
if method == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if method == "string" {
|
||||
handler.handlePostStacksStringMethod(w, r)
|
||||
} else if method == "repository" {
|
||||
handler.handlePostStacksRepositoryMethod(w, r)
|
||||
} else if method == "file" {
|
||||
handler.handlePostStacksFileMethod(w, r)
|
||||
} else {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (handler *StackHandler) handlePostStacksStringMethod(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["endpointId"])
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
endpointID := portainer.EndpointID(id)
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req postStacksRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stackName := req.Name
|
||||
if stackName == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stackFileContent := req.StackFileContent
|
||||
if stackFileContent == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
swarmID := req.SwarmID
|
||||
if swarmID == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stacks, err := handler.StackService.Stacks()
|
||||
if err != nil && err != portainer.ErrStackNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
if strings.EqualFold(stack.Name, stackName) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrStackAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackName + "_" + swarmID),
|
||||
Name: stackName,
|
||||
SwarmID: swarmID,
|
||||
EntryPoint: file.ComposeFileDefaultName,
|
||||
}
|
||||
|
||||
projectPath, err := handler.FileService.StoreStackFileFromString(string(stack.ID), stackFileContent)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
err = handler.StackService.CreateStack(stack)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.StackManager.Deploy(stack, endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &postStacksResponse{ID: string(stack.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
func (handler *StackHandler) handlePostStacksRepositoryMethod(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["endpointId"])
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
endpointID := portainer.EndpointID(id)
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req postStacksRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stackName := req.Name
|
||||
if stackName == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
swarmID := req.SwarmID
|
||||
if swarmID == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.GitRepository == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.PathInRepository == "" {
|
||||
req.PathInRepository = file.ComposeFileDefaultName
|
||||
}
|
||||
|
||||
stacks, err := handler.StackService.Stacks()
|
||||
if err != nil && err != portainer.ErrStackNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
if strings.EqualFold(stack.Name, stackName) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrStackAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackName + "_" + swarmID),
|
||||
Name: stackName,
|
||||
SwarmID: swarmID,
|
||||
EntryPoint: req.PathInRepository,
|
||||
}
|
||||
|
||||
projectPath := handler.FileService.GetStackProjectPath(string(stack.ID))
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
// Ensure projectPath is empty
|
||||
err = handler.FileService.RemoveDirectory(projectPath)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.GitService.CloneRepository(req.GitRepository, projectPath)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.StackService.CreateStack(stack)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.StackManager.Deploy(stack, endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &postStacksResponse{ID: string(stack.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
func (handler *StackHandler) handlePostStacksFileMethod(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id, err := strconv.Atoi(vars["endpointId"])
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
endpointID := portainer.EndpointID(id)
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(endpointID)
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stackName := r.FormValue("Name")
|
||||
if stackName == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
swarmID := r.FormValue("SwarmID")
|
||||
if swarmID == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stackFile, _, err := r.FormFile("file")
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
defer stackFile.Close()
|
||||
|
||||
stacks, err := handler.StackService.Stacks()
|
||||
if err != nil && err != portainer.ErrStackNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
if strings.EqualFold(stack.Name, stackName) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrStackAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
stack := &portainer.Stack{
|
||||
ID: portainer.StackID(stackName + "_" + swarmID),
|
||||
Name: stackName,
|
||||
SwarmID: swarmID,
|
||||
EntryPoint: file.ComposeFileDefaultName,
|
||||
}
|
||||
|
||||
projectPath, err := handler.FileService.StoreStackFileFromReader(string(stack.ID), stackFile)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
stack.ProjectPath = projectPath
|
||||
|
||||
err = handler.StackService.CreateStack(stack)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.StackManager.Deploy(stack, endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &postStacksResponse{ID: string(stack.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetStacks handles GET requests on /:endpointId/stacks?swarmId=<swarmId>
|
||||
func (handler *StackHandler) handleGetStacks(w http.ResponseWriter, r *http.Request) {
|
||||
swarmID := r.FormValue("swarmId")
|
||||
|
||||
vars := mux.Vars(r)
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.Atoi(vars["endpointId"])
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
endpointID := portainer.EndpointID(id)
|
||||
|
||||
_, err = handler.EndpointService.Endpoint(endpointID)
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var stacks []portainer.Stack
|
||||
if swarmID == "" {
|
||||
stacks, err = handler.StackService.Stacks()
|
||||
} else {
|
||||
stacks, err = handler.StackService.StacksBySwarmID(swarmID)
|
||||
}
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
resourceControls, err := handler.ResourceControlService.ResourceControls()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
filteredStacks := proxy.FilterStacks(stacks, resourceControls, securityContext.IsAdmin,
|
||||
securityContext.UserID, securityContext.UserMemberships)
|
||||
|
||||
encodeJSON(w, filteredStacks, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetStack handles GET requests on /:endpointId/stacks/:id
|
||||
func (handler *StackHandler) handleGetStack(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
stackID := vars["id"]
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpointID, err := strconv.Atoi(vars["endpointId"])
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stack, err := handler.StackService.Stack(portainer.StackID(stackID))
|
||||
if err == portainer.ErrStackNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
resourceControl, err := handler.ResourceControlService.ResourceControlByResourceID(stack.Name)
|
||||
if err != nil && err != portainer.ErrResourceControlNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
extendedStack := proxy.ExtendedStack{*stack, portainer.ResourceControl{}}
|
||||
if resourceControl != nil {
|
||||
if securityContext.IsAdmin || proxy.CanAccessStack(stack, resourceControl, securityContext.UserID, securityContext.UserMemberships) {
|
||||
extendedStack.ResourceControl = *resourceControl
|
||||
} else {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
encodeJSON(w, extendedStack, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePutStack handles PUT requests on /:endpointId/stacks/:id
|
||||
func (handler *StackHandler) handlePutStack(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
stackID := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(vars["endpointId"])
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stack, err := handler.StackService.Stack(portainer.StackID(stackID))
|
||||
if err == portainer.ErrStackNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putStackRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.FileService.StoreStackFileFromString(string(stack.ID), req.StackFileContent)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.StackManager.Deploy(stack, endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleGetStackFile handles GET requests on /:endpointId/stacks/:id/stackfile
|
||||
func (handler *StackHandler) handleGetStackFile(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
stackID := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(vars["endpointId"])
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stack, err := handler.StackService.Stack(portainer.StackID(stackID))
|
||||
if err == portainer.ErrStackNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stackFileContent, err := handler.FileService.GetFileContent(path.Join(stack.ProjectPath, stack.EntryPoint))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &getStackFileResponse{StackFileContent: stackFileContent}, handler.Logger)
|
||||
}
|
||||
|
||||
// handleDeleteStack handles DELETE requests on /:endpointId/stacks/:id
|
||||
func (handler *StackHandler) handleDeleteStack(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
stackID := vars["id"]
|
||||
|
||||
endpointID, err := strconv.Atoi(vars["endpointId"])
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
endpoint, err := handler.EndpointService.Endpoint(portainer.EndpointID(endpointID))
|
||||
if err == portainer.ErrEndpointNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
stack, err := handler.StackService.Stack(portainer.StackID(stackID))
|
||||
if err == portainer.ErrStackNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.StackManager.Remove(stack, endpoint)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.StackService.DeleteStack(portainer.StackID(stackID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.FileService.RemoveDirectory(stack.ProjectPath)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
38
api/http/handler/status.go
Normal file
38
api/http/handler/status.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// StatusHandler represents an HTTP API handler for managing Status.
|
||||
type StatusHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
Status *portainer.Status
|
||||
}
|
||||
|
||||
// NewStatusHandler returns a new instance of StatusHandler.
|
||||
func NewStatusHandler(bouncer *security.RequestBouncer, status *portainer.Status) *StatusHandler {
|
||||
h := &StatusHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
Status: status,
|
||||
}
|
||||
h.Handle("/status",
|
||||
bouncer.PublicAccess(http.HandlerFunc(h.handleGetStatus))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// handleGetStatus handles GET requests on /status
|
||||
func (handler *StatusHandler) handleGetStatus(w http.ResponseWriter, r *http.Request) {
|
||||
encodeJSON(w, handler.Status, handler.Logger)
|
||||
return
|
||||
}
|
||||
262
api/http/handler/team.go
Normal file
262
api/http/handler/team.go
Normal file
@@ -0,0 +1,262 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// TeamHandler represents an HTTP API handler for managing teams.
|
||||
type TeamHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
TeamService portainer.TeamService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
}
|
||||
|
||||
// NewTeamHandler returns a new instance of TeamHandler.
|
||||
func NewTeamHandler(bouncer *security.RequestBouncer) *TeamHandler {
|
||||
h := &TeamHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/teams",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostTeams))).Methods(http.MethodPost)
|
||||
h.Handle("/teams",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetTeams))).Methods(http.MethodGet)
|
||||
h.Handle("/teams/{id}",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetTeam))).Methods(http.MethodGet)
|
||||
h.Handle("/teams/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePutTeam))).Methods(http.MethodPut)
|
||||
h.Handle("/teams/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteTeam))).Methods(http.MethodDelete)
|
||||
h.Handle("/teams/{id}/memberships",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetMemberships))).Methods(http.MethodGet)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postTeamsRequest struct {
|
||||
Name string `valid:"required"`
|
||||
}
|
||||
|
||||
postTeamsResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
putTeamRequest struct {
|
||||
Name string `valid:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
// handlePostTeams handles POST requests on /teams
|
||||
func (handler *TeamHandler) handlePostTeams(w http.ResponseWriter, r *http.Request) {
|
||||
var req postTeamsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
team, err := handler.TeamService.TeamByName(req.Name)
|
||||
if err != nil && err != portainer.ErrTeamNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if team != nil {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrTeamAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
team = &portainer.Team{
|
||||
Name: req.Name,
|
||||
}
|
||||
|
||||
err = handler.TeamService.CreateTeam(team)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &postTeamsResponse{ID: int(team.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetTeams handles GET requests on /teams
|
||||
func (handler *TeamHandler) handleGetTeams(w http.ResponseWriter, r *http.Request) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
teams, err := handler.TeamService.Teams()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
filteredTeams := security.FilterUserTeams(teams, securityContext)
|
||||
|
||||
encodeJSON(w, filteredTeams, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetTeam handles GET requests on /teams/:id
|
||||
func (handler *TeamHandler) handleGetTeam(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
tid, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
teamID := portainer.TeamID(tid)
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !security.AuthorizedTeamManagement(teamID, securityContext) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
team, err := handler.TeamService.Team(teamID)
|
||||
if err == portainer.ErrTeamNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &team, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePutTeam handles PUT requests on /teams/:id
|
||||
func (handler *TeamHandler) handlePutTeam(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
teamID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putTeamRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
team, err := handler.TeamService.Team(portainer.TeamID(teamID))
|
||||
if err == portainer.ErrTeamNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name != "" {
|
||||
team.Name = req.Name
|
||||
}
|
||||
|
||||
err = handler.TeamService.UpdateTeam(team.ID, team)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleDeleteTeam handles DELETE requests on /teams/:id
|
||||
func (handler *TeamHandler) handleDeleteTeam(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
teamID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.TeamService.Team(portainer.TeamID(teamID))
|
||||
|
||||
if err == portainer.ErrTeamNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.TeamService.DeleteTeam(portainer.TeamID(teamID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.TeamMembershipService.DeleteTeamMembershipByTeamID(portainer.TeamID(teamID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleGetMemberships handles GET requests on /teams/:id/memberships
|
||||
func (handler *TeamHandler) handleGetMemberships(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
tid, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
teamID := portainer.TeamID(tid)
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !security.AuthorizedTeamManagement(teamID, securityContext) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
memberships, err := handler.TeamMembershipService.TeamMembershipsByTeamID(teamID)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, memberships, handler.Logger)
|
||||
}
|
||||
242
api/http/handler/team_membership.go
Normal file
242
api/http/handler/team_membership.go
Normal file
@@ -0,0 +1,242 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// TeamMembershipHandler represents an HTTP API handler for managing teams.
|
||||
type TeamMembershipHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
}
|
||||
|
||||
// NewTeamMembershipHandler returns a new instance of TeamMembershipHandler.
|
||||
func NewTeamMembershipHandler(bouncer *security.RequestBouncer) *TeamMembershipHandler {
|
||||
h := &TeamMembershipHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/team_memberships",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handlePostTeamMemberships))).Methods(http.MethodPost)
|
||||
h.Handle("/team_memberships",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetTeamsMemberships))).Methods(http.MethodGet)
|
||||
h.Handle("/team_memberships/{id}",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handlePutTeamMembership))).Methods(http.MethodPut)
|
||||
h.Handle("/team_memberships/{id}",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleDeleteTeamMembership))).Methods(http.MethodDelete)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postTeamMembershipsRequest struct {
|
||||
UserID int `valid:"required"`
|
||||
TeamID int `valid:"required"`
|
||||
Role int `valid:"required"`
|
||||
}
|
||||
|
||||
postTeamMembershipsResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
putTeamMembershipRequest struct {
|
||||
UserID int `valid:"required"`
|
||||
TeamID int `valid:"required"`
|
||||
Role int `valid:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
// handlePostTeamMemberships handles POST requests on /team_memberships
|
||||
func (handler *TeamMembershipHandler) handlePostTeamMemberships(w http.ResponseWriter, r *http.Request) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req postTeamMembershipsRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
userID := portainer.UserID(req.UserID)
|
||||
teamID := portainer.TeamID(req.TeamID)
|
||||
role := portainer.MembershipRole(req.Role)
|
||||
|
||||
if !security.AuthorizedTeamManagement(teamID, securityContext) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(userID)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if len(memberships) > 0 {
|
||||
for _, membership := range memberships {
|
||||
if membership.UserID == userID && membership.TeamID == teamID {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrTeamMembershipAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
membership := &portainer.TeamMembership{
|
||||
UserID: userID,
|
||||
TeamID: teamID,
|
||||
Role: role,
|
||||
}
|
||||
|
||||
err = handler.TeamMembershipService.CreateTeamMembership(membership)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &postTeamMembershipsResponse{ID: int(membership.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetTeamsMemberships handles GET requests on /team_memberships
|
||||
func (handler *TeamMembershipHandler) handleGetTeamsMemberships(w http.ResponseWriter, r *http.Request) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin && !securityContext.IsTeamLeader {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
memberships, err := handler.TeamMembershipService.TeamMemberships()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, memberships, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePutTeamMembership handles PUT requests on /team_memberships/:id
|
||||
func (handler *TeamMembershipHandler) handlePutTeamMembership(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
membershipID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putTeamMembershipRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
userID := portainer.UserID(req.UserID)
|
||||
teamID := portainer.TeamID(req.TeamID)
|
||||
role := portainer.MembershipRole(req.Role)
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !security.AuthorizedTeamManagement(teamID, securityContext) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
membership, err := handler.TeamMembershipService.TeamMembership(portainer.TeamMembershipID(membershipID))
|
||||
if err == portainer.ErrTeamMembershipNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if securityContext.IsTeamLeader && membership.Role != role {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
membership.UserID = userID
|
||||
membership.TeamID = teamID
|
||||
membership.Role = role
|
||||
|
||||
err = handler.TeamMembershipService.UpdateTeamMembership(membership.ID, membership)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleDeleteTeamMembership handles DELETE requests on /team_memberships/:id
|
||||
func (handler *TeamMembershipHandler) handleDeleteTeamMembership(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
membershipID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
membership, err := handler.TeamMembershipService.TeamMembership(portainer.TeamMembershipID(membershipID))
|
||||
if err == portainer.ErrTeamMembershipNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !security.AuthorizedTeamManagement(membership.TeamID, securityContext) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.TeamMembershipService.DeleteTeamMembership(portainer.TeamMembershipID(membershipID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
73
api/http/handler/templates.go
Normal file
73
api/http/handler/templates.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
// TemplatesHandler represents an HTTP API handler for managing templates.
|
||||
type TemplatesHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
const (
|
||||
containerTemplatesURLLinuxServerIo = "https://tools.linuxserver.io/portainer.json"
|
||||
)
|
||||
|
||||
// NewTemplatesHandler returns a new instance of TemplatesHandler.
|
||||
func NewTemplatesHandler(bouncer *security.RequestBouncer) *TemplatesHandler {
|
||||
h := &TemplatesHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/templates",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetTemplates))).Methods(http.MethodGet)
|
||||
return h
|
||||
}
|
||||
|
||||
// handleGetTemplates handles GET requests on /templates?key=<key>
|
||||
func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
key := r.FormValue("key")
|
||||
if key == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var templatesURL string
|
||||
if key == "containers" {
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
templatesURL = settings.TemplatesURL
|
||||
} else if key == "linuxserver.io" {
|
||||
templatesURL = containerTemplatesURLLinuxServerIo
|
||||
} else {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := http.Get(templatesURL)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(body)
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
package http
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
@@ -19,35 +20,31 @@ type UploadHandler struct {
|
||||
}
|
||||
|
||||
// NewUploadHandler returns a new instance of UploadHandler.
|
||||
func NewUploadHandler(mw *middleWareService) *UploadHandler {
|
||||
func NewUploadHandler(bouncer *security.RequestBouncer) *UploadHandler {
|
||||
h := &UploadHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/upload/tls/{endpointID}/{certificate:(?:ca|cert|key)}",
|
||||
mw.authenticated(http.HandlerFunc(h.handlePostUploadTLS)))
|
||||
h.Handle("/upload/tls/{certificate:(?:ca|cert|key)}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handlePostUploadTLS))).Methods(http.MethodPost)
|
||||
return h
|
||||
}
|
||||
|
||||
// handlePostUploadTLS handles POST requests on /upload/tls/{certificate:(?:ca|cert|key)}?folder=<folder>
|
||||
func (handler *UploadHandler) handlePostUploadTLS(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
handleNotAllowed(w, []string{http.MethodPost})
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
endpointID := vars["endpointID"]
|
||||
certificate := vars["certificate"]
|
||||
ID, err := strconv.Atoi(endpointID)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
|
||||
folder := r.FormValue("folder")
|
||||
if folder == "" {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
file, _, err := r.FormFile("file")
|
||||
defer file.Close()
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -60,12 +57,13 @@ func (handler *UploadHandler) handlePostUploadTLS(w http.ResponseWriter, r *http
|
||||
case "key":
|
||||
fileType = portainer.TLSFileKey
|
||||
default:
|
||||
Error(w, portainer.ErrUndefinedTLSFileType, http.StatusInternalServerError, handler.Logger)
|
||||
httperror.WriteErrorResponse(w, portainer.ErrUndefinedTLSFileType, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.FileService.StoreTLSFile(portainer.EndpointID(ID), fileType, file)
|
||||
err = handler.FileService.StoreTLSFile(folder, fileType, file)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
468
api/http/handler/user.go
Normal file
468
api/http/handler/user.go
Normal file
@@ -0,0 +1,468 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// UserHandler represents an HTTP API handler for managing users.
|
||||
type UserHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
UserService portainer.UserService
|
||||
TeamService portainer.TeamService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
CryptoService portainer.CryptoService
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
// NewUserHandler returns a new instance of UserHandler.
|
||||
func NewUserHandler(bouncer *security.RequestBouncer) *UserHandler {
|
||||
h := &UserHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/users",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handlePostUsers))).Methods(http.MethodPost)
|
||||
h.Handle("/users",
|
||||
bouncer.RestrictedAccess(http.HandlerFunc(h.handleGetUsers))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handleGetUser))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handlePutUser))).Methods(http.MethodPut)
|
||||
h.Handle("/users/{id}",
|
||||
bouncer.AdministratorAccess(http.HandlerFunc(h.handleDeleteUser))).Methods(http.MethodDelete)
|
||||
h.Handle("/users/{id}/memberships",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handleGetMemberships))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}/passwd",
|
||||
bouncer.AuthenticatedAccess(http.HandlerFunc(h.handlePostUserPasswd))).Methods(http.MethodPost)
|
||||
h.Handle("/users/admin/check",
|
||||
bouncer.PublicAccess(http.HandlerFunc(h.handleGetAdminCheck))).Methods(http.MethodGet)
|
||||
h.Handle("/users/admin/init",
|
||||
bouncer.PublicAccess(http.HandlerFunc(h.handlePostAdminInit))).Methods(http.MethodPost)
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
type (
|
||||
postUsersRequest struct {
|
||||
Username string `valid:"required"`
|
||||
Password string `valid:""`
|
||||
Role int `valid:"required"`
|
||||
}
|
||||
|
||||
postUsersResponse struct {
|
||||
ID int `json:"Id"`
|
||||
}
|
||||
|
||||
postUserPasswdRequest struct {
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
|
||||
postUserPasswdResponse struct {
|
||||
Valid bool `json:"valid"`
|
||||
}
|
||||
|
||||
putUserRequest struct {
|
||||
Password string `valid:"-"`
|
||||
Role int `valid:"-"`
|
||||
}
|
||||
|
||||
postAdminInitRequest struct {
|
||||
Username string `valid:"required"`
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
)
|
||||
|
||||
// handlePostUsers handles POST requests on /users
|
||||
func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) {
|
||||
var req postUsersRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if !securityContext.IsAdmin && !securityContext.IsTeamLeader {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if securityContext.IsTeamLeader && req.Role == 1 {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if strings.ContainsAny(req.Username, " ") {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrInvalidUsername, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := handler.UserService.UserByUsername(req.Username)
|
||||
if err != nil && err != portainer.ErrUserNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if user != nil {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrUserAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var role portainer.UserRole
|
||||
if req.Role == 1 {
|
||||
role = portainer.AdministratorRole
|
||||
} else {
|
||||
role = portainer.StandardUserRole
|
||||
}
|
||||
|
||||
user = &portainer.User{
|
||||
Username: req.Username,
|
||||
Role: role,
|
||||
}
|
||||
|
||||
settings, err := handler.SettingsService.Settings()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if settings.AuthenticationMethod == portainer.AuthenticationInternal {
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.UserService.CreateUser(user)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, &postUsersResponse{ID: int(user.ID)}, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetUsers handles GET requests on /users
|
||||
func (handler *UserHandler) handleGetUsers(w http.ResponseWriter, r *http.Request) {
|
||||
securityContext, err := security.RetrieveRestrictedRequestContext(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
users, err := handler.UserService.Users()
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
filteredUsers := security.FilterUsers(users, securityContext)
|
||||
|
||||
for i := range filteredUsers {
|
||||
filteredUsers[i].Password = ""
|
||||
}
|
||||
|
||||
encodeJSON(w, filteredUsers, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePostUserPasswd handles POST requests on /users/:id/passwd
|
||||
func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req postUserPasswdRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var password = req.Password
|
||||
|
||||
u, err := handler.UserService.User(portainer.UserID(userID))
|
||||
if err == portainer.ErrUserNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
valid := true
|
||||
err = handler.CryptoService.CompareHashAndData(u.Password, password)
|
||||
if err != nil {
|
||||
valid = false
|
||||
}
|
||||
|
||||
encodeJSON(w, &postUserPasswdResponse{Valid: valid}, handler.Logger)
|
||||
}
|
||||
|
||||
// handleGetUser handles GET requests on /users/:id
|
||||
func (handler *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := handler.UserService.User(portainer.UserID(userID))
|
||||
if err == portainer.ErrUserNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = ""
|
||||
encodeJSON(w, &user, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePutUser handles PUT requests on /users/:id
|
||||
func (handler *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(userID) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrUnauthorized, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putUserRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Password == "" && req.Role == 0 {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := handler.UserService.User(portainer.UserID(userID))
|
||||
if err == portainer.ErrUserNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Password != "" {
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.Role != 0 {
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrUnauthorized, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
if req.Role == 1 {
|
||||
user.Role = portainer.AdministratorRole
|
||||
} else {
|
||||
user.Role = portainer.StandardUserRole
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.UserService.UpdateUser(user.ID, user)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleGetAdminCheck handles GET requests on /users/admin/check
|
||||
func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if len(users) == 0 {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrUserNotFound, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handlePostAdminInit handles POST requests on /users/admin/init
|
||||
func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.Request) {
|
||||
var req postAdminInitRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if len(users) == 0 {
|
||||
user := &portainer.User{
|
||||
Username: req.Username,
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.UserService.CreateUser(user)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrAdminAlreadyInitialized, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleDeleteUser handles DELETE requests on /users/:id
|
||||
func (handler *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if userID == 1 {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrCannotRemoveAdmin, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if tokenData.ID == portainer.UserID(userID) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrAdminCannotRemoveSelf, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.UserService.User(portainer.UserID(userID))
|
||||
|
||||
if err == portainer.ErrUserNotFound {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.UserService.DeleteUser(portainer.UserID(userID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.TeamMembershipService.DeleteTeamMembershipByUserID(portainer.UserID(userID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handleGetMemberships handles GET requests on /users/:id/memberships
|
||||
func (handler *UserHandler) handleGetMemberships(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := security.RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(userID) {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrUnauthorized, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
memberships, err := handler.TeamMembershipService.TeamMembershipsByUserID(portainer.UserID(userID))
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, memberships, handler.Logger)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package http
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/crypto"
|
||||
"golang.org/x/net/websocket"
|
||||
)
|
||||
|
||||
@@ -68,12 +69,10 @@ func (handler *WebSocketHandler) webSocketDockerExec(ws *websocket.Conn) {
|
||||
host = endpointURL.Path
|
||||
}
|
||||
|
||||
// Should not be managed here
|
||||
// TODO: Should not be managed here
|
||||
var tlsConfig *tls.Config
|
||||
if endpoint.TLS {
|
||||
tlsConfig, err = createTLSConfiguration(endpoint.TLSCACertPath,
|
||||
endpoint.TLSCertPath,
|
||||
endpoint.TLSKeyPath)
|
||||
if endpoint.TLSConfig.TLS {
|
||||
tlsConfig, err = crypto.CreateTLSConfiguration(&endpoint.TLSConfig)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to create TLS configuration: %s", err)
|
||||
return
|
||||
@@ -1,119 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
// middleWareService represents a service to manage HTTP middlewares
|
||||
middleWareService struct {
|
||||
jwtService portainer.JWTService
|
||||
authDisabled bool
|
||||
}
|
||||
contextKey int
|
||||
)
|
||||
|
||||
const (
|
||||
contextAuthenticationKey contextKey = iota
|
||||
)
|
||||
|
||||
func extractTokenDataFromRequestContext(request *http.Request) (*portainer.TokenData, error) {
|
||||
contextData := request.Context().Value(contextAuthenticationKey)
|
||||
if contextData == nil {
|
||||
return nil, portainer.ErrMissingContextData
|
||||
}
|
||||
|
||||
tokenData := contextData.(*portainer.TokenData)
|
||||
return tokenData, nil
|
||||
}
|
||||
|
||||
// public defines a chain of middleware for public endpoints (no authentication required)
|
||||
func (service *middleWareService) public(h http.Handler) http.Handler {
|
||||
h = mwSecureHeaders(h)
|
||||
return h
|
||||
}
|
||||
|
||||
// authenticated defines a chain of middleware for private endpoints (authentication required)
|
||||
func (service *middleWareService) authenticated(h http.Handler) http.Handler {
|
||||
h = service.mwCheckAuthentication(h)
|
||||
h = mwSecureHeaders(h)
|
||||
return h
|
||||
}
|
||||
|
||||
// administrator defines a chain of middleware for private administrator restricted endpoints
|
||||
// (authentication and role admin required)
|
||||
func (service *middleWareService) administrator(h http.Handler) http.Handler {
|
||||
h = mwCheckAdministratorRole(h)
|
||||
h = service.mwCheckAuthentication(h)
|
||||
h = mwSecureHeaders(h)
|
||||
return h
|
||||
}
|
||||
|
||||
// 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-Content-Type-Options", "nosniff")
|
||||
w.Header().Add("X-Frame-Options", "DENY")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// 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 := extractTokenDataFromRequestContext(r)
|
||||
if err != nil {
|
||||
Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, nil)
|
||||
return
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, nil)
|
||||
return
|
||||
}
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// mwCheckAuthentication provides Authentication middleware for handlers
|
||||
func (service *middleWareService) mwCheckAuthentication(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var tokenData *portainer.TokenData
|
||||
if !service.authDisabled {
|
||||
var token string
|
||||
|
||||
// Get token from the Authorization header
|
||||
tokens, ok := r.Header["Authorization"]
|
||||
if ok && len(tokens) >= 1 {
|
||||
token = tokens[0]
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
Error(w, portainer.ErrUnauthorized, http.StatusUnauthorized, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
tokenData, err = service.jwtService.ParseAndVerifyToken(token)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusUnauthorized, nil)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
tokenData = &portainer.TokenData{
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), contextAuthenticationKey, tokenData)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
return
|
||||
})
|
||||
}
|
||||
158
api/http/proxy/access_control.go
Normal file
158
api/http/proxy/access_control.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package proxy
|
||||
|
||||
import "github.com/portainer/portainer"
|
||||
|
||||
type (
|
||||
// ExtendedStack represents a stack combined with its associated access control
|
||||
ExtendedStack struct {
|
||||
portainer.Stack
|
||||
ResourceControl portainer.ResourceControl `json:"ResourceControl"`
|
||||
}
|
||||
)
|
||||
|
||||
// applyResourceAccessControl returns an optionally decorated object as the first return value and the
|
||||
// access level for the user (granted or denied) as the second return value.
|
||||
// It will retrieve an identifier from the labels object. If an identifier exists, it will check for
|
||||
// an existing resource control associated to it.
|
||||
// Returns a decorated object and authorized access (true) when a resource control is found and the user can access the resource.
|
||||
// Returns the original object and authorized access (true) 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) {
|
||||
|
||||
if labelsObject != nil && labelsObject[labelIdentifier] != nil {
|
||||
resourceIdentifier := labelsObject[labelIdentifier].(string)
|
||||
return applyResourceAccessControl(resourceObject, resourceIdentifier, context)
|
||||
}
|
||||
return resourceObject, true
|
||||
}
|
||||
|
||||
// applyResourceAccessControl returns an optionally decorated object as the first return value and the
|
||||
// access level for the user (granted or denied) as the second return value.
|
||||
// Returns a decorated object and authorized access (true) when a resource control is found to the specified resource
|
||||
// identifier and the user can access the resource.
|
||||
// Returns the original object and authorized access (true) when no resource control is found for the specified
|
||||
// resource identifier.
|
||||
// Returns the original object and denied access (false) when a resource control is associated to the resource
|
||||
// and the user cannot access the resource.
|
||||
func applyResourceAccessControl(resourceObject map[string]interface{}, resourceIdentifier string,
|
||||
context *restrictedOperationContext) (map[string]interface{}, bool) {
|
||||
|
||||
authorizedAccess := true
|
||||
|
||||
resourceControl := getResourceControlByResourceID(resourceIdentifier, context.resourceControls)
|
||||
if resourceControl != nil {
|
||||
if context.isAdmin || canUserAccessResource(context.userID, context.userTeamIDs, resourceControl) {
|
||||
resourceObject = decorateObject(resourceObject, resourceControl)
|
||||
} else {
|
||||
authorizedAccess = false
|
||||
}
|
||||
}
|
||||
|
||||
return resourceObject, authorizedAccess
|
||||
}
|
||||
|
||||
// decorateResourceWithAccessControlFromLabel will retrieve an identifier from the labels object. If an identifier exists,
|
||||
// it will check for an existing resource control associated to it. If a resource control is found, the resource object will be
|
||||
// decorated. If no identifier can be found in the labels or no resource control is associated to the identifier, the resource
|
||||
// object will not be changed.
|
||||
func decorateResourceWithAccessControlFromLabel(labelsObject, resourceObject map[string]interface{}, labelIdentifier string,
|
||||
resourceControls []portainer.ResourceControl) map[string]interface{} {
|
||||
|
||||
if labelsObject != nil && labelsObject[labelIdentifier] != nil {
|
||||
resourceIdentifier := labelsObject[labelIdentifier].(string)
|
||||
resourceObject = decorateResourceWithAccessControl(resourceObject, resourceIdentifier, resourceControls)
|
||||
}
|
||||
|
||||
return resourceObject
|
||||
}
|
||||
|
||||
// decorateResourceWithAccessControl will check if a resource control is associated to the specified resource identifier.
|
||||
// If a resource control is found, the resource object will be decorated, otherwise it will not be changed.
|
||||
func decorateResourceWithAccessControl(resourceObject map[string]interface{}, resourceIdentifier string,
|
||||
resourceControls []portainer.ResourceControl) map[string]interface{} {
|
||||
|
||||
resourceControl := getResourceControlByResourceID(resourceIdentifier, resourceControls)
|
||||
if resourceControl != nil {
|
||||
return decorateObject(resourceObject, resourceControl)
|
||||
}
|
||||
return resourceObject
|
||||
}
|
||||
|
||||
func canUserAccessResource(userID portainer.UserID, userTeamIDs []portainer.TeamID, resourceControl *portainer.ResourceControl) bool {
|
||||
for _, authorizedUserAccess := range resourceControl.UserAccesses {
|
||||
if userID == authorizedUserAccess.UserID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
for _, authorizedTeamAccess := range resourceControl.TeamAccesses {
|
||||
for _, userTeamID := range userTeamIDs {
|
||||
if userTeamID == authorizedTeamAccess.TeamID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func decorateObject(object map[string]interface{}, resourceControl *portainer.ResourceControl) map[string]interface{} {
|
||||
metadata := make(map[string]interface{})
|
||||
metadata["ResourceControl"] = resourceControl
|
||||
object["Portainer"] = metadata
|
||||
return object
|
||||
}
|
||||
|
||||
func getResourceControlByResourceID(resourceID string, resourceControls []portainer.ResourceControl) *portainer.ResourceControl {
|
||||
for _, resourceControl := range resourceControls {
|
||||
if resourceID == resourceControl.ResourceID {
|
||||
return &resourceControl
|
||||
}
|
||||
for _, subResourceID := range resourceControl.SubResourceIDs {
|
||||
if resourceID == subResourceID {
|
||||
return &resourceControl
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CanAccessStack checks if a user can access a stack
|
||||
func CanAccessStack(stack *portainer.Stack, resourceControl *portainer.ResourceControl, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||
userTeamIDs := make([]portainer.TeamID, 0)
|
||||
for _, membership := range memberships {
|
||||
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
||||
}
|
||||
|
||||
if canUserAccessResource(userID, userTeamIDs, resourceControl) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// FilterStacks filters stacks based on user role and resource controls.
|
||||
func FilterStacks(stacks []portainer.Stack, resourceControls []portainer.ResourceControl, isAdmin bool,
|
||||
userID portainer.UserID, memberships []portainer.TeamMembership) []ExtendedStack {
|
||||
|
||||
filteredStacks := make([]ExtendedStack, 0)
|
||||
|
||||
userTeamIDs := make([]portainer.TeamID, 0)
|
||||
for _, membership := range memberships {
|
||||
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
||||
}
|
||||
|
||||
for _, stack := range stacks {
|
||||
extendedStack := ExtendedStack{stack, portainer.ResourceControl{}}
|
||||
resourceControl := getResourceControlByResourceID(stack.Name, resourceControls)
|
||||
if resourceControl == nil {
|
||||
filteredStacks = append(filteredStacks, extendedStack)
|
||||
} else if resourceControl != nil && (isAdmin || canUserAccessResource(userID, userTeamIDs, resourceControl)) {
|
||||
extendedStack.ResourceControl = *resourceControl
|
||||
filteredStacks = append(filteredStacks, extendedStack)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredStacks
|
||||
}
|
||||
193
api/http/proxy/containers.go
Normal file
193
api/http/proxy/containers.go
Normal file
@@ -0,0 +1,193 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrDockerContainerIdentifierNotFound defines an error raised when Portainer is unable to find a container identifier
|
||||
ErrDockerContainerIdentifierNotFound = portainer.Error("Docker container identifier not found")
|
||||
containerIdentifier = "Id"
|
||||
containerLabelForServiceIdentifier = "com.docker.swarm.service.id"
|
||||
containerLabelForStackIdentifier = "com.docker.stack.namespace"
|
||||
)
|
||||
|
||||
// containerListOperation extracts the response as a JSON object, loop through the containers array
|
||||
// decorate and/or filter the containers based on resource controls before rewriting the response
|
||||
func containerListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
// ContainerList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
|
||||
responseArray, err := getResponseAsJSONArray(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if executor.operationContext.isAdmin {
|
||||
responseArray, err = decorateContainerList(responseArray, executor.operationContext.resourceControls)
|
||||
} else {
|
||||
responseArray, err = filterContainerList(responseArray, executor.operationContext)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if executor.labelBlackList != nil {
|
||||
responseArray, err = filterContainersWithBlackListedLabels(responseArray, executor.labelBlackList)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return rewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// containerInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the container based on resource control (check are done based on the containerID and optional Swarm service ID)
|
||||
// and either rewrite an access denied response or a decorated container.
|
||||
func containerInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
// ContainerInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if responseObject[containerIdentifier] == nil {
|
||||
return ErrDockerContainerIdentifierNotFound
|
||||
}
|
||||
|
||||
containerID := responseObject[containerIdentifier].(string)
|
||||
responseObject, access := applyResourceAccessControl(responseObject, containerID, executor.operationContext)
|
||||
if !access {
|
||||
return rewriteAccessDeniedResponse(response)
|
||||
}
|
||||
|
||||
containerLabels := extractContainerLabelsFromContainerInspectObject(responseObject)
|
||||
responseObject, access = applyResourceAccessControlFromLabel(containerLabels, responseObject, containerLabelForServiceIdentifier, executor.operationContext)
|
||||
if !access {
|
||||
return rewriteAccessDeniedResponse(response)
|
||||
}
|
||||
|
||||
responseObject, access = applyResourceAccessControlFromLabel(containerLabels, responseObject, containerLabelForStackIdentifier, executor.operationContext)
|
||||
if !access {
|
||||
return rewriteAccessDeniedResponse(response)
|
||||
}
|
||||
|
||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
|
||||
// extractContainerLabelsFromContainerInspectObject retrieve the Labels of the container if present.
|
||||
// Container schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerInspect
|
||||
func extractContainerLabelsFromContainerInspectObject(responseObject map[string]interface{}) map[string]interface{} {
|
||||
// Labels are stored under Config.Labels
|
||||
containerConfigObject := extractJSONField(responseObject, "Config")
|
||||
if containerConfigObject != nil {
|
||||
containerLabelsObject := extractJSONField(containerConfigObject, "Labels")
|
||||
return containerLabelsObject
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractContainerLabelsFromContainerListObject retrieve the Labels of the container if present.
|
||||
// Container schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
|
||||
func extractContainerLabelsFromContainerListObject(responseObject map[string]interface{}) map[string]interface{} {
|
||||
// Labels are stored under Labels
|
||||
containerLabelsObject := extractJSONField(responseObject, "Labels")
|
||||
return containerLabelsObject
|
||||
}
|
||||
|
||||
// decorateContainerList loops through all containers and decorates any container with an existing resource control.
|
||||
// Resource controls checks are based on: resource identifier, service identifier (from label), stack identifier (from label).
|
||||
// Container object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
|
||||
func decorateContainerList(containerData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
|
||||
decoratedContainerData := make([]interface{}, 0)
|
||||
|
||||
for _, container := range containerData {
|
||||
|
||||
containerObject := container.(map[string]interface{})
|
||||
if containerObject[containerIdentifier] == nil {
|
||||
return nil, ErrDockerContainerIdentifierNotFound
|
||||
}
|
||||
|
||||
containerID := containerObject[containerIdentifier].(string)
|
||||
containerObject = decorateResourceWithAccessControl(containerObject, containerID, resourceControls)
|
||||
|
||||
containerLabels := extractContainerLabelsFromContainerListObject(containerObject)
|
||||
containerObject = decorateResourceWithAccessControlFromLabel(containerLabels, containerObject, containerLabelForServiceIdentifier, resourceControls)
|
||||
containerObject = decorateResourceWithAccessControlFromLabel(containerLabels, containerObject, containerLabelForStackIdentifier, resourceControls)
|
||||
|
||||
decoratedContainerData = append(decoratedContainerData, containerObject)
|
||||
}
|
||||
|
||||
return decoratedContainerData, nil
|
||||
}
|
||||
|
||||
// filterContainerList loops through all containers and filters public containers (no associated resource control)
|
||||
// as well as authorized containers (access granted to the user based on existing resource control).
|
||||
// Authorized containers are decorated during the process.
|
||||
// Resource controls checks are based on: resource identifier, service identifier (from label), stack identifier (from label).
|
||||
// Container object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ContainerList
|
||||
func filterContainerList(containerData []interface{}, context *restrictedOperationContext) ([]interface{}, error) {
|
||||
filteredContainerData := make([]interface{}, 0)
|
||||
|
||||
for _, container := range containerData {
|
||||
containerObject := container.(map[string]interface{})
|
||||
if containerObject[containerIdentifier] == nil {
|
||||
return nil, ErrDockerContainerIdentifierNotFound
|
||||
}
|
||||
|
||||
containerID := containerObject[containerIdentifier].(string)
|
||||
containerObject, access := applyResourceAccessControl(containerObject, containerID, context)
|
||||
if access {
|
||||
containerLabels := extractContainerLabelsFromContainerListObject(containerObject)
|
||||
containerObject, access = applyResourceAccessControlFromLabel(containerLabels, containerObject, containerLabelForServiceIdentifier, context)
|
||||
if access {
|
||||
containerObject, access = applyResourceAccessControlFromLabel(containerLabels, containerObject, containerLabelForStackIdentifier, context)
|
||||
if access {
|
||||
filteredContainerData = append(filteredContainerData, containerObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredContainerData, nil
|
||||
}
|
||||
|
||||
// filterContainersWithLabels loops through a list of containers, and filters containers that do not contains
|
||||
// any labels in the labels black list.
|
||||
func filterContainersWithBlackListedLabels(containerData []interface{}, labelBlackList []portainer.Pair) ([]interface{}, error) {
|
||||
filteredContainerData := make([]interface{}, 0)
|
||||
|
||||
for _, container := range containerData {
|
||||
containerObject := container.(map[string]interface{})
|
||||
|
||||
containerLabels := extractContainerLabelsFromContainerListObject(containerObject)
|
||||
if containerLabels != nil {
|
||||
if !containerHasBlackListedLabel(containerLabels, labelBlackList) {
|
||||
filteredContainerData = append(filteredContainerData, containerObject)
|
||||
}
|
||||
} else {
|
||||
filteredContainerData = append(filteredContainerData, containerObject)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredContainerData, nil
|
||||
}
|
||||
|
||||
func containerHasBlackListedLabel(containerLabels map[string]interface{}, labelBlackList []portainer.Pair) bool {
|
||||
for key, value := range containerLabels {
|
||||
labelName := key
|
||||
labelValue := value.(string)
|
||||
|
||||
for _, blackListedLabel := range labelBlackList {
|
||||
if blackListedLabel.Name == labelName && blackListedLabel.Value == labelValue {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
71
api/http/proxy/factory.go
Normal file
71
api/http/proxy/factory.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/crypto"
|
||||
)
|
||||
|
||||
// proxyFactory is a factory to create reverse proxies to Docker endpoints
|
||||
type proxyFactory struct {
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
|
||||
func (factory *proxyFactory) newHTTPProxy(u *url.URL) http.Handler {
|
||||
u.Scheme = "http"
|
||||
return factory.createReverseProxy(u)
|
||||
}
|
||||
|
||||
func (factory *proxyFactory) newHTTPSProxy(u *url.URL, endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
u.Scheme = "https"
|
||||
proxy := factory.createReverseProxy(u)
|
||||
config, err := crypto.CreateTLSConfiguration(&endpoint.TLSConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
proxy.Transport.(*proxyTransport).dockerTransport.TLSClientConfig = config
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
func (factory *proxyFactory) newSocketProxy(path string) http.Handler {
|
||||
proxy := &socketProxy{}
|
||||
transport := &proxyTransport{
|
||||
ResourceControlService: factory.ResourceControlService,
|
||||
TeamMembershipService: factory.TeamMembershipService,
|
||||
SettingsService: factory.SettingsService,
|
||||
dockerTransport: newSocketTransport(path),
|
||||
}
|
||||
proxy.Transport = transport
|
||||
return proxy
|
||||
}
|
||||
|
||||
func (factory *proxyFactory) createReverseProxy(u *url.URL) *httputil.ReverseProxy {
|
||||
proxy := newSingleHostReverseProxyWithHostHeader(u)
|
||||
transport := &proxyTransport{
|
||||
ResourceControlService: factory.ResourceControlService,
|
||||
TeamMembershipService: factory.TeamMembershipService,
|
||||
SettingsService: factory.SettingsService,
|
||||
dockerTransport: newHTTPTransport(),
|
||||
}
|
||||
proxy.Transport = transport
|
||||
return proxy
|
||||
}
|
||||
|
||||
func newSocketTransport(socketPath string) *http.Transport {
|
||||
return &http.Transport{
|
||||
Dial: func(proto, addr string) (conn net.Conn, err error) {
|
||||
return net.Dial("unix", socketPath)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newHTTPTransport() *http.Transport {
|
||||
return &http.Transport{}
|
||||
}
|
||||
69
api/http/proxy/manager.go
Normal file
69
api/http/proxy/manager.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/orcaman/concurrent-map"
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
// Manager represents a service used to manage Docker proxies.
|
||||
type Manager struct {
|
||||
proxyFactory *proxyFactory
|
||||
proxies cmap.ConcurrentMap
|
||||
}
|
||||
|
||||
// NewManager initializes a new proxy Service
|
||||
func NewManager(resourceControlService portainer.ResourceControlService, teamMembershipService portainer.TeamMembershipService, settingsService portainer.SettingsService) *Manager {
|
||||
return &Manager{
|
||||
proxies: cmap.New(),
|
||||
proxyFactory: &proxyFactory{
|
||||
ResourceControlService: resourceControlService,
|
||||
TeamMembershipService: teamMembershipService,
|
||||
SettingsService: settingsService,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// CreateAndRegisterProxy creates a new HTTP reverse proxy and adds it to the registered proxies.
|
||||
// It can also be used to create a new HTTP reverse proxy and replace an already registered proxy.
|
||||
func (manager *Manager) CreateAndRegisterProxy(endpoint *portainer.Endpoint) (http.Handler, error) {
|
||||
var proxy http.Handler
|
||||
|
||||
endpointURL, err := url.Parse(endpoint.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if endpointURL.Scheme == "tcp" {
|
||||
if endpoint.TLSConfig.TLS {
|
||||
proxy, err = manager.proxyFactory.newHTTPSProxy(endpointURL, endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
proxy = manager.proxyFactory.newHTTPProxy(endpointURL)
|
||||
}
|
||||
} else {
|
||||
// Assume unix:// scheme
|
||||
proxy = manager.proxyFactory.newSocketProxy(endpointURL.Path)
|
||||
}
|
||||
|
||||
manager.proxies.Set(string(endpoint.ID), proxy)
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
// GetProxy returns the proxy associated to a key
|
||||
func (manager *Manager) GetProxy(key string) http.Handler {
|
||||
proxy, ok := manager.proxies.Get(key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return proxy.(http.Handler)
|
||||
}
|
||||
|
||||
// DeleteProxy deletes the proxy associated to a key
|
||||
func (manager *Manager) DeleteProxy(key string) {
|
||||
manager.proxies.Remove(key)
|
||||
}
|
||||
134
api/http/proxy/networks.go
Normal file
134
api/http/proxy/networks.go
Normal file
@@ -0,0 +1,134 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrDockerNetworkIdentifierNotFound defines an error raised when Portainer is unable to find a network identifier
|
||||
ErrDockerNetworkIdentifierNotFound = portainer.Error("Docker network identifier not found")
|
||||
networkIdentifier = "Id"
|
||||
networkLabelForStackIdentifier = "com.docker.stack.namespace"
|
||||
)
|
||||
|
||||
// networkListOperation extracts the response as a JSON object, loop through the networks array
|
||||
// decorate and/or filter the networks based on resource controls before rewriting the response
|
||||
func networkListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
// NetworkList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
||||
responseArray, err := getResponseAsJSONArray(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if executor.operationContext.isAdmin {
|
||||
responseArray, err = decorateNetworkList(responseArray, executor.operationContext.resourceControls)
|
||||
} else {
|
||||
responseArray, err = filterNetworkList(responseArray, executor.operationContext)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return rewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// networkInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the network based on resource control and either rewrite an access denied response
|
||||
// or a decorated network.
|
||||
func networkInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
// NetworkInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if responseObject[networkIdentifier] == nil {
|
||||
return ErrDockerNetworkIdentifierNotFound
|
||||
}
|
||||
|
||||
networkID := responseObject[networkIdentifier].(string)
|
||||
responseObject, access := applyResourceAccessControl(responseObject, networkID, executor.operationContext)
|
||||
if !access {
|
||||
return rewriteAccessDeniedResponse(response)
|
||||
}
|
||||
|
||||
networkLabels := extractNetworkLabelsFromNetworkInspectObject(responseObject)
|
||||
responseObject, access = applyResourceAccessControlFromLabel(networkLabels, responseObject, networkLabelForStackIdentifier, executor.operationContext)
|
||||
if !access {
|
||||
return rewriteAccessDeniedResponse(response)
|
||||
}
|
||||
|
||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
|
||||
// extractNetworkLabelsFromNetworkInspectObject retrieve the Labels of the network if present.
|
||||
// Container schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkInspect
|
||||
func extractNetworkLabelsFromNetworkInspectObject(responseObject map[string]interface{}) map[string]interface{} {
|
||||
// Labels are stored under Labels
|
||||
return extractJSONField(responseObject, "Labels")
|
||||
}
|
||||
|
||||
// extractNetworkLabelsFromNetworkListObject retrieve the Labels of the network if present.
|
||||
// Network schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
||||
func extractNetworkLabelsFromNetworkListObject(responseObject map[string]interface{}) map[string]interface{} {
|
||||
// Labels are stored under Labels
|
||||
return extractJSONField(responseObject, "Labels")
|
||||
}
|
||||
|
||||
// decorateNetworkList loops through all networks and decorates any network with an existing resource control.
|
||||
// Resource controls checks are based on: resource identifier, stack identifier (from label).
|
||||
// Network object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
||||
func decorateNetworkList(networkData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
|
||||
decoratedNetworkData := make([]interface{}, 0)
|
||||
|
||||
for _, network := range networkData {
|
||||
|
||||
networkObject := network.(map[string]interface{})
|
||||
if networkObject[networkIdentifier] == nil {
|
||||
return nil, ErrDockerNetworkIdentifierNotFound
|
||||
}
|
||||
|
||||
networkID := networkObject[networkIdentifier].(string)
|
||||
networkObject = decorateResourceWithAccessControl(networkObject, networkID, resourceControls)
|
||||
|
||||
networkLabels := extractNetworkLabelsFromNetworkListObject(networkObject)
|
||||
networkObject = decorateResourceWithAccessControlFromLabel(networkLabels, networkObject, networkLabelForStackIdentifier, resourceControls)
|
||||
|
||||
decoratedNetworkData = append(decoratedNetworkData, networkObject)
|
||||
}
|
||||
|
||||
return decoratedNetworkData, nil
|
||||
}
|
||||
|
||||
// filterNetworkList loops through all networks and filters public networks (no associated resource control)
|
||||
// as well as authorized networks (access granted to the user based on existing resource control).
|
||||
// Authorized networks are decorated during the process.
|
||||
// Resource controls checks are based on: resource identifier, stack identifier (from label).
|
||||
// Network object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/NetworkList
|
||||
func filterNetworkList(networkData []interface{}, context *restrictedOperationContext) ([]interface{}, error) {
|
||||
filteredNetworkData := make([]interface{}, 0)
|
||||
|
||||
for _, network := range networkData {
|
||||
networkObject := network.(map[string]interface{})
|
||||
if networkObject[networkIdentifier] == nil {
|
||||
return nil, ErrDockerNetworkIdentifierNotFound
|
||||
}
|
||||
|
||||
networkID := networkObject[networkIdentifier].(string)
|
||||
networkObject, access := applyResourceAccessControl(networkObject, networkID, context)
|
||||
if access {
|
||||
networkLabels := extractNetworkLabelsFromNetworkListObject(networkObject)
|
||||
networkObject, access = applyResourceAccessControlFromLabel(networkLabels, networkObject, networkLabelForStackIdentifier, context)
|
||||
if access {
|
||||
filteredNetworkData = append(filteredNetworkData, networkObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredNetworkData, nil
|
||||
}
|
||||
95
api/http/proxy/response.go
Normal file
95
api/http/proxy/response.go
Normal file
@@ -0,0 +1,95 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrEmptyResponseBody defines an error raised when portainer excepts to parse the body of a HTTP response and there is nothing to parse
|
||||
ErrEmptyResponseBody = portainer.Error("Empty response body")
|
||||
)
|
||||
|
||||
func extractJSONField(jsonObject map[string]interface{}, key string) map[string]interface{} {
|
||||
object := jsonObject[key]
|
||||
if object != nil {
|
||||
return object.(map[string]interface{})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getResponseAsJSONOBject(response *http.Response) (map[string]interface{}, error) {
|
||||
responseData, err := getResponseBodyAsGenericJSON(response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseObject := responseData.(map[string]interface{})
|
||||
return responseObject, nil
|
||||
}
|
||||
|
||||
func getResponseAsJSONArray(response *http.Response) ([]interface{}, error) {
|
||||
responseData, err := getResponseBodyAsGenericJSON(response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseObject := responseData.([]interface{})
|
||||
return responseObject, nil
|
||||
}
|
||||
|
||||
func getResponseBodyAsGenericJSON(response *http.Response) (interface{}, error) {
|
||||
var data interface{}
|
||||
if response.Body != nil {
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = response.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
return nil, ErrEmptyResponseBody
|
||||
}
|
||||
|
||||
func writeAccessDeniedResponse() (*http.Response, error) {
|
||||
response := &http.Response{}
|
||||
err := rewriteResponse(response, portainer.ErrResourceAccessDenied, http.StatusForbidden)
|
||||
return response, err
|
||||
}
|
||||
|
||||
func rewriteAccessDeniedResponse(response *http.Response) error {
|
||||
return rewriteResponse(response, portainer.ErrResourceAccessDenied, http.StatusForbidden)
|
||||
}
|
||||
|
||||
func rewriteResponse(response *http.Response, newResponseData interface{}, statusCode int) error {
|
||||
jsonData, err := json.Marshal(newResponseData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body := ioutil.NopCloser(bytes.NewReader(jsonData))
|
||||
response.StatusCode = statusCode
|
||||
response.Body = body
|
||||
response.ContentLength = int64(len(jsonData))
|
||||
|
||||
if response.Header == nil {
|
||||
response.Header = make(http.Header)
|
||||
}
|
||||
response.Header.Set("Content-Length", strconv.Itoa(len(jsonData)))
|
||||
|
||||
return nil
|
||||
}
|
||||
46
api/http/proxy/reverse_proxy.go
Normal file
46
api/http/proxy/reverse_proxy.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// NewSingleHostReverseProxyWithHostHeader is based on NewSingleHostReverseProxy
|
||||
// from golang.org/src/net/http/httputil/reverseproxy.go and merely sets the Host
|
||||
// HTTP header, which NewSingleHostReverseProxy deliberately preserves.
|
||||
func newSingleHostReverseProxyWithHostHeader(target *url.URL) *httputil.ReverseProxy {
|
||||
targetQuery := target.RawQuery
|
||||
director := func(req *http.Request) {
|
||||
req.URL.Scheme = target.Scheme
|
||||
req.URL.Host = target.Host
|
||||
req.URL.Path = singleJoiningSlash(target.Path, req.URL.Path)
|
||||
req.Host = req.URL.Host
|
||||
if targetQuery == "" || req.URL.RawQuery == "" {
|
||||
req.URL.RawQuery = targetQuery + req.URL.RawQuery
|
||||
} else {
|
||||
req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery
|
||||
}
|
||||
if _, ok := req.Header["User-Agent"]; !ok {
|
||||
// explicitly disable User-Agent so it's not set to default value
|
||||
req.Header.Set("User-Agent", "")
|
||||
}
|
||||
}
|
||||
return &httputil.ReverseProxy{Director: director}
|
||||
}
|
||||
|
||||
// singleJoiningSlash from golang.org/src/net/http/httputil/reverseproxy.go
|
||||
// included here for use in NewSingleHostReverseProxyWithHostHeader
|
||||
// because its used in NewSingleHostReverseProxy from golang.org/src/net/http/httputil/reverseproxy.go
|
||||
func singleJoiningSlash(a, b string) string {
|
||||
aslash := strings.HasSuffix(a, "/")
|
||||
bslash := strings.HasPrefix(b, "/")
|
||||
switch {
|
||||
case aslash && bslash:
|
||||
return a + b[1:]
|
||||
case !aslash && !bslash:
|
||||
return a + "/" + b
|
||||
}
|
||||
return a + b
|
||||
}
|
||||
107
api/http/proxy/secrets.go
Normal file
107
api/http/proxy/secrets.go
Normal file
@@ -0,0 +1,107 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrDockerSecretIdentifierNotFound defines an error raised when Portainer is unable to find a secret identifier
|
||||
ErrDockerSecretIdentifierNotFound = portainer.Error("Docker secret identifier not found")
|
||||
secretIdentifier = "ID"
|
||||
)
|
||||
|
||||
// secretListOperation extracts the response as a JSON object, loop through the secrets array
|
||||
// decorate and/or filter the secrets based on resource controls before rewriting the response
|
||||
func secretListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
|
||||
// SecretList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/SecretList
|
||||
responseArray, err := getResponseAsJSONArray(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if executor.operationContext.isAdmin {
|
||||
responseArray, err = decorateSecretList(responseArray, executor.operationContext.resourceControls)
|
||||
} else {
|
||||
responseArray, err = filterSecretList(responseArray, executor.operationContext)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return rewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// secretInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the secret based on resource control (check are done based on the secretID and optional Swarm service ID)
|
||||
// and either rewrite an access denied response or a decorated secret.
|
||||
func secretInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
// SecretInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/SecretInspect
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if responseObject[secretIdentifier] == nil {
|
||||
return ErrDockerSecretIdentifierNotFound
|
||||
}
|
||||
|
||||
secretID := responseObject[secretIdentifier].(string)
|
||||
responseObject, access := applyResourceAccessControl(responseObject, secretID, executor.operationContext)
|
||||
if !access {
|
||||
return rewriteAccessDeniedResponse(response)
|
||||
}
|
||||
|
||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
|
||||
// decorateSecretList loops through all secrets and decorates any secret with an existing resource control.
|
||||
// Resource controls checks are based on: resource identifier.
|
||||
// Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList
|
||||
func decorateSecretList(secretData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
|
||||
decoratedSecretData := make([]interface{}, 0)
|
||||
|
||||
for _, secret := range secretData {
|
||||
|
||||
secretObject := secret.(map[string]interface{})
|
||||
if secretObject[secretIdentifier] == nil {
|
||||
return nil, ErrDockerSecretIdentifierNotFound
|
||||
}
|
||||
|
||||
secretID := secretObject[secretIdentifier].(string)
|
||||
secretObject = decorateResourceWithAccessControl(secretObject, secretID, resourceControls)
|
||||
|
||||
decoratedSecretData = append(decoratedSecretData, secretObject)
|
||||
}
|
||||
|
||||
return decoratedSecretData, nil
|
||||
}
|
||||
|
||||
// filterSecretList loops through all secrets and filters public secrets (no associated resource control)
|
||||
// as well as authorized secrets (access granted to the user based on existing resource control).
|
||||
// Authorized secrets are decorated during the process.
|
||||
// Resource controls checks are based on: resource identifier.
|
||||
// Secret object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/SecretList
|
||||
func filterSecretList(secretData []interface{}, context *restrictedOperationContext) ([]interface{}, error) {
|
||||
filteredSecretData := make([]interface{}, 0)
|
||||
|
||||
for _, secret := range secretData {
|
||||
secretObject := secret.(map[string]interface{})
|
||||
if secretObject[secretIdentifier] == nil {
|
||||
return nil, ErrDockerSecretIdentifierNotFound
|
||||
}
|
||||
|
||||
secretID := secretObject[secretIdentifier].(string)
|
||||
secretObject, access := applyResourceAccessControl(secretObject, secretID, context)
|
||||
if access {
|
||||
filteredSecretData = append(filteredSecretData, secretObject)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredSecretData, nil
|
||||
}
|
||||
142
api/http/proxy/services.go
Normal file
142
api/http/proxy/services.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrDockerServiceIdentifierNotFound defines an error raised when Portainer is unable to find a service identifier
|
||||
ErrDockerServiceIdentifierNotFound = portainer.Error("Docker service identifier not found")
|
||||
serviceIdentifier = "ID"
|
||||
serviceLabelForStackIdentifier = "com.docker.stack.namespace"
|
||||
)
|
||||
|
||||
// serviceListOperation extracts the response as a JSON array, loop through the service array
|
||||
// decorate and/or filter the services based on resource controls before rewriting the response
|
||||
func serviceListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
// ServiceList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
||||
responseArray, err := getResponseAsJSONArray(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if executor.operationContext.isAdmin {
|
||||
responseArray, err = decorateServiceList(responseArray, executor.operationContext.resourceControls)
|
||||
} else {
|
||||
responseArray, err = filterServiceList(responseArray, executor.operationContext)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return rewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// serviceInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the service based on resource control and either rewrite an access denied response
|
||||
// or a decorated service.
|
||||
func serviceInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
// ServiceInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if responseObject[serviceIdentifier] == nil {
|
||||
return ErrDockerServiceIdentifierNotFound
|
||||
}
|
||||
|
||||
serviceID := responseObject[serviceIdentifier].(string)
|
||||
responseObject, access := applyResourceAccessControl(responseObject, serviceID, executor.operationContext)
|
||||
if !access {
|
||||
return rewriteAccessDeniedResponse(response)
|
||||
}
|
||||
|
||||
serviceLabels := extractServiceLabelsFromServiceInspectObject(responseObject)
|
||||
responseObject, access = applyResourceAccessControlFromLabel(serviceLabels, responseObject, serviceLabelForStackIdentifier, executor.operationContext)
|
||||
if !access {
|
||||
return rewriteAccessDeniedResponse(response)
|
||||
}
|
||||
|
||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
|
||||
// extractServiceLabelsFromServiceInspectObject retrieve the Labels of the service if present.
|
||||
// Service schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceInspect
|
||||
func extractServiceLabelsFromServiceInspectObject(responseObject map[string]interface{}) map[string]interface{} {
|
||||
// Labels are stored under Spec.Labels
|
||||
serviceSpecObject := extractJSONField(responseObject, "Spec")
|
||||
if serviceSpecObject != nil {
|
||||
return extractJSONField(serviceSpecObject, "Labels")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractServiceLabelsFromServiceListObject retrieve the Labels of the service if present.
|
||||
// Service schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
||||
func extractServiceLabelsFromServiceListObject(responseObject map[string]interface{}) map[string]interface{} {
|
||||
// Labels are stored under Spec.Labels
|
||||
serviceSpecObject := extractJSONField(responseObject, "Spec")
|
||||
if serviceSpecObject != nil {
|
||||
return extractJSONField(serviceSpecObject, "Labels")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// decorateServiceList loops through all services and decorates any service with an existing resource control.
|
||||
// Resource controls checks are based on: resource identifier, stack identifier (from label).
|
||||
// Service object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
||||
func decorateServiceList(serviceData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
|
||||
decoratedServiceData := make([]interface{}, 0)
|
||||
|
||||
for _, service := range serviceData {
|
||||
|
||||
serviceObject := service.(map[string]interface{})
|
||||
if serviceObject[serviceIdentifier] == nil {
|
||||
return nil, ErrDockerServiceIdentifierNotFound
|
||||
}
|
||||
|
||||
serviceID := serviceObject[serviceIdentifier].(string)
|
||||
serviceObject = decorateResourceWithAccessControl(serviceObject, serviceID, resourceControls)
|
||||
|
||||
serviceLabels := extractServiceLabelsFromServiceListObject(serviceObject)
|
||||
serviceObject = decorateResourceWithAccessControlFromLabel(serviceLabels, serviceObject, serviceLabelForStackIdentifier, resourceControls)
|
||||
|
||||
decoratedServiceData = append(decoratedServiceData, serviceObject)
|
||||
}
|
||||
|
||||
return decoratedServiceData, nil
|
||||
}
|
||||
|
||||
// filterServiceList loops through all services and filters public services (no associated resource control)
|
||||
// as well as authorized services (access granted to the user based on existing resource control).
|
||||
// Authorized services are decorated during the process.
|
||||
// Resource controls checks are based on: resource identifier, stack identifier (from label).
|
||||
// Service object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/ServiceList
|
||||
func filterServiceList(serviceData []interface{}, context *restrictedOperationContext) ([]interface{}, error) {
|
||||
filteredServiceData := make([]interface{}, 0)
|
||||
|
||||
for _, service := range serviceData {
|
||||
serviceObject := service.(map[string]interface{})
|
||||
if serviceObject[serviceIdentifier] == nil {
|
||||
return nil, ErrDockerServiceIdentifierNotFound
|
||||
}
|
||||
|
||||
serviceID := serviceObject[serviceIdentifier].(string)
|
||||
serviceObject, access := applyResourceAccessControl(serviceObject, serviceID, context)
|
||||
if access {
|
||||
serviceLabels := extractServiceLabelsFromServiceListObject(serviceObject)
|
||||
serviceObject, access = applyResourceAccessControlFromLabel(serviceLabels, serviceObject, serviceLabelForStackIdentifier, context)
|
||||
if access {
|
||||
filteredServiceData = append(filteredServiceData, serviceObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredServiceData, nil
|
||||
}
|
||||
43
api/http/proxy/socket.go
Normal file
43
api/http/proxy/socket.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package proxy
|
||||
|
||||
// unixSocketHandler represents a handler to proxy HTTP requests via a unix:// socket
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
)
|
||||
|
||||
type socketProxy struct {
|
||||
Transport *proxyTransport
|
||||
}
|
||||
|
||||
func (proxy *socketProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
// Force URL/domain to http/unixsocket to be able to
|
||||
// use http.Transport RoundTrip to do the requests via the socket
|
||||
r.URL.Scheme = "http"
|
||||
r.URL.Host = "unixsocket"
|
||||
|
||||
res, err := proxy.Transport.proxyDockerRequest(r)
|
||||
if err != nil {
|
||||
code := http.StatusInternalServerError
|
||||
if res != nil && res.StatusCode != 0 {
|
||||
code = res.StatusCode
|
||||
}
|
||||
httperror.WriteErrorResponse(w, err, code, nil)
|
||||
return
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
for k, vv := range res.Header {
|
||||
for _, v := range vv {
|
||||
w.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
w.WriteHeader(res.StatusCode)
|
||||
|
||||
if _, err := io.Copy(w, res.Body); err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, nil)
|
||||
}
|
||||
}
|
||||
78
api/http/proxy/tasks.go
Normal file
78
api/http/proxy/tasks.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrDockerTaskServiceIdentifierNotFound defines an error raised when Portainer is unable to find the service identifier associated to a task
|
||||
ErrDockerTaskServiceIdentifierNotFound = portainer.Error("Docker task service identifier not found")
|
||||
taskServiceIdentifier = "ServiceID"
|
||||
taskLabelForStackIdentifier = "com.docker.stack.namespace"
|
||||
)
|
||||
|
||||
// taskListOperation extracts the response as a JSON object, loop through the tasks array
|
||||
// and filter the tasks based on resource controls before rewriting the response
|
||||
func taskListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
|
||||
// TaskList response is a JSON array
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/TaskList
|
||||
responseArray, err := getResponseAsJSONArray(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !executor.operationContext.isAdmin {
|
||||
responseArray, err = filterTaskList(responseArray, executor.operationContext)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return rewriteResponse(response, responseArray, http.StatusOK)
|
||||
}
|
||||
|
||||
// extractTaskLabelsFromTaskListObject retrieve the Labels of the task if present.
|
||||
// Task schema reference: https://docs.docker.com/engine/api/v1.28/#operation/TaskList
|
||||
func extractTaskLabelsFromTaskListObject(responseObject map[string]interface{}) map[string]interface{} {
|
||||
// Labels are stored under Spec.ContainerSpec.Labels
|
||||
taskSpecObject := extractJSONField(responseObject, "Spec")
|
||||
if taskSpecObject != nil {
|
||||
containerSpecObject := extractJSONField(taskSpecObject, "ContainerSpec")
|
||||
if containerSpecObject != nil {
|
||||
return extractJSONField(containerSpecObject, "Labels")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// filterTaskList loops through all tasks and filters public tasks (no associated resource control)
|
||||
// as well as authorized tasks (access granted to the user based on existing resource control).
|
||||
// Resource controls checks are based on: service identifier, stack identifier (from label).
|
||||
// Task object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/TaskList
|
||||
// any resource control giving access to the user based on the associated service identifier.
|
||||
func filterTaskList(taskData []interface{}, context *restrictedOperationContext) ([]interface{}, error) {
|
||||
filteredTaskData := make([]interface{}, 0)
|
||||
|
||||
for _, task := range taskData {
|
||||
taskObject := task.(map[string]interface{})
|
||||
if taskObject[taskServiceIdentifier] == nil {
|
||||
return nil, ErrDockerTaskServiceIdentifierNotFound
|
||||
}
|
||||
|
||||
serviceID := taskObject[taskServiceIdentifier].(string)
|
||||
taskObject, access := applyResourceAccessControl(taskObject, serviceID, context)
|
||||
if access {
|
||||
taskLabels := extractTaskLabelsFromTaskListObject(taskObject)
|
||||
taskObject, access = applyResourceAccessControlFromLabel(taskLabels, taskObject, taskLabelForStackIdentifier, context)
|
||||
if access {
|
||||
filteredTaskData = append(filteredTaskData, taskObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredTaskData, nil
|
||||
}
|
||||
342
api/http/proxy/transport.go
Normal file
342
api/http/proxy/transport.go
Normal file
@@ -0,0 +1,342 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
)
|
||||
|
||||
type (
|
||||
proxyTransport struct {
|
||||
dockerTransport *http.Transport
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
SettingsService portainer.SettingsService
|
||||
}
|
||||
restrictedOperationContext struct {
|
||||
isAdmin bool
|
||||
userID portainer.UserID
|
||||
userTeamIDs []portainer.TeamID
|
||||
resourceControls []portainer.ResourceControl
|
||||
}
|
||||
operationExecutor struct {
|
||||
operationContext *restrictedOperationContext
|
||||
labelBlackList []portainer.Pair
|
||||
}
|
||||
restrictedOperationRequest func(*http.Request, *http.Response, *operationExecutor) error
|
||||
)
|
||||
|
||||
func (p *proxyTransport) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return p.proxyDockerRequest(request)
|
||||
}
|
||||
|
||||
func (p *proxyTransport) executeDockerRequest(request *http.Request) (*http.Response, error) {
|
||||
return p.dockerTransport.RoundTrip(request)
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyDockerRequest(request *http.Request) (*http.Response, error) {
|
||||
path := request.URL.Path
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(path, "/containers"):
|
||||
return p.proxyContainerRequest(request)
|
||||
case strings.HasPrefix(path, "/services"):
|
||||
return p.proxyServiceRequest(request)
|
||||
case strings.HasPrefix(path, "/volumes"):
|
||||
return p.proxyVolumeRequest(request)
|
||||
case strings.HasPrefix(path, "/networks"):
|
||||
return p.proxyNetworkRequest(request)
|
||||
case strings.HasPrefix(path, "/secrets"):
|
||||
return p.proxySecretRequest(request)
|
||||
case strings.HasPrefix(path, "/swarm"):
|
||||
return p.proxySwarmRequest(request)
|
||||
case strings.HasPrefix(path, "/nodes"):
|
||||
return p.proxyNodeRequest(request)
|
||||
case strings.HasPrefix(path, "/tasks"):
|
||||
return p.proxyTaskRequest(request)
|
||||
default:
|
||||
return p.executeDockerRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyContainerRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/containers/create":
|
||||
return p.executeDockerRequest(request)
|
||||
|
||||
case "/containers/prune":
|
||||
return p.administratorOperation(request)
|
||||
|
||||
case "/containers/json":
|
||||
return p.rewriteOperationWithLabelFiltering(request, containerListOperation)
|
||||
|
||||
default:
|
||||
// This section assumes /containers/**
|
||||
if match, _ := path.Match("/containers/*/*", requestPath); match {
|
||||
// Handle /containers/{id}/{action} requests
|
||||
containerID := path.Base(path.Dir(requestPath))
|
||||
action := path.Base(requestPath)
|
||||
|
||||
if action == "json" {
|
||||
return p.rewriteOperation(request, containerInspectOperation)
|
||||
}
|
||||
return p.restrictedOperation(request, containerID)
|
||||
} else if match, _ := path.Match("/containers/*", requestPath); match {
|
||||
// Handle /containers/{id} requests
|
||||
containerID := path.Base(requestPath)
|
||||
return p.restrictedOperation(request, containerID)
|
||||
}
|
||||
return p.executeDockerRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyServiceRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/services/create":
|
||||
return p.executeDockerRequest(request)
|
||||
|
||||
case "/services":
|
||||
return p.rewriteOperation(request, serviceListOperation)
|
||||
|
||||
default:
|
||||
// This section assumes /services/**
|
||||
if match, _ := path.Match("/services/*/*", requestPath); match {
|
||||
// Handle /services/{id}/{action} requests
|
||||
serviceID := path.Base(path.Dir(requestPath))
|
||||
return p.restrictedOperation(request, serviceID)
|
||||
} else if match, _ := path.Match("/services/*", requestPath); match {
|
||||
// Handle /services/{id} requests
|
||||
serviceID := path.Base(requestPath)
|
||||
|
||||
if request.Method == http.MethodGet {
|
||||
return p.rewriteOperation(request, serviceInspectOperation)
|
||||
}
|
||||
return p.restrictedOperation(request, serviceID)
|
||||
}
|
||||
return p.executeDockerRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyVolumeRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/volumes/create":
|
||||
return p.executeDockerRequest(request)
|
||||
|
||||
case "/volumes/prune":
|
||||
return p.administratorOperation(request)
|
||||
|
||||
case "/volumes":
|
||||
return p.rewriteOperation(request, volumeListOperation)
|
||||
|
||||
default:
|
||||
// assume /volumes/{name}
|
||||
if request.Method == http.MethodGet {
|
||||
return p.rewriteOperation(request, volumeInspectOperation)
|
||||
}
|
||||
volumeID := path.Base(requestPath)
|
||||
return p.restrictedOperation(request, volumeID)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyNetworkRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/networks/create":
|
||||
return p.executeDockerRequest(request)
|
||||
|
||||
case "/networks":
|
||||
return p.rewriteOperation(request, networkListOperation)
|
||||
|
||||
default:
|
||||
// assume /networks/{id}
|
||||
if request.Method == http.MethodGet {
|
||||
return p.rewriteOperation(request, networkInspectOperation)
|
||||
}
|
||||
networkID := path.Base(requestPath)
|
||||
return p.restrictedOperation(request, networkID)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxySecretRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/secrets/create":
|
||||
return p.executeDockerRequest(request)
|
||||
|
||||
case "/secrets":
|
||||
return p.rewriteOperation(request, secretListOperation)
|
||||
|
||||
default:
|
||||
// assume /secrets/{id}
|
||||
if request.Method == http.MethodGet {
|
||||
return p.rewriteOperation(request, secretInspectOperation)
|
||||
}
|
||||
secretID := path.Base(requestPath)
|
||||
return p.restrictedOperation(request, secretID)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyNodeRequest(request *http.Request) (*http.Response, error) {
|
||||
requestPath := request.URL.Path
|
||||
|
||||
// assume /nodes/{id}
|
||||
if path.Base(requestPath) != "nodes" {
|
||||
return p.administratorOperation(request)
|
||||
}
|
||||
|
||||
return p.executeDockerRequest(request)
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxySwarmRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/swarm":
|
||||
return p.executeDockerRequest(request)
|
||||
default:
|
||||
// assume /swarm/{action}
|
||||
return p.administratorOperation(request)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyTaskRequest(request *http.Request) (*http.Response, error) {
|
||||
switch requestPath := request.URL.Path; requestPath {
|
||||
case "/tasks":
|
||||
return p.rewriteOperation(request, taskListOperation)
|
||||
default:
|
||||
// assume /tasks/{id}
|
||||
return p.executeDockerRequest(request)
|
||||
}
|
||||
}
|
||||
|
||||
// restrictedOperation ensures that the current user has the required authorizations
|
||||
// before executing the original request.
|
||||
func (p *proxyTransport) restrictedOperation(request *http.Request, resourceID string) (*http.Response, error) {
|
||||
var err error
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
|
||||
teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userTeamIDs := make([]portainer.TeamID, 0)
|
||||
for _, membership := range teamMemberships {
|
||||
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
||||
}
|
||||
|
||||
resourceControls, err := p.ResourceControlService.ResourceControls()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resourceControl := getResourceControlByResourceID(resourceID, resourceControls)
|
||||
if resourceControl != nil && !canUserAccessResource(tokenData.ID, userTeamIDs, resourceControl) {
|
||||
return writeAccessDeniedResponse()
|
||||
}
|
||||
}
|
||||
|
||||
return p.executeDockerRequest(request)
|
||||
}
|
||||
|
||||
// rewriteOperation will create a new operation context with data that will be used
|
||||
// to decorate the original request's response as well as retrieve all the black listed labels
|
||||
// to filter the resources.
|
||||
func (p *proxyTransport) rewriteOperationWithLabelFiltering(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) {
|
||||
operationContext, err := p.createOperationContext(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
settings, err := p.SettingsService.Settings()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
executor := &operationExecutor{
|
||||
operationContext: operationContext,
|
||||
labelBlackList: settings.BlackListedLabels,
|
||||
}
|
||||
|
||||
return p.executeRequestAndRewriteResponse(request, operation, executor)
|
||||
}
|
||||
|
||||
// rewriteOperation will create a new operation context with data that will be used
|
||||
// to decorate the original request's response.
|
||||
func (p *proxyTransport) rewriteOperation(request *http.Request, operation restrictedOperationRequest) (*http.Response, error) {
|
||||
operationContext, err := p.createOperationContext(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
executor := &operationExecutor{
|
||||
operationContext: operationContext,
|
||||
}
|
||||
|
||||
return p.executeRequestAndRewriteResponse(request, operation, executor)
|
||||
}
|
||||
|
||||
func (p *proxyTransport) executeRequestAndRewriteResponse(request *http.Request, operation restrictedOperationRequest, executor *operationExecutor) (*http.Response, error) {
|
||||
response, err := p.executeDockerRequest(request)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
err = operation(request, response, executor)
|
||||
return response, err
|
||||
}
|
||||
|
||||
// administratorOperation ensures that the user has administrator privileges
|
||||
// before executing the original request.
|
||||
func (p *proxyTransport) administratorOperation(request *http.Request) (*http.Response, error) {
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
return writeAccessDeniedResponse()
|
||||
}
|
||||
|
||||
return p.executeDockerRequest(request)
|
||||
}
|
||||
|
||||
func (p *proxyTransport) createOperationContext(request *http.Request) (*restrictedOperationContext, error) {
|
||||
var err error
|
||||
tokenData, err := security.RetrieveTokenData(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resourceControls, err := p.ResourceControlService.ResourceControls()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
operationContext := &restrictedOperationContext{
|
||||
isAdmin: true,
|
||||
userID: tokenData.ID,
|
||||
resourceControls: resourceControls,
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
operationContext.isAdmin = false
|
||||
|
||||
teamMemberships, err := p.TeamMembershipService.TeamMembershipsByUserID(tokenData.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userTeamIDs := make([]portainer.TeamID, 0)
|
||||
for _, membership := range teamMemberships {
|
||||
userTeamIDs = append(userTeamIDs, membership.TeamID)
|
||||
}
|
||||
operationContext.userTeamIDs = userTeamIDs
|
||||
}
|
||||
|
||||
return operationContext, nil
|
||||
}
|
||||
143
api/http/proxy/volumes.go
Normal file
143
api/http/proxy/volumes.go
Normal file
@@ -0,0 +1,143 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrDockerVolumeIdentifierNotFound defines an error raised when Portainer is unable to find a volume identifier
|
||||
ErrDockerVolumeIdentifierNotFound = portainer.Error("Docker volume identifier not found")
|
||||
volumeIdentifier = "Name"
|
||||
volumeLabelForStackIdentifier = "com.docker.stack.namespace"
|
||||
)
|
||||
|
||||
// volumeListOperation extracts the response as a JSON object, loop through the volume array
|
||||
// decorate and/or filter the volumes based on resource controls before rewriting the response
|
||||
func volumeListOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
var err error
|
||||
// VolumeList response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The "Volumes" field contains the list of volumes as an array of JSON objects
|
||||
// Response schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
||||
if responseObject["Volumes"] != nil {
|
||||
volumeData := responseObject["Volumes"].([]interface{})
|
||||
|
||||
if executor.operationContext.isAdmin {
|
||||
volumeData, err = decorateVolumeList(volumeData, executor.operationContext.resourceControls)
|
||||
} else {
|
||||
volumeData, err = filterVolumeList(volumeData, executor.operationContext)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Overwrite the original volume list
|
||||
responseObject["Volumes"] = volumeData
|
||||
}
|
||||
|
||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
|
||||
// volumeInspectOperation extracts the response as a JSON object, verify that the user
|
||||
// has access to the volume based on any existing resource control and either rewrite an access denied response
|
||||
// or a decorated volume.
|
||||
func volumeInspectOperation(request *http.Request, response *http.Response, executor *operationExecutor) error {
|
||||
// VolumeInspect response is a JSON object
|
||||
// https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect
|
||||
responseObject, err := getResponseAsJSONOBject(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if responseObject[volumeIdentifier] == nil {
|
||||
return ErrDockerVolumeIdentifierNotFound
|
||||
}
|
||||
|
||||
volumeID := responseObject[volumeIdentifier].(string)
|
||||
responseObject, access := applyResourceAccessControl(responseObject, volumeID, executor.operationContext)
|
||||
if !access {
|
||||
return rewriteAccessDeniedResponse(response)
|
||||
}
|
||||
|
||||
volumeLabels := extractVolumeLabelsFromVolumeInspectObject(responseObject)
|
||||
responseObject, access = applyResourceAccessControlFromLabel(volumeLabels, responseObject, volumeLabelForStackIdentifier, executor.operationContext)
|
||||
if !access {
|
||||
return rewriteAccessDeniedResponse(response)
|
||||
}
|
||||
|
||||
return rewriteResponse(response, responseObject, http.StatusOK)
|
||||
}
|
||||
|
||||
// extractVolumeLabelsFromVolumeInspectObject retrieve the Labels of the volume if present.
|
||||
// Volume schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeInspect
|
||||
func extractVolumeLabelsFromVolumeInspectObject(responseObject map[string]interface{}) map[string]interface{} {
|
||||
// Labels are stored under Labels
|
||||
return extractJSONField(responseObject, "Labels")
|
||||
}
|
||||
|
||||
// extractVolumeLabelsFromVolumeListObject retrieve the Labels of the volume if present.
|
||||
// Volume schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
||||
func extractVolumeLabelsFromVolumeListObject(responseObject map[string]interface{}) map[string]interface{} {
|
||||
// Labels are stored under Labels
|
||||
return extractJSONField(responseObject, "Labels")
|
||||
}
|
||||
|
||||
// decorateVolumeList loops through all volumes and decorates any volume with an existing resource control.
|
||||
// Resource controls checks are based on: resource identifier, stack identifier (from label).
|
||||
// Volume object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
||||
func decorateVolumeList(volumeData []interface{}, resourceControls []portainer.ResourceControl) ([]interface{}, error) {
|
||||
decoratedVolumeData := make([]interface{}, 0)
|
||||
|
||||
for _, volume := range volumeData {
|
||||
|
||||
volumeObject := volume.(map[string]interface{})
|
||||
if volumeObject[volumeIdentifier] == nil {
|
||||
return nil, ErrDockerVolumeIdentifierNotFound
|
||||
}
|
||||
|
||||
volumeID := volumeObject[volumeIdentifier].(string)
|
||||
volumeObject = decorateResourceWithAccessControl(volumeObject, volumeID, resourceControls)
|
||||
|
||||
volumeLabels := extractVolumeLabelsFromVolumeListObject(volumeObject)
|
||||
volumeObject = decorateResourceWithAccessControlFromLabel(volumeLabels, volumeObject, volumeLabelForStackIdentifier, resourceControls)
|
||||
|
||||
decoratedVolumeData = append(decoratedVolumeData, volumeObject)
|
||||
}
|
||||
|
||||
return decoratedVolumeData, nil
|
||||
}
|
||||
|
||||
// filterVolumeList loops through all volumes and filters public volumes (no associated resource control)
|
||||
// as well as authorized volumes (access granted to the user based on existing resource control).
|
||||
// Authorized volumes are decorated during the process.
|
||||
// Resource controls checks are based on: resource identifier, stack identifier (from label).
|
||||
// Volume object schema reference: https://docs.docker.com/engine/api/v1.28/#operation/VolumeList
|
||||
func filterVolumeList(volumeData []interface{}, context *restrictedOperationContext) ([]interface{}, error) {
|
||||
filteredVolumeData := make([]interface{}, 0)
|
||||
|
||||
for _, volume := range volumeData {
|
||||
volumeObject := volume.(map[string]interface{})
|
||||
if volumeObject[volumeIdentifier] == nil {
|
||||
return nil, ErrDockerVolumeIdentifierNotFound
|
||||
}
|
||||
|
||||
volumeID := volumeObject[volumeIdentifier].(string)
|
||||
volumeObject, access := applyResourceAccessControl(volumeObject, volumeID, context)
|
||||
if access {
|
||||
volumeLabels := extractVolumeLabelsFromVolumeListObject(volumeObject)
|
||||
volumeObject, access = applyResourceAccessControlFromLabel(volumeLabels, volumeObject, volumeLabelForStackIdentifier, context)
|
||||
if access {
|
||||
filteredVolumeData = append(filteredVolumeData, volumeObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredVolumeData, nil
|
||||
}
|
||||
@@ -1,664 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
type (
|
||||
proxyTransport struct {
|
||||
transport *http.Transport
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
}
|
||||
resourceControlMetadata struct {
|
||||
OwnerID portainer.UserID `json:"OwnerId"`
|
||||
}
|
||||
)
|
||||
|
||||
func (p *proxyTransport) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||
response, err := p.transport.RoundTrip(req)
|
||||
if err != nil {
|
||||
return response, err
|
||||
}
|
||||
|
||||
err = p.proxyDockerRequests(req, response)
|
||||
return response, err
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyDockerRequests(request *http.Request, response *http.Response) error {
|
||||
path := request.URL.Path
|
||||
|
||||
if strings.HasPrefix(path, "/containers") {
|
||||
return p.handleContainerRequests(request, response)
|
||||
} else if strings.HasPrefix(path, "/services") {
|
||||
return p.handleServiceRequests(request, response)
|
||||
} else if strings.HasPrefix(path, "/volumes") {
|
||||
return p.handleVolumeRequests(request, response)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) handleContainerRequests(request *http.Request, response *http.Response) error {
|
||||
requestPath := request.URL.Path
|
||||
|
||||
tokenData, err := extractTokenDataFromRequestContext(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if requestPath == "/containers/prune" && tokenData.Role != portainer.AdministratorRole {
|
||||
return writeAccessDeniedResponse(response)
|
||||
}
|
||||
if requestPath == "/containers/json" {
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
return p.decorateContainerResponse(response)
|
||||
}
|
||||
return p.proxyContainerResponseWithResourceControl(response, tokenData.ID)
|
||||
}
|
||||
// /containers/{id}/action
|
||||
if match, _ := path.Match("/containers/*/*", requestPath); match {
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
resourceID := path.Base(path.Dir(requestPath))
|
||||
return p.proxyContainerResponseWithAccessControl(response, tokenData.ID, resourceID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) handleServiceRequests(request *http.Request, response *http.Response) error {
|
||||
requestPath := request.URL.Path
|
||||
|
||||
tokenData, err := extractTokenDataFromRequestContext(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if requestPath == "/services" {
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
return p.decorateServiceResponse(response)
|
||||
}
|
||||
return p.proxyServiceResponseWithResourceControl(response, tokenData.ID)
|
||||
}
|
||||
// /services/{id}
|
||||
if match, _ := path.Match("/services/*", requestPath); match {
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
resourceID := path.Base(requestPath)
|
||||
return p.proxyServiceResponseWithAccessControl(response, tokenData.ID, resourceID)
|
||||
}
|
||||
}
|
||||
// /services/{id}/action
|
||||
if match, _ := path.Match("/services/*/*", requestPath); match {
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
resourceID := path.Base(path.Dir(requestPath))
|
||||
return p.proxyServiceResponseWithAccessControl(response, tokenData.ID, resourceID)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) handleVolumeRequests(request *http.Request, response *http.Response) error {
|
||||
requestPath := request.URL.Path
|
||||
|
||||
tokenData, err := extractTokenDataFromRequestContext(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if requestPath == "/volumes" {
|
||||
if tokenData.Role == portainer.AdministratorRole {
|
||||
return p.decorateVolumeResponse(response)
|
||||
}
|
||||
return p.proxyVolumeResponseWithResourceControl(response, tokenData.ID)
|
||||
}
|
||||
if requestPath == "/volumes/prune" && tokenData.Role != portainer.AdministratorRole {
|
||||
return writeAccessDeniedResponse(response)
|
||||
}
|
||||
// /volumes/{name}
|
||||
if match, _ := path.Match("/volumes/*", requestPath); match {
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
resourceID := path.Base(requestPath)
|
||||
return p.proxyVolumeResponseWithAccessControl(response, tokenData.ID, resourceID)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyContainerResponseWithAccessControl(response *http.Response, userID portainer.UserID, resourceID string) error {
|
||||
rcs, err := p.ResourceControlService.ResourceControls(portainer.ContainerResourceControl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userOwnedResources, err := getResourceIDsOwnedByUser(userID, rcs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isStringInArray(resourceID, userOwnedResources) && isResourceIDInRCs(resourceID, rcs) {
|
||||
return writeAccessDeniedResponse(response)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyServiceResponseWithAccessControl(response *http.Response, userID portainer.UserID, resourceID string) error {
|
||||
rcs, err := p.ResourceControlService.ResourceControls(portainer.ServiceResourceControl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userOwnedResources, err := getResourceIDsOwnedByUser(userID, rcs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isStringInArray(resourceID, userOwnedResources) && isResourceIDInRCs(resourceID, rcs) {
|
||||
return writeAccessDeniedResponse(response)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyVolumeResponseWithAccessControl(response *http.Response, userID portainer.UserID, resourceID string) error {
|
||||
rcs, err := p.ResourceControlService.ResourceControls(portainer.VolumeResourceControl)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userOwnedResources, err := getResourceIDsOwnedByUser(userID, rcs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isStringInArray(resourceID, userOwnedResources) && isResourceIDInRCs(resourceID, rcs) {
|
||||
return writeAccessDeniedResponse(response)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) decorateContainerResponse(response *http.Response) error {
|
||||
responseData, err := getResponseData(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
containers, err := p.decorateContainers(responseData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = rewriteContainerResponse(response, containers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyContainerResponseWithResourceControl(response *http.Response, userID portainer.UserID) error {
|
||||
responseData, err := getResponseData(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
containers, err := p.filterContainers(userID, responseData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = rewriteContainerResponse(response, containers)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) decorateServiceResponse(response *http.Response) error {
|
||||
responseData, err := getResponseData(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
services, err := p.decorateServices(responseData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = rewriteServiceResponse(response, services)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyServiceResponseWithResourceControl(response *http.Response, userID portainer.UserID) error {
|
||||
responseData, err := getResponseData(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volumes, err := p.filterServices(userID, responseData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = rewriteServiceResponse(response, volumes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) decorateVolumeResponse(response *http.Response) error {
|
||||
responseData, err := getResponseData(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volumes, err := p.decorateVolumes(responseData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = rewriteVolumeResponse(response, volumes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) proxyVolumeResponseWithResourceControl(response *http.Response, userID portainer.UserID) error {
|
||||
responseData, err := getResponseData(response)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
volumes, err := p.filterVolumes(userID, responseData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = rewriteVolumeResponse(response, volumes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) decorateContainers(responseData interface{}) ([]interface{}, error) {
|
||||
responseDataArray := responseData.([]interface{})
|
||||
|
||||
containerRCs, err := p.ResourceControlService.ResourceControls(portainer.ContainerResourceControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serviceRCs, err := p.ResourceControlService.ResourceControls(portainer.ServiceResourceControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decoratedResources := make([]interface{}, 0)
|
||||
|
||||
for _, container := range responseDataArray {
|
||||
jsonObject := container.(map[string]interface{})
|
||||
containerID := jsonObject["Id"].(string)
|
||||
containerRC := getRCByResourceID(containerID, containerRCs)
|
||||
if containerRC != nil {
|
||||
decoratedObject := decorateWithResourceControlMetadata(jsonObject, containerRC.OwnerID)
|
||||
decoratedResources = append(decoratedResources, decoratedObject)
|
||||
continue
|
||||
}
|
||||
|
||||
containerLabels := jsonObject["Labels"]
|
||||
if containerLabels != nil {
|
||||
jsonLabels := containerLabels.(map[string]interface{})
|
||||
serviceID := jsonLabels["com.docker.swarm.service.id"]
|
||||
if serviceID != nil {
|
||||
serviceRC := getRCByResourceID(serviceID.(string), serviceRCs)
|
||||
if serviceRC != nil {
|
||||
decoratedObject := decorateWithResourceControlMetadata(jsonObject, serviceRC.OwnerID)
|
||||
decoratedResources = append(decoratedResources, decoratedObject)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
decoratedResources = append(decoratedResources, container)
|
||||
}
|
||||
|
||||
return decoratedResources, nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) filterContainers(userID portainer.UserID, responseData interface{}) ([]interface{}, error) {
|
||||
responseDataArray := responseData.([]interface{})
|
||||
|
||||
containerRCs, err := p.ResourceControlService.ResourceControls(portainer.ContainerResourceControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serviceRCs, err := p.ResourceControlService.ResourceControls(portainer.ServiceResourceControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userOwnedContainerIDs, err := getResourceIDsOwnedByUser(userID, containerRCs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userOwnedServiceIDs, err := getResourceIDsOwnedByUser(userID, serviceRCs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publicContainers := getPublicContainers(responseDataArray, containerRCs, serviceRCs)
|
||||
|
||||
filteredResources := make([]interface{}, 0)
|
||||
|
||||
for _, container := range responseDataArray {
|
||||
jsonObject := container.(map[string]interface{})
|
||||
containerID := jsonObject["Id"].(string)
|
||||
if isStringInArray(containerID, userOwnedContainerIDs) {
|
||||
decoratedObject := decorateWithResourceControlMetadata(jsonObject, userID)
|
||||
filteredResources = append(filteredResources, decoratedObject)
|
||||
continue
|
||||
}
|
||||
|
||||
containerLabels := jsonObject["Labels"]
|
||||
if containerLabels != nil {
|
||||
jsonLabels := containerLabels.(map[string]interface{})
|
||||
serviceID := jsonLabels["com.docker.swarm.service.id"]
|
||||
if serviceID != nil && isStringInArray(serviceID.(string), userOwnedServiceIDs) {
|
||||
decoratedObject := decorateWithResourceControlMetadata(jsonObject, userID)
|
||||
filteredResources = append(filteredResources, decoratedObject)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
filteredResources = append(filteredResources, publicContainers...)
|
||||
return filteredResources, nil
|
||||
}
|
||||
|
||||
func decorateWithResourceControlMetadata(object map[string]interface{}, userID portainer.UserID) map[string]interface{} {
|
||||
metadata := make(map[string]interface{})
|
||||
metadata["ResourceControl"] = resourceControlMetadata{
|
||||
OwnerID: userID,
|
||||
}
|
||||
object["Portainer"] = metadata
|
||||
return object
|
||||
}
|
||||
|
||||
func (p *proxyTransport) decorateServices(responseData interface{}) ([]interface{}, error) {
|
||||
responseDataArray := responseData.([]interface{})
|
||||
|
||||
rcs, err := p.ResourceControlService.ResourceControls(portainer.ServiceResourceControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decoratedResources := make([]interface{}, 0)
|
||||
|
||||
for _, service := range responseDataArray {
|
||||
jsonResource := service.(map[string]interface{})
|
||||
resourceID := jsonResource["ID"].(string)
|
||||
serviceRC := getRCByResourceID(resourceID, rcs)
|
||||
if serviceRC != nil {
|
||||
decoratedObject := decorateWithResourceControlMetadata(jsonResource, serviceRC.OwnerID)
|
||||
decoratedResources = append(decoratedResources, decoratedObject)
|
||||
continue
|
||||
}
|
||||
decoratedResources = append(decoratedResources, service)
|
||||
}
|
||||
|
||||
return decoratedResources, nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) filterServices(userID portainer.UserID, responseData interface{}) ([]interface{}, error) {
|
||||
responseDataArray := responseData.([]interface{})
|
||||
|
||||
rcs, err := p.ResourceControlService.ResourceControls(portainer.ServiceResourceControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userOwnedServiceIDs, err := getResourceIDsOwnedByUser(userID, rcs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publicServices := getPublicResources(responseDataArray, rcs, "ID")
|
||||
|
||||
filteredResources := make([]interface{}, 0)
|
||||
|
||||
for _, res := range responseDataArray {
|
||||
jsonResource := res.(map[string]interface{})
|
||||
resourceID := jsonResource["ID"].(string)
|
||||
if isStringInArray(resourceID, userOwnedServiceIDs) {
|
||||
decoratedObject := decorateWithResourceControlMetadata(jsonResource, userID)
|
||||
filteredResources = append(filteredResources, decoratedObject)
|
||||
}
|
||||
}
|
||||
|
||||
filteredResources = append(filteredResources, publicServices...)
|
||||
return filteredResources, nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) decorateVolumes(responseData interface{}) ([]interface{}, error) {
|
||||
var responseDataArray []interface{}
|
||||
jsonObject := responseData.(map[string]interface{})
|
||||
if jsonObject["Volumes"] != nil {
|
||||
responseDataArray = jsonObject["Volumes"].([]interface{})
|
||||
}
|
||||
|
||||
rcs, err := p.ResourceControlService.ResourceControls(portainer.VolumeResourceControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
decoratedResources := make([]interface{}, 0)
|
||||
|
||||
for _, volume := range responseDataArray {
|
||||
jsonResource := volume.(map[string]interface{})
|
||||
resourceID := jsonResource["Name"].(string)
|
||||
volumeRC := getRCByResourceID(resourceID, rcs)
|
||||
if volumeRC != nil {
|
||||
decoratedObject := decorateWithResourceControlMetadata(jsonResource, volumeRC.OwnerID)
|
||||
decoratedResources = append(decoratedResources, decoratedObject)
|
||||
continue
|
||||
}
|
||||
decoratedResources = append(decoratedResources, volume)
|
||||
}
|
||||
|
||||
return decoratedResources, nil
|
||||
}
|
||||
|
||||
func (p *proxyTransport) filterVolumes(userID portainer.UserID, responseData interface{}) ([]interface{}, error) {
|
||||
var responseDataArray []interface{}
|
||||
jsonObject := responseData.(map[string]interface{})
|
||||
if jsonObject["Volumes"] != nil {
|
||||
responseDataArray = jsonObject["Volumes"].([]interface{})
|
||||
}
|
||||
|
||||
rcs, err := p.ResourceControlService.ResourceControls(portainer.VolumeResourceControl)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
userOwnedVolumeIDs, err := getResourceIDsOwnedByUser(userID, rcs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
publicVolumes := getPublicResources(responseDataArray, rcs, "Name")
|
||||
|
||||
filteredResources := make([]interface{}, 0)
|
||||
|
||||
for _, res := range responseDataArray {
|
||||
jsonResource := res.(map[string]interface{})
|
||||
resourceID := jsonResource["Name"].(string)
|
||||
if isStringInArray(resourceID, userOwnedVolumeIDs) {
|
||||
decoratedObject := decorateWithResourceControlMetadata(jsonResource, userID)
|
||||
filteredResources = append(filteredResources, decoratedObject)
|
||||
}
|
||||
}
|
||||
|
||||
filteredResources = append(filteredResources, publicVolumes...)
|
||||
return filteredResources, nil
|
||||
}
|
||||
|
||||
func getResourceIDsOwnedByUser(userID portainer.UserID, rcs []portainer.ResourceControl) ([]string, error) {
|
||||
ownedResources := make([]string, 0)
|
||||
for _, rc := range rcs {
|
||||
if rc.OwnerID == userID {
|
||||
ownedResources = append(ownedResources, rc.ResourceID)
|
||||
}
|
||||
}
|
||||
return ownedResources, nil
|
||||
}
|
||||
|
||||
func getOwnedServiceContainers(responseData []interface{}, serviceRCs []portainer.ResourceControl) []interface{} {
|
||||
ownedContainers := make([]interface{}, 0)
|
||||
for _, res := range responseData {
|
||||
jsonResource := res.(map[string]map[string]interface{})
|
||||
swarmServiceID := jsonResource["Labels"]["com.docker.swarm.service.id"]
|
||||
if swarmServiceID != nil {
|
||||
resourceID := swarmServiceID.(string)
|
||||
if isResourceIDInRCs(resourceID, serviceRCs) {
|
||||
ownedContainers = append(ownedContainers, res)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ownedContainers
|
||||
}
|
||||
|
||||
func getPublicContainers(responseData []interface{}, containerRCs []portainer.ResourceControl, serviceRCs []portainer.ResourceControl) []interface{} {
|
||||
publicContainers := make([]interface{}, 0)
|
||||
for _, container := range responseData {
|
||||
jsonObject := container.(map[string]interface{})
|
||||
containerID := jsonObject["Id"].(string)
|
||||
if !isResourceIDInRCs(containerID, containerRCs) {
|
||||
containerLabels := jsonObject["Labels"]
|
||||
if containerLabels != nil {
|
||||
jsonLabels := containerLabels.(map[string]interface{})
|
||||
serviceID := jsonLabels["com.docker.swarm.service.id"]
|
||||
if serviceID == nil {
|
||||
publicContainers = append(publicContainers, container)
|
||||
} else if serviceID != nil && !isResourceIDInRCs(serviceID.(string), serviceRCs) {
|
||||
publicContainers = append(publicContainers, container)
|
||||
}
|
||||
} else {
|
||||
publicContainers = append(publicContainers, container)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return publicContainers
|
||||
}
|
||||
|
||||
func getPublicResources(responseData []interface{}, rcs []portainer.ResourceControl, resourceIDKey string) []interface{} {
|
||||
publicResources := make([]interface{}, 0)
|
||||
for _, res := range responseData {
|
||||
jsonResource := res.(map[string]interface{})
|
||||
resourceID := jsonResource[resourceIDKey].(string)
|
||||
if !isResourceIDInRCs(resourceID, rcs) {
|
||||
publicResources = append(publicResources, res)
|
||||
}
|
||||
}
|
||||
return publicResources
|
||||
}
|
||||
|
||||
func isStringInArray(target string, array []string) bool {
|
||||
for _, element := range array {
|
||||
if element == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isResourceIDInRCs(resourceID string, rcs []portainer.ResourceControl) bool {
|
||||
for _, rc := range rcs {
|
||||
if resourceID == rc.ResourceID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getRCByResourceID(resourceID string, rcs []portainer.ResourceControl) *portainer.ResourceControl {
|
||||
for _, rc := range rcs {
|
||||
if resourceID == rc.ResourceID {
|
||||
return &rc
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getResponseData(response *http.Response) (interface{}, error) {
|
||||
var data interface{}
|
||||
if response.Body != nil {
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = response.Body.Close()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = json.Unmarshal(body, &data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
return nil, ErrEmptyResponseBody
|
||||
}
|
||||
|
||||
func writeAccessDeniedResponse(response *http.Response) error {
|
||||
return rewriteResponse(response, portainer.ErrResourceAccessDenied, 403)
|
||||
}
|
||||
|
||||
func rewriteContainerResponse(response *http.Response, responseData interface{}) error {
|
||||
return rewriteResponse(response, responseData, 200)
|
||||
}
|
||||
|
||||
func rewriteServiceResponse(response *http.Response, responseData interface{}) error {
|
||||
return rewriteResponse(response, responseData, 200)
|
||||
}
|
||||
|
||||
func rewriteVolumeResponse(response *http.Response, responseData interface{}) error {
|
||||
data := map[string]interface{}{}
|
||||
data["Volumes"] = responseData
|
||||
return rewriteResponse(response, data, 200)
|
||||
}
|
||||
|
||||
func rewriteResponse(response *http.Response, newContent interface{}, statusCode int) error {
|
||||
jsonData, err := json.Marshal(newContent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body := ioutil.NopCloser(bytes.NewReader(jsonData))
|
||||
response.StatusCode = statusCode
|
||||
response.Body = body
|
||||
response.ContentLength = int64(len(jsonData))
|
||||
response.Header.Set("Content-Length", strconv.Itoa(len(jsonData)))
|
||||
return nil
|
||||
}
|
||||
123
api/http/security/authorization.go
Normal file
123
api/http/security/authorization.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package security
|
||||
|
||||
import "github.com/portainer/portainer"
|
||||
|
||||
// AuthorizedResourceControlDeletion ensure that the user can delete a resource control object.
|
||||
// A non-administrator user cannot delete a resource control where:
|
||||
// * the AdministratorsOnly flag is set
|
||||
// * he is not one of the users in the user accesses
|
||||
// * he is not a member of any team within the team accesses
|
||||
func AuthorizedResourceControlDeletion(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
||||
if context.IsAdmin {
|
||||
return true
|
||||
}
|
||||
|
||||
if resourceControl.AdministratorsOnly {
|
||||
return false
|
||||
}
|
||||
|
||||
userAccessesCount := len(resourceControl.UserAccesses)
|
||||
teamAccessesCount := len(resourceControl.TeamAccesses)
|
||||
|
||||
if teamAccessesCount > 0 {
|
||||
for _, access := range resourceControl.TeamAccesses {
|
||||
for _, membership := range context.UserMemberships {
|
||||
if membership.TeamID == access.TeamID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if userAccessesCount > 0 {
|
||||
for _, access := range resourceControl.UserAccesses {
|
||||
if access.UserID == context.UserID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// AuthorizedResourceControlUpdate ensure that the user can update a resource control object.
|
||||
// It reuses the creation restrictions and adds extra checks.
|
||||
// A non-administrator user cannot update a resource control where:
|
||||
// * he wants to put one or more user in the user accesses
|
||||
func AuthorizedResourceControlUpdate(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
||||
userAccessesCount := len(resourceControl.UserAccesses)
|
||||
if !context.IsAdmin && userAccessesCount > 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return AuthorizedResourceControlCreation(resourceControl, context)
|
||||
}
|
||||
|
||||
// AuthorizedResourceControlCreation ensure that the user can create a resource control object.
|
||||
// A non-administrator user cannot create a resource control where:
|
||||
// * the AdministratorsOnly flag is set
|
||||
// * he wants to add more than one user in the user accesses
|
||||
// * he wants to add a team he is not a member of
|
||||
func AuthorizedResourceControlCreation(resourceControl *portainer.ResourceControl, context *RestrictedRequestContext) bool {
|
||||
if context.IsAdmin {
|
||||
return true
|
||||
}
|
||||
|
||||
if resourceControl.AdministratorsOnly {
|
||||
return false
|
||||
}
|
||||
|
||||
userAccessesCount := len(resourceControl.UserAccesses)
|
||||
teamAccessesCount := len(resourceControl.TeamAccesses)
|
||||
if userAccessesCount > 1 || (userAccessesCount == 1 && teamAccessesCount == 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
if userAccessesCount == 1 {
|
||||
access := resourceControl.UserAccesses[0]
|
||||
if access.UserID == context.UserID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if teamAccessesCount > 0 {
|
||||
for _, access := range resourceControl.TeamAccesses {
|
||||
isMember := false
|
||||
for _, membership := range context.UserMemberships {
|
||||
if membership.TeamID == access.TeamID {
|
||||
isMember = true
|
||||
}
|
||||
}
|
||||
if !isMember {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// AuthorizedTeamManagement ensure that access to the management of the specified team is granted.
|
||||
// It will check if the user is either administrator or leader of that team.
|
||||
func AuthorizedTeamManagement(teamID portainer.TeamID, context *RestrictedRequestContext) bool {
|
||||
if context.IsAdmin {
|
||||
return true
|
||||
}
|
||||
|
||||
for _, membership := range context.UserMemberships {
|
||||
if membership.TeamID == teamID && membership.Role == portainer.TeamLeader {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// AuthorizedUserManagement ensure that access to the management of the specified user is granted.
|
||||
// It will check if the user is either administrator or the owner of the user account.
|
||||
func AuthorizedUserManagement(userID portainer.UserID, context *RestrictedRequestContext) bool {
|
||||
if context.IsAdmin || context.UserID == userID {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
176
api/http/security/bouncer.go
Normal file
176
api/http/security/bouncer.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
httperror "github.com/portainer/portainer/http/error"
|
||||
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
// RequestBouncer represents an entity that manages API request accesses
|
||||
RequestBouncer struct {
|
||||
jwtService portainer.JWTService
|
||||
teamMembershipService portainer.TeamMembershipService
|
||||
authDisabled bool
|
||||
}
|
||||
|
||||
// RestrictedRequestContext is a data structure containing information
|
||||
// used in RestrictedAccess
|
||||
RestrictedRequestContext struct {
|
||||
IsAdmin bool
|
||||
IsTeamLeader bool
|
||||
UserID portainer.UserID
|
||||
UserMemberships []portainer.TeamMembership
|
||||
}
|
||||
)
|
||||
|
||||
// NewRequestBouncer initializes a new RequestBouncer
|
||||
func NewRequestBouncer(jwtService portainer.JWTService, teamMembershipService portainer.TeamMembershipService, authDisabled bool) *RequestBouncer {
|
||||
return &RequestBouncer{
|
||||
jwtService: jwtService,
|
||||
teamMembershipService: teamMembershipService,
|
||||
authDisabled: authDisabled,
|
||||
}
|
||||
}
|
||||
|
||||
// PublicAccess defines a security check for public 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.
|
||||
// Authentication is required to access these endpoints.
|
||||
func (bouncer *RequestBouncer) AuthenticatedAccess(h http.Handler) http.Handler {
|
||||
h = bouncer.mwCheckAuthentication(h)
|
||||
h = mwSecureHeaders(h)
|
||||
return h
|
||||
}
|
||||
|
||||
// RestrictedAccess defines a security check for restricted 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.
|
||||
func (bouncer *RequestBouncer) RestrictedAccess(h http.Handler) http.Handler {
|
||||
h = bouncer.mwUpgradeToRestrictedRequest(h)
|
||||
h = bouncer.AuthenticatedAccess(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
|
||||
}
|
||||
|
||||
// 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-Content-Type-Options", "nosniff")
|
||||
w.Header().Add("X-Frame-Options", "DENY")
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
// mwUpgradeToRestrictedRequest will enhance the current request with
|
||||
// a new RestrictedRequestContext object.
|
||||
func (bouncer *RequestBouncer) mwUpgradeToRestrictedRequest(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
tokenData, err := RetrieveTokenData(r)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, nil)
|
||||
return
|
||||
}
|
||||
|
||||
requestContext, err := bouncer.newRestrictedContextRequest(tokenData.ID, tokenData.Role)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusInternalServerError, nil)
|
||||
return
|
||||
}
|
||||
|
||||
ctx := storeRestrictedRequestContext(r, requestContext)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
|
||||
// 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.WriteErrorResponse(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, nil)
|
||||
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) {
|
||||
var tokenData *portainer.TokenData
|
||||
if !bouncer.authDisabled {
|
||||
var token string
|
||||
|
||||
// Get token from the Authorization header
|
||||
tokens, ok := r.Header["Authorization"]
|
||||
if ok && len(tokens) >= 1 {
|
||||
token = tokens[0]
|
||||
token = strings.TrimPrefix(token, "Bearer ")
|
||||
}
|
||||
|
||||
if token == "" {
|
||||
httperror.WriteErrorResponse(w, portainer.ErrUnauthorized, http.StatusUnauthorized, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var err error
|
||||
tokenData, err = bouncer.jwtService.ParseAndVerifyToken(token)
|
||||
if err != nil {
|
||||
httperror.WriteErrorResponse(w, err, http.StatusUnauthorized, nil)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
tokenData = &portainer.TokenData{
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
}
|
||||
|
||||
ctx := storeTokenData(r, tokenData)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
func (bouncer *RequestBouncer) newRestrictedContextRequest(userID portainer.UserID, userRole portainer.UserRole) (*RestrictedRequestContext, error) {
|
||||
requestContext := &RestrictedRequestContext{
|
||||
IsAdmin: true,
|
||||
UserID: userID,
|
||||
}
|
||||
|
||||
if userRole != portainer.AdministratorRole {
|
||||
requestContext.IsAdmin = false
|
||||
memberships, err := bouncer.teamMembershipService.TeamMembershipsByUserID(userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isTeamLeader := false
|
||||
for _, membership := range memberships {
|
||||
if membership.Role == portainer.TeamLeader {
|
||||
isTeamLeader = true
|
||||
}
|
||||
}
|
||||
|
||||
requestContext.IsTeamLeader = isTeamLeader
|
||||
requestContext.UserMemberships = memberships
|
||||
}
|
||||
|
||||
return requestContext, nil
|
||||
}
|
||||
50
api/http/security/context.go
Normal file
50
api/http/security/context.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
)
|
||||
|
||||
type (
|
||||
contextKey int
|
||||
)
|
||||
|
||||
const (
|
||||
contextAuthenticationKey contextKey = iota
|
||||
contextRestrictedRequest
|
||||
)
|
||||
|
||||
// storeTokenData stores a TokenData object inside the request context and returns the enhanced context.
|
||||
func storeTokenData(request *http.Request, tokenData *portainer.TokenData) context.Context {
|
||||
return context.WithValue(request.Context(), contextAuthenticationKey, tokenData)
|
||||
}
|
||||
|
||||
// RetrieveTokenData returns the TokenData object stored in the request context.
|
||||
func RetrieveTokenData(request *http.Request) (*portainer.TokenData, error) {
|
||||
contextData := request.Context().Value(contextAuthenticationKey)
|
||||
if contextData == nil {
|
||||
return nil, portainer.ErrMissingContextData
|
||||
}
|
||||
|
||||
tokenData := contextData.(*portainer.TokenData)
|
||||
return tokenData, nil
|
||||
}
|
||||
|
||||
// storeRestrictedRequestContext stores a RestrictedRequestContext object inside the request context
|
||||
// and returns the enhanced context.
|
||||
func storeRestrictedRequestContext(request *http.Request, requestContext *RestrictedRequestContext) context.Context {
|
||||
return context.WithValue(request.Context(), contextRestrictedRequest, requestContext)
|
||||
}
|
||||
|
||||
// RetrieveRestrictedRequestContext returns the RestrictedRequestContext object stored in the request context.
|
||||
func RetrieveRestrictedRequestContext(request *http.Request) (*RestrictedRequestContext, error) {
|
||||
contextData := request.Context().Value(contextRestrictedRequest)
|
||||
if contextData == nil {
|
||||
return nil, portainer.ErrMissingSecurityContext
|
||||
}
|
||||
|
||||
requestContext := contextData.(*RestrictedRequestContext)
|
||||
return requestContext, nil
|
||||
}
|
||||
129
api/http/security/filter.go
Normal file
129
api/http/security/filter.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package security
|
||||
|
||||
import "github.com/portainer/portainer"
|
||||
|
||||
// FilterUserTeams filters teams based on user role.
|
||||
// non-administrator users only have access to team they are member of.
|
||||
func FilterUserTeams(teams []portainer.Team, context *RestrictedRequestContext) []portainer.Team {
|
||||
filteredTeams := teams
|
||||
|
||||
if !context.IsAdmin {
|
||||
filteredTeams = make([]portainer.Team, 0)
|
||||
for _, membership := range context.UserMemberships {
|
||||
for _, team := range teams {
|
||||
if team.ID == membership.TeamID {
|
||||
filteredTeams = append(filteredTeams, team)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredTeams
|
||||
}
|
||||
|
||||
// FilterLeaderTeams filters teams based on user role.
|
||||
// Team leaders only have access to team they lead.
|
||||
func FilterLeaderTeams(teams []portainer.Team, context *RestrictedRequestContext) []portainer.Team {
|
||||
filteredTeams := teams
|
||||
|
||||
if context.IsTeamLeader {
|
||||
filteredTeams = make([]portainer.Team, 0)
|
||||
for _, membership := range context.UserMemberships {
|
||||
for _, team := range teams {
|
||||
if team.ID == membership.TeamID && membership.Role == portainer.TeamLeader {
|
||||
filteredTeams = append(filteredTeams, team)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredTeams
|
||||
}
|
||||
|
||||
// FilterUsers filters users based on user role.
|
||||
// Non-administrator users only have access to non-administrator users.
|
||||
func FilterUsers(users []portainer.User, context *RestrictedRequestContext) []portainer.User {
|
||||
filteredUsers := users
|
||||
|
||||
if !context.IsAdmin {
|
||||
filteredUsers = make([]portainer.User, 0)
|
||||
|
||||
for _, user := range users {
|
||||
if user.Role != portainer.AdministratorRole {
|
||||
filteredUsers = append(filteredUsers, user)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredUsers
|
||||
}
|
||||
|
||||
// FilterRegistries filters registries based on user role and team memberships.
|
||||
// Non administrator users only have access to authorized endpoints.
|
||||
func FilterRegistries(registries []portainer.Registry, context *RestrictedRequestContext) ([]portainer.Registry, error) {
|
||||
|
||||
filteredRegistries := registries
|
||||
if !context.IsAdmin {
|
||||
filteredRegistries = make([]portainer.Registry, 0)
|
||||
|
||||
for _, registry := range registries {
|
||||
if isRegistryAccessAuthorized(®istry, context.UserID, context.UserMemberships) {
|
||||
filteredRegistries = append(filteredRegistries, registry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredRegistries, nil
|
||||
}
|
||||
|
||||
// FilterEndpoints filters endpoints based on user role and team memberships.
|
||||
// Non administrator users only have access to authorized endpoints.
|
||||
func FilterEndpoints(endpoints []portainer.Endpoint, context *RestrictedRequestContext) ([]portainer.Endpoint, error) {
|
||||
filteredEndpoints := endpoints
|
||||
|
||||
if !context.IsAdmin {
|
||||
filteredEndpoints = make([]portainer.Endpoint, 0)
|
||||
|
||||
for _, endpoint := range endpoints {
|
||||
if isEndpointAccessAuthorized(&endpoint, context.UserID, context.UserMemberships) {
|
||||
filteredEndpoints = append(filteredEndpoints, endpoint)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return filteredEndpoints, nil
|
||||
}
|
||||
|
||||
func isRegistryAccessAuthorized(registry *portainer.Registry, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||
for _, authorizedUserID := range registry.AuthorizedUsers {
|
||||
if authorizedUserID == userID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, membership := range memberships {
|
||||
for _, authorizedTeamID := range registry.AuthorizedTeams {
|
||||
if membership.TeamID == authorizedTeamID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isEndpointAccessAuthorized(endpoint *portainer.Endpoint, userID portainer.UserID, memberships []portainer.TeamMembership) bool {
|
||||
for _, authorizedUserID := range endpoint.AuthorizedUsers {
|
||||
if authorizedUserID == userID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, membership := range memberships {
|
||||
for _, authorizedTeamID := range endpoint.AuthorizedTeams {
|
||||
if membership.TeamID == authorizedTeamID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -2,6 +2,9 @@ package http
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/http/handler"
|
||||
"github.com/portainer/portainer/http/proxy"
|
||||
"github.com/portainer/portainer/http/security"
|
||||
|
||||
"net/http"
|
||||
)
|
||||
@@ -12,60 +15,106 @@ type Server struct {
|
||||
AssetsPath string
|
||||
AuthDisabled bool
|
||||
EndpointManagement bool
|
||||
Status *portainer.Status
|
||||
UserService portainer.UserService
|
||||
TeamService portainer.TeamService
|
||||
TeamMembershipService portainer.TeamMembershipService
|
||||
EndpointService portainer.EndpointService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
SettingsService portainer.SettingsService
|
||||
CryptoService portainer.CryptoService
|
||||
JWTService portainer.JWTService
|
||||
FileService portainer.FileService
|
||||
Settings *portainer.Settings
|
||||
TemplatesURL string
|
||||
Handler *Handler
|
||||
RegistryService portainer.RegistryService
|
||||
DockerHubService portainer.DockerHubService
|
||||
StackService portainer.StackService
|
||||
StackManager portainer.StackManager
|
||||
LDAPService portainer.LDAPService
|
||||
GitService portainer.GitService
|
||||
Handler *handler.Handler
|
||||
SSL bool
|
||||
SSLCert string
|
||||
SSLKey string
|
||||
}
|
||||
|
||||
// Start starts the HTTP server
|
||||
func (server *Server) Start() error {
|
||||
middleWareService := &middleWareService{
|
||||
jwtService: server.JWTService,
|
||||
authDisabled: server.AuthDisabled,
|
||||
}
|
||||
requestBouncer := security.NewRequestBouncer(server.JWTService, server.TeamMembershipService, server.AuthDisabled)
|
||||
proxyManager := proxy.NewManager(server.ResourceControlService, server.TeamMembershipService, server.SettingsService)
|
||||
|
||||
var authHandler = NewAuthHandler(middleWareService)
|
||||
var fileHandler = handler.NewFileHandler(server.AssetsPath)
|
||||
var authHandler = handler.NewAuthHandler(requestBouncer, server.AuthDisabled)
|
||||
authHandler.UserService = server.UserService
|
||||
authHandler.CryptoService = server.CryptoService
|
||||
authHandler.JWTService = server.JWTService
|
||||
authHandler.authDisabled = server.AuthDisabled
|
||||
var userHandler = NewUserHandler(middleWareService)
|
||||
authHandler.LDAPService = server.LDAPService
|
||||
authHandler.SettingsService = server.SettingsService
|
||||
var userHandler = handler.NewUserHandler(requestBouncer)
|
||||
userHandler.UserService = server.UserService
|
||||
userHandler.TeamService = server.TeamService
|
||||
userHandler.TeamMembershipService = server.TeamMembershipService
|
||||
userHandler.CryptoService = server.CryptoService
|
||||
userHandler.ResourceControlService = server.ResourceControlService
|
||||
var settingsHandler = NewSettingsHandler(middleWareService)
|
||||
settingsHandler.settings = server.Settings
|
||||
var templatesHandler = NewTemplatesHandler(middleWareService)
|
||||
templatesHandler.templatesURL = server.TemplatesURL
|
||||
var dockerHandler = NewDockerHandler(middleWareService, server.ResourceControlService)
|
||||
userHandler.SettingsService = server.SettingsService
|
||||
var teamHandler = handler.NewTeamHandler(requestBouncer)
|
||||
teamHandler.TeamService = server.TeamService
|
||||
teamHandler.TeamMembershipService = server.TeamMembershipService
|
||||
var teamMembershipHandler = handler.NewTeamMembershipHandler(requestBouncer)
|
||||
teamMembershipHandler.TeamMembershipService = server.TeamMembershipService
|
||||
var statusHandler = handler.NewStatusHandler(requestBouncer, server.Status)
|
||||
var settingsHandler = handler.NewSettingsHandler(requestBouncer)
|
||||
settingsHandler.SettingsService = server.SettingsService
|
||||
settingsHandler.LDAPService = server.LDAPService
|
||||
settingsHandler.FileService = server.FileService
|
||||
var templatesHandler = handler.NewTemplatesHandler(requestBouncer)
|
||||
templatesHandler.SettingsService = server.SettingsService
|
||||
var dockerHandler = handler.NewDockerHandler(requestBouncer)
|
||||
dockerHandler.EndpointService = server.EndpointService
|
||||
var websocketHandler = NewWebSocketHandler()
|
||||
dockerHandler.TeamMembershipService = server.TeamMembershipService
|
||||
dockerHandler.ProxyManager = proxyManager
|
||||
var websocketHandler = handler.NewWebSocketHandler()
|
||||
websocketHandler.EndpointService = server.EndpointService
|
||||
var endpointHandler = NewEndpointHandler(middleWareService)
|
||||
endpointHandler.authorizeEndpointManagement = server.EndpointManagement
|
||||
var endpointHandler = handler.NewEndpointHandler(requestBouncer, server.EndpointManagement)
|
||||
endpointHandler.EndpointService = server.EndpointService
|
||||
endpointHandler.FileService = server.FileService
|
||||
var uploadHandler = NewUploadHandler(middleWareService)
|
||||
endpointHandler.ProxyManager = proxyManager
|
||||
var registryHandler = handler.NewRegistryHandler(requestBouncer)
|
||||
registryHandler.RegistryService = server.RegistryService
|
||||
var dockerHubHandler = handler.NewDockerHubHandler(requestBouncer)
|
||||
dockerHubHandler.DockerHubService = server.DockerHubService
|
||||
var resourceHandler = handler.NewResourceHandler(requestBouncer)
|
||||
resourceHandler.ResourceControlService = server.ResourceControlService
|
||||
var uploadHandler = handler.NewUploadHandler(requestBouncer)
|
||||
uploadHandler.FileService = server.FileService
|
||||
var fileHandler = newFileHandler(server.AssetsPath)
|
||||
var stackHandler = handler.NewStackHandler(requestBouncer)
|
||||
stackHandler.FileService = server.FileService
|
||||
stackHandler.StackService = server.StackService
|
||||
stackHandler.EndpointService = server.EndpointService
|
||||
stackHandler.ResourceControlService = server.ResourceControlService
|
||||
stackHandler.StackManager = server.StackManager
|
||||
stackHandler.GitService = server.GitService
|
||||
|
||||
server.Handler = &Handler{
|
||||
AuthHandler: authHandler,
|
||||
UserHandler: userHandler,
|
||||
EndpointHandler: endpointHandler,
|
||||
SettingsHandler: settingsHandler,
|
||||
TemplatesHandler: templatesHandler,
|
||||
DockerHandler: dockerHandler,
|
||||
WebSocketHandler: websocketHandler,
|
||||
FileHandler: fileHandler,
|
||||
UploadHandler: uploadHandler,
|
||||
server.Handler = &handler.Handler{
|
||||
AuthHandler: authHandler,
|
||||
UserHandler: userHandler,
|
||||
TeamHandler: teamHandler,
|
||||
TeamMembershipHandler: teamMembershipHandler,
|
||||
EndpointHandler: endpointHandler,
|
||||
RegistryHandler: registryHandler,
|
||||
DockerHubHandler: dockerHubHandler,
|
||||
ResourceHandler: resourceHandler,
|
||||
SettingsHandler: settingsHandler,
|
||||
StatusHandler: statusHandler,
|
||||
StackHandler: stackHandler,
|
||||
TemplatesHandler: templatesHandler,
|
||||
DockerHandler: dockerHandler,
|
||||
WebSocketHandler: websocketHandler,
|
||||
FileHandler: fileHandler,
|
||||
UploadHandler: uploadHandler,
|
||||
}
|
||||
|
||||
if server.SSL {
|
||||
return http.ListenAndServeTLS(server.BindAddress, server.SSLCert, server.SSLKey, server.Handler)
|
||||
}
|
||||
return http.ListenAndServe(server.BindAddress, server.Handler)
|
||||
}
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// SettingsHandler represents an HTTP API handler for managing settings.
|
||||
type SettingsHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
settings *portainer.Settings
|
||||
}
|
||||
|
||||
// NewSettingsHandler returns a new instance of SettingsHandler.
|
||||
func NewSettingsHandler(mw *middleWareService) *SettingsHandler {
|
||||
h := &SettingsHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/settings",
|
||||
mw.public(http.HandlerFunc(h.handleGetSettings)))
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// handleGetSettings handles GET requests on /settings
|
||||
func (handler *SettingsHandler) handleGetSettings(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
handleNotAllowed(w, []string{http.MethodGet})
|
||||
return
|
||||
}
|
||||
|
||||
encodeJSON(w, handler.settings, handler.Logger)
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// TemplatesHandler represents an HTTP API handler for managing templates.
|
||||
type TemplatesHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
templatesURL string
|
||||
}
|
||||
|
||||
// NewTemplatesHandler returns a new instance of TemplatesHandler.
|
||||
func NewTemplatesHandler(mw *middleWareService) *TemplatesHandler {
|
||||
h := &TemplatesHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/templates",
|
||||
mw.authenticated(http.HandlerFunc(h.handleGetTemplates)))
|
||||
return h
|
||||
}
|
||||
|
||||
// handleGetTemplates handles GET requests on /templates
|
||||
func (handler *TemplatesHandler) handleGetTemplates(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
handleNotAllowed(w, []string{http.MethodGet})
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := http.Get(handler.templatesURL)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write(body)
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
// createTLSConfiguration initializes a tls.Config using a CA certificate, a certificate and a key
|
||||
func createTLSConfiguration(caCertPath, certPath, keyPath string) (*tls.Config, error) {
|
||||
cert, err := tls.LoadX509KeyPair(certPath, keyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
caCert, err := ioutil.ReadFile(caCertPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
caCertPool := x509.NewCertPool()
|
||||
caCertPool.AppendCertsFromPEM(caCert)
|
||||
config := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
RootCAs: caCertPool,
|
||||
}
|
||||
return config, nil
|
||||
}
|
||||
@@ -1,480 +0,0 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
// UserHandler represents an HTTP API handler for managing users.
|
||||
type UserHandler struct {
|
||||
*mux.Router
|
||||
Logger *log.Logger
|
||||
UserService portainer.UserService
|
||||
ResourceControlService portainer.ResourceControlService
|
||||
CryptoService portainer.CryptoService
|
||||
}
|
||||
|
||||
// NewUserHandler returns a new instance of UserHandler.
|
||||
func NewUserHandler(mw *middleWareService) *UserHandler {
|
||||
h := &UserHandler{
|
||||
Router: mux.NewRouter(),
|
||||
Logger: log.New(os.Stderr, "", log.LstdFlags),
|
||||
}
|
||||
h.Handle("/users",
|
||||
mw.administrator(http.HandlerFunc(h.handlePostUsers))).Methods(http.MethodPost)
|
||||
h.Handle("/users",
|
||||
mw.administrator(http.HandlerFunc(h.handleGetUsers))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}",
|
||||
mw.administrator(http.HandlerFunc(h.handleGetUser))).Methods(http.MethodGet)
|
||||
h.Handle("/users/{id}",
|
||||
mw.authenticated(http.HandlerFunc(h.handlePutUser))).Methods(http.MethodPut)
|
||||
h.Handle("/users/{id}",
|
||||
mw.administrator(http.HandlerFunc(h.handleDeleteUser))).Methods(http.MethodDelete)
|
||||
h.Handle("/users/{id}/passwd",
|
||||
mw.authenticated(http.HandlerFunc(h.handlePostUserPasswd)))
|
||||
h.Handle("/users/{userId}/resources/{resourceType}",
|
||||
mw.authenticated(http.HandlerFunc(h.handlePostUserResource))).Methods(http.MethodPost)
|
||||
h.Handle("/users/{userId}/resources/{resourceType}/{resourceId}",
|
||||
mw.authenticated(http.HandlerFunc(h.handleDeleteUserResource))).Methods(http.MethodDelete)
|
||||
h.Handle("/users/admin/check",
|
||||
mw.public(http.HandlerFunc(h.handleGetAdminCheck)))
|
||||
h.Handle("/users/admin/init",
|
||||
mw.public(http.HandlerFunc(h.handlePostAdminInit)))
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// handlePostUsers handles POST requests on /users
|
||||
func (handler *UserHandler) handlePostUsers(w http.ResponseWriter, r *http.Request) {
|
||||
var req postUsersRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var role portainer.UserRole
|
||||
if req.Role == 1 {
|
||||
role = portainer.AdministratorRole
|
||||
} else {
|
||||
role = portainer.StandardUserRole
|
||||
}
|
||||
|
||||
user, err := handler.UserService.UserByUsername(req.Username)
|
||||
if err != nil && err != portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if user != nil {
|
||||
Error(w, portainer.ErrUserAlreadyExists, http.StatusConflict, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user = &portainer.User{
|
||||
Username: req.Username,
|
||||
Role: role,
|
||||
}
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.UserService.CreateUser(user)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type postUsersRequest struct {
|
||||
Username string `valid:"alphanum,required"`
|
||||
Password string `valid:"required"`
|
||||
Role int `valid:"required"`
|
||||
}
|
||||
|
||||
// handleGetUsers handles GET requests on /users
|
||||
func (handler *UserHandler) handleGetUsers(w http.ResponseWriter, r *http.Request) {
|
||||
users, err := handler.UserService.Users()
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
for i := range users {
|
||||
users[i].Password = ""
|
||||
}
|
||||
encodeJSON(w, users, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePostUserPasswd handles POST requests on /users/:id/passwd
|
||||
func (handler *UserHandler) handlePostUserPasswd(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
handleNotAllowed(w, []string{http.MethodPost})
|
||||
return
|
||||
}
|
||||
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req postUserPasswdRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var password = req.Password
|
||||
|
||||
u, err := handler.UserService.User(portainer.UserID(userID))
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
valid := true
|
||||
err = handler.CryptoService.CompareHashAndData(u.Password, password)
|
||||
if err != nil {
|
||||
valid = false
|
||||
}
|
||||
|
||||
encodeJSON(w, &postUserPasswdResponse{Valid: valid}, handler.Logger)
|
||||
}
|
||||
|
||||
type postUserPasswdRequest struct {
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
|
||||
type postUserPasswdResponse struct {
|
||||
Valid bool `json:"valid"`
|
||||
}
|
||||
|
||||
// handleGetUser handles GET requests on /users/:id
|
||||
func (handler *UserHandler) handleGetUser(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := handler.UserService.User(portainer.UserID(userID))
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user.Password = ""
|
||||
encodeJSON(w, &user, handler.Logger)
|
||||
}
|
||||
|
||||
// handlePutUser handles PUT requests on /users/:id
|
||||
func (handler *UserHandler) handlePutUser(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := extractTokenDataFromRequestContext(r)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
|
||||
if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(userID) {
|
||||
Error(w, portainer.ErrUnauthorized, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req putUserRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Password == "" && req.Role == 0 {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := handler.UserService.User(portainer.UserID(userID))
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
if req.Password != "" {
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if req.Role != 0 {
|
||||
if tokenData.Role != portainer.AdministratorRole {
|
||||
Error(w, portainer.ErrUnauthorized, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
if req.Role == 1 {
|
||||
user.Role = portainer.AdministratorRole
|
||||
} else {
|
||||
user.Role = portainer.StandardUserRole
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.UserService.UpdateUser(user.ID, user)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type putUserRequest struct {
|
||||
Password string `valid:"-"`
|
||||
Role int `valid:"-"`
|
||||
}
|
||||
|
||||
// handlePostAdminInit handles GET requests on /users/admin/check
|
||||
func (handler *UserHandler) handleGetAdminCheck(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
handleNotAllowed(w, []string{http.MethodGet})
|
||||
return
|
||||
}
|
||||
|
||||
users, err := handler.UserService.UsersByRole(portainer.AdministratorRole)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if len(users) == 0 {
|
||||
Error(w, portainer.ErrUserNotFound, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handlePostAdminInit handles POST requests on /users/admin/init
|
||||
func (handler *UserHandler) handlePostAdminInit(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
handleNotAllowed(w, []string{http.MethodPost})
|
||||
return
|
||||
}
|
||||
|
||||
var req postAdminInitRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := handler.UserService.UserByUsername("admin")
|
||||
if err == portainer.ErrUserNotFound {
|
||||
user := &portainer.User{
|
||||
Username: "admin",
|
||||
Role: portainer.AdministratorRole,
|
||||
}
|
||||
user.Password, err = handler.CryptoService.Hash(req.Password)
|
||||
if err != nil {
|
||||
Error(w, portainer.ErrCryptoHashFailure, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.UserService.CreateUser(user)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
if user != nil {
|
||||
Error(w, portainer.ErrAdminAlreadyInitialized, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type postAdminInitRequest struct {
|
||||
Password string `valid:"required"`
|
||||
}
|
||||
|
||||
// handleDeleteUser handles DELETE requests on /users/:id
|
||||
func (handler *UserHandler) handleDeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
id := vars["id"]
|
||||
|
||||
userID, err := strconv.Atoi(id)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = handler.UserService.User(portainer.UserID(userID))
|
||||
|
||||
if err == portainer.ErrUserNotFound {
|
||||
Error(w, err, http.StatusNotFound, handler.Logger)
|
||||
return
|
||||
} else if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.UserService.DeleteUser(portainer.UserID(userID))
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// handlePostUserResource handles POST requests on /users/:userId/resources/:resourceType
|
||||
func (handler *UserHandler) handlePostUserResource(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
userID := vars["userId"]
|
||||
resourceType := vars["resourceType"]
|
||||
|
||||
uid, err := strconv.Atoi(userID)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var rcType portainer.ResourceControlType
|
||||
if resourceType == "container" {
|
||||
rcType = portainer.ContainerResourceControl
|
||||
} else if resourceType == "service" {
|
||||
rcType = portainer.ServiceResourceControl
|
||||
} else if resourceType == "volume" {
|
||||
rcType = portainer.VolumeResourceControl
|
||||
} else {
|
||||
Error(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := extractTokenDataFromRequestContext(r)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
if tokenData.ID != portainer.UserID(uid) {
|
||||
Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var req postUserResourceRequest
|
||||
if err = json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
Error(w, ErrInvalidJSON, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = govalidator.ValidateStruct(req)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
resource := portainer.ResourceControl{
|
||||
OwnerID: portainer.UserID(uid),
|
||||
ResourceID: req.ResourceID,
|
||||
AccessLevel: portainer.RestrictedResourceAccessLevel,
|
||||
}
|
||||
|
||||
err = handler.ResourceControlService.CreateResourceControl(req.ResourceID, &resource, rcType)
|
||||
if err != nil {
|
||||
Error(w, ErrInvalidRequestFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type postUserResourceRequest struct {
|
||||
ResourceID string `valid:"required"`
|
||||
}
|
||||
|
||||
// handleDeleteUserResource handles DELETE requests on /users/:userId/resources/:resourceType/:resourceId
|
||||
func (handler *UserHandler) handleDeleteUserResource(w http.ResponseWriter, r *http.Request) {
|
||||
vars := mux.Vars(r)
|
||||
userID := vars["userId"]
|
||||
resourceID := vars["resourceId"]
|
||||
resourceType := vars["resourceType"]
|
||||
|
||||
uid, err := strconv.Atoi(userID)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
var rcType portainer.ResourceControlType
|
||||
if resourceType == "container" {
|
||||
rcType = portainer.ContainerResourceControl
|
||||
} else if resourceType == "service" {
|
||||
rcType = portainer.ServiceResourceControl
|
||||
} else if resourceType == "volume" {
|
||||
rcType = portainer.VolumeResourceControl
|
||||
} else {
|
||||
Error(w, ErrInvalidQueryFormat, http.StatusBadRequest, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
tokenData, err := extractTokenDataFromRequestContext(r)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
}
|
||||
if tokenData.Role != portainer.AdministratorRole && tokenData.ID != portainer.UserID(uid) {
|
||||
Error(w, portainer.ErrResourceAccessDenied, http.StatusForbidden, handler.Logger)
|
||||
return
|
||||
}
|
||||
|
||||
err = handler.ResourceControlService.DeleteResourceControl(resourceID, rcType)
|
||||
if err != nil {
|
||||
Error(w, err, http.StatusInternalServerError, handler.Logger)
|
||||
return
|
||||
}
|
||||
}
|
||||
126
api/ldap/ldap.go
Normal file
126
api/ldap/ldap.go
Normal file
@@ -0,0 +1,126 @@
|
||||
package ldap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/portainer/portainer"
|
||||
"github.com/portainer/portainer/crypto"
|
||||
|
||||
"gopkg.in/ldap.v2"
|
||||
)
|
||||
|
||||
const (
|
||||
// ErrUserNotFound defines an error raised when the user is not found via LDAP search
|
||||
// or that too many entries (> 1) are returned.
|
||||
ErrUserNotFound = portainer.Error("User not found or too many entries returned")
|
||||
)
|
||||
|
||||
// Service represents a service used to authenticate users against a LDAP/AD.
|
||||
type Service struct{}
|
||||
|
||||
func searchUser(username string, conn *ldap.Conn, settings []portainer.LDAPSearchSettings) (string, error) {
|
||||
var userDN string
|
||||
found := false
|
||||
for _, searchSettings := range settings {
|
||||
searchRequest := ldap.NewSearchRequest(
|
||||
searchSettings.BaseDN,
|
||||
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
|
||||
fmt.Sprintf("(&%s(%s=%s))", searchSettings.Filter, searchSettings.UserNameAttribute, username),
|
||||
[]string{"dn"},
|
||||
nil,
|
||||
)
|
||||
|
||||
// Deliberately skip errors on the search request so that we can jump to other search settings
|
||||
// if any issue arise with the current one.
|
||||
sr, err := conn.Search(searchRequest)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(sr.Entries) == 1 {
|
||||
found = true
|
||||
userDN = sr.Entries[0].DN
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return "", ErrUserNotFound
|
||||
}
|
||||
|
||||
return userDN, nil
|
||||
}
|
||||
|
||||
func createConnection(settings *portainer.LDAPSettings) (*ldap.Conn, error) {
|
||||
|
||||
if settings.TLSConfig.TLS || settings.StartTLS {
|
||||
config, err := crypto.CreateTLSConfiguration(&settings.TLSConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
config.ServerName = strings.Split(settings.URL, ":")[0]
|
||||
|
||||
if settings.TLSConfig.TLS {
|
||||
return ldap.DialTLS("tcp", settings.URL, config)
|
||||
}
|
||||
|
||||
conn, err := ldap.Dial("tcp", settings.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = conn.StartTLS(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
return ldap.Dial("tcp", settings.URL)
|
||||
}
|
||||
|
||||
// AuthenticateUser is used to authenticate a user against a LDAP/AD.
|
||||
func (*Service) AuthenticateUser(username, password string, settings *portainer.LDAPSettings) error {
|
||||
|
||||
connection, err := createConnection(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer connection.Close()
|
||||
|
||||
err = connection.Bind(settings.ReaderDN, settings.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userDN, err := searchUser(username, connection, settings.SearchSettings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = connection.Bind(userDN, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestConnectivity is used to test a connection against the LDAP server using the credentials
|
||||
// specified in the LDAPSettings.
|
||||
func (*Service) TestConnectivity(settings *portainer.LDAPSettings) error {
|
||||
|
||||
connection, err := createConnection(settings)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer connection.Close()
|
||||
|
||||
err = connection.Bind(settings.ReaderDN, settings.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
334
api/portainer.go
334
api/portainer.go
@@ -1,8 +1,6 @@
|
||||
package portainer
|
||||
|
||||
import (
|
||||
"io"
|
||||
)
|
||||
import "io"
|
||||
|
||||
type (
|
||||
// Pair defines a key/value string pair
|
||||
@@ -19,27 +17,70 @@ type (
|
||||
ExternalEndpoints *string
|
||||
SyncInterval *string
|
||||
Endpoint *string
|
||||
Labels *[]Pair
|
||||
Logo *string
|
||||
Templates *string
|
||||
NoAuth *bool
|
||||
NoAnalytics *bool
|
||||
TLSVerify *bool
|
||||
TLSCacert *string
|
||||
TLSCert *string
|
||||
TLSKey *string
|
||||
SSL *bool
|
||||
SSLCert *string
|
||||
SSLKey *string
|
||||
AdminPassword *string
|
||||
AdminPasswordFile *string
|
||||
// Deprecated fields
|
||||
Logo *string
|
||||
Templates *string
|
||||
Labels *[]Pair
|
||||
}
|
||||
|
||||
// Settings represents Portainer settings.
|
||||
// Status represents the application status.
|
||||
Status struct {
|
||||
Authentication bool `json:"Authentication"`
|
||||
EndpointManagement bool `json:"EndpointManagement"`
|
||||
Analytics bool `json:"Analytics"`
|
||||
Version string `json:"Version"`
|
||||
}
|
||||
|
||||
// LDAPSettings represents the settings used to connect to a LDAP server.
|
||||
LDAPSettings struct {
|
||||
ReaderDN string `json:"ReaderDN"`
|
||||
Password string `json:"Password"`
|
||||
URL string `json:"URL"`
|
||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||
StartTLS bool `json:"StartTLS"`
|
||||
SearchSettings []LDAPSearchSettings `json:"SearchSettings"`
|
||||
}
|
||||
|
||||
// TLSConfiguration represents a TLS configuration.
|
||||
TLSConfiguration struct {
|
||||
TLS bool `json:"TLS"`
|
||||
TLSSkipVerify bool `json:"TLSSkipVerify"`
|
||||
TLSCACertPath string `json:"TLSCACert,omitempty"`
|
||||
TLSCertPath string `json:"TLSCert,omitempty"`
|
||||
TLSKeyPath string `json:"TLSKey,omitempty"`
|
||||
}
|
||||
|
||||
// LDAPSearchSettings represents settings used to search for users in a LDAP server.
|
||||
LDAPSearchSettings struct {
|
||||
BaseDN string `json:"BaseDN"`
|
||||
Filter string `json:"Filter"`
|
||||
UserNameAttribute string `json:"UserNameAttribute"`
|
||||
}
|
||||
|
||||
// Settings represents the application settings.
|
||||
Settings struct {
|
||||
HiddenLabels []Pair `json:"hiddenLabels"`
|
||||
Logo string `json:"logo"`
|
||||
Authentication bool `json:"authentication"`
|
||||
Analytics bool `json:"analytics"`
|
||||
EndpointManagement bool `json:"endpointManagement"`
|
||||
TemplatesURL string `json:"TemplatesURL"`
|
||||
LogoURL string `json:"LogoURL"`
|
||||
BlackListedLabels []Pair `json:"BlackListedLabels"`
|
||||
DisplayExternalContributors bool `json:"DisplayExternalContributors"`
|
||||
AuthenticationMethod AuthenticationMethod `json:"AuthenticationMethod"`
|
||||
LDAPSettings LDAPSettings `json:"LDAPSettings"`
|
||||
AllowBindMountsForRegularUsers bool `json:"AllowBindMountsForRegularUsers"`
|
||||
AllowPrivilegedModeForRegularUsers bool `json:"AllowPrivilegedModeForRegularUsers"`
|
||||
}
|
||||
|
||||
// User represent a user account.
|
||||
// User represents a user account.
|
||||
User struct {
|
||||
ID UserID `json:"Id"`
|
||||
Username string `json:"Username"`
|
||||
@@ -51,9 +92,35 @@ type (
|
||||
UserID int
|
||||
|
||||
// UserRole represents the role of a user. It can be either an administrator
|
||||
// or a regular user.
|
||||
// or a regular user
|
||||
UserRole int
|
||||
|
||||
// AuthenticationMethod represents the authentication method used to authenticate a user.
|
||||
AuthenticationMethod int
|
||||
|
||||
// Team represents a list of user accounts.
|
||||
Team struct {
|
||||
ID TeamID `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
}
|
||||
|
||||
// TeamID represents a team identifier
|
||||
TeamID int
|
||||
|
||||
// TeamMembership represents a membership association between a user and a team
|
||||
TeamMembership struct {
|
||||
ID TeamMembershipID `json:"Id"`
|
||||
UserID UserID `json:"UserID"`
|
||||
TeamID TeamID `json:"TeamID"`
|
||||
Role MembershipRole `json:"Role"`
|
||||
}
|
||||
|
||||
// TeamMembershipID represents a team membership identifier
|
||||
TeamMembershipID int
|
||||
|
||||
// MembershipRole represents the role of a user within a team
|
||||
MembershipRole int
|
||||
|
||||
// TokenData represents the data embedded in a JWT token.
|
||||
TokenData struct {
|
||||
ID UserID
|
||||
@@ -61,35 +128,99 @@ type (
|
||||
Role UserRole
|
||||
}
|
||||
|
||||
// StackID represents a stack identifier (it must be composed of Name + "_" + SwarmID to create a unique identifier).
|
||||
StackID string
|
||||
|
||||
// Stack represents a Docker stack created via docker stack deploy.
|
||||
Stack struct {
|
||||
ID StackID `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
EntryPoint string `json:"EntryPoint"`
|
||||
SwarmID string `json:"SwarmId"`
|
||||
ProjectPath string
|
||||
}
|
||||
|
||||
// RegistryID represents a registry identifier.
|
||||
RegistryID int
|
||||
|
||||
// Registry represents a Docker registry with all the info required
|
||||
// to connect to it.
|
||||
Registry struct {
|
||||
ID RegistryID `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
URL string `json:"URL"`
|
||||
Authentication bool `json:"Authentication"`
|
||||
Username string `json:"Username"`
|
||||
Password string `json:"Password"`
|
||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
||||
}
|
||||
|
||||
// DockerHub represents all the required information to connect and use the
|
||||
// Docker Hub.
|
||||
DockerHub struct {
|
||||
Authentication bool `json:"Authentication"`
|
||||
Username string `json:"Username"`
|
||||
Password string `json:"Password"`
|
||||
}
|
||||
|
||||
// EndpointID represents an endpoint identifier.
|
||||
EndpointID int
|
||||
|
||||
// Endpoint represents a Docker endpoint with all the info required
|
||||
// to connect to it.
|
||||
Endpoint struct {
|
||||
ID EndpointID `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
URL string `json:"URL"`
|
||||
TLS bool `json:"TLS"`
|
||||
TLSCACertPath string `json:"TLSCACert,omitempty"`
|
||||
TLSCertPath string `json:"TLSCert,omitempty"`
|
||||
TLSKeyPath string `json:"TLSKey,omitempty"`
|
||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||
ID EndpointID `json:"Id"`
|
||||
Name string `json:"Name"`
|
||||
URL string `json:"URL"`
|
||||
PublicURL string `json:"PublicURL"`
|
||||
TLSConfig TLSConfiguration `json:"TLSConfig"`
|
||||
AuthorizedUsers []UserID `json:"AuthorizedUsers"`
|
||||
AuthorizedTeams []TeamID `json:"AuthorizedTeams"`
|
||||
|
||||
// Deprecated fields
|
||||
// Deprecated in DBVersion == 4
|
||||
TLS bool `json:"TLS,omitempty"`
|
||||
TLSCACertPath string `json:"TLSCACert,omitempty"`
|
||||
TLSCertPath string `json:"TLSCert,omitempty"`
|
||||
TLSKeyPath string `json:"TLSKey,omitempty"`
|
||||
}
|
||||
|
||||
// ResourceControl represent a reference to a Docker resource with specific controls
|
||||
// ResourceControlID represents a resource control identifier.
|
||||
ResourceControlID int
|
||||
|
||||
// ResourceControl represent a reference to a Docker resource with specific access controls
|
||||
ResourceControl struct {
|
||||
OwnerID UserID `json:"OwnerId"`
|
||||
ResourceID string `json:"ResourceId"`
|
||||
ID ResourceControlID `json:"Id"`
|
||||
ResourceID string `json:"ResourceId"`
|
||||
SubResourceIDs []string `json:"SubResourceIds"`
|
||||
Type ResourceControlType `json:"Type"`
|
||||
AdministratorsOnly bool `json:"AdministratorsOnly"`
|
||||
UserAccesses []UserResourceAccess `json:"UserAccesses"`
|
||||
TeamAccesses []TeamResourceAccess `json:"TeamAccesses"`
|
||||
|
||||
// Deprecated fields
|
||||
// Deprecated in DBVersion == 2
|
||||
OwnerID UserID `json:"OwnerId,omitempty"`
|
||||
AccessLevel ResourceAccessLevel `json:"AccessLevel,omitempty"`
|
||||
}
|
||||
|
||||
// ResourceControlType represents the type of resource associated to the resource control (volume, container, service...).
|
||||
ResourceControlType int
|
||||
|
||||
// UserResourceAccess represents the level of control on a resource for a specific user.
|
||||
UserResourceAccess struct {
|
||||
UserID UserID `json:"UserId"`
|
||||
AccessLevel ResourceAccessLevel `json:"AccessLevel"`
|
||||
}
|
||||
|
||||
// ResourceControlType represents a type of resource control.
|
||||
// Can be one of: container, service or volume.
|
||||
ResourceControlType int
|
||||
// TeamResourceAccess represents the level of control on a resource for a specific team.
|
||||
TeamResourceAccess struct {
|
||||
TeamID TeamID `json:"TeamId"`
|
||||
AccessLevel ResourceAccessLevel `json:"AccessLevel"`
|
||||
}
|
||||
|
||||
// ResourceAccessLevel represents the level of control associated to a resource for a specific owner.
|
||||
// Can be one of: full, restricted, limited.
|
||||
// ResourceAccessLevel represents the level of control associated to a resource.
|
||||
ResourceAccessLevel int
|
||||
|
||||
// TLSFileType represents a type of TLS file required to connect to a Docker endpoint.
|
||||
@@ -125,6 +256,29 @@ type (
|
||||
DeleteUser(ID UserID) error
|
||||
}
|
||||
|
||||
// TeamService represents a service for managing user data.
|
||||
TeamService interface {
|
||||
Team(ID TeamID) (*Team, error)
|
||||
TeamByName(name string) (*Team, error)
|
||||
Teams() ([]Team, error)
|
||||
CreateTeam(team *Team) error
|
||||
UpdateTeam(ID TeamID, team *Team) error
|
||||
DeleteTeam(ID TeamID) error
|
||||
}
|
||||
|
||||
// TeamMembershipService represents a service for managing team membership data.
|
||||
TeamMembershipService interface {
|
||||
TeamMembership(ID TeamMembershipID) (*TeamMembership, error)
|
||||
TeamMemberships() ([]TeamMembership, error)
|
||||
TeamMembershipsByUserID(userID UserID) ([]TeamMembership, error)
|
||||
TeamMembershipsByTeamID(teamID TeamID) ([]TeamMembership, error)
|
||||
CreateTeamMembership(membership *TeamMembership) error
|
||||
UpdateTeamMembership(ID TeamMembershipID, membership *TeamMembership) error
|
||||
DeleteTeamMembership(ID TeamMembershipID) error
|
||||
DeleteTeamMembershipByUserID(userID UserID) error
|
||||
DeleteTeamMembershipByTeamID(teamID TeamID) error
|
||||
}
|
||||
|
||||
// EndpointService represents a service for managing endpoint data.
|
||||
EndpointService interface {
|
||||
Endpoint(ID EndpointID) (*Endpoint, error)
|
||||
@@ -135,6 +289,37 @@ type (
|
||||
Synchronize(toCreate, toUpdate, toDelete []*Endpoint) error
|
||||
}
|
||||
|
||||
// RegistryService represents a service for managing registry data.
|
||||
RegistryService interface {
|
||||
Registry(ID RegistryID) (*Registry, error)
|
||||
Registries() ([]Registry, error)
|
||||
CreateRegistry(registry *Registry) error
|
||||
UpdateRegistry(ID RegistryID, registry *Registry) error
|
||||
DeleteRegistry(ID RegistryID) error
|
||||
}
|
||||
|
||||
// StackService represents a service for managing stack data.
|
||||
StackService interface {
|
||||
Stack(ID StackID) (*Stack, error)
|
||||
Stacks() ([]Stack, error)
|
||||
StacksBySwarmID(ID string) ([]Stack, error)
|
||||
CreateStack(stack *Stack) error
|
||||
UpdateStack(ID StackID, stack *Stack) error
|
||||
DeleteStack(ID StackID) error
|
||||
}
|
||||
|
||||
// DockerHubService represents a service for managing the DockerHub object.
|
||||
DockerHubService interface {
|
||||
DockerHub() (*DockerHub, error)
|
||||
StoreDockerHub(registry *DockerHub) error
|
||||
}
|
||||
|
||||
// SettingsService represents a service for managing application settings.
|
||||
SettingsService interface {
|
||||
Settings() (*Settings, error)
|
||||
StoreSettings(settings *Settings) error
|
||||
}
|
||||
|
||||
// VersionService represents a service for managing version data.
|
||||
VersionService interface {
|
||||
DBVersion() (int, error)
|
||||
@@ -143,10 +328,12 @@ type (
|
||||
|
||||
// ResourceControlService represents a service for managing resource control data.
|
||||
ResourceControlService interface {
|
||||
ResourceControl(resourceID string, rcType ResourceControlType) (*ResourceControl, error)
|
||||
ResourceControls(rcType ResourceControlType) ([]ResourceControl, error)
|
||||
CreateResourceControl(resourceID string, rc *ResourceControl, rcType ResourceControlType) error
|
||||
DeleteResourceControl(resourceID string, rcType ResourceControlType) error
|
||||
ResourceControl(ID ResourceControlID) (*ResourceControl, error)
|
||||
ResourceControlByResourceID(resourceID string) (*ResourceControl, error)
|
||||
ResourceControls() ([]ResourceControl, error)
|
||||
CreateResourceControl(rc *ResourceControl) error
|
||||
UpdateResourceControl(ID ResourceControlID, resourceControl *ResourceControl) error
|
||||
DeleteResourceControl(ID ResourceControlID) error
|
||||
}
|
||||
|
||||
// CryptoService represents a service for encrypting/hashing data.
|
||||
@@ -163,22 +350,47 @@ type (
|
||||
|
||||
// FileService represents a service for managing files.
|
||||
FileService interface {
|
||||
StoreTLSFile(endpointID EndpointID, fileType TLSFileType, r io.Reader) error
|
||||
GetPathForTLSFile(endpointID EndpointID, fileType TLSFileType) (string, error)
|
||||
DeleteTLSFiles(endpointID EndpointID) error
|
||||
GetFileContent(filePath string) (string, error)
|
||||
RemoveDirectory(directoryPath string) error
|
||||
StoreTLSFile(folder string, fileType TLSFileType, r io.Reader) error
|
||||
GetPathForTLSFile(folder string, fileType TLSFileType) (string, error)
|
||||
DeleteTLSFile(folder string, fileType TLSFileType) error
|
||||
DeleteTLSFiles(folder string) error
|
||||
GetStackProjectPath(stackIdentifier string) string
|
||||
StoreStackFileFromString(stackIdentifier string, stackFileContent string) (string, error)
|
||||
StoreStackFileFromReader(stackIdentifier string, r io.Reader) (string, error)
|
||||
}
|
||||
|
||||
// GitService represents a service for managing Git.
|
||||
GitService interface {
|
||||
CloneRepository(url, destination string) error
|
||||
}
|
||||
|
||||
// EndpointWatcher represents a service to synchronize the endpoints via an external source.
|
||||
EndpointWatcher interface {
|
||||
WatchEndpointFile(endpointFilePath string) error
|
||||
}
|
||||
|
||||
// LDAPService represents a service used to authenticate users against a LDAP/AD.
|
||||
LDAPService interface {
|
||||
AuthenticateUser(username, password string, settings *LDAPSettings) error
|
||||
TestConnectivity(settings *LDAPSettings) error
|
||||
}
|
||||
|
||||
// StackManager represents a service to manage stacks.
|
||||
StackManager interface {
|
||||
Deploy(stack *Stack, endpoint *Endpoint) error
|
||||
Remove(stack *Stack, endpoint *Endpoint) error
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// APIVersion is the version number of Portainer API.
|
||||
APIVersion = "1.12.1"
|
||||
// DBVersion is the version number of Portainer database.
|
||||
DBVersion = 1
|
||||
// APIVersion is the version number of the Portainer API.
|
||||
APIVersion = "1.15.0"
|
||||
// DBVersion is the version number of the Portainer database.
|
||||
DBVersion = 6
|
||||
// DefaultTemplatesURL represents the default URL for the templates definitions.
|
||||
DefaultTemplatesURL = "https://raw.githubusercontent.com/portainer/templates/master/templates.json"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -190,6 +402,14 @@ const (
|
||||
TLSFileKey
|
||||
)
|
||||
|
||||
const (
|
||||
_ MembershipRole = iota
|
||||
// TeamLeader represents a leader role inside a team
|
||||
TeamLeader
|
||||
// TeamMember represents a member role inside a team
|
||||
TeamMember
|
||||
)
|
||||
|
||||
const (
|
||||
_ UserRole = iota
|
||||
// AdministratorRole represents an administrator user role
|
||||
@@ -199,17 +419,31 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
_ ResourceControlType = iota
|
||||
// ContainerResourceControl represents a resource control for a container
|
||||
ContainerResourceControl
|
||||
// ServiceResourceControl represents a resource control for a service
|
||||
ServiceResourceControl
|
||||
// VolumeResourceControl represents a resource control for a volume
|
||||
VolumeResourceControl
|
||||
_ AuthenticationMethod = iota
|
||||
// AuthenticationInternal represents the internal authentication method (authentication against Portainer API)
|
||||
AuthenticationInternal
|
||||
// AuthenticationLDAP represents the LDAP authentication method (authentication against a LDAP server)
|
||||
AuthenticationLDAP
|
||||
)
|
||||
|
||||
const (
|
||||
_ ResourceAccessLevel = iota
|
||||
// RestrictedResourceAccessLevel represents a restricted access level on a resource (private ownership)
|
||||
RestrictedResourceAccessLevel
|
||||
// ReadWriteAccessLevel represents an access level with read-write permissions on a resource
|
||||
ReadWriteAccessLevel
|
||||
)
|
||||
|
||||
const (
|
||||
_ ResourceControlType = iota
|
||||
// ContainerResourceControl represents a resource control associated to a Docker container
|
||||
ContainerResourceControl
|
||||
// ServiceResourceControl represents a resource control associated to a Docker service
|
||||
ServiceResourceControl
|
||||
// VolumeResourceControl represents a resource control associated to a Docker volume
|
||||
VolumeResourceControl
|
||||
// NetworkResourceControl represents a resource control associated to a Docker network
|
||||
NetworkResourceControl
|
||||
// SecretResourceControl represents a resource control associated to a Docker secret
|
||||
SecretResourceControl
|
||||
// StackResourceControl represents a resource control associated to a stack composed of Docker services
|
||||
StackResourceControl
|
||||
)
|
||||
|
||||
2602
api/swagger.yaml
Normal file
2602
api/swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
68
app/__module.js
Normal file
68
app/__module.js
Normal file
@@ -0,0 +1,68 @@
|
||||
angular.module('portainer', [
|
||||
'ui.bootstrap',
|
||||
'ui.router',
|
||||
'isteven-multi-select',
|
||||
'ngCookies',
|
||||
'ngSanitize',
|
||||
'ngFileUpload',
|
||||
'angularUtils.directives.dirPagination',
|
||||
'LocalStorageModule',
|
||||
'angular-jwt',
|
||||
'angular-google-analytics',
|
||||
'portainer.templates',
|
||||
'portainer.filters',
|
||||
'portainer.rest',
|
||||
'portainer.helpers',
|
||||
'portainer.services',
|
||||
'auth',
|
||||
'dashboard',
|
||||
'container',
|
||||
'containerConsole',
|
||||
'containerLogs',
|
||||
'containerStats',
|
||||
'serviceLogs',
|
||||
'containers',
|
||||
'createContainer',
|
||||
'createNetwork',
|
||||
'createRegistry',
|
||||
'createSecret',
|
||||
'createService',
|
||||
'createVolume',
|
||||
'createStack',
|
||||
'engine',
|
||||
'endpoint',
|
||||
'endpointAccess',
|
||||
'endpoints',
|
||||
'events',
|
||||
'image',
|
||||
'images',
|
||||
'initAdmin',
|
||||
'initEndpoint',
|
||||
'main',
|
||||
'network',
|
||||
'networks',
|
||||
'node',
|
||||
'registries',
|
||||
'registry',
|
||||
'registryAccess',
|
||||
'secrets',
|
||||
'secret',
|
||||
'service',
|
||||
'services',
|
||||
'settings',
|
||||
'settingsAuthentication',
|
||||
'sidebar',
|
||||
'stack',
|
||||
'stacks',
|
||||
'swarm',
|
||||
'swarmVisualizer',
|
||||
'task',
|
||||
'team',
|
||||
'teams',
|
||||
'templates',
|
||||
'user',
|
||||
'users',
|
||||
'userSettings',
|
||||
'volume',
|
||||
'volumes',
|
||||
'rzModule']);
|
||||
598
app/app.js
598
app/app.js
@@ -1,569 +1,41 @@
|
||||
angular.module('portainer.filters', []);
|
||||
angular.module('portainer.rest', ['ngResource']);
|
||||
angular.module('portainer.services', []);
|
||||
angular.module('portainer.helpers', []);
|
||||
angular.module('portainer', [
|
||||
'ui.bootstrap',
|
||||
'ui.router',
|
||||
'ui.select',
|
||||
'ngCookies',
|
||||
'ngSanitize',
|
||||
'ngFileUpload',
|
||||
'angularUtils.directives.dirPagination',
|
||||
'LocalStorageModule',
|
||||
'angular-jwt',
|
||||
'angular-google-analytics',
|
||||
'portainer.templates',
|
||||
'portainer.filters',
|
||||
'portainer.rest',
|
||||
'portainer.helpers',
|
||||
'portainer.services',
|
||||
'auth',
|
||||
'dashboard',
|
||||
'container',
|
||||
'containerConsole',
|
||||
'containerLogs',
|
||||
'containers',
|
||||
'createContainer',
|
||||
'createNetwork',
|
||||
'createService',
|
||||
'createVolume',
|
||||
'docker',
|
||||
'endpoint',
|
||||
'endpointAccess',
|
||||
'endpointInit',
|
||||
'endpoints',
|
||||
'events',
|
||||
'image',
|
||||
'images',
|
||||
'main',
|
||||
'network',
|
||||
'networks',
|
||||
'node',
|
||||
'service',
|
||||
'services',
|
||||
'settings',
|
||||
'sidebar',
|
||||
'stats',
|
||||
'swarm',
|
||||
'task',
|
||||
'templates',
|
||||
'user',
|
||||
'users',
|
||||
'volumes'])
|
||||
.config(['$stateProvider', '$urlRouterProvider', '$httpProvider', 'localStorageServiceProvider', 'jwtOptionsProvider', 'AnalyticsProvider', function ($stateProvider, $urlRouterProvider, $httpProvider, localStorageServiceProvider, jwtOptionsProvider, AnalyticsProvider) {
|
||||
'use strict';
|
||||
angular.module('portainer')
|
||||
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Notifications', 'Analytics', function ($rootScope, $state, Authentication, authManager, StateManager, EndpointProvider, Notifications, Analytics) {
|
||||
'use strict';
|
||||
|
||||
localStorageServiceProvider
|
||||
.setStorageType('sessionStorage')
|
||||
.setPrefix('portainer');
|
||||
EndpointProvider.initialize();
|
||||
|
||||
jwtOptionsProvider.config({
|
||||
tokenGetter: ['LocalStorage', function(LocalStorage) {
|
||||
return LocalStorage.getJWT();
|
||||
}],
|
||||
unauthenticatedRedirector: ['$state', function($state) {
|
||||
$state.go('auth', {error: 'Your session has expired'});
|
||||
}]
|
||||
});
|
||||
$httpProvider.interceptors.push('jwtInterceptor');
|
||||
StateManager.initialize()
|
||||
.then(function success(state) {
|
||||
if (state.application.authentication) {
|
||||
initAuthentication(authManager, Authentication, $rootScope);
|
||||
}
|
||||
if (state.application.analytics) {
|
||||
initAnalytics(Analytics, $rootScope);
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve application settings');
|
||||
});
|
||||
|
||||
AnalyticsProvider.setAccount('@@CONFIG_GA_ID');
|
||||
AnalyticsProvider.startOffline(true);
|
||||
$rootScope.$state = $state;
|
||||
}]);
|
||||
|
||||
$urlRouterProvider.otherwise('/auth');
|
||||
|
||||
$stateProvider
|
||||
.state('root', {
|
||||
abstract: true,
|
||||
resolve: {
|
||||
requiresLogin: ['StateManager', function (StateManager) {
|
||||
var applicationState = StateManager.getState();
|
||||
return applicationState.application.authentication;
|
||||
}]
|
||||
}
|
||||
})
|
||||
.state('auth', {
|
||||
parent: 'root',
|
||||
url: '/auth',
|
||||
params: {
|
||||
logout: false,
|
||||
error: ''
|
||||
},
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/auth/auth.html',
|
||||
controller: 'AuthenticationController'
|
||||
}
|
||||
},
|
||||
data: {
|
||||
requiresLogin: false
|
||||
}
|
||||
})
|
||||
.state('containers', {
|
||||
parent: 'root',
|
||||
url: '/containers/',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/containers/containers.html',
|
||||
controller: 'ContainersController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('container', {
|
||||
url: "^/containers/:id",
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/container/container.html',
|
||||
controller: 'ContainerController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('stats', {
|
||||
url: "^/containers/:id/stats",
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/stats/stats.html',
|
||||
controller: 'StatsController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('logs', {
|
||||
url: "^/containers/:id/logs",
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/containerLogs/containerlogs.html',
|
||||
controller: 'ContainerLogsController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('console', {
|
||||
url: "^/containers/:id/console",
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/containerConsole/containerConsole.html',
|
||||
controller: 'ContainerConsoleController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('dashboard', {
|
||||
parent: 'root',
|
||||
url: '/dashboard',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/dashboard/dashboard.html',
|
||||
controller: 'DashboardController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('actions', {
|
||||
abstract: true,
|
||||
url: "/actions",
|
||||
views: {
|
||||
"content@": {
|
||||
template: '<div ui-view="content@"></div>'
|
||||
},
|
||||
"sidebar@": {
|
||||
template: '<div ui-view="sidebar@"></div>'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('actions.create', {
|
||||
abstract: true,
|
||||
url: "/create",
|
||||
views: {
|
||||
"content@": {
|
||||
template: '<div ui-view="content@"></div>'
|
||||
},
|
||||
"sidebar@": {
|
||||
template: '<div ui-view="sidebar@"></div>'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('actions.create.container', {
|
||||
url: "/container",
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/createContainer/createcontainer.html',
|
||||
controller: 'CreateContainerController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('actions.create.network', {
|
||||
url: "/network",
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/createNetwork/createnetwork.html',
|
||||
controller: 'CreateNetworkController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('actions.create.service', {
|
||||
url: "/service",
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/createService/createservice.html',
|
||||
controller: 'CreateServiceController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('actions.create.volume', {
|
||||
url: "/volume",
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/createVolume/createvolume.html',
|
||||
controller: 'CreateVolumeController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('docker', {
|
||||
url: '/docker/',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/docker/docker.html',
|
||||
controller: 'DockerController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('endpoints', {
|
||||
url: '/endpoints/',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/endpoints/endpoints.html',
|
||||
controller: 'EndpointsController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('endpoint', {
|
||||
url: '^/endpoints/:id',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/endpoint/endpoint.html',
|
||||
controller: 'EndpointController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('endpoint.access', {
|
||||
url: '^/endpoints/:id/access',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/endpointAccess/endpointAccess.html',
|
||||
controller: 'EndpointAccessController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('endpointInit', {
|
||||
url: '/init/endpoint',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/endpointInit/endpointInit.html',
|
||||
controller: 'EndpointInitController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('events', {
|
||||
url: '/events/',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/events/events.html',
|
||||
controller: 'EventsController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('images', {
|
||||
url: '/images/',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/images/images.html',
|
||||
controller: 'ImagesController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('image', {
|
||||
url: '^/images/:id/',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/image/image.html',
|
||||
controller: 'ImageController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('networks', {
|
||||
url: '/networks/',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/networks/networks.html',
|
||||
controller: 'NetworksController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('network', {
|
||||
url: '^/networks/:id/',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/network/network.html',
|
||||
controller: 'NetworkController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('node', {
|
||||
url: '^/nodes/:id/',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/node/node.html',
|
||||
controller: 'NodeController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('services', {
|
||||
url: '/services/',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/services/services.html',
|
||||
controller: 'ServicesController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('service', {
|
||||
url: '^/service/:id/',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/service/service.html',
|
||||
controller: 'ServiceController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('settings', {
|
||||
url: '/settings/',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/settings/settings.html',
|
||||
controller: 'SettingsController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('task', {
|
||||
url: '^/task/:id',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/task/task.html',
|
||||
controller: 'TaskController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('templates', {
|
||||
url: '/templates/',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/templates/templates.html',
|
||||
controller: 'TemplatesController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('volumes', {
|
||||
url: '/volumes/',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/volumes/volumes.html',
|
||||
controller: 'VolumesController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('users', {
|
||||
url: '/users/',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/users/users.html',
|
||||
controller: 'UsersController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('user', {
|
||||
url: '^/users/:id',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/user/user.html',
|
||||
controller: 'UserController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
})
|
||||
.state('swarm', {
|
||||
url: '/swarm/',
|
||||
views: {
|
||||
"content@": {
|
||||
templateUrl: 'app/components/swarm/swarm.html',
|
||||
controller: 'SwarmController'
|
||||
},
|
||||
"sidebar@": {
|
||||
templateUrl: 'app/components/sidebar/sidebar.html',
|
||||
controller: 'SidebarController'
|
||||
}
|
||||
}
|
||||
});
|
||||
function initAuthentication(authManager, Authentication, $rootScope) {
|
||||
authManager.checkAuthOnRefresh();
|
||||
authManager.redirectWhenUnauthenticated();
|
||||
Authentication.init();
|
||||
$rootScope.$on('tokenHasExpired', function() {
|
||||
$state.go('auth', {error: 'Your session has expired'});
|
||||
});
|
||||
}
|
||||
|
||||
// The Docker API likes to return plaintext errors, this catches them and disp
|
||||
$httpProvider.interceptors.push(function() {
|
||||
return {
|
||||
'response': function(response) {
|
||||
if (typeof(response.data) === 'string' &&
|
||||
(_.startsWith(response.data, 'Conflict.') || _.startsWith(response.data, 'conflict:'))) {
|
||||
$.gritter.add({
|
||||
title: 'Error',
|
||||
text: $('<div>').text(response.data).html(),
|
||||
time: 10000
|
||||
});
|
||||
}
|
||||
return response;
|
||||
}
|
||||
};
|
||||
});
|
||||
}])
|
||||
.run(['$rootScope', '$state', 'Authentication', 'authManager', 'StateManager', 'EndpointProvider', 'Messages', 'Analytics', function ($rootScope, $state, Authentication, authManager, StateManager, EndpointProvider, Messages, Analytics) {
|
||||
EndpointProvider.initialize();
|
||||
StateManager.initialize().then(function success(state) {
|
||||
if (state.application.authentication) {
|
||||
authManager.checkAuthOnRefresh();
|
||||
authManager.redirectWhenUnauthenticated();
|
||||
Authentication.init();
|
||||
$rootScope.$on('tokenHasExpired', function($state) {
|
||||
$state.go('auth', {error: 'Your session has expired'});
|
||||
});
|
||||
}
|
||||
if (state.application.analytics) {
|
||||
Analytics.offline(false);
|
||||
Analytics.registerScriptTags();
|
||||
Analytics.registerTrackers();
|
||||
$rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) {
|
||||
Analytics.trackPage(toState.url);
|
||||
Analytics.pageView();
|
||||
});
|
||||
}
|
||||
}, function error(err) {
|
||||
Messages.error("Failure", err, 'Unable to retrieve application settings');
|
||||
});
|
||||
|
||||
$rootScope.$state = $state;
|
||||
}])
|
||||
// This is your docker url that the api will use to make requests
|
||||
// You need to set this to the api endpoint without the port i.e. http://192.168.1.9
|
||||
.constant('DOCKER_PORT', '') // Docker port, leave as an empty string if no port is required. If you have a port, prefix it with a ':' i.e. :4243
|
||||
.constant('DOCKER_ENDPOINT', 'api/docker')
|
||||
.constant('CONFIG_ENDPOINT', 'api/settings')
|
||||
.constant('AUTH_ENDPOINT', 'api/auth')
|
||||
.constant('USERS_ENDPOINT', 'api/users')
|
||||
.constant('ENDPOINTS_ENDPOINT', 'api/endpoints')
|
||||
.constant('TEMPLATES_ENDPOINT', 'api/templates')
|
||||
.constant('PAGINATION_MAX_ITEMS', 10)
|
||||
.constant('UI_VERSION', 'v1.12.1');
|
||||
function initAnalytics(Analytics, $rootScope) {
|
||||
Analytics.offline(false);
|
||||
Analytics.registerScriptTags();
|
||||
Analytics.registerTrackers();
|
||||
$rootScope.$on('$stateChangeSuccess', function (event, toState, toParams, fromState, fromParams) {
|
||||
Analytics.trackPage(toState.url);
|
||||
Analytics.pageView();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,92 +1,38 @@
|
||||
<div class="page-wrapper">
|
||||
<!-- login box -->
|
||||
<div class="container simple-box">
|
||||
<div class="col-md-6 col-md-offset-3 col-sm-6 col-sm-offset-3">
|
||||
<div class="col-sm-6 col-sm-offset-3">
|
||||
<!-- login box logo -->
|
||||
<div class="row">
|
||||
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
|
||||
<img ng-if="!logo" src="images/logo_alt.png" class="simple-box-logo" alt="Portainer">
|
||||
<img ng-if="logo" ng-src="{{ logo }}" class="simple-box-logo">
|
||||
</div>
|
||||
<!-- !login box logo -->
|
||||
<!-- init password panel -->
|
||||
<div class="panel panel-default" ng-if="initPassword">
|
||||
<div class="panel-body">
|
||||
<!-- init password form -->
|
||||
<form class="login-form form-horizontal" enctype="multipart/form-data" method="POST">
|
||||
<!-- comment -->
|
||||
<div class="input-group">
|
||||
<p style="margin: 5px;">
|
||||
Please specify a password for the <b>admin</b> user account.
|
||||
</p>
|
||||
</div>
|
||||
<!-- !comment input -->
|
||||
<!-- comment -->
|
||||
<div class="input-group">
|
||||
<p style="margin: 5px;">
|
||||
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[initPasswordData.password.length >= 8]" aria-hidden="true"></i>
|
||||
Your password must be at least 8 characters long
|
||||
</p>
|
||||
</div>
|
||||
<!-- !comment input -->
|
||||
<!-- password input -->
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||
<input id="admin_password" type="password" class="form-control" name="password" ng-model="initPasswordData.password" autofocus>
|
||||
</div>
|
||||
<!-- !password input -->
|
||||
<!-- comment -->
|
||||
<div class="input-group">
|
||||
<p style="margin: 5px;">
|
||||
<i ng-class="{true: 'fa fa-check green-icon', false: 'fa fa-times red-icon'}[initPasswordData.password !== '' && initPasswordData.password === initPasswordData.password_confirmation]" aria-hidden="true"></i>
|
||||
Confirm your password
|
||||
</p>
|
||||
</div>
|
||||
<!-- !comment input -->
|
||||
<!-- password confirmation input -->
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||
<input id="password_confirmation" type="password" class="form-control" name="password" ng-model="initPasswordData.password_confirmation">
|
||||
</div>
|
||||
<!-- !password confirmation input -->
|
||||
<!-- validate button -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 controls">
|
||||
<p class="pull-left text-danger" ng-if="initPasswordData.error" style="margin: 5px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> Unable to create default user
|
||||
</p>
|
||||
<button type="submit" class="btn btn-primary pull-right" ng-disabled="initPasswordData.password.length < 8 || initPasswordData.password !== initPasswordData.password_confirmation" ng-click="createAdminUser()"><i class="fa fa-key" aria-hidden="true"></i> Validate</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !validate button -->
|
||||
</form>
|
||||
<!-- !init password form -->
|
||||
</div>
|
||||
</div>
|
||||
<!-- !init password panel -->
|
||||
<!-- login panel -->
|
||||
<div class="panel panel-default" ng-if="!initPassword">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-body">
|
||||
<!-- login form -->
|
||||
<form class="login-form form-horizontal" enctype="multipart/form-data" method="POST">
|
||||
<form class="simple-box-form form-horizontal">
|
||||
<!-- username input -->
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-user" aria-hidden="true"></i></span>
|
||||
<input id="username" type="text" class="form-control" name="username" ng-model="authData.username" placeholder="Username">
|
||||
<input id="username" type="text" class="form-control" name="username" ng-model="formValues.Username" auto-focus>
|
||||
</div>
|
||||
<!-- !username input -->
|
||||
<!-- password input -->
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon"><i class="fa fa-lock" aria-hidden="true"></i></span>
|
||||
<input id="password" type="password" class="form-control" name="password" ng-model="authData.password" autofocus>
|
||||
<input id="password" type="password" class="form-control" name="password" ng-model="formValues.Password">
|
||||
</div>
|
||||
<!-- !password input -->
|
||||
<!-- login button -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12 controls">
|
||||
<p class="pull-left text-danger" ng-if="authData.error" style="margin: 5px;">
|
||||
<i class="fa fa-exclamation-circle" aria-hidden="true"></i> {{ authData.error }}
|
||||
</p>
|
||||
<button type="submit" class="btn btn-primary pull-right" ng-click="authenticateUser()"><i class="fa fa-sign-in" aria-hidden="true"></i> Login</button>
|
||||
<div class="col-sm-12">
|
||||
<button type="submit" class="btn btn-primary btn-sm pull-right" ng-click="authenticateUser()"><i class="fa fa-sign-in" aria-hidden="true"></i> Login</button>
|
||||
<span class="pull-left" style="margin: 5px;" ng-if="state.AuthenticationError">
|
||||
<i class="fa fa-exclamation-triangle red-icon" aria-hidden="true" style="margin-right: 2px;"></i>
|
||||
<span class="small text-danger">{{ state.AuthenticationError }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- !login button -->
|
||||
|
||||
@@ -1,114 +1,110 @@
|
||||
angular.module('auth', [])
|
||||
.controller('AuthenticationController', ['$scope', '$state', '$stateParams', '$window', '$timeout', '$sanitize', 'Config', 'Authentication', 'Users', 'EndpointService', 'StateManager', 'EndpointProvider', 'Messages',
|
||||
function ($scope, $state, $stateParams, $window, $timeout, $sanitize, Config, Authentication, Users, EndpointService, StateManager, EndpointProvider, Messages) {
|
||||
.controller('AuthenticationController', ['$scope', '$state', '$transition$', '$window', '$timeout', '$sanitize', 'Authentication', 'Users', 'UserService', 'EndpointService', 'StateManager', 'EndpointProvider', 'Notifications', 'SettingsService',
|
||||
function ($scope, $state, $transition$, $window, $timeout, $sanitize, Authentication, Users, UserService, EndpointService, StateManager, EndpointProvider, Notifications, SettingsService) {
|
||||
|
||||
$scope.authData = {
|
||||
username: 'admin',
|
||||
password: '',
|
||||
error: ''
|
||||
};
|
||||
$scope.initPasswordData = {
|
||||
password: '',
|
||||
password_confirmation: '',
|
||||
error: false
|
||||
$scope.logo = StateManager.getState().application.logo;
|
||||
|
||||
$scope.formValues = {
|
||||
Username: '',
|
||||
Password: ''
|
||||
};
|
||||
|
||||
if (!$scope.applicationState.application.authentication) {
|
||||
$scope.state = {
|
||||
AuthenticationError: ''
|
||||
};
|
||||
|
||||
function setActiveEndpointAndRedirectToDashboard(endpoint) {
|
||||
var endpointID = EndpointProvider.endpointID();
|
||||
if (!endpointID) {
|
||||
EndpointProvider.setEndpointID(endpoint.Id);
|
||||
}
|
||||
StateManager.updateEndpointState(true)
|
||||
.then(function success(data) {
|
||||
$state.go('dashboard');
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to connect to the Docker endpoint');
|
||||
});
|
||||
}
|
||||
|
||||
function unauthenticatedFlow() {
|
||||
EndpointService.endpoints()
|
||||
.then(function success(data) {
|
||||
if (data.length > 0) {
|
||||
endpointID = EndpointProvider.endpointID();
|
||||
if (!endpointID) {
|
||||
endpointID = data[0].Id;
|
||||
EndpointProvider.setEndpointID(endpointID);
|
||||
}
|
||||
StateManager.updateEndpointState(true)
|
||||
.then(function success() {
|
||||
$state.go('dashboard');
|
||||
}, function error(err) {
|
||||
Messages.error("Failure", err, 'Unable to connect to the Docker endpoint');
|
||||
});
|
||||
}
|
||||
else {
|
||||
$state.go('endpointInit');
|
||||
}
|
||||
}, function error(err) {
|
||||
Messages.error("Failure", err, 'Unable to retrieve endpoints');
|
||||
});
|
||||
} else {
|
||||
Users.checkAdminUser({}, function () {},
|
||||
function (e) {
|
||||
if (e.status === 404) {
|
||||
$scope.initPassword = true;
|
||||
var endpoints = data;
|
||||
if (endpoints.length > 0) {
|
||||
setActiveEndpointAndRedirectToDashboard(endpoints[0]);
|
||||
} else {
|
||||
Messages.error("Failure", e, 'Unable to verify administrator account existence');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if ($stateParams.logout) {
|
||||
Authentication.logout();
|
||||
}
|
||||
|
||||
if ($stateParams.error) {
|
||||
$scope.authData.error = $stateParams.error;
|
||||
Authentication.logout();
|
||||
}
|
||||
|
||||
if (Authentication.isAuthenticated()) {
|
||||
$state.go('dashboard');
|
||||
}
|
||||
|
||||
Config.$promise.then(function (c) {
|
||||
$scope.logo = c.logo;
|
||||
});
|
||||
|
||||
$scope.createAdminUser = function() {
|
||||
var password = $sanitize($scope.initPasswordData.password);
|
||||
Users.initAdminUser({password: password}, function (d) {
|
||||
$scope.initPassword = false;
|
||||
$timeout(function() {
|
||||
var element = $window.document.getElementById('password');
|
||||
if(element) {
|
||||
element.focus();
|
||||
}
|
||||
});
|
||||
}, function (e) {
|
||||
$scope.initPassword.error = true;
|
||||
});
|
||||
};
|
||||
|
||||
$scope.authenticateUser = function() {
|
||||
$scope.authenticationError = false;
|
||||
var username = $sanitize($scope.authData.username);
|
||||
var password = $sanitize($scope.authData.password);
|
||||
Authentication.login(username, password)
|
||||
.then(function success(data) {
|
||||
return EndpointService.endpoints();
|
||||
})
|
||||
.then(function success(data) {
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
if (data.length > 0) {
|
||||
endpointID = EndpointProvider.endpointID();
|
||||
if (!endpointID) {
|
||||
endpointID = data[0].Id;
|
||||
EndpointProvider.setEndpointID(endpointID);
|
||||
}
|
||||
StateManager.updateEndpointState(true)
|
||||
.then(function success() {
|
||||
$state.go('dashboard');
|
||||
}, function error(err) {
|
||||
Messages.error("Failure", err, 'Unable to connect to the Docker endpoint');
|
||||
});
|
||||
}
|
||||
else if (data.length === 0 && userDetails.role === 1) {
|
||||
$state.go('endpointInit');
|
||||
} else if (data.length === 0 && userDetails.role === 2) {
|
||||
$scope.authData.error = 'User not allowed. Please contact your administrator.';
|
||||
$state.go('init.endpoint');
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
$scope.authData.error = 'Authentication error';
|
||||
Notifications.error('Failure', err, 'Unable to retrieve endpoints');
|
||||
});
|
||||
}
|
||||
|
||||
function authenticatedFlow() {
|
||||
UserService.administratorExists()
|
||||
.then(function success(exists) {
|
||||
if (!exists) {
|
||||
$state.go('init.admin');
|
||||
}
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to verify administrator account existence');
|
||||
});
|
||||
}
|
||||
|
||||
$scope.authenticateUser = function() {
|
||||
var username = $scope.formValues.Username;
|
||||
var password = $scope.formValues.Password;
|
||||
|
||||
SettingsService.publicSettings()
|
||||
.then(function success(data) {
|
||||
var settings = data;
|
||||
if (settings.AuthenticationMethod === 1) {
|
||||
username = $sanitize(username);
|
||||
password = $sanitize(password);
|
||||
}
|
||||
return Authentication.login(username, password);
|
||||
})
|
||||
.then(function success() {
|
||||
return EndpointService.endpoints();
|
||||
})
|
||||
.then(function success(data) {
|
||||
var endpoints = data;
|
||||
var userDetails = Authentication.getUserDetails();
|
||||
if (endpoints.length > 0) {
|
||||
setActiveEndpointAndRedirectToDashboard(endpoints[0]);
|
||||
} else if (endpoints.length === 0 && userDetails.role === 1) {
|
||||
$state.go('init.endpoint');
|
||||
} else if (endpoints.length === 0 && userDetails.role === 2) {
|
||||
Authentication.logout();
|
||||
$scope.state.AuthenticationError = 'User not allowed. Please contact your administrator.';
|
||||
}
|
||||
})
|
||||
.catch(function error() {
|
||||
$scope.state.AuthenticationError = 'Invalid credentials';
|
||||
});
|
||||
};
|
||||
|
||||
function initView() {
|
||||
if ($transition$.params().logout || $transition$.params().error) {
|
||||
Authentication.logout();
|
||||
$scope.state.AuthenticationError = $transition$.params().error;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Authentication.isAuthenticated()) {
|
||||
$state.go('dashboard');
|
||||
}
|
||||
|
||||
var authenticationEnabled = $scope.applicationState.application.authentication;
|
||||
if (!authenticationEnabled) {
|
||||
unauthenticatedFlow();
|
||||
} else {
|
||||
authenticatedFlow();
|
||||
}
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a>
|
||||
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a>
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
@@ -13,13 +13,15 @@
|
||||
<rd-widget-header icon="fa-cogs" title="Actions"></rd-widget-header>
|
||||
<rd-widget-body classes="padding">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<button class="btn btn-primary" ng-click="start()" ng-if="!container.State.Running"><i class="fa fa-play space-right" aria-hidden="true"></i>Start</button>
|
||||
<button class="btn btn-danger" ng-click="stop()" ng-if="container.State.Running"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</button>
|
||||
<button class="btn btn-danger" ng-click="kill()" ng-if="container.State.Running"><i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill</button>
|
||||
<button class="btn btn-primary" ng-click="restart()" ng-if="container.State.Running"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Restart</button>
|
||||
<button class="btn btn-primary" ng-click="pause()" ng-if="container.State.Running && !container.State.Paused"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button>
|
||||
<button class="btn btn-primary" ng-click="unpause()" ng-if="container.State.Paused"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
|
||||
<button class="btn btn-danger" ng-click="remove()" ng-disabled="container.State.Running"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
|
||||
<button class="btn btn-success" ng-click="start()" ng-disabled="container.State.Running"><i class="fa fa-play space-right" aria-hidden="true"></i>Start</button>
|
||||
<button class="btn btn-danger" ng-click="stop()" ng-disabled="!container.State.Running"><i class="fa fa-stop space-right" aria-hidden="true"></i>Stop</button>
|
||||
<button class="btn btn-danger" ng-click="kill()" ng-disabled="!container.State.Running"><i class="fa fa-bomb space-right" aria-hidden="true"></i>Kill</button>
|
||||
<button class="btn btn-primary" ng-click="restart()" ng-disabled="!container.State.Running"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Restart</button>
|
||||
<button class="btn btn-primary" ng-click="pause()" ng-disabled="!container.State.Running || container.State.Paused"><i class="fa fa-pause space-right" aria-hidden="true"></i>Pause</button>
|
||||
<button class="btn btn-primary" ng-click="unpause()" ng-disabled="!container.State.Paused"><i class="fa fa-play space-right" aria-hidden="true"></i>Resume</button>
|
||||
<button class="btn btn-danger" ng-click="confirmRemove()"><i class="fa fa-trash space-right" aria-hidden="true"></i>Remove</button>
|
||||
<button class="btn btn-danger" ng-click="recreate()" ng-if="!container.Config.Labels['com.docker.swarm.service.id']"><i class="fa fa-refresh space-right" aria-hidden="true"></i>Recreate</button>
|
||||
<button class="btn btn-primary" ng-click="duplicate()" ng-if="!container.Config.Labels['com.docker.swarm.service.id']"><i class="fa fa-files-o space-right" aria-hidden="true"></i>Duplicate/Edit</button>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
@@ -33,6 +35,10 @@
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>ID</td>
|
||||
<td>{{ container.Id }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Name</td>
|
||||
<td ng-if="!container.edit">
|
||||
@@ -54,15 +60,20 @@
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>
|
||||
<i ng-class="{true: 'fa fa-heartbeat space-right green-icon', false: 'fa fa-heartbeat space-right red-icon'}[container.State.Running]"></i>
|
||||
{{ container.State|getstatetext }} since {{ activityTime }}<span ng-if="!container.State.Running"> with exit code {{ container.State.ExitCode }}</span>
|
||||
<i class="fa fa-heartbeat space-right green-icon" ng-if="container.State.Running"></i>
|
||||
<i class="fa fa-heartbeat space-right red-icon" ng-if="!container.State.Running && container.State.Status !== 'created'"></i>
|
||||
{{ container.State|getstatetext }} since {{ activityTime }}<span ng-if="!container.State.Running && container.State.Status !== 'created'"> with exit code {{ container.State.ExitCode }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Created</td>
|
||||
<td>{{ container.Created|getisodate }}</td>
|
||||
</tr>
|
||||
<tr ng-if="container.State.Running">
|
||||
<td>Start time</td>
|
||||
<td>{{ container.State.StartedAt|getisodate }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!container.State.Running">
|
||||
<tr ng-if="!container.State.Running && container.State.Status !== 'created'">
|
||||
<td>Finished</td>
|
||||
<td>{{ container.State.FinishedAt|getisodate }}</td>
|
||||
</tr>
|
||||
@@ -70,7 +81,7 @@
|
||||
<td colspan="2">
|
||||
<div class="btn-group" role="group" aria-label="...">
|
||||
<a class="btn btn-outline-secondary" type="button" ui-sref="stats({id: container.Id})"><i class="fa fa-area-chart space-right" aria-hidden="true"></i>Stats</a>
|
||||
<a class="btn btn-outline-secondary" type="button" ui-sref="logs({id: container.Id})"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i>Logs</a>
|
||||
<a class="btn btn-outline-secondary" type="button" ui-sref="containerlogs({id: container.Id})"><i class="fa fa-exclamation-circle space-right" aria-hidden="true"></i>Logs</a>
|
||||
<a class="btn btn-outline-secondary" type="button" ui-sref="console({id: container.Id})"><i class="fa fa-terminal space-right" aria-hidden="true"></i>Console</a>
|
||||
</div>
|
||||
</td>
|
||||
@@ -82,6 +93,44 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- access-control-panel -->
|
||||
<por-access-control-panel
|
||||
ng-if="container && applicationState.application.authentication"
|
||||
resource-id="container.Id"
|
||||
resource-control="container.ResourceControl"
|
||||
resource-type="'container'">
|
||||
</por-access-control-panel>
|
||||
<!-- !access-control-panel -->
|
||||
|
||||
<div ng-if="container.State.Health" class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-server" title="Container health"></rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Status</td>
|
||||
<td>
|
||||
<i ng-class="{'healthy': 'fa fa-heartbeat space-right green-icon', 'unhealthy': 'fa fa-heartbeat space-right red-icon', 'starting': 'fa fa-heartbeat space-right orange-icon'}[container.State.Health.Status]"></i>
|
||||
{{ container.State.Health.Status }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Failure count</td>
|
||||
<td>{{ container.State.Health.FailingStreak }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Last output</td>
|
||||
<td>{{ container.State.Health.Log[container.State.Health.Log.length - 1].Output }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
@@ -98,18 +147,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<!-- !tag-description -->
|
||||
<!-- name-and-registry-inputs -->
|
||||
<!-- image-and-registry -->
|
||||
<div class="form-group">
|
||||
<label for="image_name" class="col-sm-1 control-label text-left">Name</label>
|
||||
<div class="col-sm-7">
|
||||
<input type="text" class="form-control" ng-model="config.Image" id="image_name" placeholder="e.g. myImage:myTag">
|
||||
</div>
|
||||
<label for="image_registry" class="col-sm-1 control-label text-left">Registry</label>
|
||||
<div class="col-sm-3">
|
||||
<input type="text" class="form-control" ng-model="config.Registry" id="image_registry" placeholder="optional">
|
||||
</div>
|
||||
<por-image-registry image="config.Image" registry="config.Registry"></por-image-registry>
|
||||
</div>
|
||||
<!-- !name-and-registry-inputs -->
|
||||
<!-- !image-and-registry -->
|
||||
<!-- tag-note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
@@ -119,7 +161,7 @@
|
||||
<!-- !tag-note -->
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<button type="button" class="btn btn-default btn-sm" ng-disabled="!config.Image" ng-click="commit()">Create</button>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!config.Image" ng-click="commit()">Create</button>
|
||||
<i id="createImageSpinner" class="fa fa-cog fa-spin" style="margin-left: 5px; display: none;"></i>
|
||||
</div>
|
||||
</div>
|
||||
@@ -156,7 +198,7 @@
|
||||
<td>ENV</td>
|
||||
<td>
|
||||
<table class="table table-bordered table-condensed">
|
||||
<tr ng-repeat="var in container.Config.Env">
|
||||
<tr ng-repeat="var in container.Config.Env track by $index">
|
||||
<td>{{ var|key: '=' }}</td>
|
||||
<td>{{ var|value: '=' }}</td>
|
||||
</tr>
|
||||
@@ -222,7 +264,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" ng-if="!(container.NetworkSettings.Networks | emptyobject)">
|
||||
<div class="row">
|
||||
<div class="col-lg-12 col-md-12 col-xs-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-sitemap" title="Connected networks">
|
||||
@@ -256,11 +298,30 @@
|
||||
<button type="button" class="btn btn-xs btn-danger" ng-click="containerLeaveNetwork(container, value.NetworkID)"><i class="fa fa-trash space-right" aria-hidden="true"></i>Leave Network</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="(container.NetworkSettings.Networks | emptyobject)">
|
||||
<td colspan="5" class="text-center text-muted">No networks connected.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
<hr />
|
||||
<form class="form-horizontal">
|
||||
<!-- network-input -->
|
||||
<div class="row">
|
||||
<label for="container_network" class="col-sm-3 col-lg-2 control-label text-left">Join a Network</label>
|
||||
<div class="col-sm-5 col-lg-4">
|
||||
<select class="form-control" ng-model="selectedNetwork" id="container_network">
|
||||
<option selected disabled hidden value="">Select a network</option>
|
||||
<option ng-repeat="net in availableNetworks" ng-value="net.Id">{{ net.Name }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-disabled="!selectedNetwork" ng-click="containerJoinNetwork(container, selectedNetwork)">Join Network</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
angular.module('container', [])
|
||||
.controller('ContainerController', ['$scope', '$state','$stateParams', '$filter', 'Container', 'ContainerCommit', 'ImageHelper', 'Network', 'Messages', 'Pagination',
|
||||
function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, ImageHelper, Network, Messages, Pagination) {
|
||||
.controller('ContainerController', ['$q', '$scope', '$state','$transition$', '$filter', 'Container', 'ContainerCommit', 'ContainerHelper', 'ContainerService', 'ImageHelper', 'Network', 'NetworkService', 'Notifications', 'Pagination', 'ModalService', 'ResourceControlService', 'RegistryService', 'ImageService',
|
||||
function ($q, $scope, $state, $transition$, $filter, Container, ContainerCommit, ContainerHelper, ContainerService, ImageHelper, Network, NetworkService, Notifications, Pagination, ModalService, ResourceControlService, RegistryService, ImageService) {
|
||||
$scope.activityTime = 0;
|
||||
$scope.portBindings = [];
|
||||
$scope.config = {
|
||||
@@ -16,24 +16,27 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
|
||||
|
||||
var update = function () {
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.get({id: $stateParams.id}, function (d) {
|
||||
$scope.container = d;
|
||||
Container.get({id: $transition$.params().id}, function (d) {
|
||||
var container = new ContainerDetailsViewModel(d);
|
||||
$scope.container = container;
|
||||
$scope.container.edit = false;
|
||||
$scope.container.newContainerName = $filter('trimcontainername')(d.Name);
|
||||
$scope.container.newContainerName = $filter('trimcontainername')(container.Name);
|
||||
|
||||
if (d.State.Running) {
|
||||
$scope.activityTime = moment.duration(moment(d.State.StartedAt).utc().diff(moment().utc())).humanize();
|
||||
if (container.State.Running) {
|
||||
$scope.activityTime = moment.duration(moment(container.State.StartedAt).utc().diff(moment().utc())).humanize();
|
||||
} else if (container.State.Status === 'created') {
|
||||
$scope.activityTime = moment.duration(moment(container.Created).utc().diff(moment().utc())).humanize();
|
||||
} else {
|
||||
$scope.activityTime = moment.duration(moment().utc().diff(moment(d.State.FinishedAt).utc())).humanize();
|
||||
$scope.activityTime = moment.duration(moment().utc().diff(moment(container.State.FinishedAt).utc())).humanize();
|
||||
}
|
||||
|
||||
$scope.portBindings = [];
|
||||
if (d.NetworkSettings.Ports) {
|
||||
angular.forEach(Object.keys(d.NetworkSettings.Ports), function(portMapping) {
|
||||
if (d.NetworkSettings.Ports[portMapping]) {
|
||||
if (container.NetworkSettings.Ports) {
|
||||
angular.forEach(Object.keys(container.NetworkSettings.Ports), function(portMapping) {
|
||||
if (container.NetworkSettings.Ports[portMapping]) {
|
||||
var mapping = {};
|
||||
mapping.container = portMapping;
|
||||
mapping.host = d.NetworkSettings.Ports[portMapping][0].HostIp + ':' + d.NetworkSettings.Ports[portMapping][0].HostPort;
|
||||
mapping.host = container.NetworkSettings.Ports[portMapping][0].HostIp + ':' + container.NetworkSettings.Ports[portMapping][0].HostPort;
|
||||
$scope.portBindings.push(mapping);
|
||||
}
|
||||
});
|
||||
@@ -41,7 +44,7 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function (e) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to retrieve container info");
|
||||
Notifications.error('Failure', e, 'Unable to retrieve container info');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -49,32 +52,32 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.start({id: $scope.container.Id}, {}, function (d) {
|
||||
update();
|
||||
Messages.send("Container started", $stateParams.id);
|
||||
Notifications.success('Container started', $transition$.params().id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", e, "Unable to start container");
|
||||
Notifications.error('Failure', e, 'Unable to start container');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.stop = function () {
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.stop({id: $stateParams.id}, function (d) {
|
||||
Container.stop({id: $transition$.params().id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container stopped", $stateParams.id);
|
||||
Notifications.success('Container stopped', $transition$.params().id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", e, "Unable to stop container");
|
||||
Notifications.error('Failure', e, 'Unable to stop container');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.kill = function () {
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.kill({id: $stateParams.id}, function (d) {
|
||||
Container.kill({id: $transition$.params().id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container killed", $stateParams.id);
|
||||
Notifications.success('Container killed', $transition$.params().id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", e, "Unable to kill container");
|
||||
Notifications.error('Failure', e, 'Unable to kill container');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -82,99 +85,231 @@ function ($scope, $state, $stateParams, $filter, Container, ContainerCommit, Ima
|
||||
$('#createImageSpinner').show();
|
||||
var image = $scope.config.Image;
|
||||
var registry = $scope.config.Registry;
|
||||
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry);
|
||||
ContainerCommit.commit({id: $stateParams.id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
|
||||
var imageConfig = ImageHelper.createImageConfigForCommit(image, registry.URL);
|
||||
ContainerCommit.commit({id: $transition$.params().id, tag: imageConfig.tag, repo: imageConfig.repo}, function (d) {
|
||||
$('#createImageSpinner').hide();
|
||||
update();
|
||||
Messages.send("Container commited", $stateParams.id);
|
||||
Notifications.success('Container commited', $transition$.params().id);
|
||||
}, function (e) {
|
||||
$('#createImageSpinner').hide();
|
||||
update();
|
||||
Messages.error("Failure", e, "Unable to commit container");
|
||||
Notifications.error('Failure', e, 'Unable to commit container');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.pause = function () {
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.pause({id: $stateParams.id}, function (d) {
|
||||
Container.pause({id: $transition$.params().id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container paused", $stateParams.id);
|
||||
Notifications.success('Container paused', $transition$.params().id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", e, "Unable to pause container");
|
||||
Notifications.error('Failure', e, 'Unable to pause container');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.unpause = function () {
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.unpause({id: $stateParams.id}, function (d) {
|
||||
Container.unpause({id: $transition$.params().id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container unpaused", $stateParams.id);
|
||||
Notifications.success('Container unpaused', $transition$.params().id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", e, "Unable to unpause container");
|
||||
Notifications.error('Failure', e, 'Unable to unpause container');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.remove = function () {
|
||||
$scope.confirmRemove = function () {
|
||||
var title = 'You are about to remove a container.';
|
||||
if ($scope.container.State.Running) {
|
||||
title = 'You are about to remove a running container.';
|
||||
}
|
||||
ModalService.confirmContainerDeletion(
|
||||
title,
|
||||
function (result) {
|
||||
if(!result) { return; }
|
||||
var cleanAssociatedVolumes = false;
|
||||
if (result[0]) {
|
||||
cleanAssociatedVolumes = true;
|
||||
}
|
||||
$scope.remove(cleanAssociatedVolumes);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
$scope.remove = function(cleanAssociatedVolumes) {
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.remove({id: $stateParams.id}, function (d) {
|
||||
if (d.message) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.send("Error", d.message);
|
||||
}
|
||||
else {
|
||||
$state.go('containers', {}, {reload: true});
|
||||
Messages.send("Container removed", $stateParams.id);
|
||||
}
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", e, "Unable to remove container");
|
||||
ContainerService.remove($scope.container, cleanAssociatedVolumes)
|
||||
.then(function success() {
|
||||
Notifications.success('Container successfully removed');
|
||||
$state.go('containers', {}, {reload: true});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to remove container');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
};
|
||||
|
||||
$scope.restart = function () {
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.restart({id: $stateParams.id}, function (d) {
|
||||
Container.restart({id: $transition$.params().id}, function (d) {
|
||||
update();
|
||||
Messages.send("Container restarted", $stateParams.id);
|
||||
Notifications.success('Container restarted', $transition$.params().id);
|
||||
}, function (e) {
|
||||
update();
|
||||
Messages.error("Failure", e, "Unable to restart container");
|
||||
Notifications.error('Failure', e, 'Unable to restart container');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.renameContainer = function () {
|
||||
Container.rename({id: $stateParams.id, 'name': $scope.container.newContainerName}, function (d) {
|
||||
var container = $scope.container;
|
||||
Container.rename({id: $transition$.params().id, 'name': container.newContainerName}, function (d) {
|
||||
if (d.message) {
|
||||
$scope.container.newContainerName = $scope.container.Name;
|
||||
Messages.error("Unable to rename container", {}, d.message);
|
||||
container.newContainerName = container.Name;
|
||||
Notifications.error('Unable to rename container', {}, d.message);
|
||||
} else {
|
||||
$scope.container.Name = $scope.container.newContainerName;
|
||||
Messages.send("Container successfully renamed", d.name);
|
||||
container.Name = container.newContainerName;
|
||||
Notifications.success('Container successfully renamed', container.Name);
|
||||
}
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, 'Unable to rename container');
|
||||
Notifications.error('Failure', e, 'Unable to rename container');
|
||||
});
|
||||
$scope.container.edit = false;
|
||||
};
|
||||
|
||||
$scope.containerLeaveNetwork = function containerLeaveNetwork(container, networkId) {
|
||||
$('#loadingViewSpinner').show();
|
||||
Network.disconnect({id: networkId}, { Container: $stateParams.id, Force: false }, function (d) {
|
||||
if (d.message) {
|
||||
Network.disconnect({id: networkId}, { Container: $transition$.params().id, Force: false }, function (d) {
|
||||
if (container.message) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.send("Error", {}, d.message);
|
||||
Notifications.error('Error', d, 'Unable to disconnect container from network');
|
||||
} else {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.send("Container left network", $stateParams.id);
|
||||
$state.go('container', {id: $stateParams.id}, {reload: true});
|
||||
Notifications.success('Container left network', $transition$.params().id);
|
||||
$state.go('container', {id: $transition$.params().id}, {reload: true});
|
||||
}
|
||||
}, function (e) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to disconnect container from network");
|
||||
Notifications.error('Failure', e, 'Unable to disconnect container from network');
|
||||
});
|
||||
};
|
||||
|
||||
$scope.duplicate = function() {
|
||||
ModalService.confirmExperimentalFeature(function (experimental) {
|
||||
if(!experimental) { return; }
|
||||
$state.go('actions.create.container', {from: $transition$.params().id}, {reload: true});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.confirmRemove = function () {
|
||||
var title = 'You are about to remove a container.';
|
||||
if ($scope.container.State.Running) {
|
||||
title = 'You are about to remove a running container.';
|
||||
}
|
||||
ModalService.confirmContainerDeletion(
|
||||
title,
|
||||
function (result) {
|
||||
if(!result) { return; }
|
||||
var cleanAssociatedVolumes = false;
|
||||
if (result[0]) {
|
||||
cleanAssociatedVolumes = true;
|
||||
}
|
||||
$scope.remove(cleanAssociatedVolumes);
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
function recreateContainer(pullImage) {
|
||||
$('#loadingViewSpinner').show();
|
||||
var container = $scope.container;
|
||||
var config = ContainerHelper.configFromContainer(container.Model);
|
||||
ContainerService.remove(container, true)
|
||||
.then(function success() {
|
||||
return RegistryService.retrieveRegistryFromRepository(container.Config.Image);
|
||||
})
|
||||
.then(function success(data) {
|
||||
return $q.when(!pullImage || ImageService.pullImage(container.Config.Image, data, true));
|
||||
})
|
||||
.then(function success() {
|
||||
return ContainerService.createAndStartContainer(config);
|
||||
})
|
||||
.then(function success(data) {
|
||||
if (!container.ResourceControl) {
|
||||
return true;
|
||||
} else {
|
||||
var containerIdentifier = data.Id;
|
||||
var resourceControl = container.ResourceControl;
|
||||
var users = resourceControl.UserAccesses.map(function(u) {
|
||||
return u.UserId;
|
||||
});
|
||||
var teams = resourceControl.TeamAccesses.map(function(t) {
|
||||
return t.TeamId;
|
||||
});
|
||||
return ResourceControlService.createResourceControl(resourceControl.AdministratorsOnly,
|
||||
users, teams, containerIdentifier, 'container', []);
|
||||
}
|
||||
})
|
||||
.then(function success(data) {
|
||||
Notifications.success('Container successfully re-created');
|
||||
$state.go('containers', {}, {reload: true});
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to re-create container');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
$scope.recreate = function() {
|
||||
ModalService.confirmExperimentalFeature(function (experimental) {
|
||||
if(!experimental) { return; }
|
||||
|
||||
ModalService.confirmContainerRecreation(function (result) {
|
||||
if(!result) { return; }
|
||||
var pullImage = false;
|
||||
if (result[0]) {
|
||||
pullImage = true;
|
||||
}
|
||||
recreateContainer(pullImage);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.containerJoinNetwork = function containerJoinNetwork(container, networkId) {
|
||||
$('#joinNetworkSpinner').show();
|
||||
Network.connect({id: networkId}, { Container: $transition$.params().id }, function (d) {
|
||||
if (container.message) {
|
||||
$('#joinNetworkSpinner').hide();
|
||||
Notifications.error('Error', d, 'Unable to connect container to network');
|
||||
} else {
|
||||
$('#joinNetworkSpinner').hide();
|
||||
Notifications.success('Container joined network', $transition$.params().id);
|
||||
$state.go('container', {id: $transition$.params().id}, {reload: true});
|
||||
}
|
||||
}, function (e) {
|
||||
$('#joinNetworkSpinner').hide();
|
||||
Notifications.error('Failure', e, 'Unable to connect container to network');
|
||||
});
|
||||
};
|
||||
|
||||
var provider = $scope.applicationState.endpoint.mode.provider;
|
||||
var apiVersion = $scope.applicationState.endpoint.apiVersion;
|
||||
NetworkService.networks(
|
||||
provider === 'DOCKER_STANDALONE' || provider === 'DOCKER_SWARM_MODE',
|
||||
false,
|
||||
provider === 'DOCKER_SWARM_MODE' && apiVersion >= 1.25,
|
||||
provider === 'DOCKER_SWARM'
|
||||
)
|
||||
.then(function success(data) {
|
||||
var networks = data;
|
||||
$scope.availableNetworks = networks;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve networks');
|
||||
});
|
||||
|
||||
update();
|
||||
}]);
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content ng-if="state.loaded">
|
||||
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Console
|
||||
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Console
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
@@ -16,29 +16,53 @@
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form>
|
||||
<div class="row">
|
||||
<form class="form-horizontal">
|
||||
<div ng-if="!state.connected">
|
||||
<!-- command-list -->
|
||||
<div class="col-sm-4">
|
||||
<div class="input-group">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-linux" aria-hidden="true" ng-if="imageOS == 'linux'"></i>
|
||||
<i class="fa fa-windows" aria-hidden="true" ng-if="imageOS == 'windows'"></i>
|
||||
</span>
|
||||
<select class="form-control" ng-model="state.command" id="command">
|
||||
<option value="bash" ng-if="imageOS == 'linux'">/bin/bash</option>
|
||||
<option value="sh" ng-if="imageOS == 'linux'">/bin/sh</option>
|
||||
<option value="powershell" ng-if="imageOS == 'windows'">powershell</option>
|
||||
<option value="cmd.exe" ng-if="imageOS == 'windows'">cmd.exe</option>
|
||||
</select>
|
||||
<div class="form-group">
|
||||
<label for="command" class="col-lg-1 text-left col-sm-2 control-label">Command</label>
|
||||
<div class="col-lg-11 col-sm-10">
|
||||
<div class="input-group" ng-if="!formValues.isCustomCommand">
|
||||
<span class="input-group-addon">
|
||||
<i class="fa fa-linux" aria-hidden="true" ng-if="imageOS == 'linux'"></i>
|
||||
<i class="fa fa-windows" aria-hidden="true" ng-if="imageOS == 'windows'"></i>
|
||||
</span>
|
||||
<select class="form-control" ng-model="formValues.command" id="command">
|
||||
<option value="bash" ng-if="imageOS == 'linux'">/bin/bash</option>
|
||||
<option value="sh" ng-if="imageOS == 'linux'">/bin/sh</option>
|
||||
<option value="powershell" ng-if="imageOS == 'windows'">powershell</option>
|
||||
<option value="cmd.exe" ng-if="imageOS == 'windows'">cmd.exe</option>
|
||||
</select>
|
||||
</div>
|
||||
<input class="form-control" ng-if="formValues.isCustomCommand" type="text" name="custom-command" ng-model="formValues.customCommand" placeholder="e.g. ps aux">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !command-list -->
|
||||
<div class="form-group col-lg-12">
|
||||
<label for="command" class="text-left control-label">Use custom command</label>
|
||||
<label class="switch" style="margin-left: 20px;">
|
||||
<input type="checkbox" ng-model="formValues.isCustomCommand"><i></i>
|
||||
</label>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="username" class="col-lg-1 text-left col-sm-2 control-label">
|
||||
User
|
||||
<portainer-tooltip position="bottom" message="Format is one of: user, user:group, uid or uid:gid"></portainer-tooltip>
|
||||
</label>
|
||||
<div class="col-lg-11 col-sm-10">
|
||||
<input class="form-control" type="text" name="username" ng-model="formValues.user" placeholder="root">
|
||||
</div>
|
||||
</div>
|
||||
<!-- !command-list -->
|
||||
<div class="col-sm-8">
|
||||
<button type="button" class="btn btn-primary" ng-click="connect()" ng-disabled="state.connected">Connect</button>
|
||||
<button type="button" class="btn btn-default" ng-click="disconnect()" ng-disabled="!state.connected">Disconnect</button>
|
||||
<div class="form-group">
|
||||
<div class="col-lg-offset-1 col-sm-offset-2 col-lg-11 col-sm-10">
|
||||
<button type="button" class="btn btn-primary" ng-click="connect()">Connect</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-if="state.connected">
|
||||
<label>Exec into container as <code>{{ ::formValues.user || 'default user' }}</code> using command <code>{{ formValues.isCustomCommand ? formValues.customCommand : formValues.command }}</code></label>
|
||||
<button type="button" class="btn btn-default" ng-click="disconnect()">Disconnect</button>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
angular.module('containerConsole', [])
|
||||
.controller('ContainerConsoleController', ['$scope', '$stateParams', 'Settings', 'Container', 'Image', 'Exec', '$timeout', 'EndpointProvider', 'Messages',
|
||||
function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, EndpointProvider, Messages) {
|
||||
.controller('ContainerConsoleController', ['$scope', '$transition$', 'Container', 'Image', 'EndpointProvider', 'Notifications', 'ContainerHelper', 'ContainerService', 'ExecService',
|
||||
function ($scope, $transition$, Container, Image, EndpointProvider, Notifications, ContainerHelper, ContainerService, ExecService) {
|
||||
$scope.state = {};
|
||||
$scope.state.loaded = false;
|
||||
$scope.state.connected = false;
|
||||
$scope.formValues = {};
|
||||
|
||||
var socket, term;
|
||||
|
||||
@@ -14,58 +15,61 @@ function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Endp
|
||||
}
|
||||
});
|
||||
|
||||
Container.get({id: $stateParams.id}, function(d) {
|
||||
Container.get({id: $transition$.params().id}, function(d) {
|
||||
$scope.container = d;
|
||||
if (d.message) {
|
||||
Messages.error("Error", d, 'Unable to retrieve container details');
|
||||
Notifications.error('Error', d, 'Unable to retrieve container details');
|
||||
$('#loadingViewSpinner').hide();
|
||||
} else {
|
||||
Image.get({id: d.Image}, function(imgData) {
|
||||
$scope.imageOS = imgData.Os;
|
||||
$scope.state.command = imgData.Os === 'windows' ? 'powershell' : 'bash';
|
||||
$scope.formValues.command = imgData.Os === 'windows' ? 'powershell' : 'bash';
|
||||
$scope.state.loaded = true;
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, 'Unable to retrieve image details');
|
||||
Notifications.error('Failure', e, 'Unable to retrieve image details');
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
}, function (e) {
|
||||
Messages.error("Failure", e, 'Unable to retrieve container details');
|
||||
Notifications.error('Failure', e, 'Unable to retrieve container details');
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
|
||||
$scope.connect = function() {
|
||||
$('#loadConsoleSpinner').show();
|
||||
var termWidth = Math.round($('#terminal-container').width() / 8.2);
|
||||
var termWidth = Math.floor(($('#terminal-container').width() - 20) / 8.39);
|
||||
var termHeight = 30;
|
||||
var command = $scope.formValues.isCustomCommand ?
|
||||
$scope.formValues.customCommand : $scope.formValues.command;
|
||||
var execConfig = {
|
||||
id: $stateParams.id,
|
||||
id: $transition$.params().id,
|
||||
AttachStdin: true,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
Tty: true,
|
||||
Cmd: $scope.state.command.replace(" ", ",").split(",")
|
||||
User: $scope.formValues.user,
|
||||
Cmd: ContainerHelper.commandStringToArray(command)
|
||||
};
|
||||
|
||||
Container.exec(execConfig, function(d) {
|
||||
if (d.message) {
|
||||
$('#loadConsoleSpinner').hide();
|
||||
Messages.error("Error", {}, d.message);
|
||||
var execId;
|
||||
ContainerService.createExec(execConfig)
|
||||
.then(function success(data) {
|
||||
execId = data.Id;
|
||||
var url = window.location.href.split('#')[0] + 'api/websocket/exec?id=' + execId + '&endpointId=' + EndpointProvider.endpointID();
|
||||
if (url.indexOf('https') > -1) {
|
||||
url = url.replace('https://', 'wss://');
|
||||
} else {
|
||||
var execId = d.Id;
|
||||
resizeTTY(execId, termHeight, termWidth);
|
||||
var url = window.location.href.split('#')[0] + 'api/websocket/exec?id=' + execId + '&endpointId=' + EndpointProvider.endpointID();
|
||||
if (url.indexOf('https') > -1) {
|
||||
url = url.replace('https://', 'wss://');
|
||||
} else {
|
||||
url = url.replace('http://', 'ws://');
|
||||
}
|
||||
initTerm(url, termHeight, termWidth);
|
||||
url = url.replace('http://', 'ws://');
|
||||
}
|
||||
}, function (e) {
|
||||
initTerm(url, termHeight, termWidth);
|
||||
return ExecService.resizeTTY(execId, termHeight, termWidth, 2000);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to exec into container');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadConsoleSpinner').hide();
|
||||
Messages.error("Failure", e, 'Unable to start an exec instance');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -79,19 +83,6 @@ function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Endp
|
||||
}
|
||||
};
|
||||
|
||||
function resizeTTY(execId, height, width) {
|
||||
$timeout(function() {
|
||||
Exec.resize({id: execId, height: height, width: width}, function (d) {
|
||||
if (d.message) {
|
||||
Messages.error('Error', {}, 'Unable to resize TTY');
|
||||
}
|
||||
}, function (e) {
|
||||
Messages.error("Failure", {}, 'Unable to resize TTY');
|
||||
});
|
||||
}, 2000);
|
||||
|
||||
}
|
||||
|
||||
function initTerm(url, height, width) {
|
||||
socket = new WebSocket(url);
|
||||
|
||||
@@ -103,9 +94,14 @@ function ($scope, $stateParams, Settings, Container, Image, Exec, $timeout, Endp
|
||||
term.on('data', function (data) {
|
||||
socket.send(data);
|
||||
});
|
||||
term.open(document.getElementById('terminal-container'));
|
||||
term.open(document.getElementById('terminal-container'), true);
|
||||
term.resize(width, height);
|
||||
term.setOption('cursorBlink', true);
|
||||
term.fit();
|
||||
|
||||
window.onresize = function() {
|
||||
term.fit();
|
||||
};
|
||||
|
||||
socket.onmessage = function (e) {
|
||||
term.write(e.data);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
angular.module('containerLogs', [])
|
||||
.controller('ContainerLogsController', ['$scope', '$stateParams', '$anchorScroll', 'ContainerLogs', 'Container',
|
||||
function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container) {
|
||||
.controller('ContainerLogsController', ['$scope', '$transition$', '$anchorScroll', 'ContainerLogs', 'Container',
|
||||
function ($scope, $transition$, $anchorScroll, ContainerLogs, Container) {
|
||||
$scope.state = {};
|
||||
$scope.state.displayTimestampsOut = false;
|
||||
$scope.state.displayTimestampsErr = false;
|
||||
@@ -9,12 +9,12 @@ function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container) {
|
||||
$scope.tailLines = 2000;
|
||||
|
||||
$('#loadingViewSpinner').show();
|
||||
Container.get({id: $stateParams.id}, function (d) {
|
||||
Container.get({id: $transition$.params().id}, function (d) {
|
||||
$scope.container = d;
|
||||
$('#loadingViewSpinner').hide();
|
||||
}, function (e) {
|
||||
$('#loadingViewSpinner').hide();
|
||||
Messages.error("Failure", e, "Unable to retrieve container info");
|
||||
Notifications.error('Failure', e, 'Unable to retrieve container info');
|
||||
});
|
||||
|
||||
function getLogs() {
|
||||
@@ -25,7 +25,7 @@ function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container) {
|
||||
}
|
||||
|
||||
function getLogsStderr() {
|
||||
ContainerLogs.get($stateParams.id, {
|
||||
ContainerLogs.get($transition$.params().id, {
|
||||
stdout: 0,
|
||||
stderr: 1,
|
||||
timestamps: $scope.state.displayTimestampsErr,
|
||||
@@ -41,7 +41,7 @@ function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container) {
|
||||
}
|
||||
|
||||
function getLogsStdout() {
|
||||
ContainerLogs.get($stateParams.id, {
|
||||
ContainerLogs.get($transition$.params().id, {
|
||||
stdout: 1,
|
||||
stderr: 0,
|
||||
timestamps: $scope.state.displayTimestampsOut,
|
||||
@@ -60,7 +60,7 @@ function ($scope, $stateParams, $anchorScroll, ContainerLogs, Container) {
|
||||
getLogs();
|
||||
var logIntervalId = window.setInterval(getLogs, 5000);
|
||||
|
||||
$scope.$on("$destroy", function () {
|
||||
$scope.$on('$destroy', function () {
|
||||
// clearing interval when view changes
|
||||
clearInterval(logIntervalId);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs
|
||||
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Logs
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
|
||||
131
app/components/containerStats/containerStats.html
Normal file
131
app/components/containerStats/containerStats.html
Normal file
@@ -0,0 +1,131 @@
|
||||
<rd-header>
|
||||
<rd-header-title title="Container statistics">
|
||||
<i id="loadingViewSpinner" class="fa fa-cog fa-spin"></i>
|
||||
</rd-header-title>
|
||||
<rd-header-content>
|
||||
<a ui-sref="containers">Containers</a> > <a ui-sref="container({id: container.Id})">{{ container.Name|trimcontainername }}</a> > Stats
|
||||
</rd-header-content>
|
||||
</rd-header>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-info-circle" title="About statistics">
|
||||
</rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<form class="form-horizontal">
|
||||
<div class="form-group">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">
|
||||
This view displays real-time statistics about the container <b>{{ container.Name|trimcontainername }}</b> as well as a list of the running processes
|
||||
inside this container.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="refreshRate" class="col-sm-3 col-md-2 col-lg-2 margin-sm-top control-label text-left">
|
||||
Refresh rate
|
||||
</label>
|
||||
<div class="col-sm-3 col-md-2">
|
||||
<select id="refreshRate" ng-model="state.refreshRate" ng-change="changeUpdateRepeater()" class="form-control">
|
||||
<option value="5">5s</option>
|
||||
<option value="10">10s</option>
|
||||
<option value="30">30s</option>
|
||||
<option value="60">60s</option>
|
||||
</select>
|
||||
</div>
|
||||
<span>
|
||||
<i id="refreshRateChange" class="fa fa-check green-icon" aria-hidden="true" style="margin-top: 7px; display: none;"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="form-group" ng-if="state.networkStatsUnavailable">
|
||||
<div class="col-sm-12">
|
||||
<span class="small text-muted">
|
||||
<i class="fa fa-exclamation-triangle orange-icon" aria-hidden="true"></i> Network stats are unavailable for this container.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div ng-class="{true: 'col-md-6 col-sm-12', false: 'col-lg-4 col-md-6 col-sm-12'}[state.networkStatsUnavailable]">
|
||||
<!-- <div class="col-lg-4 col-md-6 col-sm-12"> -->
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-area-chart" title="Memory usage"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<div class="chart-container" style="position: relative;">
|
||||
<canvas id="memoryChart" width="770" height="300"></canvas>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div ng-class="{true: 'col-md-6 col-sm-12', false: 'col-lg-4 col-md-6 col-sm-12'}[state.networkStatsUnavailable]">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-area-chart" title="CPU usage"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<div class="chart-container" style="position: relative;">
|
||||
<canvas id="cpuChart" width="770" height="300"></canvas>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-lg-4 col-md-12 col-sm-12" ng-if="!state.networkStatsUnavailable">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-area-chart" title="Network usage"></rd-widget-header>
|
||||
<rd-widget-body>
|
||||
<div class="chart-container" style="position: relative;">
|
||||
<canvas id="networkChart" width="770" height="300"></canvas>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
<div class="col-sm-12" ng-if="applicationState.endpoint.mode.provider !== 'VMWARE_VIC'">
|
||||
<rd-widget>
|
||||
<rd-widget-header icon="fa-tasks" title="Processes">
|
||||
<div class="pull-right">
|
||||
Items per page:
|
||||
<select ng-model="state.pagination_count" ng-change="changePaginationCount()">
|
||||
<option value="0">All</option>
|
||||
<option value="10">10</option>
|
||||
<option value="25">25</option>
|
||||
<option value="50">50</option>
|
||||
<option value="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
</rd-widget-header>
|
||||
<rd-widget-body classes="no-padding">
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th ng-repeat="title in processInfo.Titles">
|
||||
<a ng-click="order(title)">
|
||||
{{ title }}
|
||||
<span ng-show="sortType == title && !sortReverse" class="glyphicon glyphicon-chevron-down"></span>
|
||||
<span ng-show="sortType == title && sortReverse" class="glyphicon glyphicon-chevron-up"></span>
|
||||
</a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr dir-paginate="processDetails in state.filteredProcesses = (processInfo.Processes | orderBy:sortType:sortReverse | itemsPerPage: state.pagination_count)">
|
||||
<td ng-repeat="procInfo in processDetails track by $index">{{ procInfo }}</td>
|
||||
</tr>
|
||||
<tr ng-if="!processInfo.Processes">
|
||||
<td colspan="processInfo.Titles.length" class="text-center text-muted">Loading...</td>
|
||||
</tr>
|
||||
<tr ng-if="state.filteredProcesses.length === 0">
|
||||
<td colspan="processInfo.Titles.length" class="text-center text-muted">No processes available.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div ng-if="processInfo.Processes" class="pagination-controls">
|
||||
<dir-pagination-controls></dir-pagination-controls>
|
||||
</div>
|
||||
</rd-widget-body>
|
||||
</rd-widget>
|
||||
</div>
|
||||
</div>
|
||||
165
app/components/containerStats/containerStatsController.js
Normal file
165
app/components/containerStats/containerStatsController.js
Normal file
@@ -0,0 +1,165 @@
|
||||
angular.module('containerStats', [])
|
||||
.controller('ContainerStatsController', ['$q', '$scope', '$transition$', '$document', '$interval', 'ContainerService', 'ChartService', 'Notifications', 'Pagination',
|
||||
function ($q, $scope, $transition$, $document, $interval, ContainerService, ChartService, Notifications, Pagination) {
|
||||
|
||||
$scope.state = {
|
||||
refreshRate: '5',
|
||||
networkStatsUnavailable: false
|
||||
};
|
||||
|
||||
$scope.state.pagination_count = Pagination.getPaginationCount('stats_processes');
|
||||
$scope.sortType = 'CMD';
|
||||
$scope.sortReverse = false;
|
||||
|
||||
$scope.order = function (sortType) {
|
||||
$scope.sortReverse = ($scope.sortType === sortType) ? !$scope.sortReverse : false;
|
||||
$scope.sortType = sortType;
|
||||
};
|
||||
|
||||
$scope.changePaginationCount = function() {
|
||||
Pagination.setPaginationCount('stats_processes', $scope.state.pagination_count);
|
||||
};
|
||||
|
||||
$scope.$on('$destroy', function() {
|
||||
stopRepeater();
|
||||
});
|
||||
|
||||
function stopRepeater() {
|
||||
var repeater = $scope.repeater;
|
||||
if (angular.isDefined(repeater)) {
|
||||
$interval.cancel(repeater);
|
||||
repeater = null;
|
||||
}
|
||||
}
|
||||
|
||||
function updateNetworkChart(stats, chart) {
|
||||
if (stats.Networks.length > 0) {
|
||||
var rx = stats.Networks[0].rx_bytes;
|
||||
var tx = stats.Networks[0].tx_bytes;
|
||||
var label = moment(stats.Date).format('HH:mm:ss');
|
||||
|
||||
ChartService.UpdateNetworkChart(label, rx, tx, chart);
|
||||
}
|
||||
}
|
||||
|
||||
function updateMemoryChart(stats, chart) {
|
||||
var label = moment(stats.Date).format('HH:mm:ss');
|
||||
var value = stats.MemoryUsage;
|
||||
|
||||
ChartService.UpdateMemoryChart(label, value, chart);
|
||||
}
|
||||
|
||||
function updateCPUChart(stats, chart) {
|
||||
var label = moment(stats.Date).format('HH:mm:ss');
|
||||
var value = calculateCPUPercentUnix(stats);
|
||||
|
||||
ChartService.UpdateCPUChart(label, value, chart);
|
||||
}
|
||||
|
||||
function calculateCPUPercentUnix(stats) {
|
||||
var cpuPercent = 0.0;
|
||||
var cpuDelta = stats.CurrentCPUTotalUsage - stats.PreviousCPUTotalUsage;
|
||||
var systemDelta = stats.CurrentCPUSystemUsage - stats.PreviousCPUSystemUsage;
|
||||
|
||||
if (systemDelta > 0.0 && cpuDelta > 0.0) {
|
||||
cpuPercent = (cpuDelta / systemDelta) * stats.CPUCores * 100.0;
|
||||
}
|
||||
|
||||
return cpuPercent;
|
||||
}
|
||||
|
||||
$scope.changeUpdateRepeater = function() {
|
||||
var networkChart = $scope.networkChart;
|
||||
var cpuChart = $scope.cpuChart;
|
||||
var memoryChart = $scope.memoryChart;
|
||||
|
||||
stopRepeater();
|
||||
setUpdateRepeater(networkChart, cpuChart, memoryChart);
|
||||
$('#refreshRateChange').show();
|
||||
$('#refreshRateChange').fadeOut(1500);
|
||||
};
|
||||
|
||||
function startChartUpdate(networkChart, cpuChart, memoryChart) {
|
||||
$('#loadingViewSpinner').show();
|
||||
$q.all({
|
||||
stats: ContainerService.containerStats($transition$.params().id),
|
||||
top: ContainerService.containerTop($transition$.params().id)
|
||||
})
|
||||
.then(function success(data) {
|
||||
var stats = data.stats;
|
||||
$scope.processInfo = data.top;
|
||||
if (stats.Networks.length === 0) {
|
||||
$scope.state.networkStatsUnavailable = true;
|
||||
}
|
||||
updateNetworkChart(stats, networkChart);
|
||||
updateMemoryChart(stats, memoryChart);
|
||||
updateCPUChart(stats, cpuChart);
|
||||
setUpdateRepeater(networkChart, cpuChart, memoryChart);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
stopRepeater();
|
||||
Notifications.error('Failure', err, 'Unable to retrieve container statistics');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
}
|
||||
|
||||
function setUpdateRepeater(networkChart, cpuChart, memoryChart) {
|
||||
var refreshRate = $scope.state.refreshRate;
|
||||
$scope.repeater = $interval(function() {
|
||||
$q.all({
|
||||
stats: ContainerService.containerStats($transition$.params().id),
|
||||
top: ContainerService.containerTop($transition$.params().id)
|
||||
})
|
||||
.then(function success(data) {
|
||||
var stats = data.stats;
|
||||
$scope.processInfo = data.top;
|
||||
updateNetworkChart(stats, networkChart);
|
||||
updateMemoryChart(stats, memoryChart);
|
||||
updateCPUChart(stats, cpuChart);
|
||||
})
|
||||
.catch(function error(err) {
|
||||
stopRepeater();
|
||||
Notifications.error('Failure', err, 'Unable to retrieve container statistics');
|
||||
});
|
||||
}, refreshRate * 1000);
|
||||
}
|
||||
|
||||
function initCharts() {
|
||||
var networkChartCtx = $('#networkChart');
|
||||
var networkChart = ChartService.CreateNetworkChart(networkChartCtx);
|
||||
$scope.networkChart = networkChart;
|
||||
|
||||
var cpuChartCtx = $('#cpuChart');
|
||||
var cpuChart = ChartService.CreateCPUChart(cpuChartCtx);
|
||||
$scope.cpuChart = cpuChart;
|
||||
|
||||
var memoryChartCtx = $('#memoryChart');
|
||||
var memoryChart = ChartService.CreateMemoryChart(memoryChartCtx);
|
||||
$scope.memoryChart = memoryChart;
|
||||
|
||||
startChartUpdate(networkChart, cpuChart, memoryChart);
|
||||
}
|
||||
|
||||
function initView() {
|
||||
$('#loadingViewSpinner').show();
|
||||
|
||||
ContainerService.container($transition$.params().id)
|
||||
.then(function success(data) {
|
||||
$scope.container = data;
|
||||
})
|
||||
.catch(function error(err) {
|
||||
Notifications.error('Failure', err, 'Unable to retrieve container information');
|
||||
})
|
||||
.finally(function final() {
|
||||
$('#loadingViewSpinner').hide();
|
||||
});
|
||||
|
||||
$document.ready(function() {
|
||||
initCharts();
|
||||
});
|
||||
}
|
||||
|
||||
initView();
|
||||
}]);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user